12 Commits
76 changed files with 2223 additions and 64 deletions
+27
View File
@@ -4,6 +4,7 @@ extends Node
const MAIN_MENU_SCENE := preload("res://menus/scenes/main_menu.tscn")
const JUGGLEBOARD_SCENE := preload("res://games/juggleboard/scenes/juggleboard.tscn")
const JUGGLING_GYM_SCENE := preload("res://games/juggling_gym/scenes/juggling_gym.tscn")
var main_menu: MainMenuController
var active_game: Node
@@ -19,6 +20,7 @@ func _create_menus() -> void:
# P2A avvia direttamente Target: il vecchio dialog Target/Infinita non viene
# più creato né mostrato.
main_menu.mode_requested.connect(_start_juggleboard)
main_menu.gym_requested.connect(_start_juggling_gym)
main_menu.exit_requested.connect(_exit_app)
add_child(main_menu)
@@ -29,6 +31,10 @@ func _show_debug_target_or_home() -> void:
_start_juggleboard()
if args.has("--capture-menus"):
_capture_game_after_frame()
elif args.has("--start-gym"):
_start_juggling_gym()
if args.has("--capture-menus"):
_capture_gym_after_frame()
elif args.has("--capture-mode-dialog"):
# Compatibilità del flag di debug: il dialog non fa più parte del flusso.
main_menu.show_juggle()
@@ -47,6 +53,13 @@ func _return_to_juggle_menu() -> void:
main_menu.show_juggle()
func _return_to_gym_menu() -> void:
if is_instance_valid(active_game):
active_game.queue_free()
active_game = null
main_menu.show_gym()
func _start_juggleboard() -> void:
main_menu.hide_menu()
if is_instance_valid(active_game):
@@ -56,11 +69,25 @@ func _start_juggleboard() -> void:
add_child(active_game)
func _start_juggling_gym() -> void:
main_menu.hide_menu()
if is_instance_valid(active_game):
active_game.queue_free()
active_game = JUGGLING_GYM_SCENE.instantiate()
active_game.return_requested.connect(_return_to_gym_menu)
add_child(active_game)
func _capture_game_after_frame() -> void:
await RenderingServer.frame_post_draw
get_viewport().get_texture().get_image().save_png("/tmp/autgame_juggleboard.png")
func _capture_gym_after_frame() -> void:
await RenderingServer.frame_post_draw
get_viewport().get_texture().get_image().save_png("/tmp/autgame_juggling_gym.png")
func _exit_app() -> void:
if not OS.has_feature("web"):
get_tree().quit()
+19 -4
View File
@@ -21,7 +21,7 @@ Questo documento descrive la struttura del repository dopo il refactoring che ha
| --- | --- | --- | --- |
| Home | `menus/scenes/p1_home.tscn` | `menus/scripts/screens/p1_home_screen.gd` | Disponibile |
| Menu JuggleBoard | `menus/scenes/p2a_juggle_menu.tscn` | `menus/scripts/screens/p2a_juggle_menu_screen.gd` | Disponibile |
| Palestra di giocoleria | `menus/scenes/p3a_gym_menu.tscn` | `menus/scripts/screens/p3a_gym_menu_screen.gd` | Anteprima: il pulsante mostra “Presto disponibile” |
| Palestra di giocoleria | `menus/scenes/p3a_gym_menu.tscn` | `menus/scripts/screens/p3a_gym_menu_screen.gd` | Disponibile: avvia il prototipo P3B |
| Selettore modalità | `menus/scenes/mode_select_dialog.tscn` | `menus/scripts/mode_select_dialog.gd` e `mode_select_view.gd` | Disponibile |
I componenti `menus/scripts/components/menu_ui.gd` e `menu_audio.gd` raccolgono rispettivamente costruzione visuale e feedback sonori. Le immagini e lo shader del menu risiedono in `menus/assets/`.
@@ -58,13 +58,22 @@ Le scene accessorie sono `options_menu.tscn`, `lock_menu.tscn` e `victory_overla
- `shared/settings/settings.gd`: persiste i parametri in `user://settings.cfg`. Incrementare `VERSION` quando cambiano i valori predefiniti.
- `shared/audio/audio_factory.gd`: genera e riproduce feedback sonori morbidi.
- `menus/assets/audio/upendi_sign_drop.wav` e `upendi_sign_bounce.wav`: effetti CC0 OpenGameArt dell’insegna, convertiti in WAV PCM sedici bit e riprodotti da `MenuAudio`.
- `shared/ui/`: definisce tema, finiture e cornici circensi riusabili.
- `shared/assets/`: contiene font e texture comuni.
- `shared/branding/`: contiene logo UPENDI e risorse del boot splash.
## Dominio futuro: Palestra di giocoleria
## Dominio Palestra di giocoleria
`games/juggling_gym/` conserva solo asset provvisori. Non esiste ancora una scena o un controller per questo dominio e `AppShell` non lo istanzia.
`games/juggling_gym/scenes/juggling_gym.tscn` usa `JugglingGymGame` per il prototipo P3B. Il mockup completo resta lo sfondo della scena; `GymTool` crea aree di trascinamento trasparenti sopra la barra inferiore e `GymDropZone` gestisce i tre bersagli sui giocolieri.
Nel primo prototipo sono attivi tre attrezzi, con associazione univoca:
- palline → giocoliere sinistro;
- piatti cinesi → giocoliere destro;
- sfera → giocoliere centrale.
Rola bola e clave sono visibili nel mockup ma non ancora disponibili. Un rilascio corretto incrementa il progresso, disabilita l’attrezzo già posizionato e mostra il feedback; un rilascio errato lascia l’attrezzo riprovabile.
## Verifica visuale
@@ -77,7 +86,13 @@ godot --path . -- --capture-menus --capture-p3a
godot --path . -- --capture-menus --capture-mode-dialog
```
Le immagini risultanti sono salvate in `/tmp/autgame_menu_*.png`. Per una cattura diretta del gioco usare:
Le immagini risultanti sono salvate in `/tmp/autgame_menu_*.png`. Per una cattura diretta della palestra usare:
```bash
godot --path . -- --start-gym --capture-menus
```
La cattura viene salvata in `/tmp/autgame_juggling_gym.png`. Per una cattura diretta di JuggleBoard usare:
```bash
godot --path . -- --start-juggleboard --capture-menus
+2 -2
View File
@@ -75,8 +75,8 @@ architectures/arm64-v8a=true
architectures/armeabi-v7a=true
architectures/x86=false
architectures/x86_64=false
version/code=1
version/name="0.1"
version/code=2
version/name="0.3"
package/unique_name="net.enne2.autgame"
package/name="JuggleBoard"
package/signed=true
@@ -1,7 +1,6 @@
extends Node2D
## Fondale P2B: plancia illustrata, pannello laterale e cinque corsie.
const VIEW := Vector2(1280, 720)
const P2B_BACKGROUND := preload("res://games/juggleboard/assets/p2b/board_background.png")
@@ -11,6 +10,6 @@ func _ready() -> void:
func _draw() -> void:
# L'illustrazione 2B contiene pannello, plancia vuota e alloggiamenti;
# gli elementi dinamici sono aggiunti sopra dal controller e dall'HUD.
draw_texture_rect(P2B_BACKGROUND, Rect2(Vector2.ZERO, VIEW), false)
# L'illustrazione P2B occupa il viewport realmente disponibile: con
# stretch=expand i display 20:9 non ricevono bande laterali.
draw_texture_rect(P2B_BACKGROUND, get_viewport_rect(), false)
+6 -2
View File
@@ -64,10 +64,14 @@ func _unhandled_input(event: InputEvent) -> void:
func _build_balls() -> void:
var names := JuggleBoardConfig.COLORS.keys()
var width_scale := JuggleBoardConfig.horizontal_scale(get_viewport().get_visible_rect().size.x)
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 lane_y := JuggleBoardConfig.LANE_TOP + index * JuggleBoardConfig.LANE_GAP
# Il fondale si espande in orizzontale: alloggiamenti e biglie seguono
# la stessa scala X, mentre dimensione e coordinate Y restano invariate.
var home := Vector2(JuggleBoardConfig.HOME_X * width_scale, lane_y)
var left := Vector2(JuggleBoardConfig.LEFT_X * width_scale, lane_y)
var ball: Area2D = BALL_SCRIPT.new()
add_child(ball)
ball.setup(color_name, JuggleBoardConfig.COLORS[color_name], home, left, JuggleBoardConfig.BALL_RADIUS)
@@ -23,6 +23,10 @@ const COLORS := {
"viola": Color("9b7ede"),
}
static func horizontal_scale(viewport_width: float) -> float:
return viewport_width / VIEW.x
const BALL_NOTES := {
"rosso": 261.63,
"blu": 293.66,
+8 -17
View File
@@ -26,25 +26,27 @@ func _ready() -> void:
func _build() -> void:
var ink := Color("#251b17")
var x_scale := JuggleBoardConfig.horizontal_scale(get_viewport().get_visible_rect().size.x)
# Node2D ha pivot locale (0, 0); target_dot disegna l'asset simmetricamente
# intorno a quel pivot, quindi si usa il centro misurato della cornice P2B.
target_dot = TARGET_SCRIPT.new()
target_dot.position = JuggleBoardConfig.TARGET_CENTER
target_dot.position = Vector2(JuggleBoardConfig.TARGET_CENTER.x * x_scale, JuggleBoardConfig.TARGET_CENTER.y)
add_child(target_dot)
back_button = _make_back_button()
add_child(back_button)
MenuUI.set_normalized_rect(back_button, MenuUI.BACK_BUTTON_CENTER, MenuUI.BACK_BUTTON_SIZE)
score_label = _make_label("", Vector2(112, 520), Vector2(220, 45), 25, ink)
score_label = _make_label("", Vector2(112 * x_scale, 520), Vector2(220 * x_scale, 45), 25, ink)
score_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
score_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
combo_label = _make_label("", Vector2(112, 606), Vector2(220, 45), 24, ink)
combo_label = _make_label("", Vector2(112 * x_scale, 606), Vector2(220 * x_scale, 45), 24, ink)
combo_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
combo_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
hearts = HEARTS_SCRIPT.new()
hearts.position = Vector2(84, 655)
hearts.position = Vector2(84 * x_scale, 655)
hearts.visible = false
add_child(hearts)
@@ -53,7 +55,7 @@ func _build() -> void:
help_label = _make_label("", Vector2.ZERO, Vector2.ZERO, 1, ink)
help_label.visible = false
milestone_label = _make_label("", Vector2(350, 20), Vector2(820, 80), 46, Color("#9e5420"))
milestone_label = _make_label("", Vector2(350 * x_scale, 20), Vector2(820 * x_scale, 80), 46, Color("#9e5420"))
milestone_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
milestone_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
milestone_label.visible = false
@@ -61,7 +63,7 @@ func _build() -> void:
flash = ColorRect.new()
flash.color = Color(1, 0.95, 0.72)
flash.position = Vector2.ZERO
flash.size = JuggleBoardConfig.VIEW
flash.size = get_viewport().get_visible_rect().size
flash.mouse_filter = Control.MOUSE_FILTER_IGNORE
flash.modulate.a = 0.0
flash.visible = false
@@ -76,17 +78,6 @@ func _make_back_button() -> TextureButton:
button.ignore_texture_size = true
button.stretch_mode = TextureButton.STRETCH_KEEP_ASPECT_CENTERED
button.tooltip_text = "Torna al menu Juggle"
# Margine sicuro relativo, uguale al bordo utile dei controlli nei menu.
# Ancore coincidenti: gli offset definiscono soltanto la dimensione del tasto.
button.set_anchors_preset(Control.PRESET_TOP_LEFT)
button.anchor_left = 0.03
button.anchor_top = 0.06
button.anchor_right = 0.03
button.anchor_bottom = 0.06
button.offset_left = 0.0
button.offset_top = 0.0
button.offset_right = 76.0
button.offset_bottom = 64.0
button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
button.mouse_entered.connect(func(): button.modulate = Color(1.10, 1.10, 1.10, 1.0))
button.mouse_exited.connect(func(): button.modulate = Color.WHITE)
Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://co326h8regi2h"
path="res://.godot/imported/3b_clave.png-c537858b9fbd3956ab9a550df77e804c.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://games/juggling_gym/assets/p3b/3b_clave.png"
dest_files=["res://.godot/imported/3b_clave.png-c537858b9fbd3956ab9a550df77e804c.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
Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://d0t5uuhs8h5pc"
path="res://.godot/imported/3b_palline.png-5ddf0613d452b9e0b2c85acbca6b4346.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://games/juggling_gym/assets/p3b/3b_palline.png"
dest_files=["res://.godot/imported/3b_palline.png-5ddf0613d452b9e0b2c85acbca6b4346.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
Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bksyo5eyln3bn"
path="res://.godot/imported/3b_piatti_cinesi.png-6fcf58842a46eddaee8c18c9eb5f8f1b.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://games/juggling_gym/assets/p3b/3b_piatti_cinesi.png"
dest_files=["res://.godot/imported/3b_piatti_cinesi.png-6fcf58842a46eddaee8c18c9eb5f8f1b.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
Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://e817cu4h4atb"
path="res://.godot/imported/3b_rola_bola.png-be80aae165f27e5790e26f3ecae6c880.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://games/juggling_gym/assets/p3b/3b_rola_bola.png"
dest_files=["res://.godot/imported/3b_rola_bola.png-be80aae165f27e5790e26f3ecae6c880.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
Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bfvmhj6feyxmh"
path="res://.godot/imported/3b_sfera.png-b06e77b042b6114f92baf1d2df6fcfe9.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://games/juggling_gym/assets/p3b/3b_sfera.png"
dest_files=["res://.godot/imported/3b_sfera.png-b06e77b042b6114f92baf1d2df6fcfe9.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
Binary file not shown.

After

Width:  |  Height:  |  Size: 875 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dpd4a5c8x3cwo"
path="res://.godot/imported/p3b_mockup.jpg-bc51bca42378d5dfd4553980fe5b07a0.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://games/juggling_gym/assets/p3b/p3b_mockup.jpg"
dest_files=["res://.godot/imported/p3b_mockup.jpg-bc51bca42378d5dfd4553980fe5b07a0.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
@@ -0,0 +1,12 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://games/juggling_gym/scripts/juggling_gym_game.gd" id="1"]
[node name="JugglingGym" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1")
@@ -0,0 +1,33 @@
class_name GymDropZone
extends ColorRect
## Zona invisibile di rilascio sopra un giocoliere del mockup P3B.
signal tool_dropped(zone_id: String, data: Dictionary)
var zone_id := ""
var _base_color := Color(0.95, 0.78, 0.28, 0.0)
func configure(id: String) -> void:
zone_id = id
color = _base_color
mouse_filter = Control.MOUSE_FILTER_STOP
func _can_drop_data(_at_position: Vector2, data: Variant) -> bool:
var valid: bool = data is Dictionary and data.has("tool_id")
color = Color(0.95, 0.78, 0.28, 0.18) if valid else _base_color
return valid
func _drop_data(_at_position: Vector2, data: Variant) -> void:
color = _base_color
if data is Dictionary:
tool_dropped.emit(zone_id, data)
func flash(success: bool) -> void:
var flash_color := Color(0.35, 0.82, 0.42, 0.42) if success else Color(0.9, 0.28, 0.24, 0.38)
var tween := create_tween()
tween.tween_property(self, "color", flash_color, 0.08)
tween.tween_property(self, "color", _base_color, 0.34)
@@ -0,0 +1 @@
uid://s74wpadiim52
+39
View File
@@ -0,0 +1,39 @@
class_name GymTool
extends Button
## Area interattiva trasparente sopra un attrezzo della barra P3B.
var tool_id := ""
var tool_texture: Texture2D
var available := true
func configure(id: String, texture: Texture2D, is_available: bool) -> void:
tool_id = id
tool_texture = texture
available = is_available
disabled = not available
mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND if available else Control.CURSOR_ARROW
func _ready() -> void:
flat = true
focus_mode = Control.FOCUS_NONE
mouse_filter = Control.MOUSE_FILTER_STOP
# L'attrezzo è già disegnato nel mockup: questo Button resta invisibile.
modulate.a = 0.01
func _get_drag_data(_at_position: Vector2) -> Variant:
if not available or tool_texture == null:
return null
var preview := TextureRect.new()
preview.texture = tool_texture
preview.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
preview.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
preview.custom_minimum_size = Vector2(180.0, 120.0)
preview.size = Vector2(180.0, 120.0)
preview.mouse_filter = Control.MOUSE_FILTER_IGNORE
preview.modulate = Color(1.0, 1.0, 1.0, 0.92)
set_drag_preview(preview)
return {"tool_id": tool_id}
@@ -0,0 +1 @@
uid://bhlimljig44q5
@@ -0,0 +1,118 @@
class_name JugglingGymGame
extends Control
## Prototipo P3B: trascina un attrezzo sul giocoliere corretto.
signal return_requested
const MOCKUP := preload("res://games/juggling_gym/assets/p3b/p3b_mockup.jpg")
const BALLS := preload("res://games/juggling_gym/assets/p3b/3b_palline.png")
const ROLA_BOLA := preload("res://games/juggling_gym/assets/p3b/3b_rola_bola.png")
const CLUBS := preload("res://games/juggling_gym/assets/p3b/3b_clave.png")
const PLATES := preload("res://games/juggling_gym/assets/p3b/3b_piatti_cinesi.png")
const SPHERE := preload("res://games/juggling_gym/assets/p3b/3b_sfera.png")
const ACTIVE_TOOLS := ["palline", "piatti", "sfera"]
const TOOL_DATA := {
"palline": {"texture": BALLS, "target": "left", "center": Vector2(0.18, 0.80), "size": Vector2(0.14, 0.18), "available": true},
"rola_bola": {"texture": ROLA_BOLA, "target": "center", "center": Vector2(0.34, 0.80), "size": Vector2(0.15, 0.16), "available": false},
"clave": {"texture": CLUBS, "target": "left", "center": Vector2(0.50, 0.80), "size": Vector2(0.15, 0.18), "available": false},
"piatti": {"texture": PLATES, "target": "right", "center": Vector2(0.66, 0.80), "size": Vector2(0.16, 0.18), "available": true},
"sfera": {"texture": SPHERE, "target": "center", "center": Vector2(0.82, 0.80), "size": Vector2(0.14, 0.18), "available": true},
}
var score := 0
var score_label: Label
var message_label: Label
var tool_controls: Dictionary = {}
var drop_zones: Dictionary = {}
func _ready() -> void:
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
_build()
func _build() -> void:
var artwork := TextureRect.new()
artwork.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
artwork.texture = MOCKUP
artwork.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
artwork.stretch_mode = TextureRect.STRETCH_SCALE
artwork.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(artwork)
var back := MenuUI.add_hotspot(self, MenuUI.BACK_BUTTON_CENTER, MenuUI.BACK_BUTTON_SIZE)
back.tooltip_text = "Torna alla palestra"
back.pressed.connect(func(): return_requested.emit())
back.z_index = 20
_add_drop_zone("left", Vector2(0.27, 0.41), Vector2(0.20, 0.42))
_add_drop_zone("center", Vector2(0.50, 0.39), Vector2(0.20, 0.45))
_add_drop_zone("right", Vector2(0.73, 0.41), Vector2(0.20, 0.44))
_build_tools()
_build_hud()
func _add_drop_zone(id: String, center: Vector2, size: Vector2) -> void:
var zone := GymDropZone.new()
zone.configure(id)
zone.tool_dropped.connect(_on_tool_dropped)
zone.z_index = 5
add_child(zone)
MenuUI.set_normalized_rect(zone, center, size)
drop_zones[id] = zone
func _build_tools() -> void:
for tool_id in TOOL_DATA:
var config: Dictionary = TOOL_DATA[tool_id]
var tool := GymTool.new()
tool.configure(tool_id, config["texture"], config["available"])
tool.tooltip_text = "Trascina sul giocoliere corretto" if config["available"] else "Disponibile in una prossima versione"
tool.z_index = 10
add_child(tool)
MenuUI.set_normalized_rect(tool, config["center"], config["size"])
tool_controls[tool_id] = tool
func _build_hud() -> void:
var instruction := MenuUI.add_label(self, "Trascina ogni attrezzo sul giocoliere", Vector2(0.50, 0.10), Vector2(0.34, 0.08), 22)
instruction.add_theme_stylebox_override("normal", MenuUI.note_style())
instruction.z_index = 20
score_label = MenuUI.add_label(self, "0 / %d" % ACTIVE_TOOLS.size(), Vector2(0.91, 0.12), Vector2(0.12, 0.08), 24)
score_label.add_theme_stylebox_override("normal", MenuUI.note_style())
score_label.z_index = 20
message_label = MenuUI.add_label(self, "", Vector2(0.50, 0.91), Vector2(0.28, 0.07), 22)
message_label.add_theme_stylebox_override("normal", MenuUI.note_style())
message_label.modulate.a = 0.0
message_label.z_index = 20
func _on_tool_dropped(zone_id: String, data: Dictionary) -> void:
var tool_id := str(data.get("tool_id", ""))
if not TOOL_DATA.has(tool_id):
return
var config: Dictionary = TOOL_DATA[tool_id]
var correct: bool = str(config["target"]) == zone_id
var zone: GymDropZone = drop_zones[zone_id]
zone.flash(correct)
if not correct:
_show_message("Prova con un altro giocoliere", false)
return
if tool_id in tool_controls and tool_controls[tool_id].disabled:
return
tool_controls[tool_id].disabled = true
score += 1
score_label.text = "%d / %d" % [score, ACTIVE_TOOLS.size()]
_show_message("Ben fatto!" if score < ACTIVE_TOOLS.size() else "Palestra completata!", true)
func _show_message(text: String, success: bool) -> void:
message_label.text = text
message_label.add_theme_color_override("font_color", Color("#2c6e3f") if success else Color("#9b352b"))
message_label.modulate.a = 1.0
var tween := create_tween()
tween.tween_interval(1.25)
tween.tween_property(message_label, "modulate:a", 0.0, 0.28)
@@ -0,0 +1 @@
uid://7mjhgowxtlc3
Binary file not shown.
@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://drb4yw2tp74m3"
path="res://.godot/imported/upendi_sign_bounce.wav-de9a54436e18f67098ab487f877643b2.sample"
[deps]
source_file="res://menus/assets/audio/upendi_sign_bounce.wav"
dest_files=["res://.godot/imported/upendi_sign_bounce.wav-de9a54436e18f67098ab487f877643b2.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2
Binary file not shown.
@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://jhlcbd0mtjhi"
path="res://.godot/imported/upendi_sign_drop.wav-485f7cd967f72e5f0964ed94ac11b7fd.sample"
[deps]
source_file="res://menus/assets/audio/upendi_sign_drop.wav"
dest_files=["res://.godot/imported/upendi_sign_drop.wav-485f7cd967f72e5f0964ed94ac11b7fd.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2
Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bn2t4knffd3n"
path="res://.godot/imported/curtain_00.png-9f0d1112d7c078353d46dc0cd923f1a1.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://menus/assets/p1/curtain_anim/curtain_00.png"
dest_files=["res://.godot/imported/curtain_00.png-9f0d1112d7c078353d46dc0cd923f1a1.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
Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dh0gj5aetn5us"
path="res://.godot/imported/curtain_01.png-2aa80e3cba63bc6d921dcf29d97198bd.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://menus/assets/p1/curtain_anim/curtain_01.png"
dest_files=["res://.godot/imported/curtain_01.png-2aa80e3cba63bc6d921dcf29d97198bd.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
Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cscj22s168j1c"
path="res://.godot/imported/curtain_02.png-9e1d1f7ce0266d1da830c1e4a58992f8.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://menus/assets/p1/curtain_anim/curtain_02.png"
dest_files=["res://.godot/imported/curtain_02.png-9e1d1f7ce0266d1da830c1e4a58992f8.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
Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://iw6xfqb882wr"
path="res://.godot/imported/curtain_03.png-f3f04b160b8ae4275d2c1b60da78ec9c.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://menus/assets/p1/curtain_anim/curtain_03.png"
dest_files=["res://.godot/imported/curtain_03.png-f3f04b160b8ae4275d2c1b60da78ec9c.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
Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bj0564i0rxa6x"
path="res://.godot/imported/curtain_04.png-e21854186643d232305e198528d76b35.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://menus/assets/p1/curtain_anim/curtain_04.png"
dest_files=["res://.godot/imported/curtain_04.png-e21854186643d232305e198528d76b35.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
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dfkhpaccg22k7"
path="res://.godot/imported/curtain_05.png-0fab7af970e77da94fba7e98267b384e.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://menus/assets/p1/curtain_anim/curtain_05.png"
dest_files=["res://.godot/imported/curtain_05.png-0fab7af970e77da94fba7e98267b384e.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
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://csvmjinsa8ct5"
path="res://.godot/imported/curtain_06.png-4beeda4b533be6cefd4aab87117936ed.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://menus/assets/p1/curtain_anim/curtain_06.png"
dest_files=["res://.godot/imported/curtain_06.png-4beeda4b533be6cefd4aab87117936ed.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
Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dqyga2dtnatrw"
path="res://.godot/imported/lights_00.png-9c87aa8587b4ae63db312b0b1c331079.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://menus/assets/p1/garland_anim/lights_00.png"
dest_files=["res://.godot/imported/lights_00.png-9c87aa8587b4ae63db312b0b1c331079.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
Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dfkqds6xp0tju"
path="res://.godot/imported/lights_01.png-015d6f38bb58d82ed9b8078ea32b269e.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://menus/assets/p1/garland_anim/lights_01.png"
dest_files=["res://.godot/imported/lights_01.png-015d6f38bb58d82ed9b8078ea32b269e.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
Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://c8670kiyy0k83"
path="res://.godot/imported/lights_02.png-2fa8069910e0537d302506600eeca44d.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://menus/assets/p1/garland_anim/lights_02.png"
dest_files=["res://.godot/imported/lights_02.png-2fa8069910e0537d302506600eeca44d.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
Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dgr3wno4bmimg"
path="res://.godot/imported/lights_03.png-b14f87015201a2decbafe2bec7276a61.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://menus/assets/p1/garland_anim/lights_03.png"
dest_files=["res://.godot/imported/lights_03.png-b14f87015201a2decbafe2bec7276a61.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
Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cu38618jhaxb"
path="res://.godot/imported/lights_04.png-1f84cc03a726eb56bb479508bfc88cb4.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://menus/assets/p1/garland_anim/lights_04.png"
dest_files=["res://.godot/imported/lights_04.png-1f84cc03a726eb56bb479508bfc88cb4.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
Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://diqlap0wh55rh"
path="res://.godot/imported/lights_05.png-953bc7308a81f09c6fa8d9579b06b985.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://menus/assets/p1/garland_anim/lights_05.png"
dest_files=["res://.godot/imported/lights_05.png-953bc7308a81f09c6fa8d9579b06b985.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
Binary file not shown.

After

Width:  |  Height:  |  Size: 467 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cbp7fcsgcud4u"
path="res://.godot/imported/upendi_circus_sign_panel.png-fb1ab388766e4b8103b06357951ad4d1.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://menus/assets/p1/upendi_circus_sign_panel.png"
dest_files=["res://.godot/imported/upendi_circus_sign_panel.png-fb1ab388766e4b8103b06357951ad4d1.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
Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cc5el5lnpxldi"
path="res://.godot/imported/upendi_circus_sign_ropes.png-ac90efc2477b7d052aec26561e0cad89.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://menus/assets/p1/upendi_circus_sign_ropes.png"
dest_files=["res://.godot/imported/upendi_circus_sign_ropes.png-ac90efc2477b7d052aec26561e0cad89.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
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bkjprgojtsi6o"
path="res://.godot/imported/upendi_circus_sign_static.png-6c2cab69a8be792119e748799d05b6cd.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://menus/assets/p1/upendi_circus_sign_static.png"
dest_files=["res://.godot/imported/upendi_circus_sign_static.png-6c2cab69a8be792119e748799d05b6cd.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
@@ -0,0 +1,89 @@
shader_type canvas_item;
// Shader per l'insegna centrale UPENDI CIRCUS:
// Lucine colorate lungo la cornice del pannello che si accendono in modo rado e casuale
// solo dopo che l'insegna ha finito di atterrare e rimbalzare (sign_lights_active = 1.0).
uniform float sign_lights_active : hint_range(0.0, 1.0) = 0.0;
uniform float blink_speed : hint_range(0.5, 8.0) = 2.2;
// 37 posizioni lampadina distribuite lungo il contorno in legno dell'insegna (UV su canvas 880x561)
const vec2 sign_bulbs[37] = vec2[37](
// Arco superiore
vec2(0.2107, 0.3839), vec2(0.2590, 0.3851), vec2(0.3223, 0.3828), vec2(0.3725, 0.3774),
vec2(0.4209, 0.3707), vec2(0.4946, 0.3551), vec2(0.5700, 0.3261), vec2(0.6281, 0.3031),
vec2(0.6769, 0.2784), vec2(0.7503, 0.2651), vec2(0.7999, 0.2504), vec2(0.8620, 0.2458),
// Bordo destro
vec2(0.8565, 0.3564), vec2(0.8585, 0.4328), vec2(0.8579, 0.5043), vec2(0.8628, 0.5807),
vec2(0.8640, 0.6594), vec2(0.8703, 0.7179), vec2(0.8680, 0.7879),
// Arco inferiore
vec2(0.8264, 0.8164), vec2(0.7829, 0.8452), vec2(0.7112, 0.8651), vec2(0.6513, 0.8823),
vec2(0.5675, 0.8988), vec2(0.5082, 0.9059), vec2(0.4341, 0.9007), vec2(0.3600, 0.8980),
vec2(0.3098, 0.8763), vec2(0.2447, 0.8536), vec2(0.1897, 0.8309), vec2(0.1363, 0.7941),
// Bordo sinistro
vec2(0.1328, 0.6995), vec2(0.1297, 0.6286), vec2(0.1256, 0.5398), vec2(0.1239, 0.4893),
vec2(0.1221, 0.4318), vec2(0.1208, 0.3685)
);
// Palette di colori circensi vivaci
vec3 get_bulb_color(int idx) {
int c = idx % 6;
if (c == 0) return vec3(1.0, 0.25, 0.25); // Rosso rubino
if (c == 1) return vec3(1.0, 0.84, 0.15); // Giallo oro
if (c == 2) return vec3(0.25, 0.88, 0.40); // Verde smeraldo
if (c == 3) return vec3(0.25, 0.70, 1.00); // Blu ciano
if (c == 4) return vec3(0.95, 0.38, 0.90); // Magenta fucsia
return vec3(1.0, 0.55, 0.15); // Arancio caldo
}
// Generatore pseudo-casuale deterministico per lampadina e tempo
float pseudo_rand(float n) {
return fract(sin(n) * 43758.5453123);
}
void fragment() {
vec4 color = texture(TEXTURE, UV);
if (sign_lights_active > 0.001) {
vec2 px = UV * vec2(880.0, 561.0);
// Calcolo delle 5 lampadine attive nei 5 settori della cornice
// Durate leggermente sfalsate per evitare che si sincronizzino
const float durations[5] = float[5](1.3, 1.6, 1.9, 1.5, 2.1);
const int ranges_start[5] = int[5](0, 7, 15, 22, 30);
const int ranges_count[5] = int[5](7, 8, 7, 8, 7);
for (int c = 0; c < 5; c++) {
float dur = durations[c] / (blink_speed / 2.2);
float step = floor(TIME / dur);
float sub_t = fract(TIME / dur);
// Campana morbida di accensione (fade-in -> picco -> fade-out)
float lit = sin(sub_t * 3.14159265);
if (lit <= 0.15) {
continue;
}
// Indice lampadina estratta pseudo-casualmente per questo settore e questo ciclo
float r_val = pseudo_rand(step * 17.13 + float(c) * 31.7);
int chosen_bulb = ranges_start[c] + int(floor(r_val * float(ranges_count[c])));
chosen_bulb = clamp(chosen_bulb, 0, 36);
vec2 delta = px - sign_bulbs[chosen_bulb] * vec2(880.0, 561.0);
float r = length(delta);
// Nucleo piccolo e brillante ~5px + alone diffuso ~12px
float core = 1.0 - smoothstep(2.0, 6.0, r);
float halo = exp(-r / 7.5);
float intensity = lit * sign_lights_active;
vec3 bulb_col = get_bulb_color(chosen_bulb);
// Colorazione del nucleo e alone additivo
color.rgb = mix(color.rgb, bulb_col, core * intensity);
color.rgb += bulb_col * (halo * 0.75 * intensity * (1.0 - core));
color.a = max(color.a, halo * 0.5 * intensity);
}
}
color.rgb = min(color.rgb, vec3(1.0));
COLOR = color;
}
@@ -0,0 +1 @@
uid://bk0kwrttwfdl3
@@ -0,0 +1,103 @@
shader_type canvas_item;
// Shader per i menu di secondo livello P2A e P3A.
// Coordinate UV relative al TextureRect del mockup (2796x1290).
// Tracciate e verificate ad alto contrasto con fondo nero:
// 1. Stella centrata sul numero 1
// 2. 20 lucine lungo le due falde del tetto a cono
// 3. 4 grandi punti di ancoraggio sulla balza ondulata
uniform float lights_strength : hint_range(0.0, 1.5) = 1.0;
uniform float star_strength : hint_range(0.0, 1.5) = 1.0;
uniform vec2 star_center = vec2(0.269671, 0.155039);
uniform float garland_fps : hint_range(1.0, 60.0) = 12.5;
const vec3 GOLD_CORE = vec3(0.945, 0.80, 0.385);
const vec3 GOLD_HALO = vec3(1.0, 0.78, 0.35);
// 20 lampadine lungo le due falde del tetto a cono
// Coordinate UV nella texture del mockup (2796x1290), z = fase del loop (4, 2, 0)
// Coordinate rilevate automaticamente dal PNG 2a_tendone.png:
// dodici lampadine reali per lato, non dieci e non interpolate su una retta.
const vec3 p2a_bulbs[24] = vec3[24](
// Sinistra, dal colletto verso il bordo esterno
vec3(0.251808, 0.249698, 4.0), // L0
vec3(0.234388, 0.289171, 2.0), // L1
vec3(0.214765, 0.320653, 0.0), // L2
vec3(0.194605, 0.356896, 4.0), // L3
vec3(0.181462, 0.375494, 2.0), // L4
vec3(0.167428, 0.398533, 0.0), // L5
vec3(0.150578, 0.433674, 4.0), // L6
vec3(0.129082, 0.459304, 2.0), // L7
vec3(0.107434, 0.492707, 0.0), // L8
vec3(0.088943, 0.512666, 4.0), // L9
vec3(0.070246, 0.536780, 2.0), // L10
vec3(0.042324, 0.560880, 0.0), // L11
// Destra, dal colletto verso il bordo esterno
vec3(0.285755, 0.241321, 0.0), // R0
vec3(0.300670, 0.272138, 2.0), // R1
vec3(0.320168, 0.303235, 4.0), // R2
vec3(0.339147, 0.339426, 0.0), // R3
vec3(0.357906, 0.370273, 2.0), // R4
vec3(0.376152, 0.403834, 4.0), // R5
vec3(0.393310, 0.431054, 0.0), // R6
vec3(0.413618, 0.461451, 2.0), // R7
vec3(0.439441, 0.496182, 4.0), // R8
vec3(0.462399, 0.522573, 0.0), // R9
vec3(0.482340, 0.544497, 2.0), // R10
vec3(0.500324, 0.560728, 4.0) // R11
);
// 4 grandi punti di ancoraggio lungo la balza ondulata del tendone
const vec3 p2a_anchor_bulbs[4] = vec3[4](
vec3(0.08727, 0.46822, 4.0), // A0 (Estremità sinistra della balza)
vec3(0.19170, 0.51783, 0.0), // A1 (Angolo ingresso SX)
vec3(0.27182, 0.51783, 2.0), // A2 (Angolo ingresso DX)
vec3(0.37697, 0.46822, 4.0) // A3 (Estremità destra della balza)
);
float chase_intensity(float d) {
if (d < 1.0) return 0.30;
if (d < 2.0) return 0.60;
if (d < 3.0) return 1.00;
if (d < 4.0) return 0.95;
return 0.0;
}
void fragment() {
vec4 color = texture(TEXTURE, UV);
// 1. Stella grande in cima al tendone
if (star_strength > 0.001) {
vec2 aspect_diff = (UV - star_center) * vec2(2.1674, 1.0);
float star_dist = length(aspect_diff);
float star_area = 1.0 - smoothstep(0.020, 0.060, star_dist);
float pulse = 0.88 + 0.12 * sin(TIME * 5.0);
vec3 gold_glow = vec3(1.0, 0.75, 0.20) * star_area * star_strength * pulse;
color.rgb += gold_glow;
}
// 2. Lucine lungo le falde del tetto e punti sulla balza
if (lights_strength > 0.001) {
float frame_time = TIME * garland_fps;
float cycle = mod(floor(frame_time), 6.0);
// 20 Lucine lungo le due falde del tetto a cono
for (int i = 0; i < 24; i++) {
float d = mod(cycle - p2a_bulbs[i].z + 6.0, 6.0);
float intensity = chase_intensity(d);
if (intensity <= 0.0) {
continue;
}
vec2 delta = (UV - p2a_bulbs[i].xy) * vec2(2796.0, 1290.0);
float r = length(delta);
float core = 1.0 - smoothstep(4.0, 10.0, r);
float halo = exp(-r / 15.0);
float ga = clamp(intensity * lights_strength, 0.0, 1.0);
color.rgb = mix(color.rgb, GOLD_CORE, core * ga);
color.rgb += GOLD_HALO * (halo * ga * 0.75 * (1.0 - core));
}
}
color.rgb = min(color.rgb, vec3(1.0));
COLOR = color;
}
@@ -0,0 +1 @@
uid://cw1cvmopgvx1n
+131
View File
@@ -0,0 +1,131 @@
shader_type canvas_item;
// Tendone del menu: chase delle lucine delle garlande, attivo solo in hover.
// Il chase replica l'animazione sorgente (animazione_tendone.gif, 12,5 fps):
// periodo 6 frame, 3 fasi da 2 frame; ogni lampadina fa rampa per 2 frame,
// brilla a pieno intensità per 2 frame e resta spenta per 2 frame.
// Lato sinistro: l'onda sale dal fondo verso il picco (fasi [4,2,0] per i mod 3).
// Lato destro: l'onda scende dal picco al fondo (fasi [0,2,4] per i mod 3);
// al picco L0 (fase 0) precede R0 (fase 1) di 2 frame, come nella GIF.
uniform float hover_strength : hint_range(0.0, 1.0) = 0.0;
uniform vec2 star_center = vec2(0.50, 0.13);
uniform float drift_strength : hint_range(0.0, 4.0) = 0.65;
uniform float drift_speed : hint_range(0.0, 2.0) = 0.20;
// Intensità del chase lucine: 0 = spento a riposo, 1.0 = attivo in hover.
uniform float garland_strength : hint_range(0.0, 1.0) = 0.0;
// FPS sorgente dell'animazione (GIF a 8 centesimi di secondo per frame).
uniform float garland_fps : hint_range(1.0, 60.0) = 12.5;
// Frame assoluto con cui congelare il chase (debug/catture); -1 = tempo reale.
uniform float garland_freeze_frame = -1.0;
// Nucleo della lampadina accesa e colore dell'alone, campionati dalla GIF.
const vec3 GOLD_CORE = vec3(0.945, 0.80, 0.385);
const vec3 GOLD_HALO = vec3(1.0, 0.78, 0.35);
// Posizioni lampadina in UV texture (canvas 880x750) + offset di fase in frame
// nel ciclo di 6: z = frame di inizio rampa. L0..L9 dal picco al fondo sinistro,
// R0..R9 dal picco al fondo destro.
const vec3 bulbs[20] = vec3[20](
// garlanda sinistra: onda che sale
vec3(0.4500, 0.2893, 4.0), vec3(0.4330, 0.3187, 2.0), vec3(0.4125, 0.3453, 0.0),
vec3(0.3875, 0.3747, 4.0), vec3(0.3591, 0.4080, 2.0), vec3(0.3341, 0.4360, 0.0),
vec3(0.3000, 0.4693, 4.0), vec3(0.2670, 0.5013, 2.0), vec3(0.2295, 0.5307, 0.0),
vec3(0.1920, 0.5587, 4.0),
// garlanda destra: onda che scende
vec3(0.5307, 0.3000, 0.0), vec3(0.5443, 0.3333, 2.0), vec3(0.5648, 0.3573, 4.0),
vec3(0.5852, 0.3840, 0.0), vec3(0.6205, 0.4253, 2.0), vec3(0.6420, 0.4573, 4.0),
vec3(0.6761, 0.4880, 0.0), vec3(0.7114, 0.5160, 2.0), vec3(0.7466, 0.5427, 4.0),
vec3(0.7773, 0.5613, 0.0)
);
// 4 grandi luci sui punti di ancoraggio delle funi a terra lungo la balza:
// 0: Esterno Sinistro (131.3, 502.1) -> fase 4 (in fase con L9 in fondo a sinistra)
// 1: Palo/Fune SX Ingresso (306.5, 544.1) -> fase 0
// 2: Palo/Fune DX Ingresso (558.6, 550.7) -> fase 2
// 3: Esterno Destro (737.2, 499.3) -> fase 4 (in fase con la discesa a destra)
const vec3 anchor_bulbs[4] = vec3[4](
vec3(0.1492, 0.6695, 4.0),
vec3(0.3483, 0.7255, 0.0),
vec3(0.6348, 0.7342, 2.0),
vec3(0.8378, 0.6657, 4.0)
);
// Intensità nel ciclo di 6 frame della singola lampadina (d = frame dalla rampa),
// ricavata dalle aree misurate sui frame della GIF: ~25%, ~50%, pieno, pieno, 0, 0.
float chase_intensity(float d) {
if (d < 1.0) {
return 0.3;
}
if (d < 2.0) {
return 0.6;
}
if (d < 3.0) {
return 1.0;
}
if (d < 4.0) {
return 0.95;
}
return 0.0;
}
void vertex() {
float wave = sin(TIME * drift_speed + VERTEX.y * 0.012) * drift_strength;
VERTEX.x += wave * (UV.y * 0.7 + 0.3);
}
void fragment() {
vec4 color = texture(TEXTURE, UV);
// Maschera morbida limitata alla stella in cima al tendone, non al tendone.
vec2 aspect_uv = (UV - star_center) * vec2(1.0, 1.35);
float star_area = 1.0 - smoothstep(0.105, 0.185, length(aspect_uv));
float pulse = 0.88 + 0.12 * sin(TIME * 5.0);
vec3 gold_glow = vec3(1.0, 0.70, 0.14) * star_area * hover_strength * pulse;
color.rgb += gold_glow * color.a;
// Chase lucine: sincronizzato a 12,5 fps come la GIF sorgente.
if (garland_strength > 0.001) {
float frame_time = TIME * garland_fps;
if (garland_freeze_frame >= 0.0) {
frame_time = garland_freeze_frame;
}
float cycle = mod(floor(frame_time), 6.0);
vec2 px = UV * vec2(880.0, 750.0);
for (int i = 0; i < 20; i++) {
float d = mod(cycle - bulbs[i].z + 6.0, 6.0);
float intensity = chase_intensity(d);
if (intensity <= 0.0) {
continue;
}
float r = length(px - bulbs[i].xy * vec2(880.0, 750.0));
// Nucleo pieno ~6px + alone ampio e diffuso fino a ~25px (come nella GIF).
float core = 1.0 - smoothstep(3.0, 8.0, r);
float halo = exp(-r / 12.0);
float ga = clamp(intensity * garland_strength, 0.0, 1.0);
// Il nucleo porta la lampadina all'oro pieno della GIF.
color.rgb = mix(color.rgb, GOLD_CORE, core * ga);
// Alone caldo additivo solo attorno al nucleo (senza sforare sul nucleo,
// altrimenti il clamp sbiadisce l'oro verso il bianco).
color.rgb += GOLD_HALO * (halo * ga * 0.7 * (1.0 - core));
color.a = max(color.a, halo * ga * 0.5);
}
// 4 grandi luci sui punti di ancoraggio delle funi (intermittenti a inseguimento con ritmo proprio)
for (int j = 0; j < 4; j++) {
float d = mod(cycle - anchor_bulbs[j].z + 6.0, 6.0);
float intensity = chase_intensity(d);
if (intensity <= 0.0) {
continue;
}
float r = length(px - anchor_bulbs[j].xy * vec2(880.0, 750.0));
float core = 1.0 - smoothstep(4.0, 10.0, r);
float halo = exp(-r / 15.0);
float ga = clamp(intensity * garland_strength, 0.0, 1.0);
color.rgb = mix(color.rgb, GOLD_CORE, core * ga);
color.rgb += GOLD_HALO * (halo * ga * 0.8 * (1.0 - core));
color.a = max(color.a, halo * ga * 0.6);
}
}
color.rgb = min(color.rgb, vec3(1.0));
COLOR = color;
}
@@ -0,0 +1 @@
uid://c06koaxcymtib
+11
View File
@@ -2,7 +2,18 @@ class_name MenuAudio
extends Node
## Feedback sonori brevi dei menu, separati dal gameplay.
const SIGN_DROP := preload("res://menus/assets/audio/upendi_sign_drop.wav")
const SIGN_BOUNCE := preload("res://menus/assets/audio/upendi_sign_bounce.wav")
func play_tone(freq: float) -> void:
var stream := AudioFactory.create_tone(freq, 0.10, 0.16)
AudioFactory.play_once(self, stream, -4.0)
func play_sign_drop() -> void:
AudioFactory.play_once(self, SIGN_DROP, -8.0)
func play_sign_bounce() -> void:
AudioFactory.play_once(self, SIGN_BOUNCE, -7.0)
+29
View File
@@ -5,6 +5,13 @@ extends RefCounted
const DISPLAY_FONT := preload("res://shared/assets/fonts/NotoSerifDisplay-CondensedExtraBold.ttf")
const MENU_SHADER := preload("res://menus/assets/shaders/ambient_drift.gdshader")
const TENT_STAR_SHADER := preload("res://menus/assets/shaders/tent_star_glow.gdshader")
const TENT_LIGHTS_SHADER := preload("res://menus/assets/shaders/tent_lights.gdshader")
const SIGN_LIGHTS_SHADER := preload("res://menus/assets/shaders/circus_sign_lights.gdshader")
const SUBMENU_LIGHTS_SHADER := preload("res://menus/assets/shaders/submenu_tent_lights.gdshader")
# Posizione e ingombro condivisi della freccia indietro in tutte le schermate.
const BACK_BUTTON_CENTER := Vector2(0.12, 0.14)
const BACK_BUTTON_SIZE := Vector2(0.09, 0.14)
static func set_normalized_rect(control: Control, center: Vector2, normalized_size: Vector2) -> void:
@@ -40,6 +47,28 @@ static func tent_star_material() -> ShaderMaterial:
return material
static func tent_material(garland_strength := 0.0) -> ShaderMaterial:
var material := ShaderMaterial.new()
material.shader = TENT_LIGHTS_SHADER
material.set_shader_parameter("garland_strength", garland_strength)
return material
static func sign_lights_material() -> ShaderMaterial:
var material := ShaderMaterial.new()
material.shader = SIGN_LIGHTS_SHADER
material.set_shader_parameter("sign_lights_active", 0.0)
return material
static func submenu_lights_material() -> ShaderMaterial:
var material := ShaderMaterial.new()
material.shader = SUBMENU_LIGHTS_SHADER
material.set_shader_parameter("lights_strength", 1.0)
material.set_shader_parameter("star_strength", 1.0)
return material
static func add_image(parent: Control, texture: Texture2D, center: Vector2, normalized_size: Vector2, material: Material = null) -> TextureRect:
var image := TextureRect.new()
image.texture = texture
+8
View File
@@ -3,6 +3,7 @@ extends CanvasLayer
## Coordina navigazione e transizioni tra P1, P2a e P3a.
signal mode_requested
signal gym_requested
signal exit_requested
const HOME_SCENE := preload("res://menus/scenes/p1_home.tscn")
@@ -15,12 +16,14 @@ var menu_audio: MenuAudio
var current_screen: Control
var _transitioning := false
var _capture_enabled := false
var _capture_settled := false
func _ready() -> void:
process_mode = Node.PROCESS_MODE_ALWAYS
layer = 20
_capture_enabled = OS.get_cmdline_user_args().has("--capture-menus")
_capture_settled = OS.get_cmdline_user_args().has("--capture-settled")
_build_shell()
visible = false
@@ -105,6 +108,8 @@ func _bind_screen(screen: Control) -> void:
if screen is P1HomeScreen:
screen.juggle_requested.connect(func(focal: Control): _navigate(JUGGLE_SCENE, "p2a", focal))
screen.gym_requested.connect(func(focal: Control): _navigate(GYM_SCENE, "p3a", focal))
screen.sign_drop_started.connect(menu_audio.play_sign_drop)
screen.sign_landed.connect(menu_audio.play_sign_bounce)
screen.exit_requested.connect(func():
menu_audio.play_tone(220.0)
exit_requested.emit()
@@ -115,6 +120,7 @@ func _bind_screen(screen: Control) -> void:
screen.feedback_requested.connect(menu_audio.play_tone)
elif screen is P3AGymMenuScreen:
screen.back_requested.connect(func(): _navigate(HOME_SCENE, "p1"))
screen.game_requested.connect(func(): gym_requested.emit())
screen.feedback_requested.connect(menu_audio.play_tone)
@@ -122,5 +128,7 @@ func _capture_after_frame(label: String) -> void:
if not _capture_enabled:
return
await RenderingServer.frame_post_draw
if _capture_settled:
await get_tree().create_timer(1.5, true, false, true).timeout
var image := get_viewport().get_texture().get_image()
image.save_png("/tmp/autgame_menu_%s.png" % label)
+337 -18
View File
@@ -5,6 +5,9 @@ extends Control
signal juggle_requested(focal: Control)
signal gym_requested(focal: Control)
signal exit_requested
signal sign_drop_started
signal sign_landed
signal sign_bounced
const HOME_BG := preload("res://menus/assets/common/1_sfondo.png")
const CLOUDS := preload("res://menus/assets/common/1_nuvole.png")
@@ -12,7 +15,49 @@ const BLUE_TENT := preload("res://menus/assets/p1/1_tendone_1.png")
const RED_TENT := preload("res://menus/assets/p1/1_tendone_2.png")
const BACK_BUTTON := preload("res://menus/assets/common/pulsante_indietro.png")
const SETTINGS_BUTTON := preload("res://menus/assets/common/pulsante_rotellina.png")
const CIRCUS_SIGN := preload("res://menus/assets/p1/upendi_circus_sign_mockup.png")
const CIRCUS_SIGN_PANEL := preload("res://menus/assets/p1/upendi_circus_sign_panel.png")
const CIRCUS_SIGN_ROPES := preload("res://menus/assets/p1/upendi_circus_sign_ropes.png")
const PLAY_BUTTON := preload("res://menus/assets/common/pulsante_gioca.png")
var piedone: Control
var piedone_title: Label
var piedone_desc: Label
var piedone_play: TextureButton
var selected_tent: String = "" # "juggle" o "gym"
var piedone_tween: Tween
var blue_tent_control: Control
var red_tent_control: Control
# Riferimenti alle animazioni e shader per ciascun tendone
var blue_mat: ShaderMaterial
var red_mat: ShaderMaterial
var blue_curtain_dir: Array = [0]
var blue_curtain_timer: Timer
var blue_lights: TextureRect
var red_curtain_dir: Array = [0]
var red_curtain_timer: Timer
var red_lights: TextureRect
const CURTAIN_FRAMES := [
preload("res://menus/assets/p1/curtain_anim/curtain_00.png"),
preload("res://menus/assets/p1/curtain_anim/curtain_01.png"),
preload("res://menus/assets/p1/curtain_anim/curtain_02.png"),
preload("res://menus/assets/p1/curtain_anim/curtain_03.png"),
preload("res://menus/assets/p1/curtain_anim/curtain_04.png"),
preload("res://menus/assets/p1/curtain_anim/curtain_05.png"),
preload("res://menus/assets/p1/curtain_anim/curtain_06.png"),
]
const GARLAND_FRAMES := [
preload("res://menus/assets/p1/garland_anim/lights_00.png"),
preload("res://menus/assets/p1/garland_anim/lights_01.png"),
preload("res://menus/assets/p1/garland_anim/lights_02.png"),
preload("res://menus/assets/p1/garland_anim/lights_03.png"),
preload("res://menus/assets/p1/garland_anim/lights_04.png"),
preload("res://menus/assets/p1/garland_anim/lights_05.png"),
]
func _ready() -> void:
@@ -25,46 +70,320 @@ func _build() -> void:
MenuUI.add_full_texture(self, CLOUDS, MenuUI.ambient_material(1.0, 0.16))
_add_sign()
var blue := _add_tent_button(BLUE_TENT, Vector2(0.29, 0.46))
blue.pressed.connect(func(): juggle_requested.emit(blue))
var red := _add_tent_button(RED_TENT, Vector2(0.71, 0.46))
red.pressed.connect(func(): gym_requested.emit(red))
# Costruzione dei due tendoni selezionabili al click (entrambi con luci e sipario)
var blue := _add_tent_button(BLUE_TENT, Vector2(0.29, 0.46), "juggle")
blue_tent_control = blue
blue.pressed.connect(func(): _select_tent("juggle"))
var red := _add_tent_button(RED_TENT, Vector2(0.71, 0.46), "gym")
red_tent_control = red
red.pressed.connect(func(): _select_tent("gym"))
_add_bottom_bar()
MenuUI.add_image(self, SETTINGS_BUTTON, Vector2(0.91, 0.13), Vector2(0.09, 0.14))
if not OS.has_feature("web"):
var exit_button := MenuUI.add_image_button(self, BACK_BUTTON, Vector2(0.075, 0.13), Vector2(0.09, 0.14))
var exit_button := MenuUI.add_image_button(self, BACK_BUTTON, MenuUI.BACK_BUTTON_CENTER, MenuUI.BACK_BUTTON_SIZE)
exit_button.pressed.connect(func(): exit_requested.emit())
func _add_tent_button(texture: Texture2D, center: Vector2) -> TextureButton:
func _add_tent_button(texture: Texture2D, center: Vector2, tent_id: String) -> TextureButton:
var button := MenuUI.add_image_button(self, texture, center, Vector2(0.36, 0.59))
var star_material := MenuUI.tent_star_material()
button.material = star_material
button.tooltip_text = "Apri questo percorso"
# L'hover non altera più geometria e hitbox del tendone: mette in risalto
# esclusivamente la stella, tramite il parametro dello shader.
button.mouse_entered.connect(func(): star_material.set_shader_parameter("hover_strength", 1.0))
button.mouse_exited.connect(func(): star_material.set_shader_parameter("hover_strength", 0.0))
var mat := MenuUI.tent_material(0.0)
button.material = mat
button.tooltip_text = "Seleziona questo percorso"
if tent_id == "juggle":
blue_mat = mat
_setup_tent_interactive(button, center, "blue")
elif tent_id == "gym":
red_mat = mat
_setup_tent_interactive(button, center, "red")
return button
func _setup_tent_interactive(parent_button: TextureButton, center: Vector2, color_id: String) -> void:
# Sipario: parte chiuso (frame 0). Alla selezione si apre (0 -> 6).
# Alla deselezione si richiude al contrario (6 -> 0).
var curtain := TextureRect.new()
curtain.name = color_id.capitalize() + "Curtain"
curtain.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
curtain.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
curtain.mouse_filter = Control.MOUSE_FILTER_IGNORE
curtain.texture = CURTAIN_FRAMES[0]
add_child(curtain)
MenuUI.set_normalized_rect(curtain, center, Vector2(0.36, 0.59))
var curtain_dir := [0] # 0 = fermo, 1 = apre, -1 = chiude
var curtain_timer := Timer.new()
curtain_timer.name = color_id.capitalize() + "CurtainTimer"
curtain_timer.wait_time = 1.0 / 12.5
curtain_timer.process_mode = Node.PROCESS_MODE_ALWAYS
var curtain_idx := [0]
curtain_timer.timeout.connect(func():
if curtain_dir[0] == 1:
if curtain_idx[0] < CURTAIN_FRAMES.size() - 1:
curtain_idx[0] += 1
curtain.texture = CURTAIN_FRAMES[curtain_idx[0]]
curtain.visible = true
else:
# Completamente aperto
curtain.texture = CURTAIN_FRAMES[curtain_idx[0]]
curtain_dir[0] = 0
curtain_timer.stop()
elif curtain_dir[0] == -1:
if curtain_idx[0] > 0:
curtain_idx[0] -= 1
curtain.texture = CURTAIN_FRAMES[curtain_idx[0]]
curtain.visible = true
else:
# Completamente chiuso
curtain.texture = CURTAIN_FRAMES[0]
curtain_dir[0] = 0
curtain_timer.stop()
)
add_child(curtain_timer)
# Lucine a inseguimento
var lights := TextureRect.new()
lights.name = color_id.capitalize() + "Garland"
lights.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
lights.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
lights.mouse_filter = Control.MOUSE_FILTER_IGNORE
lights.texture = GARLAND_FRAMES[0]
lights.visible = false
add_child(lights)
MenuUI.set_normalized_rect(lights, center, Vector2(0.36, 0.59))
var lights_idx := [0]
var lights_timer := Timer.new()
lights_timer.name = color_id.capitalize() + "GarlandTimer"
lights_timer.wait_time = 1.0 / 12.5
lights_timer.process_mode = Node.PROCESS_MODE_ALWAYS
lights_timer.timeout.connect(func():
lights_idx[0] = (lights_idx[0] + 1) % GARLAND_FRAMES.size()
if lights.visible:
lights.texture = GARLAND_FRAMES[lights_idx[0]]
)
add_child(lights_timer)
lights_timer.start()
if color_id == "blue":
blue_curtain_dir = curtain_dir
blue_curtain_timer = curtain_timer
blue_lights = lights
elif color_id == "red":
red_curtain_dir = curtain_dir
red_curtain_timer = curtain_timer
red_lights = lights
func _select_tent(which: String) -> void:
if selected_tent == which:
return
selected_tent = which
# Aggiorna animazioni sui tendoni
if which == "juggle":
# Attiva tendone blu
if blue_mat:
blue_mat.set_shader_parameter("hover_strength", 1.0)
blue_mat.set_shader_parameter("garland_strength", 1.0)
if blue_curtain_dir and blue_curtain_timer:
blue_curtain_dir[0] = 1
blue_curtain_timer.start()
if blue_lights:
blue_lights.visible = true
# Spegne e richiude tendone rosso
if red_mat:
red_mat.set_shader_parameter("hover_strength", 0.0)
red_mat.set_shader_parameter("garland_strength", 0.0)
if red_curtain_dir and red_curtain_timer:
red_curtain_dir[0] = -1
red_curtain_timer.start()
if red_lights:
red_lights.visible = false
elif which == "gym":
# Attiva tendone rosso
if red_mat:
red_mat.set_shader_parameter("hover_strength", 1.0)
red_mat.set_shader_parameter("garland_strength", 1.0)
if red_curtain_dir and red_curtain_timer:
red_curtain_dir[0] = 1
red_curtain_timer.start()
if red_lights:
red_lights.visible = true
# Spegne e richiude tendone blu
if blue_mat:
blue_mat.set_shader_parameter("hover_strength", 0.0)
blue_mat.set_shader_parameter("garland_strength", 0.0)
if blue_curtain_dir and blue_curtain_timer:
blue_curtain_dir[0] = -1
blue_curtain_timer.start()
if blue_lights:
blue_lights.visible = false
# Transizione del piedone: scende e risale con le nuove info
_transition_bottom_bar(which)
func _transition_bottom_bar(which: String) -> void:
if not is_instance_valid(piedone):
return
var v_height := get_viewport_rect().size.y
var hidden_y := v_height * 0.29
var target_y := 0.0
if is_instance_valid(piedone_tween) and piedone_tween.is_running():
piedone_tween.kill()
piedone_tween = create_tween()
piedone_tween.set_pause_mode(Tween.TWEEN_PAUSE_PROCESS)
# Quando l'utente tocca un tendone, Piedone scende in modo fluido e risale con molleggio morbido e lento
piedone_tween.tween_property(piedone, "position:y", hidden_y, 0.40).set_trans(Tween.TRANS_CUBIC).set_ease(Tween.EASE_IN)
piedone_tween.tween_callback(func(): _update_piedone_content(which))
piedone_tween.tween_property(piedone, "position:y", -v_height * 0.015, 0.70).set_trans(Tween.TRANS_CUBIC).set_ease(Tween.EASE_OUT)
piedone_tween.tween_property(piedone, "position:y", v_height * 0.006, 0.40).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT)
piedone_tween.tween_property(piedone, "position:y", target_y, 0.35).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_OUT)
func _update_piedone_content(which: String) -> void:
piedone_play.visible = true
# Riposiziona il titolo sopra alla descrizione
MenuUI.set_normalized_rect(piedone_title, Vector2(0.38, 0.845), Vector2(0.52, 0.065))
piedone_title.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT
if which == "juggle":
piedone_title.text = "JUGGLEBOARD"
piedone_desc.text = "Allena ritmo e coordinazione con le biglie e i lanci a bersaglio."
elif which == "gym":
piedone_title.text = "PALESTRA DI GIOCOLERIA"
piedone_desc.text = "Scopri gli attrezzi del circo: clave, palline, piatti e rola bola."
piedone_desc.visible = true
func _on_play_pressed() -> void:
if selected_tent == "juggle":
juggle_requested.emit(blue_tent_control)
elif selected_tent == "gym":
gym_requested.emit(red_tent_control)
func _add_bottom_bar() -> void:
# Piedone parte nascosto e sale all'avvio con testo centrale "Tocca uno dei tendoni"
piedone = Control.new()
piedone.name = "Piedone"
piedone.mouse_filter = Control.MOUSE_FILTER_IGNORE
piedone.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
add_child(piedone)
var bar := ColorRect.new()
bar.color = Color("#f1dfbd")
bar.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(bar)
piedone.add_child(bar)
MenuUI.set_normalized_rect(bar, Vector2(0.50, 0.885), Vector2(1.0, 0.23))
for item in [[0.774, 3.0], [0.787, 2.0]]:
var rule := ColorRect.new()
rule.color = Color("#28201c")
rule.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(rule)
piedone.add_child(rule)
MenuUI.set_normalized_rect(rule, Vector2(0.50, item[0]), Vector2(1.0, item[1] / 720.0))
MenuUI.add_label(self, "Tocca un tendone e scopri il gioco!", Vector2(0.38, 0.89), Vector2(0.66, 0.10), 24)
# Titolo / invito iniziale: allineato armoniosamente nella colonna sinistra
piedone_title = MenuUI.add_label(piedone, "Tocca uno dei tendoni", Vector2(0.38, 0.885), Vector2(0.52, 0.09), 26)
piedone_title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
# Descrizione del gioco: posizionata direttamente sotto al titolo
piedone_desc = Label.new()
piedone_desc.text = ""
piedone_desc.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT
piedone_desc.vertical_alignment = VERTICAL_ALIGNMENT_TOP
piedone_desc.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
piedone_desc.add_theme_font_override("font", MenuUI.DISPLAY_FONT)
piedone_desc.add_theme_font_size_override("font_size", 17)
piedone_desc.add_theme_color_override("font_color", Color("#423832"))
piedone_desc.mouse_filter = Control.MOUSE_FILTER_IGNORE
piedone_desc.visible = false
piedone.add_child(piedone_desc)
MenuUI.set_normalized_rect(piedone_desc, Vector2(0.38, 0.925), Vector2(0.52, 0.08))
# Pulsante GIOCA sul lato destro del piedone (coerente con i mockup P1 e P2a)
piedone_play = TextureButton.new()
piedone_play.ignore_texture_size = true
piedone_play.stretch_mode = TextureButton.STRETCH_KEEP_ASPECT_CENTERED
piedone_play.texture_normal = PLAY_BUTTON
piedone_play.texture_hover = PLAY_BUTTON
piedone_play.texture_pressed = PLAY_BUTTON
piedone_play.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
piedone_play.mouse_entered.connect(func(): piedone_play.modulate = Color(1.08, 1.08, 1.08, 1.0))
piedone_play.mouse_exited.connect(func(): piedone_play.modulate = Color.WHITE)
piedone_play.pressed.connect(_on_play_pressed)
piedone_play.visible = false
piedone.add_child(piedone_play)
MenuUI.set_normalized_rect(piedone_play, Vector2(0.79, 0.885), Vector2(0.26, 0.14))
# Parte nascosto sotto lo schermo e sale con molleggio morbido, lento e disteso
var viewport_height := get_viewport_rect().size.y
piedone.position.y = viewport_height * 0.29
var intro_tween := create_tween()
intro_tween.set_pause_mode(Tween.TWEEN_PAUSE_PROCESS)
intro_tween.tween_property(piedone, "position:y", -viewport_height * 0.018, 1.10).set_trans(Tween.TRANS_CUBIC).set_ease(Tween.EASE_OUT)
intro_tween.tween_property(piedone, "position:y", viewport_height * 0.007, 0.45).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT)
intro_tween.tween_property(piedone, "position:y", 0.0, 0.40).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_OUT)
func _add_sign() -> void:
MenuUI.add_image(self, CIRCUS_SIGN, Vector2(0.50, 0.17), Vector2(0.43, 0.34))
# Il pannello si muove, mentre le funi restano ancorate al bordo superiore.
# Separare i due livelli conserva la texture disegnata originale senza lasciare
# un vuoto quando il pannello supera verso il basso il punto di riposo.
var rig := Control.new()
rig.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(rig)
MenuUI.set_normalized_rect(rig, Vector2(0.50, 0.17), Vector2(0.43, 0.34))
_add_sign_layer(rig, CIRCUS_SIGN_ROPES)
var panel := _add_sign_layer(rig, CIRCUS_SIGN_PANEL)
var sign_mat := MenuUI.sign_lights_material()
panel.material = sign_mat
var final_y := panel.position.y
panel.position.y = final_y - panel.size.y * 1.15
var tween := create_tween()
# Il menu viene messo in pausa dalla shell: l'ingresso deve continuare comunque.
tween.set_pause_mode(Tween.TWEEN_PAUSE_PROCESS)
# Discesa rapida, atterraggio evidente e rimbalzo elastico.
call_deferred("_emit_sign_drop_started")
tween.tween_property(panel, "position:y", final_y + panel.size.y * 0.11, 1.10).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN)
# L'impatto è sincronizzato con la massima estensione, prima del ritorno elastico.
tween.tween_callback(func(): sign_landed.emit())
tween.tween_property(panel, "position:y", final_y, 0.50).set_trans(Tween.TRANS_BOUNCE).set_ease(Tween.EASE_OUT)
tween.tween_callback(func():
sign_bounced.emit()
# Attiva le lucine colorate dell'insegna con una dolce dissolvenza solo a fine animazione
var lights_tween := create_tween()
lights_tween.set_pause_mode(Tween.TWEEN_PAUSE_PROCESS)
lights_tween.tween_method(func(val: float): sign_mat.set_shader_parameter("sign_lights_active", val), 0.0, 1.0, 0.6).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_OUT)
)
func _add_sign_layer(parent: Control, texture: Texture2D) -> TextureRect:
var layer := TextureRect.new()
layer.texture = texture
layer.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
layer.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
layer.mouse_filter = Control.MOUSE_FILTER_IGNORE
parent.add_child(layer)
layer.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
return layer
func _emit_sign_drop_started() -> void:
if is_inside_tree():
sign_drop_started.emit()
@@ -28,9 +28,10 @@ func _build() -> void:
artwork.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
artwork.stretch_mode = TextureRect.STRETCH_SCALE
artwork.mouse_filter = Control.MOUSE_FILTER_IGNORE
artwork.material = MenuUI.submenu_lights_material()
add_child(artwork)
_add_hotspot(Vector2(0.12, 0.14), Vector2(0.10, 0.14), func(): back_requested.emit())
_add_hotspot(MenuUI.BACK_BUTTON_CENTER, MenuUI.BACK_BUTTON_SIZE, func(): back_requested.emit())
_add_hotspot(Vector2(0.775, 0.64), Vector2(0.07, 0.10), func(): _change_balls(-1))
_add_hotspot(Vector2(0.895, 0.64), Vector2(0.07, 0.10), func(): _change_balls(1))
_add_hotspot(Vector2(0.79, 0.88), Vector2(0.30, 0.14), _start_game)
+7 -14
View File
@@ -3,6 +3,7 @@ extends Control
## P3A segue la stessa composizione mockup + hotspot trasparenti di P2A.
signal back_requested
signal game_requested
signal feedback_requested(frequency: float)
const MOCKUP := preload("res://menus/assets/p3a/p3a_menu_mockup.jpg")
@@ -24,12 +25,13 @@ func _build() -> void:
artwork.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
artwork.stretch_mode = TextureRect.STRETCH_SCALE
artwork.mouse_filter = Control.MOUSE_FILTER_IGNORE
artwork.material = MenuUI.submenu_lights_material()
add_child(artwork)
_add_hotspot(Vector2(0.12, 0.14), Vector2(0.10, 0.14), func(): back_requested.emit())
_add_hotspot(MenuUI.BACK_BUTTON_CENTER, MenuUI.BACK_BUTTON_SIZE, func(): back_requested.emit())
_add_hotspot(Vector2(0.775, 0.64), Vector2(0.07, 0.10), func(): _change_players(-1))
_add_hotspot(Vector2(0.895, 0.64), Vector2(0.07, 0.10), func(): _change_players(1))
_add_hotspot(Vector2(0.79, 0.88), Vector2(0.30, 0.14), _show_coming_soon)
_add_hotspot(Vector2(0.79, 0.88), Vector2(0.30, 0.14), _start_game)
_build_dynamic_count()
@@ -59,15 +61,6 @@ func _change_players(delta: int) -> void:
feedback_requested.emit(440.0 + 45.0 * players_count)
func _show_coming_soon() -> void:
feedback_requested.emit(260.0)
var note := MenuUI.add_label(self, "Presto disponibile", Vector2(0.79, 0.72), Vector2(0.28, 0.09), 24)
note.add_theme_color_override("font_color", Color("#4c2917"))
note.add_theme_stylebox_override("normal", MenuUI.note_style())
note.modulate.a = 0.0
var tween := create_tween()
tween.set_pause_mode(Tween.TWEEN_PAUSE_PROCESS)
tween.tween_property(note, "modulate:a", 1.0, 0.16)
tween.tween_interval(1.6)
tween.tween_property(note, "modulate:a", 0.0, 0.28)
tween.tween_callback(note.queue_free)
func _start_game() -> void:
feedback_requested.emit(784.0)
game_requested.emit()
+4 -2
View File
@@ -21,9 +21,11 @@ Settings="*res://shared/settings/settings.gd"
window/size/viewport_width=1280
window/size/viewport_height=720
window/size/resizable=false
# Edge 60 e altri telefoni moderni sono più larghi di 16:9 in landscape.
# expand usa tutto il display senza deformare né introdurre pillarbox.
window/size/resizable=true
window/stretch/mode="canvas_items"
window/stretch/aspect="keep"
window/stretch/aspect="expand"
window/handheld/orientation=4
[input_devices]
+297
View File
@@ -0,0 +1,297 @@
#!/usr/bin/env python3
"""Rileva con precisione stella e lampadine nel PNG RGBA del tendone.
Il rilevamento non stima rette: segmenta direttamente il colore oro, ricompone
le pennellate di ogni lampadina e usa il centroide del blob reale. Può inoltre
registrare il PNG su un mockup tramite SIFT+RANSAC e trasferire le coordinate.
Esempio:
python3 tools/analyze_tent_lights.py \
menus/assets/p2a/2a_tendone.png \
--reference menus/assets/p2a/p2a_menu_mockup.jpg.bak \
--output-dir /tmp/tent-lights-analysis \
--debug-scene-asset menus/assets/p2a/p2a_menu_mockup.jpg
"""
from __future__ import annotations
import argparse
import json
from pathlib import Path
import cv2
import numpy as np
from PIL import Image, ImageDraw
GOLD_HSV_LOW = np.array((12, 45, 90), dtype=np.uint8)
GOLD_HSV_HIGH = np.array((45, 255, 255), dtype=np.uint8)
def gold_mask(rgba: np.ndarray) -> np.ndarray:
hsv = cv2.cvtColor(rgba[:, :, :3], cv2.COLOR_RGB2HSV)
color = cv2.inRange(hsv, GOLD_HSV_LOW, GOLD_HSV_HIGH) > 0
return (color & (rgba[:, :, 3] > 15)).astype(np.uint8) * 255
def connected_gold_blobs(mask: np.ndarray) -> list[dict]:
# Chiude soltanto le micro-interruzioni della pennellata, senza fondere
# lampadine adiacenti. La dilatazione rende stabile il centro del blob.
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (7, 7))
merged = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
merged = cv2.dilate(
merged, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))
)
count, _, stats, centroids = cv2.connectedComponentsWithStats(merged, 8)
blobs: list[dict] = []
for label in range(1, count):
x, y, width, height, area = map(int, stats[label])
blobs.append(
{
"x": float(centroids[label][0]),
"y": float(centroids[label][1]),
"bbox": [x, y, width, height],
"area": area,
}
)
return blobs
def detect_star(mask: np.ndarray) -> dict:
height, _ = mask.shape
top = mask[: int(height * 0.25)].copy()
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (11, 11))
top = cv2.morphologyEx(top, cv2.MORPH_CLOSE, kernel)
blobs = connected_gold_blobs(top)
if not blobs:
raise RuntimeError("Stella non rilevata nella parte alta dell'asset")
star = max(blobs, key=lambda blob: blob["area"])
x, y, width, height = star["bbox"]
# Per il bagliore serve il centro geometrico della stella, non il centroide
# colorimetrico (spostato dalle cinque punte e dal numero nero interno).
star["x"] = x + (width - 1) / 2.0
star["y"] = y + (height - 1) / 2.0
return star
def detect_bulbs(mask: np.ndarray, star: dict) -> tuple[list[dict], list[dict]]:
height, width = mask.shape
roi = np.zeros_like(mask)
# Le garlande finiscono prima della modanatura dorata della balza. Fermarsi
# al 59% evita che frammenti orizzontali del bordo vengano scambiati per
# lampadine (nel PNG 2796x1290 il bordo comincia attorno a y=768).
roi[int(height * 0.16) : int(height * 0.59), : int(width * 0.60)] = 255
blobs = connected_gold_blobs(cv2.bitwise_and(mask, roi))
candidates = []
for blob in blobs:
_, _, blob_width, blob_height = blob["bbox"]
if (
6 <= blob_width <= 80
and 6 <= blob_height <= 80
and 10 <= blob["area"] <= 3000
and blob["y"] > star["bbox"][1] + star["bbox"][3]
):
candidates.append(blob)
center_x = star["x"]
left = sorted((b for b in candidates if b["x"] < center_x), key=lambda b: b["y"])
right = sorted((b for b in candidates if b["x"] > center_x), key=lambda b: b["y"])
if len(left) != 12 or len(right) != 12:
raise RuntimeError(
"Rilevamento ambiguo: attese 12 lampadine per lato, "
f"rilevate {len(left)} a sinistra e {len(right)} a destra"
)
return left, right
def estimate_transform(source_rgba: np.ndarray, reference_rgb: np.ndarray) -> tuple[np.ndarray, dict]:
source_gray = cv2.cvtColor(source_rgba[:, :, :3], cv2.COLOR_RGB2GRAY)
reference_gray = cv2.cvtColor(reference_rgb, cv2.COLOR_RGB2GRAY)
source_mask = (source_rgba[:, :, 3] > 128).astype(np.uint8) * 255
reference_mask = np.zeros_like(reference_gray)
reference_mask[:, : int(reference_gray.shape[1] * 0.60)] = 255
sift = cv2.SIFT_create(nfeatures=10000, contrastThreshold=0.02)
source_keys, source_desc = sift.detectAndCompute(source_gray, source_mask)
reference_keys, reference_desc = sift.detectAndCompute(reference_gray, reference_mask)
if source_desc is None or reference_desc is None:
raise RuntimeError("Feature insufficienti per registrare asset e mockup")
matches = cv2.BFMatcher().knnMatch(source_desc, reference_desc, k=2)
good = [first for first, second in matches if first.distance < 0.70 * second.distance]
if len(good) < 20:
raise RuntimeError(f"Registrazione instabile: soltanto {len(good)} match validi")
source_points = np.float32([source_keys[m.queryIdx].pt for m in good])
reference_points = np.float32([reference_keys[m.trainIdx].pt for m in good])
matrix, inliers = cv2.estimateAffine2D(
source_points,
reference_points,
method=cv2.RANSAC,
ransacReprojThreshold=3.0,
maxIters=10000,
confidence=0.999,
)
if matrix is None or inliers is None:
raise RuntimeError("RANSAC non ha trovato una trasformazione affine")
inlier_count = int(inliers.sum())
if inlier_count < 100 or inlier_count / len(good) < 0.80:
raise RuntimeError(
f"Registrazione non affidabile: {inlier_count}/{len(good)} inlier"
)
return matrix, {
"source_features": len(source_keys),
"reference_features": len(reference_keys),
"matches": len(good),
"inliers": inlier_count,
"inlier_ratio": inlier_count / len(good),
}
def transform_point(point: dict, matrix: np.ndarray) -> dict:
mapped = matrix @ np.array((point["x"], point["y"], 1.0))
result = dict(point)
result["x"] = float(mapped[0])
result["y"] = float(mapped[1])
return result
def normalized(point: dict, width: int, height: int) -> list[float]:
return [point["x"] / width, point["y"] / height]
def draw_marker(draw: ImageDraw.ImageDraw, point: dict, label: str, radius: int) -> None:
x, y = point["x"], point["y"]
draw.ellipse(
(x - radius, y - radius, x + radius, y + radius),
outline=(255, 0, 0, 255),
width=max(4, radius // 5),
)
cross = max(7, radius // 3)
draw.line((x - cross, y, x + cross, y), fill=(0, 255, 255, 255), width=3)
draw.line((x, y - cross, x, y + cross), fill=(0, 255, 255, 255), width=3)
draw.text((x + radius + 4, y - radius), label, fill=(255, 255, 255, 255), stroke_width=2, stroke_fill=(0, 0, 0, 255))
def source_overlay(rgba: np.ndarray, star: dict, left: list[dict], right: list[dict]) -> Image.Image:
alpha = rgba[:, :, 3:4].astype(np.float32) / 255.0
black = np.zeros_like(rgba[:, :, :3], dtype=np.float32)
composited = rgba[:, :, :3] * alpha + black * (1.0 - alpha)
image = Image.fromarray(np.uint8(np.clip(composited, 0, 255)), "RGB").convert("RGBA")
draw = ImageDraw.Draw(image)
draw_marker(draw, star, "STAR", 60)
for index, point in enumerate(left):
draw_marker(draw, point, f"L{index:02d}", 25)
for index, point in enumerate(right):
draw_marker(draw, point, f"R{index:02d}", 25)
return image
def reference_overlay(
reference_rgb: np.ndarray,
source_rgba: np.ndarray,
star: dict,
left: list[dict],
right: list[dict],
) -> Image.Image:
# Nero fuori dalla sagoma del PNG: impedisce al cielo di mascherare gli errori.
image = reference_rgb.copy()
foreground = source_rgba[:, :, 3] > 15
image[~foreground] = 0
result = Image.fromarray(image, "RGB").convert("RGBA")
draw = ImageDraw.Draw(result)
draw_marker(draw, star, "STAR", 60)
for index, point in enumerate(left):
draw_marker(draw, point, f"L{index:02d}", 25)
for index, point in enumerate(right):
draw_marker(draw, point, f"R{index:02d}", 25)
return result
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("asset", type=Path, help="PNG RGBA contenente il tendone")
parser.add_argument("--reference", type=Path, help="Mockup sul quale trasferire i punti")
parser.add_argument("--output-dir", type=Path, default=Path("/tmp/tent-lights-analysis"))
parser.add_argument(
"--debug-scene-asset",
type=Path,
help="Scrive una copia JPEG di debug per la scena Godot (sfondo nero e marker)",
)
args = parser.parse_args()
source_rgba = np.array(Image.open(args.asset).convert("RGBA"))
height, width = source_rgba.shape[:2]
mask = gold_mask(source_rgba)
star = detect_star(mask)
left, right = detect_bulbs(mask, star)
args.output_dir.mkdir(parents=True, exist_ok=True)
overlay = source_overlay(source_rgba, star, left, right)
overlay_path = args.output_dir / "detected_on_source.png"
overlay.save(overlay_path)
report: dict = {
"asset": str(args.asset),
"size": [width, height],
"star": {"pixel": [star["x"], star["y"]], "uv": normalized(star, width, height)},
"left": [
{"pixel": [p["x"], p["y"]], "uv": normalized(p, width, height), "bbox": p["bbox"]}
for p in left
],
"right": [
{"pixel": [p["x"], p["y"]], "uv": normalized(p, width, height), "bbox": p["bbox"]}
for p in right
],
}
if args.reference:
reference_rgb = np.array(Image.open(args.reference).convert("RGB"))
matrix, diagnostics = estimate_transform(source_rgba, reference_rgb)
mapped_star = transform_point(star, matrix)
mapped_left = [transform_point(p, matrix) for p in left]
mapped_right = [transform_point(p, matrix) for p in right]
ref_height, ref_width = reference_rgb.shape[:2]
mapped_overlay = reference_overlay(
reference_rgb, source_rgba, mapped_star, mapped_left, mapped_right
)
reference_path = args.output_dir / "detected_on_reference.png"
mapped_overlay.save(reference_path)
if args.debug_scene_asset:
mapped_overlay.convert("RGB").save(args.debug_scene_asset, quality=96)
report["reference"] = {
"path": str(args.reference),
"size": [ref_width, ref_height],
"affine_matrix": matrix.tolist(),
"diagnostics": diagnostics,
"star": {
"pixel": [mapped_star["x"], mapped_star["y"]],
"uv": normalized(mapped_star, ref_width, ref_height),
},
"left": [
{"pixel": [p["x"], p["y"]], "uv": normalized(p, ref_width, ref_height)}
for p in mapped_left
],
"right": [
{"pixel": [p["x"], p["y"]], "uv": normalized(p, ref_width, ref_height)}
for p in mapped_right
],
}
report_path = args.output_dir / "coordinates.json"
report_path.write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8")
print(f"Asset: {width}x{height}")
print(f"Stella: ({star['x']:.2f}, {star['y']:.2f})")
print("Sinistra:", ", ".join(f"({p['x']:.2f},{p['y']:.2f})" for p in left))
print("Destra:", ", ".join(f"({p['x']:.2f},{p['y']:.2f})" for p in right))
print(f"Overlay: {overlay_path}")
print(f"Coordinate: {report_path}")
if __name__ == "__main__":
main()