Refactor Godot architecture into AppShell, modular menus and game domains
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user