Refactor Godot architecture into AppShell, modular menus and game domains

This commit is contained in:
enne2
2026-09-06 17:07:05 +02:00
parent a73fb4b55f
commit f4a9da6bc3
209 changed files with 2726 additions and 2072 deletions
@@ -0,0 +1,138 @@
extends RefCounted
## Genera proceduralmente un loop musicale da circo (valzer calliope) in WAV.
## Nessun file audio esterno: tutto sintetizzato a runtime, con loop seamless.
const SR := 22050
static func generate() -> AudioStreamWAV:
var bpm := 132.0
var beat := 60.0 / bpm
var bars := 8
var total_beats := bars * 3
var total_seconds := total_beats * beat
var n := int(SR * total_seconds)
var mix := PackedFloat32Array()
mix.resize(n)
# Melodia (calliope) — 8 battute in 3/4
var melody := [
[72, 1.0], [76, 1.0], [79, 1.0],
[72, 1.0], [76, 1.0], [79, 1.0],
[69, 1.0], [72, 1.0], [76, 1.0],
[69, 1.0], [72, 1.0], [76, 1.0],
[65, 1.0], [69, 1.0], [72, 1.0],
[65, 1.0], [69, 1.0], [72, 1.0],
[67, 1.0], [71, 1.0], [74, 1.0],
[72, 2.0], [0, 1.0],
]
# Accompagnamento oom-pah-pah: [basso, [accordo]]
var chords := [
[48, [60, 64, 67]],
[48, [60, 64, 67]],
[45, [57, 60, 64]],
[45, [57, 60, 64]],
[41, [53, 57, 60]],
[41, [53, 57, 60]],
[43, [55, 59, 62]],
[48, [60, 64, 67]],
]
# Melodia
var t := 0.0
for note in melody:
var midi: int = note[0]
var beats: float = note[1]
var dur := beats * beat
if midi > 0:
_add_note(mix, t, dur, _freq(midi), 0.5, "calliope")
t += dur
# Accompagnamento
t = 0.0
for bar in bars:
var bass: int = chords[bar][0]
var chord: Array = chords[bar][1]
_add_note(mix, t, beat * 0.9, _freq(bass), 0.42, "bass")
for b in [1, 2]:
var ct: float = t + b * beat
for m in chord:
_add_note(mix, ct, beat * 0.8, _freq(int(m)), 0.16, "chord")
t += 3.0 * beat
# Fade in/out (30ms) per loop seamless senza click
var fade := int(SR * 0.03)
for i in fade:
var g := float(i) / fade
mix[i] *= g
mix[n - 1 - i] *= g
# Normalizzazione
var peak := 0.0
for i in n:
var a := absf(mix[i])
if a > peak:
peak = a
var norm := 1.0
if peak > 0.0:
norm = 0.9 / peak
var data := PackedByteArray()
data.resize(n)
for i in n:
var v := int(clampf(mix[i] * norm * 127.0, -128.0, 127.0))
data[i] = v & 0xFF
var stream := AudioStreamWAV.new()
stream.format = AudioStreamWAV.FORMAT_8_BITS
stream.mix_rate = SR
stream.data = data
stream.loop_mode = AudioStreamWAV.LOOP_FORWARD
stream.loop_begin = 0
stream.loop_end = n
return stream
static func _freq(midi: int) -> float:
return 440.0 * pow(2.0, (midi - 69) / 12.0)
static func _square(t: float, f: float) -> float:
return 1.0 if sin(t * f * TAU) > 0.0 else -1.0
static func _saw(t: float, f: float) -> float:
return fmod(t * f, 1.0) * 2.0 - 1.0
static func _envelope(t: float, dur: float) -> float:
var a := 0.02
var r := 0.06
var e := 1.0
if t < a:
e = t / a
var rem := dur - t
if rem < r:
e = minf(e, rem / r)
return e
static func _add_note(mix: PackedFloat32Array, start: float, dur: float, freq: float, amp: float, kind: String) -> void:
var start_i := int(start * SR)
var cnt := int(dur * SR)
for i in cnt:
var idx := start_i + i
if idx < 0 or idx >= mix.size():
break
var t := float(i) / SR
var env := _envelope(t, dur)
var s := 0.0
if kind == "calliope":
var vib := 1.0 + 0.006 * sin(t * 2.0 * PI * 5.0)
var f := freq * vib
s = 0.5 * _square(t, f) + 0.3 * _saw(t, f * 1.004) + 0.2 * _square(t, f * 0.996)
elif kind == "bass":
s = 0.6 * _saw(t, freq) + 0.4 * _square(t, freq)
elif kind == "chord":
s = 0.5 * _square(t, freq) + 0.5 * _saw(t, freq)
mix[idx] += s * env * amp
@@ -0,0 +1 @@
uid://xlg6u5nv5rue
@@ -0,0 +1,58 @@
class_name JuggleBoardAudio
extends Node
## Audio e BGM del JuggleBoard. Nessuna logica di gameplay.
const BGM_STREAM := preload("res://games/juggleboard/assets/audio/circus.ogg")
const MUSIC_FALLBACK := preload("res://games/juggleboard/scripts/audio/circus_music.gd")
var launch: AudioStreamWAV
var error: AudioStreamWAV
var timeout: AudioStreamWAV
var milestone: AudioStreamWAV
var victory: AudioStreamWAV
var defeat: AudioStreamWAV
var lose_heart: AudioStreamWAV
var ball_tones: Dictionary = {}
var bgm: AudioStreamPlayer
func _ready() -> void:
_build_effects()
_build_bgm()
func _build_effects() -> void:
launch = AudioFactory.create_tone(660.0, 0.18, 0.3)
error = AudioFactory.create_tone(220.0, 0.15, 0.16)
timeout = AudioFactory.create_tone(330.0, 0.2, 0.16)
milestone = AudioFactory.create_arpeggio([523.25, 659.25, 783.99, 1046.5], 0.12)
victory = AudioFactory.create_arpeggio([523.25, 659.25, 783.99, 1046.5, 1318.51, 1567.98], 0.16)
defeat = AudioFactory.create_arpeggio([392.0, 329.63, 261.63], 0.18)
lose_heart = AudioFactory.create_tone(330.0, 0.15, 0.2)
for color_name in JuggleBoardConfig.BALL_NOTES:
ball_tones[color_name] = AudioFactory.create_tone(JuggleBoardConfig.BALL_NOTES[color_name], 0.2, 0.3)
func _build_bgm() -> void:
bgm = AudioStreamPlayer.new()
bgm.stream = BGM_STREAM if BGM_STREAM else MUSIC_FALLBACK.generate()
bgm.volume_db = -14.0
bgm.process_mode = Node.PROCESS_MODE_ALWAYS
add_child(bgm)
func play(stream: AudioStream) -> void:
if stream:
AudioFactory.play_once(self, stream)
func play_ball(color_name: String) -> void:
play(ball_tones.get(color_name, launch))
func start_music() -> void:
bgm.play()
func stop_music() -> void:
bgm.stop()
@@ -0,0 +1 @@
uid://dsywfqv2xa0vx
@@ -0,0 +1,159 @@
extends Area2D
## Una singola pallina nella sua corsia. Partendo da destra, se lanciata
## scivola verso il bordo sinistro e poi torna alla posizione di partenza.
## Il rilevamento del tocco/click è gestito centralmente da main.gd tramite
## distanza: qui NON ci sono handler di input dedicati.
signal returned # emesso quando la pallina torna a casa
var color_name := ""
var ball_color := Color.WHITE
var radius := 40.0
var home_pos := Vector2.ZERO
var left_pos := Vector2.ZERO
var in_motion := false
var roll_angle := 0.0:
set(v):
roll_angle = v
queue_redraw() # aggiorna il disegno a ogni step di rotazione
var veins: Array[PackedVector2Array] = []
var _rng := RandomNumberGenerator.new()
var _tween: Tween
func _draw() -> void:
# ombra a terra
draw_circle(Vector2(0, 6), radius, Color(0, 0, 0, 0.25))
# corpo base
draw_circle(Vector2.ZERO, radius, ball_color)
# ombreggiatura inferiore-destra (profondità 3D)
draw_circle(Vector2(radius * 0.18, radius * 0.22), radius * 0.92, Color(0, 0, 0, 0.16))
# luce superiore-sinistra
draw_circle(Vector2(-radius * 0.18, -radius * 0.22), radius * 0.92, Color(1, 1, 1, 0.2))
# marmorizzazione a vortici che ruota col rotolamento (design realistico)
_draw_marbling()
# bordo nero spesso (stile china)
draw_arc(Vector2.ZERO, radius, 0.0, TAU, 48, Color(0.1, 0.1, 0.1, 0.9), 4.0)
# riflesso speculare (fisso: la luce non ruota con la superficie)
draw_circle(Vector2(-radius * 0.3, -radius * 0.34), radius * 0.16, Color(1, 1, 1, 0.85))
func _draw_marbling() -> void:
# Venature curve, irregolari e casuali per ogni biglia. Tutta la geometria
# superficiale ruota; illuminazione e riflesso vengono disegnati dopo.
var vein_color: Color = ball_color.darkened(0.42)
vein_color.a = 0.20
var soft_color: Color = ball_color.darkened(0.28)
soft_color.a = 0.12
for i: int in range(veins.size()):
var rotated_points := PackedVector2Array()
var vis_sum := 0.0
for point: Vector2 in veins[i]:
# Rotazione attorno all'asse Y (perpendicolare al moto orizzontale):
# la vena scorre in orizzontale (sin(θ+roll)) e si schiaccia ai bordi
# (cos = foreshortening). I punti sul retro (cos<=0) non sono visibili.
var theta: float = asin(clampf(point.x / radius, -1.0, 1.0))
var new_theta: float = theta + roll_angle
var c := cos(new_theta)
if c <= 0.0:
continue # retro della sfera: nascosto
var vis := clampf(c, 0.0, 1.0)
vis_sum += vis
rotated_points.append(Vector2(
sin(new_theta) * radius,
point.y * (0.3 + 0.7 * vis)))
if rotated_points.size() < 2:
continue
var vis_avg := vis_sum / veins[i].size()
var width: float = radius * (0.075 if i == 0 else 0.045) * (0.3 + 0.7 * vis_avg)
draw_polyline(rotated_points, soft_color, width * 1.9, true)
draw_polyline(rotated_points, vein_color, width, true)
func _generate_veins() -> Array[PackedVector2Array]:
# 7 venature distribuite uniformemente su TUTTA la longitudine (360°):
# così la superficie non resta mai liscia durante la rotazione.
var result: Array[PackedVector2Array] = []
var count := 7
for i in count:
var center: float = -PI + TAU * (float(i) + _rng.randf_range(0.0, 0.9)) / count
var spread := _rng.randf_range(0.25, 0.7)
var yc := _rng.randf_range(-0.5, 0.5)
var y0 := clampf(yc - _rng.randf_range(0.0, 0.3), -0.6, 0.6)
var y1 := clampf(yc + _rng.randf_range(0.0, 0.3), -0.6, 0.6)
var bend := _rng.randf_range(0.05, 0.22)
var ripple := _rng.randf_range(0.03, 0.11)
result.append(_make_vein(sin(center - spread), y0, sin(center + spread), y1, bend, ripple))
return result
func _make_vein(x0: float, y0: float, x1: float, y1: float,
bend: float, ripple: float) -> PackedVector2Array:
var points := PackedVector2Array()
var direction := Vector2(x1 - x0, y1 - y0)
var normal := direction.normalized().orthogonal()
for step: int in range(17):
var t: float = float(step) / 16.0
var base := Vector2(lerpf(x0, x1, t), lerpf(y0, y1, t))
var curve: float = sin(t * PI) * bend
var wave: float = sin(t * TAU * 1.35 + x0 * 4.0) * ripple
points.append((base + normal * (curve + wave)) * radius)
return points
func setup(cn: String, c: Color, home: Vector2, left: Vector2, r: float) -> void:
color_name = cn
ball_color = c
radius = r
_rng.seed = hash(cn) # venature uniche ma stabili per ogni colore
veins = _generate_veins()
home_pos = home
left_pos = left
position = home
var shape := CollisionShape2D.new()
var cs := CircleShape2D.new()
cs.radius = r * 1.35 # area di tocco ampliata (ma l'input è via distanza in main)
shape.shape = cs
add_child(shape)
queue_redraw()
func is_home() -> bool:
return not in_motion
func reset() -> void:
if _tween and _tween.is_valid():
_tween.kill()
in_motion = false
position = home_pos
roll_angle = 0.0
## Lancia la pallina verso sinistra e la fa tornare a casa. `mt` è il tempo
## totale (andata + ritorno) in secondi.
func launch(mt: float) -> void:
in_motion = true
if _tween and _tween.is_valid():
_tween.kill()
var half: float = maxf(mt * 0.5, 0.01)
# rotazione proporzionale alla distanza percorsa (rotolamento)
var dist: float = home_pos.x - left_pos.x
var total_angle: float = dist / radius
_tween = create_tween()
_tween.tween_property(self, "position", left_pos, half) \
.set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN_OUT)
_tween.parallel().tween_property(self, "roll_angle", total_angle, half) \
.set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN_OUT)
_tween.tween_property(self, "position", home_pos, half) \
.set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN_OUT)
_tween.parallel().tween_property(self, "roll_angle", 0.0, half) \
.set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN_OUT)
_tween.finished.connect(_on_motion_done)
func _on_motion_done() -> void:
in_motion = false
returned.emit()
@@ -0,0 +1 @@
uid://d3kyc2ecf0aco
@@ -0,0 +1,88 @@
extends Node2D
## Plancia Juggle Board in legno: binari a TUTTA larghezza con larghezza
## variabile (svasati alle estremità, stretti al centro), alloggiamenti
## circolari incassati e indicatori numerici 1-5, come nel mockup.
const VIEW := Vector2(1280, 720)
const LANE_TOP := 210.0
const LANE_GAP := 98.0
const LANE_LEFT := 100.0 # centro alloggiamento sinistro (dentro i bordi)
const LANE_RIGHT := 1180.0 # centro alloggiamento destro (dentro i bordi)
const SOCKET_R := 46.0 # raggio alloggiamento (ampio alle estremità)
const H_NARROW := 24.0 # semi-altezza canale al centro (stretto)
const CENTER_X := (LANE_LEFT + LANE_RIGHT) / 2.0
const HALF_SPAN := (LANE_RIGHT - LANE_LEFT) / 2.0
const LANE_FILL := Color("#c9ac7c")
const LANE_EDGE := Color(0.22, 0.15, 0.08, 0.9)
var wood_tex: Texture2D
var circus_tex: Texture2D
func _ready() -> void:
z_index = -10
wood_tex = load("res://shared/assets/textures/wood_board_rounded.png") # angoli arrotondati coerenti con la cornice
circus_tex = load("res://games/juggleboard/assets/textures/circus_arena_bg.png")
queue_redraw()
func _draw() -> void:
# Arena circense quieta ai margini; la plancia copre il centro più ricco.
if circus_tex:
draw_texture_rect(circus_tex, Rect2(Vector2.ZERO, VIEW), false)
# Ombra e corpo smussato del tabellone.
var shadow := StyleBoxFlat.new()
shadow.bg_color = Color(0.12, 0.055, 0.02, 0.5)
shadow.corner_radius_top_left = 28
shadow.corner_radius_top_right = 28
shadow.corner_radius_bottom_left = 28
shadow.corner_radius_bottom_right = 28
draw_style_box(shadow, Rect2(32, 151, 1216, 560))
if wood_tex:
draw_texture_rect(wood_tex, Rect2(36, 142, 1208, 558), false)
var frame := StyleBoxFlat.new()
frame.bg_color = Color(0, 0, 0, 0)
frame.border_color = Color("#81542f")
frame.set_border_width_all(8)
frame.corner_radius_top_left = 27
frame.corner_radius_top_right = 27
frame.corner_radius_bottom_left = 27
frame.corner_radius_bottom_right = 27
draw_style_box(frame, Rect2(32, 138, 1216, 566))
for i in range(5):
_draw_lane(LANE_TOP + i * LANE_GAP)
func _lane_half_width(x: float) -> float:
# larghezza corsia: ampia alle estremità (d=1), stretta al centro (d=0)
var d := absf(x - CENTER_X) / HALF_SPAN
d = clampf(d, 0.0, 1.0)
return H_NARROW + (SOCKET_R - H_NARROW) * d * d
func _draw_lane(y: float) -> void:
var pts := PackedVector2Array()
var steps := 48
for k in range(steps + 1):
var x := lerpf(LANE_LEFT, LANE_RIGHT, float(k) / steps)
var hw := _lane_half_width(x)
pts.append(Vector2(x, y - hw))
for k in range(steps, -1, -1):
var x := lerpf(LANE_LEFT, LANE_RIGHT, float(k) / steps)
var hw := _lane_half_width(x)
pts.append(Vector2(x, y + hw))
draw_polygon(pts, [LANE_FILL])
var closed := pts.duplicate()
closed.append(pts[0])
draw_polyline(closed, LANE_EDGE, 3.0, true)
# Alloggiamenti a calotta: anelli concentrici e luce/ombra danno profondità.
for cx in [LANE_LEFT, LANE_RIGHT]:
var c := Vector2(cx, y)
draw_circle(c + Vector2(0, 4), SOCKET_R + 3.0, Color(0.12, 0.06, 0.02, 0.38))
draw_circle(c, SOCKET_R, Color("#8b6841"))
draw_circle(c + Vector2(0, 3), SOCKET_R - 6.0, Color("#6d4b2d"))
draw_circle(c + Vector2(-2, -4), SOCKET_R - 9.0, Color("#c5a476"))
draw_circle(c + Vector2(3, 5), SOCKET_R - 13.0, Color(0.2, 0.1, 0.04, 0.16))
draw_arc(c, SOCKET_R, 0.0, TAU, 48, Color("#4a301d"), 3.0)
draw_arc(c - Vector2(1, 2), SOCKET_R - 8.0, PI, TAU, 24, Color(1, 0.92, 0.72, 0.28), 2.0)
@@ -0,0 +1 @@
uid://l7l3t71lt50f
@@ -0,0 +1,82 @@
extends Node2D
## Pallina di colore che mostra il bersaglio attuale nella barra in alto.
## Stile coerente con le biglie della plancia: glossy con venature marmorizzate.
var dot_color := Color.WHITE
const R := 36.0
var veins: Array[PackedVector2Array] = []
var _rng := RandomNumberGenerator.new()
func _draw() -> void:
# ombra
draw_circle(Vector2(0, 4), R, Color(0, 0, 0, 0.25))
# corpo
draw_circle(Vector2.ZERO, R, dot_color)
# ombreggiatura inferiore-destra e luce superiore-sinistra
draw_circle(Vector2(6.5, 8.0), R * 0.92, Color(0, 0, 0, 0.16))
draw_circle(Vector2(-6.5, -8.0), R * 0.92, Color(1, 1, 1, 0.2))
# venature marmorizzate (come le biglie della plancia, statiche in HUD)
_draw_marbling()
# bordo nero stile china
draw_arc(Vector2.ZERO, R, 0.0, TAU, 40, Color(0.1, 0.1, 0.1, 0.9), 4.0)
# riflesso speculare fisso
draw_circle(Vector2(-11.0, -12.0), 6.0, Color(1, 1, 1, 0.85))
func _draw_marbling() -> void:
var vein_color: Color = dot_color.darkened(0.42)
vein_color.a = 0.2
var soft_color: Color = dot_color.darkened(0.28)
soft_color.a = 0.12
for i: int in range(veins.size()):
var pts := PackedVector2Array()
for point: Vector2 in veins[i]:
# visibile solo la metà frontale (longitudine cos>0): copertura 360°
var theta: float = asin(clampf(point.x / R, -1.0, 1.0))
if cos(theta) <= 0.0:
continue
pts.append(point)
if pts.size() < 2:
continue
var width: float = R * (0.075 if i == 0 else 0.045)
draw_polyline(pts, soft_color, width * 1.9, true)
draw_polyline(pts, vein_color, width, true)
func _generate_veins() -> Array[PackedVector2Array]:
# 7 venature distribuite uniformemente su tutta la longitudine (360°)
var result: Array[PackedVector2Array] = []
var count := 7
for i in count:
var center: float = -PI + TAU * (float(i) + _rng.randf_range(0.0, 0.9)) / count
var spread := _rng.randf_range(0.25, 0.7)
var yc := _rng.randf_range(-0.5, 0.5)
var y0 := clampf(yc - _rng.randf_range(0.0, 0.3), -0.6, 0.6)
var y1 := clampf(yc + _rng.randf_range(0.0, 0.3), -0.6, 0.6)
var bend := _rng.randf_range(0.05, 0.22)
var ripple := _rng.randf_range(0.03, 0.11)
result.append(_make_vein(sin(center - spread), y0, sin(center + spread), y1, bend, ripple))
return result
func _make_vein(x0: float, y0: float, x1: float, y1: float,
bend: float, ripple: float) -> PackedVector2Array:
var points := PackedVector2Array()
var direction := Vector2(x1 - x0, y1 - y0)
var normal := direction.normalized().orthogonal()
for step: int in range(17):
var t: float = float(step) / 16.0
var base := Vector2(lerpf(x0, x1, t), lerpf(y0, y1, t))
var curve: float = sin(t * PI) * bend
var wave: float = sin(t * TAU * 1.35 + x0 * 4.0) * ripple
points.append((base + normal * (curve + wave)) * R)
return points
func set_color(c: Color) -> void:
dot_color = c
_rng.seed = hash(c) # pattern unico ma stabile per ogni colore
veins = _generate_veins()
queue_redraw()
@@ -0,0 +1 @@
uid://b1xr5s67yiov
@@ -0,0 +1,267 @@
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)
@@ -0,0 +1 @@
uid://cemlq3d1gmot2
@@ -0,0 +1,27 @@
class_name JuggleBoardConfig
extends RefCounted
## Costanti di layout, colori e note del JuggleBoard.
const VIEW := Vector2(1280, 720)
const LANE_TOP := 210.0
const LANE_GAP := 98.0
const BALL_RADIUS := 40.0
const HOME_X := 1180.0
const LEFT_X := 100.0
const HIT_RADIUS := BALL_RADIUS * 1.4
const COLORS := {
"rosso": Color("de6b7e"),
"blu": Color("1852de"),
"verde": Color("88ce96"),
"giallo": Color("e8b84b"),
"viola": Color("9b7ede"),
}
const BALL_NOTES := {
"rosso": 261.63,
"blu": 293.66,
"verde": 329.63,
"giallo": 392.00,
"viola": 440.00,
}
@@ -0,0 +1 @@
uid://dna4tat3kjoy4
+138
View File
@@ -0,0 +1,138 @@
class_name JuggleBoardHud
extends CanvasLayer
## HUD del JuggleBoard: costruzione e aggiornamenti visuali.
signal options_requested
const DISPLAY_FONT := preload("res://shared/assets/fonts/NotoSerifDisplay-CondensedExtraBold.ttf")
const CARTOUCHE_SCRIPT := preload("res://games/juggleboard/scripts/ui/hud_cartouche.gd")
const TARGET_SCRIPT := preload("res://games/juggleboard/scripts/components/target_dot.gd")
const HEARTS_SCRIPT := preload("res://games/juggleboard/scripts/ui/hearts_display.gd")
var target_dot: Node2D
var score_label: Label
var combo_label: Label
var help_label: Label
var milestone_label: Label
var flash: ColorRect
var hearts: Node2D
func _ready() -> void:
_build()
func _build() -> void:
var ink := Color("#5b2f19")
var cartouche: Node2D = CARTOUCHE_SCRIPT.new()
add_child(cartouche)
var target_label := _make_label("BERSAGLIO", Vector2(94, 44), Vector2(270, 54), 34, ink)
target_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
target_dot = TARGET_SCRIPT.new()
target_dot.position = Vector2(430, 78)
add_child(target_dot)
help_label = _make_label(
"Tocca la pallina corrispondente al bersaglio per lanciarla!",
Vector2(JuggleBoardConfig.VIEW.x / 2 - 350, 154), Vector2(700, 30), 17, ink
)
help_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
score_label = _make_label("", Vector2(925, 34), Vector2(245, 48), 36, ink)
score_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
combo_label = _make_label("", Vector2(925, 80), Vector2(245, 34), 21, ink)
combo_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
hearts = HEARTS_SCRIPT.new()
hearts.position = Vector2(121, 99)
hearts.visible = false
add_child(hearts)
var options_button := Button.new()
options_button.text = "Opzioni"
options_button.position = Vector2(580, 49)
options_button.size = Vector2(120, 57)
options_button.add_theme_font_override("font", DISPLAY_FONT)
options_button.add_theme_font_size_override("font_size", 25)
options_button.add_theme_color_override("font_color", ink)
options_button.add_theme_color_override("font_hover_color", Color("#7b421f"))
var option_style := _button_style(Color("#ddb983"))
options_button.add_theme_stylebox_override("normal", option_style)
var option_hover := _button_style(Color("#ebcb96"))
options_button.add_theme_stylebox_override("hover", option_hover)
options_button.add_theme_stylebox_override("pressed", option_style)
options_button.pressed.connect(func(): options_requested.emit())
add_child(options_button)
milestone_label = _make_label("", Vector2(0, 160), Vector2(JuggleBoardConfig.VIEW.x, 120), 56, Color(0.8, 0.55, 0.1))
milestone_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
milestone_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
milestone_label.visible = false
flash = ColorRect.new()
flash.color = Color(1, 1, 0.7)
flash.position = Vector2.ZERO
flash.size = JuggleBoardConfig.VIEW
flash.mouse_filter = Control.MOUSE_FILTER_IGNORE
flash.modulate.a = 0.0
flash.visible = false
add_child(flash)
func reset(score: int, combo: float, lives: int, survival: bool) -> void:
set_score(score, combo)
hearts.set_lives(lives)
hearts.visible = survival
help_label.visible = true
func set_score(score: int, combo: float) -> void:
score_label.text = "PUNTI %d" % score
combo_label.text = "Combo x%d" % int(combo)
func set_target(color: Color, shown: bool = true) -> void:
if shown:
target_dot.set_color(color)
target_dot.visible = shown
func celebrate(amount: int) -> void:
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 tween := create_tween()
tween.set_parallel(true)
tween.tween_property(milestone_label, "scale", Vector2(1.3, 1.3), 0.4).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
tween.tween_property(milestone_label, "modulate:a", 0.0, 0.8).set_delay(0.6)
tween.tween_property(flash, "modulate:a", 0.0, 0.4)
tween.chain().tween_callback(func():
milestone_label.visible = false
flash.visible = false
)
func _make_label(text: String, position: Vector2, size: Vector2, font_size: int, color: Color) -> Label:
var label := Label.new()
label.text = text
label.position = position
label.size = size
label.add_theme_font_override("font", DISPLAY_FONT)
label.add_theme_font_size_override("font_size", font_size)
label.add_theme_color_override("font_color", color)
add_child(label)
return label
func _button_style(color: Color) -> StyleBoxFlat:
var style := StyleBoxFlat.new()
style.bg_color = color
style.border_color = Color("#9b6738")
style.set_border_width_all(3)
style.set_corner_radius_all(9)
return style
@@ -0,0 +1 @@
uid://c86rp8mvnrabe
@@ -0,0 +1,64 @@
extends Node2D
## Mostra i cuori della modalità Survival: 4 cuori, pieni o vuoti.
const MAX_LIVES := 4
const HEART_GAP := 44.0
const FILLED_COLOR := Color("#e0566e")
const EMPTY_COLOR := Color("#aaa49a")
const OUTLINE_COLOR := Color("#633725")
var lives := MAX_LIVES
func _ready() -> void:
queue_redraw()
func set_lives(v: int) -> void:
lives = clampi(v, 0, MAX_LIVES)
queue_redraw()
func _draw() -> void:
for i in MAX_LIVES:
var filled := i < lives
_draw_heart(Vector2(i * HEART_GAP, 0.0), FILLED_COLOR if filled else EMPTY_COLOR, filled)
func _draw_heart(origin: Vector2, color: Color, filled: bool) -> void:
var points := _heart_points(origin)
var shadow := PackedVector2Array()
for point in points:
shadow.append(point + Vector2(0.0, 3.0))
draw_colored_polygon(shadow, Color(0.18, 0.10, 0.07, 0.24))
draw_colored_polygon(points, color)
var outline := points.duplicate()
outline.append(points[0])
draw_polyline(outline, OUTLINE_COLOR, 2.5, true)
# Piccolo riflesso da pallina smaltata, coerente con gli altri elementi HUD.
var shine_alpha := 0.72 if filled else 0.38
draw_circle(origin + Vector2(10.5, 9.0), 3.4, Color(1.0, 0.94, 0.91, shine_alpha))
func _heart_points(origin: Vector2) -> PackedVector2Array:
# Quattro curve cubiche formano un cuore pieno di 36x35 px.
var points := PackedVector2Array()
var segments := [
[Vector2(18, 34), Vector2(15, 30), Vector2(2, 22), Vector2(2, 11)],
[Vector2(2, 11), Vector2(2, 3), Vector2(12, 0), Vector2(18, 7)],
[Vector2(18, 7), Vector2(24, 0), Vector2(34, 3), Vector2(34, 11)],
[Vector2(34, 11), Vector2(34, 22), Vector2(21, 30), Vector2(18, 34)],
]
for segment in segments:
for step in range(8):
var t := float(step) / 8.0
points.append(origin + _cubic(segment[0], segment[1], segment[2], segment[3], t))
points.append(origin + Vector2(18, 34))
return points
func _cubic(a: Vector2, b: Vector2, c: Vector2, d: Vector2, t: float) -> Vector2:
var u := 1.0 - t
return u * u * u * a + 3.0 * u * u * t * b + 3.0 * u * t * t * c + t * t * t * d
@@ -0,0 +1 @@
uid://x0ialp40fqlk
@@ -0,0 +1,55 @@
extends Node2D
## Cartiglio circense in legno, puramente decorativo: la UI interattiva
## rimane costruita da main.gd sopra questo nodo.
const INK := Color("#5b2f19")
const WOOD_LIGHT := Color("#efd6aa")
const WOOD_MID := Color("#d8ad72")
const WOOD_DARK := Color("#8a552d")
func _draw() -> void:
# Ombra morbida e sagoma con angoli tagliati a 45 gradi.
var shadow := PackedVector2Array([
Vector2(56, 27), Vector2(75, 10), Vector2(1205, 10), Vector2(1224, 27),
Vector2(1224, 128), Vector2(1205, 145), Vector2(75, 145), Vector2(56, 128)
])
for i in range(shadow.size()):
shadow[i] += Vector2(0, 7)
draw_colored_polygon(shadow, Color(0.12, 0.055, 0.02, 0.42))
var body := PackedVector2Array([
Vector2(56, 27), Vector2(75, 10), Vector2(1205, 10), Vector2(1224, 27),
Vector2(1224, 128), Vector2(1205, 145), Vector2(75, 145), Vector2(56, 128)
])
draw_colored_polygon(body, WOOD_LIGHT)
var rim := body.duplicate()
rim.append(body[0])
draw_polyline(rim, WOOD_DARK, 5.0, true)
# Incisioni e venatura tenue.
draw_line(Vector2(79, 25), Vector2(1201, 25), Color(1, 0.93, 0.78, 0.7), 2.0)
draw_line(Vector2(78, 130), Vector2(1202, 130), Color(0.38, 0.18, 0.07, 0.25), 2.0)
for y in [42.0, 104.0, 119.0]:
draw_arc(Vector2(640, y), 420.0, 0.08, PI - 0.08, 40, Color(0.45, 0.24, 0.1, 0.09), 1.0)
# Tasca incassata della biglia bersaglio.
_draw_socket(Vector2(430, 78), 43.0)
# Borchie dorate del cartiglio e della targhetta Opzioni.
for p in [Vector2(78, 34), Vector2(1202, 34), Vector2(78, 120), Vector2(1202, 120),
Vector2(567, 76), Vector2(713, 76)]:
_draw_stud(p)
func _draw_stud(p: Vector2) -> void:
draw_circle(p + Vector2(1, 3), 8.0, Color(0.25, 0.12, 0.03, 0.32))
draw_circle(p, 7.0, Color("#b77a24"))
draw_circle(p - Vector2(1.5, 2), 4.2, Color("#edc668"))
draw_circle(p - Vector2(2.5, 3), 1.5, Color(1, 0.94, 0.7, 0.9))
draw_arc(p, 7.0, 0.0, TAU, 20, Color("#714318"), 1.5)
func _draw_socket(p: Vector2, r: float) -> void:
draw_circle(p + Vector2(0, 3), r + 4.0, Color(0.28, 0.14, 0.06, 0.55))
draw_circle(p, r, WOOD_DARK)
draw_circle(p + Vector2(0, 3), r - 7.0, Color("#5b351f"))
draw_circle(p + Vector2(-2, -3), r - 10.0, Color(0.93, 0.76, 0.51, 0.28))
draw_arc(p, r, 0.0, TAU, 36, INK, 2.5)
@@ -0,0 +1 @@
uid://bp7dvrdihdl13
@@ -0,0 +1,73 @@
class_name LockMarbleButton
extends Control
## Biglia interattiva del lock adulto.
signal chosen(color_name: String)
var color_name := ""
var ball_color := Color.WHITE
var radius := 47.0
var hovered := false
var held := false
var veins: Array[PackedVector2Array] = []
func setup(name: String, color: Color) -> void:
color_name = name
ball_color = color
custom_minimum_size = Vector2(108, 108)
mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
mouse_entered.connect(func(): hovered = true; queue_redraw())
mouse_exited.connect(func(): hovered = false; held = false; queue_redraw())
_generate_veins()
queue_redraw()
func _gui_input(event: InputEvent) -> void:
# Il touch emulato produce già MouseButton: gestire anche ScreenTouch raddoppierebbe l'evento.
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
held = event.pressed
queue_redraw()
if not event.pressed:
chosen.emit(color_name)
accept_event()
func _draw() -> void:
var center := size * 0.5 + Vector2(0, 2 if held else 0)
var draw_radius := radius * (0.96 if held else (1.03 if hovered else 1.0))
draw_circle(center + Vector2(0, 6), draw_radius, Color(0, 0, 0, 0.28))
draw_circle(center, draw_radius, ball_color)
draw_circle(center + Vector2(draw_radius * 0.18, draw_radius * 0.22), draw_radius * 0.92, Color(0, 0, 0, 0.16))
draw_circle(center - Vector2(draw_radius * 0.18, draw_radius * 0.22), draw_radius * 0.92, Color(1, 1, 1, 0.20))
var vein_color := ball_color.darkened(0.42)
vein_color.a = 0.22
var soft_color := ball_color.darkened(0.28)
soft_color.a = 0.13
for vein in veins:
var points := PackedVector2Array()
for point in vein:
points.append(center + point * (draw_radius / radius))
draw_polyline(points, soft_color, draw_radius * 0.095, true)
draw_polyline(points, vein_color, draw_radius * 0.045, true)
draw_arc(center, draw_radius, 0.0, TAU, 56, Color(0.1, 0.1, 0.1, 0.92), 4.0, true)
draw_circle(center - Vector2(draw_radius * 0.30, draw_radius * 0.34), draw_radius * 0.16, Color(1, 1, 1, 0.86))
if hovered:
draw_arc(center, draw_radius + 4.0, 0.0, TAU, 56, Color(1, 1, 1, 0.75), 2.0, true)
func _generate_veins() -> void:
var rng := RandomNumberGenerator.new()
rng.seed = hash(color_name)
for index in 5:
var points := PackedVector2Array()
var y_base := rng.randf_range(-0.55, 0.55) * radius
var phase := rng.randf_range(0.0, TAU)
var x_start := rng.randf_range(-0.78, -0.35) * radius
var x_end := rng.randf_range(0.35, 0.78) * radius
for point_index in 13:
var factor := float(point_index) / 12.0
var x := lerpf(x_start, x_end, factor)
var y := y_base + sin(factor * TAU * 1.2 + phase) * radius * 0.13
points.append(Vector2(x, y))
veins.append(points)
@@ -0,0 +1 @@
uid://cx4sel5v04nxh
+171
View File
@@ -0,0 +1,171 @@
extends CanvasLayer
## Lock adulto: sequenza giallo → verde → blu → rosso.
signal unlocked
const COLORS := {
"giallo": Color("#e8b84b"), "verde": Color("#88ce96"), "blu": Color("#1852de"),
"rosso": Color("#de6b7e"), "viola": Color("#9b7ede"),
}
const BALL_ORDER := ["giallo", "verde", "blu", "rosso", "viola"]
const SEQUENCE := ["giallo", "verde", "blu", "rosso"]
var sequence_attempt: Array[String] = []
var progress_dots: Array[LockProgressDot] = []
var _busy := false
var _click: AudioStreamWAV
var _wrong: AudioStreamWAV
var _unlock: AudioStreamWAV
func _ready() -> void:
process_mode = Node.PROCESS_MODE_ALWAYS
visible = false
_click = AudioFactory.create_tone(660.0, 0.09, 0.28)
_wrong = AudioFactory.create_tone(330.0, 0.11, 0.18)
_unlock = AudioFactory.create_arpeggio([523.25, 659.25, 783.99], 0.09, 0.24)
_build()
func _build() -> void:
var dim := ColorRect.new()
dim.color = Color(0.06, 0.035, 0.025, 0.78)
dim.set_anchors_preset(Control.PRESET_FULL_RECT)
dim.mouse_filter = Control.MOUSE_FILTER_STOP
add_child(dim)
var center := CenterContainer.new()
center.set_anchors_preset(Control.PRESET_FULL_RECT)
add_child(center)
var frame := PanelContainer.new()
frame.custom_minimum_size = Vector2(720, 420)
var frame_style := CircusTheme.flat_style(Color("#70411f"), Color("#e1ba59"), 6, 30)
frame_style.shadow_color = Color(0, 0, 0, 0.52)
frame_style.shadow_size = 20
frame_style.shadow_offset = Vector2(0, 8)
for side in [SIDE_LEFT, SIDE_TOP, SIDE_RIGHT, SIDE_BOTTOM]:
frame_style.set_content_margin(side, 10.0)
frame.add_theme_stylebox_override("panel", frame_style)
center.add_child(frame)
var panel := PanelContainer.new()
panel.add_theme_stylebox_override("panel", CircusTheme.wood_style())
frame.add_child(panel)
var margin := MarginContainer.new()
margin.add_theme_constant_override("margin_left", 42)
margin.add_theme_constant_override("margin_right", 42)
margin.add_theme_constant_override("margin_top", 27)
margin.add_theme_constant_override("margin_bottom", 25)
panel.add_child(margin)
var content := VBoxContainer.new()
content.add_theme_constant_override("separation", 12)
margin.add_child(content)
_add_header(content)
_add_marbles(content)
_add_progress(content)
_add_cancel(content)
func _add_header(parent: VBoxContainer) -> void:
var title := Label.new()
title.text = "AREA RISERVATA"
title.add_theme_font_override("font", CircusTheme.FONT)
title.add_theme_font_size_override("font_size", 36)
title.add_theme_color_override("font_color", Color("#ffe08a"))
title.add_theme_color_override("font_outline_color", Color("#673018"))
title.add_theme_constant_override("outline_size", 8)
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
parent.add_child(title)
var hint := Label.new()
hint.text = "Chiedi a un adulto di premere la sequenza di colori"
hint.add_theme_font_override("font", CircusTheme.FONT)
hint.add_theme_font_size_override("font_size", 19)
hint.add_theme_color_override("font_color", Color("#56321e"))
hint.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
parent.add_child(hint)
func _add_marbles(parent: VBoxContainer) -> void:
var row := HBoxContainer.new()
row.add_theme_constant_override("separation", 14)
row.alignment = BoxContainer.ALIGNMENT_CENTER
parent.add_child(row)
for color_name in BALL_ORDER:
var marble := LockMarbleButton.new()
marble.setup(color_name, COLORS[color_name])
marble.chosen.connect(_on_press)
row.add_child(marble)
func _add_progress(parent: VBoxContainer) -> void:
var row := HBoxContainer.new()
row.alignment = BoxContainer.ALIGNMENT_CENTER
row.add_theme_constant_override("separation", 12)
parent.add_child(row)
for index in SEQUENCE.size():
var dot := LockProgressDot.new()
progress_dots.append(dot)
row.add_child(dot)
func _add_cancel(parent: VBoxContainer) -> void:
var cancel := Button.new()
cancel.text = "ANNULLA"
cancel.custom_minimum_size = Vector2(210, 52)
cancel.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
cancel.add_theme_font_override("font", CircusTheme.FONT)
cancel.add_theme_font_size_override("font_size", 21)
cancel.add_theme_color_override("font_color", Color("#fff0c4"))
cancel.add_theme_stylebox_override("normal", CircusTheme.button_style(Color("#744020"), Color("#ce9b3d"), 4, 13))
cancel.add_theme_stylebox_override("hover", CircusTheme.button_style(Color("#8b5129"), Color("#f0ca68"), 4, 13))
cancel.add_theme_stylebox_override("pressed", CircusTheme.button_style(Color("#5c3019"), Color("#ae7927"), 4, 13))
cancel.pressed.connect(hide_menu)
parent.add_child(cancel)
func show_menu() -> void:
sequence_attempt.clear()
_update_progress()
visible = true
get_tree().paused = true
func hide_menu() -> void:
visible = false
get_tree().paused = false
func _on_press(color_name: String) -> void:
if _busy:
return
AudioFactory.play_once(self, _click)
sequence_attempt.append(color_name)
_update_progress()
if sequence_attempt.size() == SEQUENCE.size():
if sequence_attempt == SEQUENCE:
call_deferred("_finish_unlock")
else:
_handle_wrong()
func _handle_wrong() -> void:
_busy = true
AudioFactory.play_once(self, _wrong)
for dot in progress_dots:
dot.set_color(Color("#d9534f"))
await get_tree().create_timer(2.0).timeout
sequence_attempt.clear()
_update_progress()
_busy = false
func _finish_unlock() -> void:
AudioFactory.play_once(self, _unlock)
hide_menu()
unlocked.emit()
func _update_progress() -> void:
for index in progress_dots.size():
var color: Color = COLORS[sequence_attempt[index]] if index < sequence_attempt.size() else Color.TRANSPARENT
progress_dots[index].set_color(color)
@@ -0,0 +1 @@
uid://bsxirosmw4y0r
@@ -0,0 +1,27 @@
class_name LockProgressDot
extends Control
## Indicatore di avanzamento della sequenza del lock.
var dot_color := Color.TRANSPARENT
func _init() -> void:
custom_minimum_size = Vector2(38, 38)
func set_color(color: Color) -> void:
dot_color = color
queue_redraw()
func _draw() -> void:
var center := size * 0.5
var radius := 12.0
draw_circle(center + Vector2(1.0, 2.0), radius + 2.0, Color(0.18, 0.07, 0.03, 0.32))
if dot_color.a > 0.01:
draw_circle(center, radius, dot_color)
draw_circle(center - Vector2(3.0, 3.0), 3.2, Color(1, 1, 1, 0.5))
draw_arc(center, radius, 0.0, TAU, 32, Color("#4b2818"), 3.0, true)
else:
draw_circle(center, radius, Color("#eee2cf"))
draw_arc(center, radius, 0.0, TAU, 32, Color("#69401f"), 3.0, true)
@@ -0,0 +1 @@
uid://bqk3w1u4snehi
@@ -0,0 +1,60 @@
class_name OptionSliderRow
extends HBoxContainer
## Riga riutilizzabile Label + Slider + SpinBox.
signal setting_changed(key: String, value: float)
var setting_key := ""
var slider: HSlider
var spin: SpinBox
var _syncing := false
func setup(key: String, label_text: String, minimum: float, maximum: float, step: float, value: float) -> void:
setting_key = key
add_theme_constant_override("separation", 6)
var label := Label.new()
label.text = label_text
label.custom_minimum_size.x = 230
add_child(label)
slider = HSlider.new()
slider.min_value = minimum
slider.max_value = maximum
slider.step = step
slider.custom_minimum_size = Vector2(260, 40)
slider.value = value
add_child(slider)
spin = SpinBox.new()
spin.min_value = minimum
spin.max_value = maximum
spin.step = step
spin.custom_minimum_size.x = 90
spin.value = value
add_child(spin)
slider.value_changed.connect(_on_slider_changed)
spin.value_changed.connect(_on_spin_changed)
func set_value(value: float) -> void:
_syncing = true
slider.value = value
spin.value = value
_syncing = false
func _on_slider_changed(value: float) -> void:
if _syncing:
return
_syncing = true
spin.value = value
_syncing = false
setting_changed.emit(setting_key, value)
func _on_spin_changed(value: float) -> void:
if _syncing:
return
_syncing = true
slider.value = value
_syncing = false
setting_changed.emit(setting_key, value)
@@ -0,0 +1 @@
uid://pq8vdvqcab8q
@@ -0,0 +1,120 @@
extends CanvasLayer
## Configurazione in-game; la definizione delle righe è in OptionsSchema.
var rows: Dictionary = {}
var mode_option: OptionButton
var gentle_button: CheckButton
func _ready() -> void:
process_mode = Node.PROCESS_MODE_ALWAYS
visible = false
_build()
func _build() -> void:
var dim := ColorRect.new()
dim.color = Color(0, 0, 0, 0.65)
dim.set_anchors_preset(Control.PRESET_FULL_RECT)
dim.mouse_filter = Control.MOUSE_FILTER_STOP
add_child(dim)
var center := CenterContainer.new()
center.set_anchors_preset(Control.PRESET_FULL_RECT)
add_child(center)
var panel := PanelContainer.new()
center.add_child(panel)
var margin := MarginContainer.new()
margin.add_theme_constant_override("margin_left", 24)
margin.add_theme_constant_override("margin_right", 24)
margin.add_theme_constant_override("margin_top", 16)
margin.add_theme_constant_override("margin_bottom", 16)
panel.add_child(margin)
var content := VBoxContainer.new()
content.add_theme_constant_override("separation", 8)
margin.add_child(content)
_add_title(content)
_add_settings_rows(content)
_add_mode_controls(content)
_add_actions(content)
func _add_title(parent: VBoxContainer) -> void:
var title := Label.new()
title.text = "CONFIGURAZIONE"
title.add_theme_font_size_override("font_size", 30)
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
parent.add_child(title)
func _add_settings_rows(parent: VBoxContainer) -> void:
for entry in JuggleBoardOptionsSchema.ENTRIES:
var row := OptionSliderRow.new()
row.setup(entry[0], entry[1], entry[2], entry[3], entry[4], Settings.get(entry[0]))
row.setting_changed.connect(func(key: String, value: float): Settings.set(key, value))
parent.add_child(row)
rows[entry[0]] = row
func _add_mode_controls(parent: VBoxContainer) -> void:
parent.add_child(HSeparator.new())
var row := HBoxContainer.new()
row.custom_minimum_size.y = 44
row.alignment = BoxContainer.ALIGNMENT_CENTER
parent.add_child(row)
var label := Label.new()
label.text = "Modalità di gioco:"
label.custom_minimum_size.x = 230
label.add_theme_font_size_override("font_size", 18)
row.add_child(label)
mode_option = OptionButton.new()
mode_option.custom_minimum_size = Vector2(300, 40)
mode_option.add_theme_font_size_override("font_size", 17)
mode_option.add_item("Classica (a punteggio)")
mode_option.add_item("Zen (senza tempo)")
mode_option.add_item("Survival (a vite)")
mode_option.item_selected.connect(func(index: int): Settings.game_mode = index)
row.add_child(mode_option)
gentle_button = CheckButton.new()
gentle_button.text = "Errori gentili (combo -1)"
gentle_button.toggled.connect(func(enabled: bool): Settings.gentle_errors = enabled)
parent.add_child(gentle_button)
func _add_actions(parent: VBoxContainer) -> void:
var row := HBoxContainer.new()
row.add_theme_constant_override("separation", 8)
parent.add_child(row)
for spec in [["Salva e riavvia", _on_save], ["Ripristina default", _on_reset], ["Annulla", hide_menu]]:
var button := Button.new()
button.text = spec[0]
button.pressed.connect(spec[1])
row.add_child(button)
func show_menu() -> void:
_refresh_values()
visible = true
get_tree().paused = true
func hide_menu() -> void:
visible = false
get_tree().paused = false
func _refresh_values() -> void:
for key in rows:
rows[key].set_value(Settings.get(key))
mode_option.select(Settings.game_mode)
gentle_button.button_pressed = Settings.gentle_errors
func _on_save() -> void:
Settings.save_settings()
hide_menu()
get_tree().call_deferred("reload_current_scene")
func _on_reset() -> void:
Settings.reset_defaults()
_refresh_values()
@@ -0,0 +1 @@
uid://dnr1wdhqehwnu
@@ -0,0 +1,15 @@
class_name JuggleBoardOptionsSchema
extends RefCounted
## Definizione dichiarativa dei controlli configurabili.
const ENTRIES := [
["start_time", "Tempo iniziale (s)", 1.0, 10.0, 0.1],
["min_time", "Tempo minimo (s)", 0.3, 3.0, 0.1],
["score_ramp", "Punteggio per velocità max", 100.0, 20000.0, 100.0],
["points_per_combo", "Punti per combo", 1.0, 50.0, 1.0],
["target_score", "Punteggio vittoria", 100.0, 50000.0, 100.0],
["max_combo", "Combo massimo", 1.0, 20.0, 1.0],
["combo_decay_threshold", "Soglia decay combo", 0.0, 30.0, 1.0],
["combo_decay_rate", "Rate decay combo", 0.0, 0.5, 0.01],
["difficulty_decay_rate", "Rate decay difficoltà", 0.0, 2.0, 0.05],
]
@@ -0,0 +1 @@
uid://dus44rc6j5u3
@@ -0,0 +1,141 @@
extends CanvasLayer
## Overlay condiviso per vittoria e game over.
signal reset_requested
var title_label: Label
var score_label: Label
var subtitle_label: Label
var card: Control
var reset_button: Button
func _ready() -> void:
process_mode = Node.PROCESS_MODE_ALWAYS
visible = false
_build()
func _build() -> void:
var dim := ColorRect.new()
dim.color = Color(0.045, 0.018, 0.012, 0.76)
dim.set_anchors_preset(Control.PRESET_FULL_RECT)
dim.mouse_filter = Control.MOUSE_FILTER_STOP
add_child(dim)
var center := CenterContainer.new()
center.set_anchors_preset(Control.PRESET_FULL_RECT)
add_child(center)
card = PanelContainer.new()
card.custom_minimum_size = Vector2(680, 510)
var frame := CircusTheme.flat_style(Color("#70411f"), Color("#edca69"), 6, 31)
frame.shadow_color = Color(0, 0, 0, 0.55)
frame.shadow_size = 22
frame.shadow_offset = Vector2(0, 9)
for side in [SIDE_LEFT, SIDE_RIGHT, SIDE_TOP, SIDE_BOTTOM]:
frame.set_content_margin(side, 10.0)
card.add_theme_stylebox_override("panel", frame)
center.add_child(card)
var wood := PanelContainer.new()
wood.add_theme_stylebox_override("panel", CircusTheme.wood_style())
card.add_child(wood)
var studs := BrassStuds.new()
studs.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
wood.add_child(studs)
var margin := MarginContainer.new()
margin.add_theme_constant_override("margin_left", 58)
margin.add_theme_constant_override("margin_right", 58)
margin.add_theme_constant_override("margin_top", 36)
margin.add_theme_constant_override("margin_bottom", 38)
wood.add_child(margin)
var content := VBoxContainer.new()
content.add_theme_constant_override("separation", 15)
content.alignment = BoxContainer.ALIGNMENT_CENTER
margin.add_child(content)
_add_content(content)
func _add_content(parent: VBoxContainer) -> void:
var bunting := CircusTrim.new()
bunting.flag_count = 9
bunting.span = 360.0
bunting.custom_minimum_size = Vector2(0, 40)
parent.add_child(bunting)
title_label = _label(parent, "VITTORIA!", 68, Color("#ffe08a"))
title_label.add_theme_color_override("font_outline_color", Color("#713019"))
title_label.add_theme_constant_override("outline_size", 10)
subtitle_label = _label(parent, "Hai raggiunto il punteggio target!", 23, CircusTheme.INK)
var plaque := PanelContainer.new()
plaque.add_theme_stylebox_override("panel", CircusTheme.flat_style(Color("#f7e6bc"), Color("#9d6b29"), 3, 14))
var margin := MarginContainer.new()
margin.add_theme_constant_override("margin_left", 30)
margin.add_theme_constant_override("margin_right", 30)
margin.add_theme_constant_override("margin_top", 8)
margin.add_theme_constant_override("margin_bottom", 8)
plaque.add_child(margin)
score_label = _label(margin, "", 34, CircusTheme.INK)
parent.add_child(plaque)
reset_button = Button.new()
reset_button.text = "↻ RIGIOCA"
reset_button.custom_minimum_size = Vector2(250, 66)
reset_button.add_theme_font_override("font", CircusTheme.FONT)
reset_button.add_theme_font_size_override("font_size", 27)
reset_button.add_theme_color_override("font_color", Color("#fff1c7"))
reset_button.add_theme_stylebox_override("normal", CircusTheme.button_style(Color("#784121"), Color("#d9a947"), 5, 15))
reset_button.add_theme_stylebox_override("hover", CircusTheme.button_style(Color("#8e5129"), CircusTheme.BRASS_LIGHT, 5, 15))
reset_button.add_theme_stylebox_override("pressed", CircusTheme.button_style(Color("#5f311b"), Color("#b7832e"), 4, 15))
reset_button.pressed.connect(func(): reset_requested.emit())
parent.add_child(reset_button)
func _label(parent: Node, text: String, size: int, color: Color) -> Label:
var label := Label.new()
label.text = text
label.add_theme_font_override("font", CircusTheme.FONT)
label.add_theme_font_size_override("font_size", size)
label.add_theme_color_override("font_color", color)
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
parent.add_child(label)
return label
func show_victory(score: int) -> void:
_show_end(true, score)
func show_game_over(score: int) -> void:
_show_end(false, score)
func _show_end(victory: bool, score: int) -> void:
title_label.text = "VITTORIA!" if victory else "GAME OVER"
title_label.add_theme_color_override("font_color", Color("#ffe08a") if victory else Color("#f0b4b4"))
subtitle_label.text = "Hai raggiunto il punteggio target!" if victory else "Hai perso tutti i cuori... Ritenta!"
score_label.text = "Punteggio: %d" % score
visible = true
_animate_in()
func _animate_in() -> void:
card.modulate.a = 0.0
card.scale = Vector2(0.94, 0.94)
card.pivot_offset = card.size * 0.5
title_label.scale = Vector2(0.45, 0.45)
title_label.modulate.a = 0.0
score_label.modulate.a = 0.0
reset_button.modulate.a = 0.0
var tween := create_tween()
tween.set_pause_mode(Tween.TWEEN_PAUSE_PROCESS)
tween.set_parallel(true)
tween.tween_property(card, "modulate:a", 1.0, 0.22)
tween.tween_property(card, "scale", Vector2.ONE, 0.34)
tween.tween_property(title_label, "modulate:a", 1.0, 0.22).set_delay(0.12)
tween.tween_property(title_label, "scale", Vector2.ONE, 0.42).set_delay(0.10).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
tween.tween_property(score_label, "modulate:a", 1.0, 0.35).set_delay(0.38)
tween.tween_property(reset_button, "modulate:a", 1.0, 0.35).set_delay(0.52)
func hide_victory() -> void:
visible = false
@@ -0,0 +1 @@
uid://5reli744dkgr