extends Panel const PULSE_TOP_MARGIN := 0.0 const PULSE_BOTTOM_MARGIN := 0.0 const BLOCK_ZONE_RIGHT_MARGIN := 4.0 const PERFECT_ZONE_RATIO := 0.3 const PERFECT_ZONE_COLOR := Color(1.0, 0.95, 0.4, 0.85) const QUOTA_DOT_SIZE := 7.0 const QUOTA_DOT_SPACING := 3.0 const QUOTA_DOT_Y := 6.0 const QUOTA_DOT_RIGHT_MARGIN := 8.0 const QUOTA_EMPTY_COLOR := Color(0.07, 0.14, 0.09, 1.0) const QUOTA_EMPTY_BORDER := Color(0.18, 0.35, 0.2, 1.0) const QUOTA_FILLED_COLOR := Color(0.2, 1.0, 0.2, 1.0) var block_zone_shrink := 3.0 var block_zone_min := 12.0 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 var base_color: Color var active := false var _ready_pulse_tween: Tween = null var _perfect_zone: ColorRect var _face_scale_tween: Tween var _face_shake_tween: Tween var _flash_tween: Tween var _floor_label: Label var _quota_dots: Array[Panel] = [] var _quota_threshold := 0 var _quota_filled_style: StyleBoxFlat var _quota_empty_style: StyleBoxFlat var _idle_image: TextureRect var _static_overlay: ColorRect var _score_label: Label var _score_value := 0 @onready var _pulse = $Pulse signal pulse_started(duration: float) signal pulse_blocked signal pulse_blocked_perfect signal doors_closing var pulse_active: bool: get: return _pulse.pulse_active if _pulse else false func _ready(): var bz = $TargetZone bz.size = Vector2(50, size.y - PULSE_TOP_MARGIN - PULSE_BOTTOM_MARGIN) bz.position = Vector2(size.x - bz.size.x - BLOCK_ZONE_RIGHT_MARGIN, PULSE_TOP_MARGIN) _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() var sl = $SweepLine sl.size = Vector2(4, size.y - PULSE_TOP_MARGIN - PULSE_BOTTOM_MARGIN) sl.position = Vector2(0, PULSE_TOP_MARGIN) sl.visible = false 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 _pulse.configure(sl, bz, _perfect_zone, size.x) _pulse.pulse_started.connect(_on_pulse_started) _pulse.pulse_blocked.connect(_on_pulse_blocked) _pulse.pulse_blocked_perfect.connect(_on_pulse_blocked_perfect) _pulse.pulse_escaped.connect(_on_pulse_escaped) var style = get_theme_stylebox("panel") as StyleBoxFlat if style: style = style.duplicate() add_theme_stylebox_override("panel", style) base_color = style.bg_color _floor_label = Label.new() _floor_label.add_theme_font_size_override("font_size", 14) _floor_label.position = Vector2(8, 4) _floor_label.mouse_filter = Control.MOUSE_FILTER_IGNORE add_child(_floor_label) EventBus.floor_changed.connect(_update_floor_label) EventBus.people_changed.connect(_update_quota_dots) _idle_image = TextureRect.new() _idle_image.texture = load("res://images/goatech_screen.png") _idle_image.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST _idle_image.anchor_right = 1.0 _idle_image.anchor_bottom = 1.0 _idle_image.mouse_filter = Control.MOUSE_FILTER_IGNORE _idle_image.visible = false add_child(_idle_image) _static_overlay = ColorRect.new() _static_overlay.color = Color(1.0, 1.0, 1.0, 0.6) _static_overlay.anchor_right = 1.0 _static_overlay.anchor_bottom = 1.0 _static_overlay.mouse_filter = Control.MOUSE_FILTER_IGNORE _static_overlay.visible = false add_child(_static_overlay) _score_label = Label.new() _score_label.add_theme_font_size_override("font_size", 12) _score_label.add_theme_color_override("font_color", Color(0.2, 1, 0.2, 1)) _score_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT _score_label.anchor_left = 0.0 _score_label.anchor_right = 1.0 _score_label.anchor_top = 0.0 _score_label.anchor_bottom = 0.0 _score_label.offset_top = QUOTA_DOT_Y + QUOTA_DOT_SIZE + 2 _score_label.offset_bottom = QUOTA_DOT_Y + QUOTA_DOT_SIZE + 18 _score_label.offset_right = -QUOTA_DOT_RIGHT_MARGIN _score_label.mouse_filter = Control.MOUSE_FILTER_IGNORE _score_label.visible = false _score_label.text = "SCORE: 0" add_child(_score_label) EventBus.score_changed.connect(_on_score_changed) enter_idle() func start(floor_num: int = EventBus.STARTING_FLOOR): active = true $AIFace.text = ">:)" $TargetZone.visible = true block_zone_moving = false _pulse.start_floor(floor_num) func set_block_zone_movement(speed: float): var bz = $TargetZone block_zone_speed = speed block_zone_moving = speed > 0.0 block_zone_min_x = (size.x - bz.size.x) / 2.0 block_zone_max_x = size.x - bz.size.x - BLOCK_ZONE_RIGHT_MARGIN func stop(): active = false _pulse.stop() _stop_ready_pulse() func launch_pulse(): if not active: return $AIFace.text = ">:)" _start_ready_pulse() _pulse.launch() func get_pulse_gap() -> float: return _pulse.get_pulse_gap() func attempt_block() -> bool: return _pulse.attempt_block() func shrink_block_zone(): var bz = $TargetZone var new_width = max(bz.size.x - block_zone_shrink, block_zone_min) var new_x = bz.position.x + (bz.size.x - new_width) / 2.0 var tween = create_tween() tween.set_parallel(true) tween.tween_property(bz, "size:x", new_width, 0.3) tween.tween_property(bz, "position:x", new_x, 0.3) tween.tween_method(func(_v): _update_perfect_zone(), 0.0, 1.0, 0.3) var _displayed_floor := -1 func _update_floor_label(floor_num: int): if _displayed_floor == floor_num: return var new_text = "FL " + str(floor_num) if _displayed_floor == -1: _floor_label.text = new_text _displayed_floor = floor_num return _displayed_floor = floor_num UIUtils.flip_label_text(_floor_label, new_text) func _build_quota_styles(): var r := int(QUOTA_DOT_SIZE / 2.0) _quota_filled_style = StyleBoxFlat.new() _quota_filled_style.corner_radius_top_left = r _quota_filled_style.corner_radius_top_right = r _quota_filled_style.corner_radius_bottom_left = r _quota_filled_style.corner_radius_bottom_right = r _quota_filled_style.bg_color = QUOTA_FILLED_COLOR _quota_empty_style = StyleBoxFlat.new() _quota_empty_style.corner_radius_top_left = r _quota_empty_style.corner_radius_top_right = r _quota_empty_style.corner_radius_bottom_left = r _quota_empty_style.corner_radius_bottom_right = r _quota_empty_style.bg_color = QUOTA_EMPTY_COLOR _quota_empty_style.border_width_top = 1 _quota_empty_style.border_width_bottom = 1 _quota_empty_style.border_width_left = 1 _quota_empty_style.border_width_right = 1 _quota_empty_style.border_color = QUOTA_EMPTY_BORDER func _update_quota_dots(count: int, threshold: int): if _quota_filled_style == null: _build_quota_styles() if threshold != _quota_threshold: _quota_threshold = threshold for dot in _quota_dots: dot.queue_free() _quota_dots.clear() var total_w = threshold * QUOTA_DOT_SIZE + max(0, threshold - 1) * QUOTA_DOT_SPACING var start_x = size.x - total_w - QUOTA_DOT_RIGHT_MARGIN for i in threshold: var dot = Panel.new() dot.size = Vector2(QUOTA_DOT_SIZE, QUOTA_DOT_SIZE) dot.position = Vector2(start_x + i * (QUOTA_DOT_SIZE + QUOTA_DOT_SPACING), QUOTA_DOT_Y) dot.mouse_filter = Control.MOUSE_FILTER_IGNORE add_child(dot) _quota_dots.append(dot) for i in _quota_dots.size(): var style = _quota_filled_style if i < count else _quota_empty_style _quota_dots[i].add_theme_stylebox_override("panel", style) func _on_score_changed(new_score: int): _score_value = new_score _score_label.text = "SCORE: " + str(new_score) 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) func _process(delta): if not active: return if block_zone_moving and $TargetZone.visible: 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 func _on_pulse_started(duration: float): pulse_started.emit(duration) func _on_pulse_blocked(): $AIFace.text = ">:(" _punch_face(1.4, 4.0) flash(Color.GREEN) pulse_blocked.emit() func _on_pulse_blocked_perfect(): pulse_blocked_perfect.emit() func _on_pulse_escaped(): $AIFace.text = ":D" _punch_face(1.5, 0.0) flash(Color.RED) active = false doors_closing.emit() func flash(color: Color): var style = get_theme_stylebox("panel") as StyleBoxFlat if not style: return if _flash_tween: _flash_tween.kill() style.bg_color = color _flash_tween = create_tween() _flash_tween.tween_property(style, "bg_color", base_color, 0.3) func show_countdown(): $AIFace.visible = true 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 = "") func show_win(): $AIFace.visible = true stop() $TargetZone.visible = false $AIFace.text = "ESCAPED" flash(Color.GREEN) func show_loss(message: String): $AIFace.visible = true 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 $TargetZone.visible = false func end_onboarding(): $AIFace.add_theme_font_size_override("font_size", 32) $AIFace.size = Vector2(size.x, 30) $TargetZone.visible = true func enter_idle() -> void: _idle_image.visible = true $AIFace.visible = false $TargetZone.visible = false $SweepLine.visible = false _static_overlay.visible = false _score_label.visible = true move_child(_score_label, get_child_count() - 1) func enter_static() -> void: move_child(_static_overlay, get_child_count() - 1) _static_overlay.visible = true block_zone_moving = false _score_label.visible = false func enter_hacked() -> void: _idle_image.visible = false $AIFace.visible = true $TargetZone.visible = true _static_overlay.visible = false _score_label.visible = false block_zone_moving = block_zone_speed > 0.0 launch_pulse() 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 _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)