Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aaa5098067 | ||
|
|
1bed6318a1 | ||
|
|
06fd7a0fad | ||
|
|
918246fdcd | ||
|
|
84a15c7d4c | ||
|
|
95ccfc4c92 | ||
|
|
717a702fb2 | ||
|
|
882302c6d2 | ||
|
|
7af475aec0 | ||
|
|
3e5fd25f85 | ||
|
|
0e93e646b0 | ||
|
|
d0991a0edf |
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
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
|
||||
|
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
|
||||
|
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
|
||||
|
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
|
||||
|
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
|
||||
|
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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
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
|
||||
|
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
|
||||
|
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
|
||||
|
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
|
||||
|
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
|
||||
|
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
|
||||
|
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
|
||||
|
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
|
||||
|
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
|
||||
|
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
|
||||
|
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
|
||||
|
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
|
||||
|
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
|
||||
|
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
|
||||
|
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
|
||||
|
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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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()
|
||||