class_name JuggleBoardGame extends Node2D ## Controller del gameplay JuggleBoard. Menu, HUD e audio sono moduli separati. const BOARD_SCRIPT := preload("res://games/juggleboard/scripts/components/board.gd") const BALL_SCRIPT := preload("res://games/juggleboard/scripts/components/ball.gd") const HUD_SCRIPT := preload("res://games/juggleboard/scripts/ui/game_hud.gd") const AUDIO_SCRIPT := preload("res://games/juggleboard/scripts/audio/game_audio.gd") const OPTIONS_SCENE := preload("res://games/juggleboard/scenes/options_menu.tscn") const LOCK_SCENE := preload("res://games/juggleboard/scenes/lock_menu.tscn") const END_SCENE := preload("res://games/juggleboard/scenes/victory_overlay.tscn") var balls: Dictionary = {} var target_color := "" var score := 0 var combo := 0.0 var lives := 4 var elapsed := 0.0 var window_timer := 0.0 var window_time := Settings.start_time var next_milestone := 100 var end_shown := false var hud: JuggleBoardHud var audio: JuggleBoardAudio var options_menu: CanvasLayer var lock_menu: CanvasLayer var end_overlay: CanvasLayer func _ready() -> void: add_child(BOARD_SCRIPT.new()) _build_balls() _build_services() _new_game() audio.start_music() func _process(delta: float) -> void: elapsed += delta if target_color == "" or Settings.game_mode == 1: return if combo > 0.0: combo = maxf(0.0, combo - _combo_decay() * delta) window_timer += delta if window_timer >= window_time: _on_timeout() func _unhandled_input(event: InputEvent) -> void: if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE: _toggle_options() return var position := Vector2.INF if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT: position = event.position elif event is InputEventScreenTouch and event.pressed: position = event.position if position != Vector2.INF: _handle_click(position) func _build_balls() -> void: var names := JuggleBoardConfig.COLORS.keys() for index in range(names.size()): var color_name: String = names[index] var home := Vector2(JuggleBoardConfig.HOME_X, JuggleBoardConfig.LANE_TOP + index * JuggleBoardConfig.LANE_GAP) var left := Vector2(JuggleBoardConfig.LEFT_X, JuggleBoardConfig.LANE_TOP + index * JuggleBoardConfig.LANE_GAP) var ball: Area2D = BALL_SCRIPT.new() add_child(ball) ball.setup(color_name, JuggleBoardConfig.COLORS[color_name], home, left, JuggleBoardConfig.BALL_RADIUS) ball.returned.connect(_on_ball_returned) balls[color_name] = ball func _build_services() -> void: hud = HUD_SCRIPT.new() hud.options_requested.connect(_request_options) add_child(hud) audio = AUDIO_SCRIPT.new() add_child(audio) options_menu = OPTIONS_SCENE.instantiate() add_child(options_menu) lock_menu = LOCK_SCENE.instantiate() lock_menu.unlocked.connect(_open_options) add_child(lock_menu) end_overlay = END_SCENE.instantiate() end_overlay.reset_requested.connect(_reset_after_end) add_child(end_overlay) func _request_options() -> void: lock_menu.show_menu() func _open_options() -> void: options_menu.show_menu() func _toggle_options() -> void: if options_menu.visible: options_menu.hide_menu() else: _request_options() func _new_game() -> void: score = 0 combo = 0.0 elapsed = 0.0 window_timer = 0.0 window_time = Settings.start_time next_milestone = 100 lives = 4 for ball in balls.values(): ball.reset() _pick_target() hud.reset(score, combo, lives, Settings.game_mode == 2) func _handle_click(position: Vector2) -> void: var clicked: Area2D var best := JuggleBoardConfig.HIT_RADIUS for ball in balls.values(): if not ball.is_home(): continue var distance := position.distance_to(ball.position) if distance < best: best = distance clicked = ball if clicked == null: return if clicked.color_name == target_color: _on_correct_ball(clicked) else: _on_wrong_ball() func _on_correct_ball(ball: Area2D) -> void: combo = minf(float(Settings.max_combo), combo + 1.0) score += _points_for_combo() audio.play_ball(ball.color_name) var current_time := _current_time() ball.launch(current_time) window_time = current_time window_timer = 0.0 _pick_target() hud.set_score(score, combo) _check_milestone() _check_victory() func _on_wrong_ball() -> void: audio.play(audio.error) _apply_combo_penalty() hud.set_score(score, combo) if Settings.game_mode == 2: _lose_life() func _on_timeout() -> void: audio.play(audio.timeout) _apply_combo_penalty() window_timer = 0.0 _pick_target() hud.set_score(score, combo) if Settings.game_mode == 2: _lose_life() func _apply_combo_penalty() -> void: if Settings.gentle_errors: combo = maxf(0.0, combo - 1.0) else: combo = 0.0 func _on_ball_returned() -> void: if target_color == "": _pick_target() func _current_time() -> float: return lerpf(Settings.start_time, Settings.min_time, _difficulty()) func _difficulty() -> float: var factor := clampf(score / Settings.score_ramp, 0.0, 1.0) return factor * factor * (3.0 - 2.0 * factor) func _combo_decay() -> float: var combo_penalty := maxf(0.0, combo - Settings.combo_decay_threshold) * Settings.combo_decay_rate return combo_penalty + _difficulty() * Settings.difficulty_decay_rate func _points_for_combo() -> int: return int(combo) * Settings.points_per_combo func _pick_target() -> void: var homes := _home_names() if homes.is_empty(): target_color = "" hud.set_target(Color.WHITE, false) return var pool := homes.duplicate() if target_color in pool and pool.size() > 1: pool.erase(target_color) target_color = pool[randi() % pool.size()] hud.set_target(JuggleBoardConfig.COLORS[target_color]) func _home_names() -> Array: var result := [] for color_name in balls: if balls[color_name].is_home(): result.append(color_name) return result func _check_victory() -> void: if Settings.game_mode != 2 and not end_shown and score >= Settings.target_score: _show_end(true) func _lose_life() -> void: if end_shown: return lives -= 1 audio.play(audio.lose_heart) hud.hearts.set_lives(maxi(lives, 0)) if lives <= 0: _show_end(false) func _show_end(victory: bool) -> void: if end_shown: return end_shown = true audio.stop_music() audio.play(audio.victory if victory else audio.defeat) get_tree().paused = true if victory: end_overlay.show_victory(score) else: end_overlay.show_game_over(score) func _reset_after_end() -> void: get_tree().paused = false end_shown = false end_overlay.hide_victory() audio.start_music() _new_game() func _check_milestone() -> void: if score < next_milestone: return var celebrated := next_milestone while next_milestone <= score: celebrated = next_milestone next_milestone *= 10 audio.play(audio.milestone) hud.celebrate(celebrated)