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
+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