275 lines
6.9 KiB
GDScript
275 lines
6.9 KiB
GDScript
class_name JuggleBoardGame
|
|
extends Node2D
|
|
## Controller del gameplay JuggleBoard. Menu, HUD e audio sono moduli separati.
|
|
|
|
signal return_requested
|
|
|
|
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()
|
|
var width_scale := JuggleBoardConfig.horizontal_scale(get_viewport().get_visible_rect().size.x)
|
|
for index in range(names.size()):
|
|
var color_name: String = names[index]
|
|
var lane_y := JuggleBoardConfig.LANE_TOP + index * JuggleBoardConfig.LANE_GAP
|
|
# Il fondale si espande in orizzontale: alloggiamenti e biglie seguono
|
|
# la stessa scala X, mentre dimensione e coordinate Y restano invariate.
|
|
var home := Vector2(JuggleBoardConfig.HOME_X * width_scale, lane_y)
|
|
var left := Vector2(JuggleBoardConfig.LEFT_X * width_scale, lane_y)
|
|
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)
|
|
hud.back_requested.connect(func(): return_requested.emit())
|
|
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)
|