extends Panel # Pulse movement var pulse_speed := 120.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 # Block zone (shrinks per floor, not per block) var block_zone_shrink := 3.0 var block_zone_min := 12.0 # Block zone movement (enabled per-floor by elevator_panel) var block_zone_moving := false var block_zone_speed := 0.0 var block_zone_direction := -1 var block_zone_min_x := 0.0 var block_zone_max_x := 0.0 const BLOCK_ZONE_RANGE_LEFT_RATIO := 0.5 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) const PERFECT_ZONE_RATIO := 0.3 const PERFECT_ZONE_COLOR := Color(1.0, 0.95, 0.4, 0.85) # Visual var base_color: Color # State var active := false var pulses_blocked := 0 var _ready_pulse_tween: Tween = null var _trail: Array[ColorRect] = [] var _glow: ColorRect var _perfect_zone: ColorRect var _face_scale_tween: Tween var _face_shake_tween: Tween signal pulse_blocked signal pulse_blocked_perfect signal doors_closing func _ready(): # Block zone on right side of screen var bz = $TargetZone bz.size = Vector2(50, size.y - 40) bz.position = Vector2(size.x - bz.size.x - 10, 20) _perfect_zone = ColorRect.new() _perfect_zone.color = PERFECT_ZONE_COLOR _perfect_zone.mouse_filter = Control.MOUSE_FILTER_IGNORE bz.add_child(_perfect_zone) _update_perfect_zone() # Pulse line (hidden until first pulse) var sl = $SweepLine sl.size = Vector2(4, size.y - 40) sl.position = Vector2(0, 20) sl.visible = false # AI face var face = $AIFace face.position = Vector2(0, 0) face.size = Vector2(size.x, 30) face.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER face.text = ">:)" face.pivot_offset = face.size / 2 _glow = ColorRect.new() _glow.color = GLOW_COLOR _glow.size = Vector2($SweepLine.size.x + GLOW_PADDING.x * 2, $SweepLine.size.y + GLOW_PADDING.y * 2) _glow.visible = false add_child(_glow) move_child(_glow, 0) for i in TRAIL_GHOSTS: var ghost := ColorRect.new() ghost.color = Color($SweepLine.color.r, $SweepLine.color.g, $SweepLine.color.b, TRAIL_ALPHAS[i]) ghost.size = $SweepLine.size ghost.visible = false add_child(ghost) move_child(ghost, 0) _trail.append(ghost) # Duplicate the StyleBoxFlat so flash tweens don't bleed into other panels var style = get_theme_stylebox("panel") as StyleBoxFlat if style: style = style.duplicate() add_theme_stylebox_override("panel", style) base_color = style.bg_color # --- Per-floor lifecycle --- func start(): active = true pulses_blocked = 0 pulse_speed = 120.0 pulse_gap = 1.0 $AIFace.text = ">:)" $TargetZone.visible = true block_zone_moving = false $TargetZone.position.x = size.x - $TargetZone.size.x - 10 func set_block_zone_movement(speed: float): block_zone_speed = speed block_zone_moving = speed > 0.0 var bz = $TargetZone block_zone_max_x = size.x - bz.size.x - 10 block_zone_min_x = size.x * BLOCK_ZONE_RANGE_LEFT_RATIO bz.position.x = block_zone_max_x block_zone_direction = -1 func stop(): active = false pulse_active = false _hide_pulse_visuals() _stop_ready_pulse() func shrink_block_zone(): var bz = $TargetZone var new_width = max(bz.size.x - block_zone_shrink, block_zone_min) bz.size.x = new_width bz.position.x = size.x - new_width - 10 _update_perfect_zone() func _update_perfect_zone(): var bz = $TargetZone var width = bz.size.x * PERFECT_ZONE_RATIO _perfect_zone.size = Vector2(width, bz.size.y) _perfect_zone.position = Vector2((bz.size.x - width) / 2.0, 0) # --- Pulse logic --- # Called by elevator_panel when a survivor reaches the door. func launch_pulse(): if not active: return pulse_x = 0.0 pulse_active = true $SweepLine.visible = true $SweepLine.position.x = 0 _glow.visible = true for ghost in _trail: ghost.visible = true $AIFace.text = ">:)" _start_ready_pulse() func get_pulse_gap() -> float: return pulse_gap func _process(delta): if not active: return if block_zone_moving: var bz = $TargetZone bz.position.x += block_zone_speed * block_zone_direction * delta if bz.position.x >= block_zone_max_x: bz.position.x = block_zone_max_x block_zone_direction = -1 elif bz.position.x <= block_zone_min_x: bz.position.x = block_zone_min_x block_zone_direction = 1 if not pulse_active: return pulse_x += pulse_speed * delta $SweepLine.position.x = pulse_x _glow.position = Vector2(pulse_x - GLOW_PADDING.x, $SweepLine.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, $SweepLine.position.y) # Pulse reached the far side unblocked: AI wins, doors close if pulse_x >= size.x: pulse_active = false _hide_pulse_visuals() $AIFace.text = ":D" _punch_face(1.5, 0.0) flash(Color.RED) active = false doors_closing.emit() # Called by elevator_panel when player presses BLOCK. func attempt_block() -> bool: if not pulse_active: return false # no pulse on screen, button does nothing var bz = $TargetZone var bz_left = bz.position.x var bz_right = bz.position.x + bz.size.x if pulse_x >= bz_left and pulse_x <= bz_right: # Blocked: doors stay open, this survivor gets in 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_pulse_visuals() $AIFace.text = ">:(" _punch_face(1.4, 4.0) flash(Color.GREEN) pulse_blocked.emit() if was_perfect: pulse_blocked_perfect.emit() return true else: # Mistimed: same result as letting it through pulse_active = false _hide_pulse_visuals() $AIFace.text = ":D" _punch_face(1.5, 0.0) flash(Color.RED) active = false doors_closing.emit() return false # --- Display helpers --- func flash(color: Color): var style = get_theme_stylebox("panel") as StyleBoxFlat if not style: return style.bg_color = color var tween = create_tween() tween.tween_property(style, "bg_color", base_color, 0.3) func show_countdown(): stop() $AIFace.text = "3" var tween = create_tween() tween.tween_interval(0.6) tween.tween_callback(func(): $AIFace.text = "2") tween.tween_interval(0.6) tween.tween_callback(func(): $AIFace.text = "1") tween.tween_interval(0.6) tween.tween_callback(func(): $AIFace.text = "CLOSED") func show_win(): stop() $TargetZone.visible = false $AIFace.text = "ESCAPED" flash(Color.GREEN) func show_loss(message: String): stop() $TargetZone.visible = false $AIFace.text = message flash(Color.RED) func show_onboarding(msg: String): $AIFace.add_theme_font_size_override("font_size", 18) $AIFace.size = Vector2(size.x, 60) $AIFace.text = msg func end_onboarding(): $AIFace.add_theme_font_size_override("font_size", 32) $AIFace.size = Vector2(size.x, 30) func _start_ready_pulse(): if _ready_pulse_tween: _ready_pulse_tween.kill() $TargetZone.modulate.a = 1.0 _ready_pulse_tween = create_tween() _ready_pulse_tween.set_loops() _ready_pulse_tween.tween_property($TargetZone, "modulate:a", 0.6, 0.5) _ready_pulse_tween.tween_property($TargetZone, "modulate:a", 1.0, 0.5) func _stop_ready_pulse(): if _ready_pulse_tween: _ready_pulse_tween.kill() _ready_pulse_tween = null $TargetZone.modulate.a = 1.0 func _hide_pulse_visuals(): $SweepLine.visible = false _glow.visible = false for ghost in _trail: ghost.visible = false func _punch_face(scale_amount: float, shake_amount: float): if _face_scale_tween: _face_scale_tween.kill() if _face_shake_tween: _face_shake_tween.kill() var face := $AIFace face.position = Vector2(0, 0) face.scale = Vector2(scale_amount, scale_amount) _face_scale_tween = create_tween() _face_scale_tween.tween_property(face, "scale", Vector2.ONE, 0.25).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT) if shake_amount > 0: _face_shake_tween = create_tween() _face_shake_tween.tween_property(face, "position:x", -shake_amount, 0.04) _face_shake_tween.tween_property(face, "position:x", shake_amount, 0.04) _face_shake_tween.tween_property(face, "position:x", -shake_amount * 0.5, 0.04) _face_shake_tween.tween_property(face, "position:x", 0.0, 0.04)