JuggleBoard game v0.1: Godot 4.7 arcade, combo decay, config menu, circus BGM
This commit is contained in:
+13
@@ -0,0 +1,13 @@
|
|||||||
|
# Godot editor cache
|
||||||
|
.godot/
|
||||||
|
|
||||||
|
# Build output (web export)
|
||||||
|
build/
|
||||||
|
|
||||||
|
# Os temporanei
|
||||||
|
*.tmp
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# JuggleBoard Game
|
||||||
|
|
||||||
|
Un gioco arcade **reaction & chaining** per dispositivi mobili (orizzontale), realizzato in **Godot 4.7** (GDScript), ispirato al prodotto riabilitativo "Juggle Board".
|
||||||
|
|
||||||
|
## Gameplay
|
||||||
|
- **5 corsie orizzontali**, ognuna con una **pallina colorata** a riposo sul lato destro.
|
||||||
|
- Un **bersaglio in alto** indica quale pallina toccare.
|
||||||
|
- Toccando la pallina giusta: scivola a sinistra e poi torna a destra, con un **suono di conferma**.
|
||||||
|
- Un nuovo bersaglio appare **solo se c'è almeno una pallina a riposo**.
|
||||||
|
- Devi concatenare i lanci **prima che scada il timeout**: più concateni, più punti (combo).
|
||||||
|
- **Difficoltà crescente**: il tempo parte molto disteso (~4,5s) e **si riduce** con il tempo trascorso e il punteggio.
|
||||||
|
- **Tap sbagliato** → suono di errore e reset combo (nessun game over).
|
||||||
|
- **Combo con decadimento**: il combo cresce con i tap rapidi ma **si riduce pian piano** in proporzione al tempo che aspetti prima di cliccare la pallina bersaglio (rate configurabile `COMBO_DECAY`).
|
||||||
|
- **Timeout scaduto** → suono dedicato, reset combo, nuovo bersaglio (nessun game over).
|
||||||
|
- I suoni (lancio, errore, timeout) sono generati proceduralmente in 8-bit.
|
||||||
|
|
||||||
|
## Requisiti
|
||||||
|
- Godot 4.x (sviluppato e testato con 4.7.1, renderer GL Compatibility).
|
||||||
|
|
||||||
|
## Eseguire
|
||||||
|
```bash
|
||||||
|
# dal percorso del progetto
|
||||||
|
godot --path .
|
||||||
|
```
|
||||||
|
Oppure apri il progetto con l'editor Godot (`godot -e .`) e premi **Play** (F5).
|
||||||
|
|
||||||
|
## Controlli
|
||||||
|
- **Desktop**: click del mouse sulla pallina.
|
||||||
|
- **Mobile**: tocco sullo schermo.
|
||||||
|
|
||||||
|
## Struttura
|
||||||
|
```
|
||||||
|
project.godot # configurazione (viewport 1280x720, landscape, GL Compatibility)
|
||||||
|
icon.svg
|
||||||
|
scenes/
|
||||||
|
main.tscn # scena principale (radice Node2D + script)
|
||||||
|
scripts/
|
||||||
|
main.gd # logica di gioco, HUD, punteggio, difficoltà
|
||||||
|
ball.gd # pallina: Area2D, scivolata sinistra↔destra, input
|
||||||
|
target_dot.gd # indicatore del colore bersaglio
|
||||||
|
```
|
||||||
|
|
||||||
|
## Personalizzare la difficoltà
|
||||||
|
In `scripts/main.gd` (sezione "Tuning difficoltà"):
|
||||||
|
- `START_TIME` → tempo iniziale tra un ordine e l'altro (s). Più alto = partenza più distesa.
|
||||||
|
- `MIN_TIME` → tempo minimo (velocità massima) raggiunto a difficoltà piena.
|
||||||
|
- `TIME_RAMP` → secondi di gioco per raggiungere la difficoltà piena.
|
||||||
|
- `SCORE_RAMP` → punteggio per raggiungere la difficoltà piena.
|
||||||
|
|
||||||
|
La difficoltà cresce col massimo tra il fattore-tempo e il fattore-punteggio.
|
||||||
|
|
||||||
|
## Esportazione mobile
|
||||||
|
Installa dal Project Manager il template **Android** (o iOS su macOS), poi
|
||||||
|
*Project → Export* con preset `Android` / `Web`.
|
||||||
Binary file not shown.
@@ -0,0 +1,19 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="oggvorbisstr"
|
||||||
|
type="AudioStreamOggVorbis"
|
||||||
|
uid="uid://b2gfccy54hwyd"
|
||||||
|
path="res://.godot/imported/circus.ogg-5b91588fe544e7aa78021ac22d7fcfc9.oggvorbisstr"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://audio/circus.ogg"
|
||||||
|
dest_files=["res://.godot/imported/circus.ogg-5b91588fe544e7aa78021ac22d7fcfc9.oggvorbisstr"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
loop=true
|
||||||
|
loop_offset=0
|
||||||
|
bpm=0
|
||||||
|
beat_count=0
|
||||||
|
bar_beats=4
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
[preset.0]
|
||||||
|
|
||||||
|
name="Web"
|
||||||
|
platform="Web"
|
||||||
|
runnable=true
|
||||||
|
advanced_options=false
|
||||||
|
dedicated_server=false
|
||||||
|
custom_features=""
|
||||||
|
export_filter="all_resources"
|
||||||
|
include_filter=""
|
||||||
|
exclude_filter=""
|
||||||
|
export_path="build/web/index.html"
|
||||||
|
patches=PackedStringArray()
|
||||||
|
encryption_include_filters=""
|
||||||
|
encryption_exclude_filters=""
|
||||||
|
seed=0
|
||||||
|
encrypt_pck=false
|
||||||
|
encrypt_directory=false
|
||||||
|
script_export_mode=2
|
||||||
|
|
||||||
|
[preset.0.options]
|
||||||
|
|
||||||
|
custom_template/debug=""
|
||||||
|
custom_template/release=""
|
||||||
|
variant/extensions_support=false
|
||||||
|
variant/thread_support=false
|
||||||
|
vram_texture_compression/for_desktop=true
|
||||||
|
vram_texture_compression/for_mobile=false
|
||||||
|
html/export_icon=true
|
||||||
|
html/custom_html_shell=""
|
||||||
|
html/head_include=""
|
||||||
|
html/canvas_resize_policy=2
|
||||||
|
html/focus_canvas_on_start=true
|
||||||
|
html/experimental_virtual_keyboard=false
|
||||||
|
progressive_web_app/enabled=false
|
||||||
|
progressive_web_app/offline_page=""
|
||||||
|
progressive_web_app/display=1
|
||||||
|
progressive_web_app/orientation=0
|
||||||
|
progressive_web_app/icon_144x144=""
|
||||||
|
progressive_web_app/icon_180x180=""
|
||||||
|
progressive_web_app/icon_512x512=""
|
||||||
|
progressive_web_app/background_color=Color(0, 0, 0, 1)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128"><rect width="128" height="128" fill="#2b2b33"/><circle cx="64" cy="64" r="36" fill="#e74c3c"/><circle cx="52" cy="52" r="8" fill="#ffffff" opacity="0.5"/></svg>
|
||||||
|
After Width: | Height: | Size: 226 B |
@@ -0,0 +1,43 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://d1bxk6k1q1xn8"
|
||||||
|
path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://icon.svg"
|
||||||
|
dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
|
svg/scale=1.0
|
||||||
|
editor/scale_with_editor_scale=false
|
||||||
|
editor/convert_colors_with_editor_theme=false
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
; Engine configuration file.
|
||||||
|
config_version=5
|
||||||
|
|
||||||
|
[application]
|
||||||
|
|
||||||
|
config/name="JuggleBoard"
|
||||||
|
run/main_scene="res://scenes/main.tscn"
|
||||||
|
config/features=PackedStringArray("4.7", "GL Compatibility")
|
||||||
|
config/icon="res://icon.svg"
|
||||||
|
|
||||||
|
[autoload]
|
||||||
|
|
||||||
|
Settings="*res://scripts/settings.gd"
|
||||||
|
|
||||||
|
[display]
|
||||||
|
|
||||||
|
window/size/viewport_width=1280
|
||||||
|
window/size/viewport_height=720
|
||||||
|
window/size/resizable=false
|
||||||
|
window/stretch/mode="canvas_items"
|
||||||
|
window/stretch/aspect="keep"
|
||||||
|
window/handheld/orientation=4
|
||||||
|
|
||||||
|
[input_devices]
|
||||||
|
|
||||||
|
pointing/emulate_touch_from_mouse=true
|
||||||
|
pointing/emulate_mouse_from_touch=true
|
||||||
|
|
||||||
|
[rendering]
|
||||||
|
|
||||||
|
renderer/rendering_method="gl_compatibility"
|
||||||
|
renderer/rendering_method.mobile="gl_compatibility"
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
[gd_scene load_steps=2 format=3 uid="uid://juggleboard_main"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://scripts/main.gd" id="1_main"]
|
||||||
|
|
||||||
|
[node name="Main" type="Node2D"]
|
||||||
|
script = ExtResource("1_main")
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
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 _tween: Tween
|
||||||
|
|
||||||
|
|
||||||
|
func _draw() -> void:
|
||||||
|
# ombra
|
||||||
|
draw_circle(Vector2(0, 5), radius, Color(0, 0, 0, 0.3))
|
||||||
|
# corpo
|
||||||
|
draw_circle(Vector2.ZERO, radius, ball_color)
|
||||||
|
# bordo
|
||||||
|
draw_arc(Vector2.ZERO, radius, 0.0, TAU, 48, Color(0, 0, 0, 0.45), 3.0)
|
||||||
|
# riflesso
|
||||||
|
draw_circle(Vector2(-radius * 0.28, -radius * 0.32), radius * 0.18, Color(1, 1, 1, 0.55))
|
||||||
|
|
||||||
|
|
||||||
|
func setup(cn: String, c: Color, home: Vector2, left: Vector2, r: float) -> void:
|
||||||
|
color_name = cn
|
||||||
|
ball_color = c
|
||||||
|
radius = r
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
## 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 := maxf(mt * 0.5, 0.01)
|
||||||
|
_tween = create_tween()
|
||||||
|
_tween.tween_property(self, "position", left_pos, 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.finished.connect(_on_motion_done)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_motion_done() -> void:
|
||||||
|
in_motion = false
|
||||||
|
returned.emit()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://d3kyc2ecf0aco
|
||||||
@@ -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
|
||||||
+456
@@ -0,0 +1,456 @@
|
|||||||
|
extends Node2D
|
||||||
|
|
||||||
|
## JuggleBoard Game — reaction & chaining arcade.
|
||||||
|
##
|
||||||
|
## Layout: 5 corsie orizzontali, 5 palline colorate a riposo a destra.
|
||||||
|
## Un bersaglio in alto indica quale pallina premere. Toccata, la pallina
|
||||||
|
## scivola a sinistra e poi torna a destra. Prima che scada il timeout devi
|
||||||
|
## premere il nuovo bersaglio per concatenare. Più concateni, più punti fai
|
||||||
|
## e più la difficoltà (velocità) cresce con il tempo trascorso e il punteggio.
|
||||||
|
##
|
||||||
|
## Un nuovo bersaglio appare SOLO se c'è almeno una pallina a riposo.
|
||||||
|
|
||||||
|
const VIEW := Vector2(1280, 720)
|
||||||
|
const LANE_TOP := 210.0
|
||||||
|
const LANE_GAP := 98.0
|
||||||
|
const BALL_RADIUS := 40.0
|
||||||
|
const HOME_X := 1130.0
|
||||||
|
const LEFT_X := 300.0
|
||||||
|
const HIT_RADIUS := BALL_RADIUS * 1.4 # area di tocco/clic (maggiorata)
|
||||||
|
|
||||||
|
# I valori di calibrazione (velocità, moltiplicatori, decay) sono gestiti dal
|
||||||
|
# singleton Settings e modificabili dal menù di configurazione in-game.
|
||||||
|
|
||||||
|
const COLORS := {
|
||||||
|
"rosso": Color("e74c3c"),
|
||||||
|
"blu": Color("3498db"),
|
||||||
|
"verde": Color("2ecc71"),
|
||||||
|
"giallo": Color("f1c40f"),
|
||||||
|
"viola": Color("9b59b6"),
|
||||||
|
}
|
||||||
|
|
||||||
|
var balls := {} # color_name -> Ball
|
||||||
|
var target_color := "" # bersaglio corrente (vuoto = nessun bersaglio)
|
||||||
|
var score := 0
|
||||||
|
var combo := 0.0 # float: decade col ritardo di reazione
|
||||||
|
var elapsed := 0.0
|
||||||
|
var window_timer := 0.0
|
||||||
|
var window_time := Settings.start_time
|
||||||
|
var next_milestone := 100
|
||||||
|
|
||||||
|
# UI
|
||||||
|
var target_dot: Node2D
|
||||||
|
var score_label: Label
|
||||||
|
var combo_label: Label
|
||||||
|
var help_label: Label
|
||||||
|
var milestone_label: Label
|
||||||
|
var flash: ColorRect
|
||||||
|
|
||||||
|
# Audio
|
||||||
|
var sound_launch: AudioStreamWAV
|
||||||
|
var sound_error: AudioStreamWAV
|
||||||
|
var sound_timeout: AudioStreamWAV
|
||||||
|
var sound_milestone: AudioStreamWAV
|
||||||
|
var bgm: AudioStreamPlayer
|
||||||
|
var options_menu: CanvasLayer
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
_build_lanes()
|
||||||
|
_build_balls()
|
||||||
|
_build_ui()
|
||||||
|
_init_audio()
|
||||||
|
_setup_bgm()
|
||||||
|
options_menu = load("res://scripts/options_menu.gd").new()
|
||||||
|
add_child(options_menu)
|
||||||
|
_new_game()
|
||||||
|
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
elapsed += delta
|
||||||
|
if target_color == "":
|
||||||
|
return # in attesa che una pallina torni a casa: nessun countdown
|
||||||
|
# il combo decade in proporzione al tempo che aspetto prima di cliccare
|
||||||
|
if combo > 0.0:
|
||||||
|
combo = maxf(0.0, combo - _combo_decay() * delta)
|
||||||
|
window_timer += delta
|
||||||
|
if window_timer >= window_time:
|
||||||
|
_on_timeout()
|
||||||
|
|
||||||
|
|
||||||
|
# Input globale: mouse click e touch tap, convertiti in coordinate di gioco.
|
||||||
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
|
if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE:
|
||||||
|
_toggle_options()
|
||||||
|
return
|
||||||
|
var pos := Vector2.INF
|
||||||
|
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
|
||||||
|
pos = event.position
|
||||||
|
elif event is InputEventScreenTouch and event.pressed:
|
||||||
|
pos = event.position
|
||||||
|
if pos == Vector2.INF:
|
||||||
|
return
|
||||||
|
_handle_click(pos)
|
||||||
|
|
||||||
|
|
||||||
|
func _open_options() -> void:
|
||||||
|
if options_menu:
|
||||||
|
options_menu.show_menu()
|
||||||
|
|
||||||
|
|
||||||
|
func _toggle_options() -> void:
|
||||||
|
if options_menu:
|
||||||
|
if options_menu.visible:
|
||||||
|
options_menu.hide_menu()
|
||||||
|
else:
|
||||||
|
options_menu.show_menu()
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- costruzione
|
||||||
|
|
||||||
|
func _build_lanes() -> void:
|
||||||
|
for i in 5:
|
||||||
|
var y := LANE_TOP + i * LANE_GAP
|
||||||
|
var line := Line2D.new()
|
||||||
|
line.add_point(Vector2(90, y))
|
||||||
|
line.add_point(Vector2(VIEW.x - 90, y))
|
||||||
|
line.width = 2
|
||||||
|
line.default_color = Color(1, 1, 1, 0.12)
|
||||||
|
add_child(line)
|
||||||
|
var lbl := Label.new()
|
||||||
|
lbl.text = COLORS.keys()[i].to_upper()
|
||||||
|
lbl.position = Vector2(100, y - 44)
|
||||||
|
lbl.add_theme_font_size_override("font_size", 16)
|
||||||
|
lbl.add_theme_color_override("font_color", Color(1, 1, 1, 0.35))
|
||||||
|
add_child(lbl)
|
||||||
|
|
||||||
|
|
||||||
|
func _build_balls() -> void:
|
||||||
|
var names := COLORS.keys()
|
||||||
|
for i in names.size():
|
||||||
|
var cn: String = names[i]
|
||||||
|
var home := Vector2(HOME_X, LANE_TOP + i * LANE_GAP)
|
||||||
|
var left := Vector2(LEFT_X, LANE_TOP + i * LANE_GAP)
|
||||||
|
var b: Area2D = load("res://scripts/ball.gd").new()
|
||||||
|
add_child(b)
|
||||||
|
b.setup(cn, COLORS[cn], home, left, BALL_RADIUS)
|
||||||
|
b.returned.connect(_on_ball_returned)
|
||||||
|
balls[cn] = b
|
||||||
|
|
||||||
|
|
||||||
|
func _build_ui() -> void:
|
||||||
|
var layer := CanvasLayer.new()
|
||||||
|
add_child(layer)
|
||||||
|
|
||||||
|
var bar := ColorRect.new()
|
||||||
|
bar.color = Color(0.08, 0.09, 0.12, 0.95)
|
||||||
|
bar.position = Vector2.ZERO
|
||||||
|
bar.size = Vector2(VIEW.x, 140)
|
||||||
|
layer.add_child(bar)
|
||||||
|
|
||||||
|
var edge := ColorRect.new()
|
||||||
|
edge.color = Color(1, 1, 1, 0.15)
|
||||||
|
edge.position = Vector2(0, 140)
|
||||||
|
edge.size = Vector2(VIEW.x, 3)
|
||||||
|
layer.add_child(edge)
|
||||||
|
|
||||||
|
var tlabel := Label.new()
|
||||||
|
tlabel.text = "BERSAGLIO"
|
||||||
|
tlabel.position = Vector2(46, 26)
|
||||||
|
tlabel.add_theme_font_size_override("font_size", 22)
|
||||||
|
tlabel.add_theme_color_override("font_color", Color(1, 1, 1, 0.8))
|
||||||
|
layer.add_child(tlabel)
|
||||||
|
|
||||||
|
target_dot = load("res://scripts/target_dot.gd").new()
|
||||||
|
target_dot.position = Vector2(128, 84)
|
||||||
|
layer.add_child(target_dot)
|
||||||
|
|
||||||
|
help_label = Label.new()
|
||||||
|
help_label.text = "Tocca la pallina corrispondente al bersaglio per lanciarla!"
|
||||||
|
help_label.position = Vector2(210, 96)
|
||||||
|
help_label.add_theme_font_size_override("font_size", 18)
|
||||||
|
help_label.add_theme_color_override("font_color", Color(1, 1, 1, 0.6))
|
||||||
|
layer.add_child(help_label)
|
||||||
|
|
||||||
|
score_label = Label.new()
|
||||||
|
score_label.position = Vector2(VIEW.x - 320, 22)
|
||||||
|
score_label.add_theme_font_size_override("font_size", 34)
|
||||||
|
layer.add_child(score_label)
|
||||||
|
|
||||||
|
combo_label = Label.new()
|
||||||
|
combo_label.position = Vector2(VIEW.x - 320, 68)
|
||||||
|
combo_label.add_theme_font_size_override("font_size", 20)
|
||||||
|
combo_label.add_theme_color_override("font_color", Color(1, 1, 1, 0.65))
|
||||||
|
layer.add_child(combo_label)
|
||||||
|
|
||||||
|
# pulsante Opzioni
|
||||||
|
var opts_btn := Button.new()
|
||||||
|
opts_btn.text = "⚙ Opzioni (Esc)"
|
||||||
|
opts_btn.position = Vector2(VIEW.x / 2 - 100, 26)
|
||||||
|
opts_btn.pressed.connect(_open_options)
|
||||||
|
layer.add_child(opts_btn)
|
||||||
|
|
||||||
|
# feedback celebrativo alle milestone
|
||||||
|
milestone_label = Label.new()
|
||||||
|
milestone_label.position = Vector2(0, 160)
|
||||||
|
milestone_label.size = Vector2(VIEW.x, 120)
|
||||||
|
milestone_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
milestone_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||||
|
milestone_label.add_theme_font_size_override("font_size", 56)
|
||||||
|
milestone_label.add_theme_color_override("font_color", Color(1.0, 0.9, 0.4))
|
||||||
|
milestone_label.visible = false
|
||||||
|
layer.add_child(milestone_label)
|
||||||
|
|
||||||
|
flash = ColorRect.new()
|
||||||
|
flash.color = Color(1, 1, 0.7)
|
||||||
|
flash.position = Vector2.ZERO
|
||||||
|
flash.size = VIEW
|
||||||
|
flash.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
flash.modulate.a = 0.0
|
||||||
|
flash.visible = false
|
||||||
|
layer.add_child(flash)
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------- audio
|
||||||
|
|
||||||
|
func _init_audio() -> void:
|
||||||
|
sound_launch = _create_tone(587.33, 0.12, "sine") # Re5
|
||||||
|
sound_error = _create_tone(164.81, 0.18, "square") # Mi3 basso
|
||||||
|
sound_timeout = _create_tone(220.0, 0.22, "saw") # La3
|
||||||
|
sound_milestone = _create_arpeggio([523.25, 659.25, 783.99, 1046.5], 0.12) # Do5 Mi5 Sol5 Do6
|
||||||
|
|
||||||
|
|
||||||
|
func _setup_bgm() -> void:
|
||||||
|
# Musica di sottofondo da circo: traccia lunga generata via API Lyria,
|
||||||
|
# compressa in OGG Vorbis (loop abilitato nelle impostazioni di import),
|
||||||
|
# con fallback alla versione procedurale se il file non è presente.
|
||||||
|
bgm = AudioStreamPlayer.new()
|
||||||
|
var stream: AudioStream = load("res://audio/circus.ogg")
|
||||||
|
if stream:
|
||||||
|
bgm.stream = stream
|
||||||
|
else:
|
||||||
|
bgm.stream = load("res://scripts/circus_music.gd").generate()
|
||||||
|
bgm.volume_db = -14.0
|
||||||
|
bgm.process_mode = Node.PROCESS_MODE_ALWAYS # continua anche a menù aperto
|
||||||
|
add_child(bgm)
|
||||||
|
bgm.play()
|
||||||
|
|
||||||
|
|
||||||
|
func _create_arpeggio(freqs: Array, note_dur: float = 0.12) -> AudioStreamWAV:
|
||||||
|
var sample_rate := 22050
|
||||||
|
var total := 0
|
||||||
|
for f in freqs:
|
||||||
|
total += int(sample_rate * note_dur)
|
||||||
|
var data := PackedByteArray()
|
||||||
|
data.resize(total)
|
||||||
|
var idx := 0
|
||||||
|
for f in freqs:
|
||||||
|
var n := int(sample_rate * note_dur)
|
||||||
|
for i in n:
|
||||||
|
var t := float(i) / sample_rate
|
||||||
|
var env := 1.0 - (float(i) / n)
|
||||||
|
var s := sin(t * f * TAU) * env
|
||||||
|
var val := int(clampf(s * 0.35 * 127.0, -128.0, 127.0))
|
||||||
|
data[idx] = val & 0xFF
|
||||||
|
idx += 1
|
||||||
|
var stream := AudioStreamWAV.new()
|
||||||
|
stream.format = AudioStreamWAV.FORMAT_8_BITS
|
||||||
|
stream.mix_rate = sample_rate
|
||||||
|
stream.data = data
|
||||||
|
return stream
|
||||||
|
|
||||||
|
|
||||||
|
func _create_tone(freq: float, duration: float, wave_type: String = "sine") -> AudioStreamWAV:
|
||||||
|
var sample_rate := 22050
|
||||||
|
var num_samples := int(sample_rate * duration)
|
||||||
|
var data := PackedByteArray()
|
||||||
|
data.resize(num_samples)
|
||||||
|
for i in num_samples:
|
||||||
|
var t := float(i) / sample_rate
|
||||||
|
var env := 1.0 - (float(i) / num_samples)
|
||||||
|
var s := 0.0
|
||||||
|
if wave_type == "sine":
|
||||||
|
s = sin(t * freq * TAU)
|
||||||
|
elif wave_type == "square":
|
||||||
|
s = 1.0 if sin(t * freq * TAU) > 0.0 else -1.0
|
||||||
|
elif wave_type == "saw":
|
||||||
|
s = fmod(t * freq, 1.0) * 2.0 - 1.0
|
||||||
|
var val := int(clampf(s * env * 0.35 * 127.0, -128.0, 127.0))
|
||||||
|
data[i] = val & 0xFF
|
||||||
|
var stream := AudioStreamWAV.new()
|
||||||
|
stream.format = AudioStreamWAV.FORMAT_8_BITS
|
||||||
|
stream.mix_rate = sample_rate
|
||||||
|
stream.data = data
|
||||||
|
return stream
|
||||||
|
|
||||||
|
|
||||||
|
func _play_sound(stream: AudioStreamWAV) -> void:
|
||||||
|
if stream == null:
|
||||||
|
return
|
||||||
|
var player := AudioStreamPlayer.new()
|
||||||
|
player.stream = stream
|
||||||
|
add_child(player)
|
||||||
|
player.finished.connect(player.queue_free)
|
||||||
|
player.play()
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------- gioco
|
||||||
|
|
||||||
|
func _new_game() -> void:
|
||||||
|
score = 0
|
||||||
|
combo = 0.0
|
||||||
|
elapsed = 0.0
|
||||||
|
window_timer = 0.0
|
||||||
|
window_time = Settings.start_time
|
||||||
|
next_milestone = 100
|
||||||
|
for b in balls.values():
|
||||||
|
b.reset()
|
||||||
|
_pick_target()
|
||||||
|
help_label.visible = true
|
||||||
|
_update_hud()
|
||||||
|
|
||||||
|
|
||||||
|
func _handle_click(pos: Vector2) -> void:
|
||||||
|
var clicked: Area2D = null
|
||||||
|
var best := HIT_RADIUS
|
||||||
|
for b in balls.values():
|
||||||
|
if not b.is_home():
|
||||||
|
continue
|
||||||
|
var d := pos.distance_to(b.position)
|
||||||
|
if d < best:
|
||||||
|
best = d
|
||||||
|
clicked = b
|
||||||
|
if clicked == null:
|
||||||
|
return # click su spazio vuoto
|
||||||
|
|
||||||
|
if clicked.color_name == target_color:
|
||||||
|
# corretto: suono lancio, punti, nuovo bersaglio, reset timer
|
||||||
|
combo += 1.0
|
||||||
|
score += _points_for_combo()
|
||||||
|
_play_sound(sound_launch)
|
||||||
|
var mt := _current_time()
|
||||||
|
clicked.launch(mt)
|
||||||
|
window_time = mt
|
||||||
|
window_timer = 0.0
|
||||||
|
_pick_target()
|
||||||
|
_update_hud()
|
||||||
|
_check_milestone()
|
||||||
|
else:
|
||||||
|
# tasto sbagliato: suono errore, reset combo, nessun game over
|
||||||
|
_play_sound(sound_error)
|
||||||
|
combo = 0.0
|
||||||
|
_update_hud()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_timeout() -> void:
|
||||||
|
# tempo scaduto: suono timeout, reset combo, nuovo bersaglio (se possibile), nessun game over
|
||||||
|
_play_sound(sound_timeout)
|
||||||
|
combo = 0.0
|
||||||
|
window_timer = 0.0
|
||||||
|
_pick_target()
|
||||||
|
_update_hud()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_ball_returned() -> void:
|
||||||
|
# quando una pallina torna a casa e non c'è bersaglio, se ne può mostrare uno
|
||||||
|
if target_color == "":
|
||||||
|
_pick_target()
|
||||||
|
|
||||||
|
|
||||||
|
func _current_time() -> float:
|
||||||
|
var d := _difficulty()
|
||||||
|
return lerpf(Settings.start_time, Settings.min_time, d)
|
||||||
|
|
||||||
|
|
||||||
|
func _difficulty() -> float:
|
||||||
|
# fattori lineari di tempo e punteggio
|
||||||
|
var tf := clampf(elapsed / Settings.time_ramp, 0.0, 1.0)
|
||||||
|
var sf := clampf(score / Settings.score_ramp, 0.0, 1.0)
|
||||||
|
# curva dolce: smoothstep rende l'accelerazione quasi impercettibile all'inizio
|
||||||
|
return _smoothstep(maxf(tf, sf))
|
||||||
|
|
||||||
|
|
||||||
|
func _combo_decay() -> float:
|
||||||
|
# Decadimento proporzionale: quasi nullo a combo basso / inizio partita,
|
||||||
|
# cresce man mano che combo e difficoltà aumentano.
|
||||||
|
var combo_pen := maxf(0.0, combo - Settings.combo_decay_threshold) * Settings.combo_decay_rate
|
||||||
|
var diff_pen := _difficulty() * Settings.difficulty_decay_rate
|
||||||
|
return combo_pen + diff_pen
|
||||||
|
|
||||||
|
|
||||||
|
func _points_for_combo() -> int:
|
||||||
|
# Punti per un tap corretto, in base alla curva selezionata nelle Opzioni.
|
||||||
|
match Settings.score_curve:
|
||||||
|
1: # scaglioni: +0.5x ogni 5 tap di combo
|
||||||
|
var mult := 1.0 + floori(combo / 5.0) * 0.5
|
||||||
|
return int(Settings.points_per_combo * mult)
|
||||||
|
2: # radice quadrata
|
||||||
|
return int(Settings.points_per_combo * sqrt(combo))
|
||||||
|
3: # additiva: base + combo
|
||||||
|
return Settings.points_per_combo + int(combo)
|
||||||
|
_: # 0 = quadratica (attuale): base × combo
|
||||||
|
return Settings.points_per_combo * int(combo)
|
||||||
|
|
||||||
|
|
||||||
|
func _smoothstep(x: float) -> float:
|
||||||
|
return x * x * (3.0 - 2.0 * x)
|
||||||
|
|
||||||
|
|
||||||
|
func _pick_target() -> void:
|
||||||
|
var homes := _home_names()
|
||||||
|
if homes.is_empty():
|
||||||
|
target_color = ""
|
||||||
|
target_dot.visible = false
|
||||||
|
return
|
||||||
|
var pool := homes.duplicate()
|
||||||
|
if target_color in pool and pool.size() > 1:
|
||||||
|
pool.erase(target_color)
|
||||||
|
target_color = pool[randi() % pool.size()]
|
||||||
|
target_dot.set_color(COLORS[target_color])
|
||||||
|
target_dot.visible = true
|
||||||
|
|
||||||
|
|
||||||
|
func _home_names() -> Array:
|
||||||
|
var arr := []
|
||||||
|
for cn in balls.keys():
|
||||||
|
if balls[cn].is_home():
|
||||||
|
arr.append(cn)
|
||||||
|
return arr
|
||||||
|
|
||||||
|
|
||||||
|
func _update_hud() -> void:
|
||||||
|
score_label.text = "PUNTI %d" % score
|
||||||
|
combo_label.text = "Combo x%d" % int(combo)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------- feedback alle milestone
|
||||||
|
|
||||||
|
func _check_milestone() -> void:
|
||||||
|
if score < next_milestone:
|
||||||
|
return
|
||||||
|
# supera più traguardi in una volta: celebra il più alto raggiunto
|
||||||
|
var celebrated := next_milestone
|
||||||
|
while next_milestone <= score:
|
||||||
|
celebrated = next_milestone
|
||||||
|
next_milestone *= 10
|
||||||
|
_celebrate(celebrated)
|
||||||
|
|
||||||
|
|
||||||
|
func _celebrate(amount: int) -> void:
|
||||||
|
_play_sound(sound_milestone)
|
||||||
|
milestone_label.text = "%d PUNTI!" % amount
|
||||||
|
milestone_label.visible = true
|
||||||
|
milestone_label.modulate.a = 1.0
|
||||||
|
milestone_label.scale = Vector2(0.6, 0.6)
|
||||||
|
flash.visible = true
|
||||||
|
flash.modulate.a = 0.18
|
||||||
|
var tw := create_tween()
|
||||||
|
tw.set_parallel(true)
|
||||||
|
tw.tween_property(milestone_label, "scale", Vector2(1.3, 1.3), 0.4) \
|
||||||
|
.set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
|
||||||
|
tw.tween_property(milestone_label, "modulate:a", 0.0, 0.8).set_delay(0.6)
|
||||||
|
tw.tween_property(flash, "modulate:a", 0.0, 0.4)
|
||||||
|
tw.chain().tween_callback(func():
|
||||||
|
milestone_label.visible = false
|
||||||
|
flash.visible = false
|
||||||
|
)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://4vue3sdlahr5
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
extends CanvasLayer
|
||||||
|
## Menù di configurazione in-game: slider/spinbox per calibrare velocità e
|
||||||
|
## moltiplicatori. "Salva e riavvia" persiste le impostazioni e ricarica la
|
||||||
|
## scena applicando i nuovi valori.
|
||||||
|
|
||||||
|
var rows := {} # key -> {slider, spin}
|
||||||
|
var curve_option: OptionButton
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
# Critico: il menù deve ricevere input anche quando il gioco è in pausa,
|
||||||
|
# altrimenti slider/spinbox/pulsanti risultano congelati e inutilizzabili.
|
||||||
|
process_mode = Node.PROCESS_MODE_ALWAYS
|
||||||
|
visible = false
|
||||||
|
_build()
|
||||||
|
|
||||||
|
|
||||||
|
func _build() -> void:
|
||||||
|
# sfondo scuro che blocca i click sottostanti
|
||||||
|
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)
|
||||||
|
|
||||||
|
# pannello centrato
|
||||||
|
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 vb := VBoxContainer.new()
|
||||||
|
vb.add_theme_constant_override("separation", 8)
|
||||||
|
margin.add_child(vb)
|
||||||
|
|
||||||
|
var title := Label.new()
|
||||||
|
title.text = "CONFIGURAZIONE"
|
||||||
|
title.add_theme_font_size_override("font_size", 30)
|
||||||
|
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
vb.add_child(title)
|
||||||
|
|
||||||
|
var entries := [
|
||||||
|
["start_time", "Tempo iniziale (s)", 1.0, 10.0, 0.1],
|
||||||
|
["min_time", "Tempo minimo (s)", 0.3, 3.0, 0.1],
|
||||||
|
["time_ramp", "Rampa tempo (s)", 30.0, 600.0, 10.0],
|
||||||
|
["score_ramp", "Rampa punteggio", 100.0, 10000.0, 100.0],
|
||||||
|
["points_per_combo", "Punti per combo", 1.0, 50.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],
|
||||||
|
]
|
||||||
|
for e in entries:
|
||||||
|
rows[e[0]] = _build_row(vb, e[0], e[1], e[2], e[3], e[4])
|
||||||
|
|
||||||
|
# selettore curva punteggio
|
||||||
|
var ccurve := HBoxContainer.new()
|
||||||
|
ccurve.add_theme_constant_override("separation", 6)
|
||||||
|
vb.add_child(ccurve)
|
||||||
|
var clbl := Label.new()
|
||||||
|
clbl.text = "Curva punteggio"
|
||||||
|
clbl.custom_minimum_size.x = 230
|
||||||
|
ccurve.add_child(clbl)
|
||||||
|
curve_option = OptionButton.new()
|
||||||
|
curve_option.custom_minimum_size.x = 360
|
||||||
|
curve_option.add_item("Quadratica (attuale)")
|
||||||
|
curve_option.add_item("A scaglioni (ogni 5 tap)")
|
||||||
|
curve_option.add_item("Radice quadrata")
|
||||||
|
curve_option.add_item("Additiva (base + combo)")
|
||||||
|
curve_option.select(Settings.score_curve)
|
||||||
|
curve_option.item_selected.connect(func(idx: int): Settings.score_curve = idx)
|
||||||
|
ccurve.add_child(curve_option)
|
||||||
|
|
||||||
|
# pulsanti
|
||||||
|
var hb := HBoxContainer.new()
|
||||||
|
hb.add_theme_constant_override("separation", 8)
|
||||||
|
vb.add_child(hb)
|
||||||
|
var save := Button.new()
|
||||||
|
save.text = "Salva e riavvia"
|
||||||
|
save.pressed.connect(_on_save)
|
||||||
|
var reset := Button.new()
|
||||||
|
reset.text = "Ripristina default"
|
||||||
|
reset.pressed.connect(_on_reset)
|
||||||
|
var cancel := Button.new()
|
||||||
|
cancel.text = "Annulla"
|
||||||
|
cancel.pressed.connect(_on_cancel)
|
||||||
|
hb.add_child(save)
|
||||||
|
hb.add_child(reset)
|
||||||
|
hb.add_child(cancel)
|
||||||
|
|
||||||
|
|
||||||
|
func _build_row(parent: Node, key: String, label_text: String, mn: float, mx: float, step: float) -> Dictionary:
|
||||||
|
var hb := HBoxContainer.new()
|
||||||
|
hb.add_theme_constant_override("separation", 6)
|
||||||
|
parent.add_child(hb)
|
||||||
|
|
||||||
|
var lbl := Label.new()
|
||||||
|
lbl.text = label_text
|
||||||
|
lbl.custom_minimum_size.x = 230
|
||||||
|
hb.add_child(lbl)
|
||||||
|
|
||||||
|
var slider := HSlider.new()
|
||||||
|
slider.min_value = mn
|
||||||
|
slider.max_value = mx
|
||||||
|
slider.step = step
|
||||||
|
slider.custom_minimum_size.x = 260
|
||||||
|
slider.custom_minimum_size.y = 40
|
||||||
|
slider.add_theme_constant_override("grabber_radius", 14) # più facile da toccare
|
||||||
|
slider.value = Settings.get(key)
|
||||||
|
hb.add_child(slider)
|
||||||
|
|
||||||
|
var spin := SpinBox.new()
|
||||||
|
spin.min_value = mn
|
||||||
|
spin.max_value = mx
|
||||||
|
spin.step = step
|
||||||
|
spin.value = Settings.get(key)
|
||||||
|
spin.custom_minimum_size.x = 90
|
||||||
|
hb.add_child(spin)
|
||||||
|
|
||||||
|
slider.value_changed.connect(func(v: float):
|
||||||
|
spin.value = v
|
||||||
|
Settings.set(key, v))
|
||||||
|
spin.value_changed.connect(func(v: float):
|
||||||
|
slider.value = v
|
||||||
|
Settings.set(key, v))
|
||||||
|
|
||||||
|
return {"slider": slider, "spin": spin}
|
||||||
|
|
||||||
|
|
||||||
|
func show_menu() -> void:
|
||||||
|
# aggiorna i valori correnti ogni volta che si apre
|
||||||
|
for key in rows:
|
||||||
|
var val: float = Settings.get(key)
|
||||||
|
rows[key].slider.value = val
|
||||||
|
rows[key].spin.value = val
|
||||||
|
if curve_option:
|
||||||
|
curve_option.select(Settings.score_curve)
|
||||||
|
visible = true
|
||||||
|
get_tree().paused = true
|
||||||
|
|
||||||
|
|
||||||
|
func hide_menu() -> void:
|
||||||
|
visible = false
|
||||||
|
get_tree().paused = false
|
||||||
|
|
||||||
|
|
||||||
|
func _on_save() -> void:
|
||||||
|
Settings.save_settings()
|
||||||
|
hide_menu()
|
||||||
|
get_tree().call_deferred("reload_current_scene")
|
||||||
|
|
||||||
|
|
||||||
|
func _on_reset() -> void:
|
||||||
|
Settings.reset_defaults()
|
||||||
|
for key in rows:
|
||||||
|
var val: float = Settings.get(key)
|
||||||
|
rows[key].slider.value = val
|
||||||
|
rows[key].spin.value = val
|
||||||
|
if curve_option:
|
||||||
|
curve_option.select(Settings.score_curve)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_cancel() -> void:
|
||||||
|
hide_menu()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dnr1wdhqehwnu
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
extends Node
|
||||||
|
## Singleton Settings: valori di calibrazione del gameplay, caricati/salvati
|
||||||
|
## in user://settings.cfg. Modificati dal menù di configurazione e applicati
|
||||||
|
## al riavvio della scena.
|
||||||
|
|
||||||
|
const PATH := "user://settings.cfg"
|
||||||
|
|
||||||
|
var start_time := 4.5 # tempo iniziale (s) tra un ordine e l'altro
|
||||||
|
var min_time := 0.8 # tempo minimo (velocità massima)
|
||||||
|
var time_ramp := 180.0 # secondi di gioco per difficoltà piena
|
||||||
|
var score_ramp := 2000.0 # punteggio per difficoltà piena
|
||||||
|
var points_per_combo := 10 # punti base per ogni livello di combo
|
||||||
|
var combo_decay_threshold := 5.0 # combo sopra cui inizia il decay
|
||||||
|
var combo_decay_rate := 0.06 # combo persi per unità sopra la soglia
|
||||||
|
var difficulty_decay_rate := 0.3 # decay aggiuntivo legato alla difficoltà
|
||||||
|
# Curva del punteggio: 0=quadratica, 1=scaglioni, 2=radice, 3=additiva
|
||||||
|
var score_curve := 0
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
load_settings()
|
||||||
|
|
||||||
|
|
||||||
|
func load_settings() -> void:
|
||||||
|
var cfg := ConfigFile.new()
|
||||||
|
if cfg.load(PATH) != OK:
|
||||||
|
return # usa i default
|
||||||
|
start_time = cfg.get_value("gameplay", "start_time", start_time)
|
||||||
|
min_time = cfg.get_value("gameplay", "min_time", min_time)
|
||||||
|
time_ramp = cfg.get_value("gameplay", "time_ramp", time_ramp)
|
||||||
|
score_ramp = cfg.get_value("gameplay", "score_ramp", score_ramp)
|
||||||
|
points_per_combo = cfg.get_value("gameplay", "points_per_combo", points_per_combo)
|
||||||
|
combo_decay_threshold = cfg.get_value("gameplay", "combo_decay_threshold", combo_decay_threshold)
|
||||||
|
combo_decay_rate = cfg.get_value("gameplay", "combo_decay_rate", combo_decay_rate)
|
||||||
|
difficulty_decay_rate = cfg.get_value("gameplay", "difficulty_decay_rate", difficulty_decay_rate)
|
||||||
|
score_curve = cfg.get_value("gameplay", "score_curve", score_curve)
|
||||||
|
|
||||||
|
|
||||||
|
func save_settings() -> void:
|
||||||
|
var cfg := ConfigFile.new()
|
||||||
|
cfg.set_value("gameplay", "start_time", start_time)
|
||||||
|
cfg.set_value("gameplay", "min_time", min_time)
|
||||||
|
cfg.set_value("gameplay", "time_ramp", time_ramp)
|
||||||
|
cfg.set_value("gameplay", "score_ramp", score_ramp)
|
||||||
|
cfg.set_value("gameplay", "points_per_combo", points_per_combo)
|
||||||
|
cfg.set_value("gameplay", "combo_decay_threshold", combo_decay_threshold)
|
||||||
|
cfg.set_value("gameplay", "combo_decay_rate", combo_decay_rate)
|
||||||
|
cfg.set_value("gameplay", "difficulty_decay_rate", difficulty_decay_rate)
|
||||||
|
cfg.set_value("gameplay", "score_curve", score_curve)
|
||||||
|
cfg.save(PATH)
|
||||||
|
|
||||||
|
|
||||||
|
func reset_defaults() -> void:
|
||||||
|
start_time = 4.5
|
||||||
|
min_time = 0.8
|
||||||
|
time_ramp = 180.0
|
||||||
|
score_ramp = 2000.0
|
||||||
|
points_per_combo = 10
|
||||||
|
combo_decay_threshold = 5.0
|
||||||
|
combo_decay_rate = 0.06
|
||||||
|
difficulty_decay_rate = 0.3
|
||||||
|
score_curve = 0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://tvu3ae4txaud
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
extends Node2D
|
||||||
|
|
||||||
|
## Pallina di colore che mostra il bersaglio attuale nella barra in alto.
|
||||||
|
|
||||||
|
var dot_color := Color.WHITE
|
||||||
|
|
||||||
|
|
||||||
|
func _draw() -> void:
|
||||||
|
draw_circle(Vector2(0, 4), 36.0, Color(0, 0, 0, 0.3))
|
||||||
|
draw_circle(Vector2.ZERO, 36.0, dot_color)
|
||||||
|
draw_arc(Vector2.ZERO, 36.0, 0.0, TAU, 40, Color(0, 0, 0, 0.4), 3.0)
|
||||||
|
|
||||||
|
|
||||||
|
func set_color(c: Color) -> void:
|
||||||
|
dot_color = c
|
||||||
|
queue_redraw()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://b1xr5s67yiov
|
||||||
Reference in New Issue
Block a user