Files
autgame/scripts/main.gd
T

457 lines
13 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
extends Node2D
## JuggleBoard Game — reaction & chaining arcade.
##
## Layout: 5 corsie orizzontali, 5 palline colorate a riposo a destra.
## Un bersaglio in alto indica quale pallina premere. Toccata, la pallina
## scivola a sinistra e poi torna a destra. Prima che scada il timeout devi
## premere il nuovo bersaglio per concatenare. Più concateni, più punti fai
## e più la difficoltà (velocità) cresce con il tempo trascorso e il punteggio.
##
## Un nuovo bersaglio appare SOLO se c'è almeno una pallina a riposo.
const VIEW := Vector2(1280, 720)
const LANE_TOP := 210.0
const LANE_GAP := 98.0
const BALL_RADIUS := 40.0
const HOME_X := 1130.0
const LEFT_X := 300.0
const HIT_RADIUS := BALL_RADIUS * 1.4 # area di tocco/clic (maggiorata)
# I valori di calibrazione (velocità, moltiplicatori, decay) sono gestiti dal
# singleton Settings e modificabili dal menù di configurazione in-game.
const COLORS := {
"rosso": Color("e74c3c"),
"blu": Color("3498db"),
"verde": Color("2ecc71"),
"giallo": Color("f1c40f"),
"viola": Color("9b59b6"),
}
var balls := {} # color_name -> Ball
var target_color := "" # bersaglio corrente (vuoto = nessun bersaglio)
var score := 0
var combo := 0.0 # float: decade col ritardo di reazione
var elapsed := 0.0
var window_timer := 0.0
var window_time := Settings.start_time
var next_milestone := 100
# UI
var target_dot: Node2D
var score_label: Label
var combo_label: Label
var help_label: Label
var milestone_label: Label
var flash: ColorRect
# Audio
var sound_launch: AudioStreamWAV
var sound_error: AudioStreamWAV
var sound_timeout: AudioStreamWAV
var sound_milestone: AudioStreamWAV
var bgm: AudioStreamPlayer
var options_menu: CanvasLayer
func _ready() -> void:
_build_lanes()
_build_balls()
_build_ui()
_init_audio()
_setup_bgm()
options_menu = load("res://scripts/options_menu.gd").new()
add_child(options_menu)
_new_game()
func _process(delta: float) -> void:
elapsed += delta
if target_color == "":
return # in attesa che una pallina torni a casa: nessun countdown
# il combo decade in proporzione al tempo che aspetto prima di cliccare
if combo > 0.0:
combo = maxf(0.0, combo - _combo_decay() * delta)
window_timer += delta
if window_timer >= window_time:
_on_timeout()
# Input globale: mouse click e touch tap, convertiti in coordinate di gioco.
func _unhandled_input(event: InputEvent) -> void:
if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE:
_toggle_options()
return
var pos := Vector2.INF
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
pos = event.position
elif event is InputEventScreenTouch and event.pressed:
pos = event.position
if pos == Vector2.INF:
return
_handle_click(pos)
func _open_options() -> void:
if options_menu:
options_menu.show_menu()
func _toggle_options() -> void:
if options_menu:
if options_menu.visible:
options_menu.hide_menu()
else:
options_menu.show_menu()
# ---------------------------------------------------------------- costruzione
func _build_lanes() -> void:
for i in 5:
var y := LANE_TOP + i * LANE_GAP
var line := Line2D.new()
line.add_point(Vector2(90, y))
line.add_point(Vector2(VIEW.x - 90, y))
line.width = 2
line.default_color = Color(1, 1, 1, 0.12)
add_child(line)
var lbl := Label.new()
lbl.text = COLORS.keys()[i].to_upper()
lbl.position = Vector2(100, y - 44)
lbl.add_theme_font_size_override("font_size", 16)
lbl.add_theme_color_override("font_color", Color(1, 1, 1, 0.35))
add_child(lbl)
func _build_balls() -> void:
var names := COLORS.keys()
for i in names.size():
var cn: String = names[i]
var home := Vector2(HOME_X, LANE_TOP + i * LANE_GAP)
var left := Vector2(LEFT_X, LANE_TOP + i * LANE_GAP)
var b: Area2D = load("res://scripts/ball.gd").new()
add_child(b)
b.setup(cn, COLORS[cn], home, left, BALL_RADIUS)
b.returned.connect(_on_ball_returned)
balls[cn] = b
func _build_ui() -> void:
var layer := CanvasLayer.new()
add_child(layer)
var bar := ColorRect.new()
bar.color = Color(0.08, 0.09, 0.12, 0.95)
bar.position = Vector2.ZERO
bar.size = Vector2(VIEW.x, 140)
layer.add_child(bar)
var edge := ColorRect.new()
edge.color = Color(1, 1, 1, 0.15)
edge.position = Vector2(0, 140)
edge.size = Vector2(VIEW.x, 3)
layer.add_child(edge)
var tlabel := Label.new()
tlabel.text = "BERSAGLIO"
tlabel.position = Vector2(46, 26)
tlabel.add_theme_font_size_override("font_size", 22)
tlabel.add_theme_color_override("font_color", Color(1, 1, 1, 0.8))
layer.add_child(tlabel)
target_dot = load("res://scripts/target_dot.gd").new()
target_dot.position = Vector2(128, 84)
layer.add_child(target_dot)
help_label = Label.new()
help_label.text = "Tocca la pallina corrispondente al bersaglio per lanciarla!"
help_label.position = Vector2(210, 96)
help_label.add_theme_font_size_override("font_size", 18)
help_label.add_theme_color_override("font_color", Color(1, 1, 1, 0.6))
layer.add_child(help_label)
score_label = Label.new()
score_label.position = Vector2(VIEW.x - 320, 22)
score_label.add_theme_font_size_override("font_size", 34)
layer.add_child(score_label)
combo_label = Label.new()
combo_label.position = Vector2(VIEW.x - 320, 68)
combo_label.add_theme_font_size_override("font_size", 20)
combo_label.add_theme_color_override("font_color", Color(1, 1, 1, 0.65))
layer.add_child(combo_label)
# pulsante Opzioni
var opts_btn := Button.new()
opts_btn.text = "⚙ Opzioni (Esc)"
opts_btn.position = Vector2(VIEW.x / 2 - 100, 26)
opts_btn.pressed.connect(_open_options)
layer.add_child(opts_btn)
# feedback celebrativo alle milestone
milestone_label = Label.new()
milestone_label.position = Vector2(0, 160)
milestone_label.size = Vector2(VIEW.x, 120)
milestone_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
milestone_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
milestone_label.add_theme_font_size_override("font_size", 56)
milestone_label.add_theme_color_override("font_color", Color(1.0, 0.9, 0.4))
milestone_label.visible = false
layer.add_child(milestone_label)
flash = ColorRect.new()
flash.color = Color(1, 1, 0.7)
flash.position = Vector2.ZERO
flash.size = VIEW
flash.mouse_filter = Control.MOUSE_FILTER_IGNORE
flash.modulate.a = 0.0
flash.visible = false
layer.add_child(flash)
# ------------------------------------------------------------------- audio
func _init_audio() -> void:
sound_launch = _create_tone(587.33, 0.12, "sine") # Re5
sound_error = _create_tone(164.81, 0.18, "square") # Mi3 basso
sound_timeout = _create_tone(220.0, 0.22, "saw") # La3
sound_milestone = _create_arpeggio([523.25, 659.25, 783.99, 1046.5], 0.12) # Do5 Mi5 Sol5 Do6
func _setup_bgm() -> void:
# Musica di sottofondo da circo: traccia lunga generata via API Lyria,
# compressa in OGG Vorbis (loop abilitato nelle impostazioni di import),
# con fallback alla versione procedurale se il file non è presente.
bgm = AudioStreamPlayer.new()
var stream: AudioStream = load("res://audio/circus.ogg")
if stream:
bgm.stream = stream
else:
bgm.stream = load("res://scripts/circus_music.gd").generate()
bgm.volume_db = -14.0
bgm.process_mode = Node.PROCESS_MODE_ALWAYS # continua anche a menù aperto
add_child(bgm)
bgm.play()
func _create_arpeggio(freqs: Array, note_dur: float = 0.12) -> AudioStreamWAV:
var sample_rate := 22050
var total := 0
for f in freqs:
total += int(sample_rate * note_dur)
var data := PackedByteArray()
data.resize(total)
var idx := 0
for f in freqs:
var n := int(sample_rate * note_dur)
for i in n:
var t := float(i) / sample_rate
var env := 1.0 - (float(i) / n)
var s := sin(t * f * TAU) * env
var val := int(clampf(s * 0.35 * 127.0, -128.0, 127.0))
data[idx] = val & 0xFF
idx += 1
var stream := AudioStreamWAV.new()
stream.format = AudioStreamWAV.FORMAT_8_BITS
stream.mix_rate = sample_rate
stream.data = data
return stream
func _create_tone(freq: float, duration: float, wave_type: String = "sine") -> AudioStreamWAV:
var sample_rate := 22050
var num_samples := int(sample_rate * duration)
var data := PackedByteArray()
data.resize(num_samples)
for i in num_samples:
var t := float(i) / sample_rate
var env := 1.0 - (float(i) / num_samples)
var s := 0.0
if wave_type == "sine":
s = sin(t * freq * TAU)
elif wave_type == "square":
s = 1.0 if sin(t * freq * TAU) > 0.0 else -1.0
elif wave_type == "saw":
s = fmod(t * freq, 1.0) * 2.0 - 1.0
var val := int(clampf(s * env * 0.35 * 127.0, -128.0, 127.0))
data[i] = val & 0xFF
var stream := AudioStreamWAV.new()
stream.format = AudioStreamWAV.FORMAT_8_BITS
stream.mix_rate = sample_rate
stream.data = data
return stream
func _play_sound(stream: AudioStreamWAV) -> void:
if stream == null:
return
var player := AudioStreamPlayer.new()
player.stream = stream
add_child(player)
player.finished.connect(player.queue_free)
player.play()
# ------------------------------------------------------------------- gioco
func _new_game() -> void:
score = 0
combo = 0.0
elapsed = 0.0
window_timer = 0.0
window_time = Settings.start_time
next_milestone = 100
for b in balls.values():
b.reset()
_pick_target()
help_label.visible = true
_update_hud()
func _handle_click(pos: Vector2) -> void:
var clicked: Area2D = null
var best := HIT_RADIUS
for b in balls.values():
if not b.is_home():
continue
var d := pos.distance_to(b.position)
if d < best:
best = d
clicked = b
if clicked == null:
return # click su spazio vuoto
if clicked.color_name == target_color:
# corretto: suono lancio, punti, nuovo bersaglio, reset timer
combo += 1.0
score += _points_for_combo()
_play_sound(sound_launch)
var mt := _current_time()
clicked.launch(mt)
window_time = mt
window_timer = 0.0
_pick_target()
_update_hud()
_check_milestone()
else:
# tasto sbagliato: suono errore, reset combo, nessun game over
_play_sound(sound_error)
combo = 0.0
_update_hud()
func _on_timeout() -> void:
# tempo scaduto: suono timeout, reset combo, nuovo bersaglio (se possibile), nessun game over
_play_sound(sound_timeout)
combo = 0.0
window_timer = 0.0
_pick_target()
_update_hud()
func _on_ball_returned() -> void:
# quando una pallina torna a casa e non c'è bersaglio, se ne può mostrare uno
if target_color == "":
_pick_target()
func _current_time() -> float:
var d := _difficulty()
return lerpf(Settings.start_time, Settings.min_time, d)
func _difficulty() -> float:
# fattori lineari di tempo e punteggio
var tf := clampf(elapsed / Settings.time_ramp, 0.0, 1.0)
var sf := clampf(score / Settings.score_ramp, 0.0, 1.0)
# curva dolce: smoothstep rende l'accelerazione quasi impercettibile all'inizio
return _smoothstep(maxf(tf, sf))
func _combo_decay() -> float:
# Decadimento proporzionale: quasi nullo a combo basso / inizio partita,
# cresce man mano che combo e difficoltà aumentano.
var combo_pen := maxf(0.0, combo - Settings.combo_decay_threshold) * Settings.combo_decay_rate
var diff_pen := _difficulty() * Settings.difficulty_decay_rate
return combo_pen + diff_pen
func _points_for_combo() -> int:
# Punti per un tap corretto, in base alla curva selezionata nelle Opzioni.
match Settings.score_curve:
1: # scaglioni: +0.5x ogni 5 tap di combo
var mult := 1.0 + floori(combo / 5.0) * 0.5
return int(Settings.points_per_combo * mult)
2: # radice quadrata
return int(Settings.points_per_combo * sqrt(combo))
3: # additiva: base + combo
return Settings.points_per_combo + int(combo)
_: # 0 = quadratica (attuale): base × combo
return Settings.points_per_combo * int(combo)
func _smoothstep(x: float) -> float:
return x * x * (3.0 - 2.0 * x)
func _pick_target() -> void:
var homes := _home_names()
if homes.is_empty():
target_color = ""
target_dot.visible = 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()]
target_dot.set_color(COLORS[target_color])
target_dot.visible = true
func _home_names() -> Array:
var arr := []
for cn in balls.keys():
if balls[cn].is_home():
arr.append(cn)
return arr
func _update_hud() -> void:
score_label.text = "PUNTI %d" % score
combo_label.text = "Combo x%d" % int(combo)
# ---------------------------------------------- feedback alle milestone
func _check_milestone() -> void:
if score < next_milestone:
return
# supera più traguardi in una volta: celebra il più alto raggiunto
var celebrated := next_milestone
while next_milestone <= score:
celebrated = next_milestone
next_milestone *= 10
_celebrate(celebrated)
func _celebrate(amount: int) -> void:
_play_sound(sound_milestone)
milestone_label.text = "%d PUNTI!" % amount
milestone_label.visible = true
milestone_label.modulate.a = 1.0
milestone_label.scale = Vector2(0.6, 0.6)
flash.visible = true
flash.modulate.a = 0.18
var tw := create_tween()
tw.set_parallel(true)
tw.tween_property(milestone_label, "scale", Vector2(1.3, 1.3), 0.4) \
.set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
tw.tween_property(milestone_label, "modulate:a", 0.0, 0.8).set_delay(0.6)
tw.tween_property(flash, "modulate:a", 0.0, 0.4)
tw.chain().tween_callback(func():
milestone_label.visible = false
flash.visible = false
)