Release v0.2: refine menus and JuggleBoard
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 212 KiB |
@@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dpqfmbyrhjwu5"
|
||||
path="res://.godot/imported/2b_pallina_150__blu.png-fbee7d8a6af9e82bc1aa68345fa35b50.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://games/juggleboard/assets/p2b/2b_pallina_150__blu.png"
|
||||
dest_files=["res://.godot/imported/2b_pallina_150__blu.png-fbee7d8a6af9e82bc1aa68345fa35b50.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 254 KiB |
@@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://chh47nt0hwikk"
|
||||
path="res://.godot/imported/2b_pallina_150__gialla.png-f0f6338d028fd5c066c4e5cf989d400f.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://games/juggleboard/assets/p2b/2b_pallina_150__gialla.png"
|
||||
dest_files=["res://.godot/imported/2b_pallina_150__gialla.png-f0f6338d028fd5c066c4e5cf989d400f.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 262 KiB |
@@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dp7betlnoa0o5"
|
||||
path="res://.godot/imported/2b_pallina_150__rosa.png-f0099789f3d8dde8d375070e08e9c5d3.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://games/juggleboard/assets/p2b/2b_pallina_150__rosa.png"
|
||||
dest_files=["res://.godot/imported/2b_pallina_150__rosa.png-f0099789f3d8dde8d375070e08e9c5d3.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 254 KiB |
@@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://oamjgbblksst"
|
||||
path="res://.godot/imported/2b_pallina_150__rossa.png-78b1e52842e087a16fc59f0ce831a2d1.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://games/juggleboard/assets/p2b/2b_pallina_150__rossa.png"
|
||||
dest_files=["res://.godot/imported/2b_pallina_150__rossa.png-78b1e52842e087a16fc59f0ce831a2d1.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 259 KiB |
@@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://tj3ifbperj5j"
|
||||
path="res://.godot/imported/2b_pallina_150__verde.png-95b5b437807ff5e94e5f452ba653e202.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://games/juggleboard/assets/p2b/2b_pallina_150__verde.png"
|
||||
dest_files=["res://.godot/imported/2b_pallina_150__verde.png-95b5b437807ff5e94e5f452ba653e202.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 13 MiB |
@@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://cykrbve556tom"
|
||||
path="res://.godot/imported/board_background.png-f880d79e6bc48224225b63a494d21f64.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://games/juggleboard/assets/p2b/board_background.png"
|
||||
dest_files=["res://.godot/imported/board_background.png-f880d79e6bc48224225b63a494d21f64.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
|
||||
@@ -1,125 +1,130 @@
|
||||
extends Area2D
|
||||
## Biglia dinamica P2B: usa gli asset illustrati del mockup e conserva
|
||||
## movimento, ritorno e hitbox della logica originale.
|
||||
|
||||
## Una singola pallina nella sua corsia. Partendo da destra, se lanciata
|
||||
## scivola verso il bordo sinistro e poi torna alla posizione di partenza.
|
||||
## Il rilevamento del tocco/click è gestito centralmente da main.gd tramite
|
||||
## distanza: qui NON ci sono handler di input dedicati.
|
||||
|
||||
signal returned # emesso quando la pallina torna a casa
|
||||
signal returned
|
||||
|
||||
const ART := {
|
||||
"rosso": preload("res://games/juggleboard/assets/p2b/2b_pallina_150__rossa.png"),
|
||||
"blu": preload("res://games/juggleboard/assets/p2b/2b_pallina_150__blu.png"),
|
||||
"verde": preload("res://games/juggleboard/assets/p2b/2b_pallina_150__verde.png"),
|
||||
"giallo": preload("res://games/juggleboard/assets/p2b/2b_pallina_150__gialla.png"),
|
||||
"viola": preload("res://games/juggleboard/assets/p2b/2b_pallina_150__rosa.png"),
|
||||
}
|
||||
|
||||
var color_name := ""
|
||||
var ball_color := Color.WHITE
|
||||
var radius := 40.0
|
||||
var radius := 48.0
|
||||
var home_pos := Vector2.ZERO
|
||||
var left_pos := Vector2.ZERO
|
||||
var in_motion := false
|
||||
var roll_angle := 0.0:
|
||||
set(v):
|
||||
roll_angle = v
|
||||
queue_redraw() # aggiorna il disegno a ogni step di rotazione
|
||||
var veins: Array[PackedVector2Array] = []
|
||||
var _rng := RandomNumberGenerator.new()
|
||||
set(value):
|
||||
roll_angle = value
|
||||
queue_redraw()
|
||||
var texture: Texture2D
|
||||
var surface_marks: Array[Dictionary] = []
|
||||
var _tween: Tween
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
# ombra a terra
|
||||
draw_circle(Vector2(0, 6), radius, Color(0, 0, 0, 0.25))
|
||||
# corpo base
|
||||
draw_circle(Vector2.ZERO, radius, ball_color)
|
||||
# ombreggiatura inferiore-destra (profondità 3D)
|
||||
draw_circle(Vector2(radius * 0.18, radius * 0.22), radius * 0.92, Color(0, 0, 0, 0.16))
|
||||
# luce superiore-sinistra
|
||||
draw_circle(Vector2(-radius * 0.18, -radius * 0.22), radius * 0.92, Color(1, 1, 1, 0.2))
|
||||
# marmorizzazione a vortici che ruota col rotolamento (design realistico)
|
||||
_draw_marbling()
|
||||
# bordo nero spesso (stile china)
|
||||
draw_arc(Vector2.ZERO, radius, 0.0, TAU, 48, Color(0.1, 0.1, 0.1, 0.9), 4.0)
|
||||
# riflesso speculare (fisso: la luce non ruota con la superficie)
|
||||
draw_circle(Vector2(-radius * 0.3, -radius * 0.34), radius * 0.16, Color(1, 1, 1, 0.85))
|
||||
if texture == null:
|
||||
return
|
||||
# La sagoma non ruota nel piano (asse Z). Venature, puntinature e piccoli
|
||||
# riflessi scorrono invece sulla superficie come su una sfera che ruota
|
||||
# attorno all'asse verticale, con schiacciamento ai lati.
|
||||
draw_texture_rect(texture, Rect2(-radius, -radius, radius * 2.0, radius * 2.0), false)
|
||||
_draw_surface_texture()
|
||||
|
||||
|
||||
func _draw_marbling() -> void:
|
||||
# Venature curve, irregolari e casuali per ogni biglia. Tutta la geometria
|
||||
# superficiale ruota; illuminazione e riflesso vengono disegnati dopo.
|
||||
var vein_color: Color = ball_color.darkened(0.42)
|
||||
vein_color.a = 0.20
|
||||
var soft_color: Color = ball_color.darkened(0.28)
|
||||
soft_color.a = 0.12
|
||||
for i: int in range(veins.size()):
|
||||
var rotated_points := PackedVector2Array()
|
||||
var vis_sum := 0.0
|
||||
for point: Vector2 in veins[i]:
|
||||
# Rotazione attorno all'asse Y (perpendicolare al moto orizzontale):
|
||||
# la vena scorre in orizzontale (sin(θ+roll)) e si schiaccia ai bordi
|
||||
# (cos = foreshortening). I punti sul retro (cos<=0) non sono visibili.
|
||||
var theta: float = asin(clampf(point.x / radius, -1.0, 1.0))
|
||||
var new_theta: float = theta + roll_angle
|
||||
var c := cos(new_theta)
|
||||
if c <= 0.0:
|
||||
continue # retro della sfera: nascosto
|
||||
var vis := clampf(c, 0.0, 1.0)
|
||||
vis_sum += vis
|
||||
rotated_points.append(Vector2(
|
||||
sin(new_theta) * radius,
|
||||
point.y * (0.3 + 0.7 * vis)))
|
||||
if rotated_points.size() < 2:
|
||||
func _draw_surface_texture() -> void:
|
||||
var dark := ball_color.darkened(0.55)
|
||||
var light := ball_color.lightened(0.18)
|
||||
for mark in surface_marks:
|
||||
var points := PackedVector2Array()
|
||||
var visible_sum := 0.0
|
||||
var angle := float(mark["angle"])
|
||||
var span := float(mark["span"])
|
||||
var latitude := float(mark["latitude"])
|
||||
var wobble := float(mark["wobble"])
|
||||
var waves := float(mark["waves"])
|
||||
var phase := float(mark["phase"])
|
||||
for step in range(13):
|
||||
var t := float(step) / 12.0
|
||||
var theta := angle + lerpf(-span, span, t) + roll_angle
|
||||
var front := cos(theta)
|
||||
if front <= 0.0:
|
||||
continue
|
||||
var wave: float = sin(t * TAU * waves + phase) * wobble
|
||||
var y: float = (latitude + wave) * radius * (0.32 + 0.68 * front)
|
||||
points.append(Vector2(sin(theta) * radius, y))
|
||||
visible_sum += front
|
||||
if points.size() < 2:
|
||||
continue
|
||||
var vis_avg := vis_sum / veins[i].size()
|
||||
var width: float = radius * (0.075 if i == 0 else 0.045) * (0.3 + 0.7 * vis_avg)
|
||||
draw_polyline(rotated_points, soft_color, width * 1.9, true)
|
||||
draw_polyline(rotated_points, vein_color, width, true)
|
||||
var visibility := visible_sum / points.size()
|
||||
_draw_mark_segments(points, dark, 0.10 + 0.20 * visibility,
|
||||
1.2 + float(mark["weight"]) * visibility)
|
||||
if bool(mark["highlight"]):
|
||||
_draw_mark_segments(points, light, 0.08 + 0.13 * visibility, 0.7)
|
||||
|
||||
|
||||
func _generate_veins() -> Array[PackedVector2Array]:
|
||||
# 7 venature distribuite uniformemente su TUTTA la longitudine (360°):
|
||||
# così la superficie non resta mai liscia durante la rotazione.
|
||||
var result: Array[PackedVector2Array] = []
|
||||
var count := 7
|
||||
for i in count:
|
||||
var center: float = -PI + TAU * (float(i) + _rng.randf_range(0.0, 0.9)) / count
|
||||
var spread := _rng.randf_range(0.25, 0.7)
|
||||
var yc := _rng.randf_range(-0.5, 0.5)
|
||||
var y0 := clampf(yc - _rng.randf_range(0.0, 0.3), -0.6, 0.6)
|
||||
var y1 := clampf(yc + _rng.randf_range(0.0, 0.3), -0.6, 0.6)
|
||||
var bend := _rng.randf_range(0.05, 0.22)
|
||||
var ripple := _rng.randf_range(0.03, 0.11)
|
||||
result.append(_make_vein(sin(center - spread), y0, sin(center + spread), y1, bend, ripple))
|
||||
return result
|
||||
func _draw_mark_segments(points: PackedVector2Array, color: Color, base_alpha: float, width: float) -> void:
|
||||
# Il pallino bianco è il riflesso speculare dell'asset: qualunque venatura
|
||||
# che lo attraversa viene nascosta sotto di esso e sfuma ai suoi bordi.
|
||||
for index in range(1, points.size()):
|
||||
var a := points[index - 1]
|
||||
var b := points[index]
|
||||
var specular_visibility := (_outside_specular(a) + _outside_specular(b)) * 0.5
|
||||
if specular_visibility <= 0.01:
|
||||
continue
|
||||
var segment_color := color
|
||||
segment_color.a = base_alpha * specular_visibility
|
||||
draw_line(a, b, segment_color, width, true)
|
||||
|
||||
|
||||
func _make_vein(x0: float, y0: float, x1: float, y1: float,
|
||||
bend: float, ripple: float) -> PackedVector2Array:
|
||||
var points := PackedVector2Array()
|
||||
var direction := Vector2(x1 - x0, y1 - y0)
|
||||
var normal := direction.normalized().orthogonal()
|
||||
for step: int in range(17):
|
||||
var t: float = float(step) / 16.0
|
||||
var base := Vector2(lerpf(x0, x1, t), lerpf(y0, y1, t))
|
||||
var curve: float = sin(t * PI) * bend
|
||||
var wave: float = sin(t * TAU * 1.35 + x0 * 4.0) * ripple
|
||||
points.append((base + normal * (curve + wave)) * radius)
|
||||
return points
|
||||
func _outside_specular(point: Vector2) -> float:
|
||||
# Posizione e raggio del punto luce dell'illustrazione P2B, in coordinate
|
||||
# locali della sfera. Nel nucleo la venatura sparisce; la transizione è soft.
|
||||
var highlight_center := Vector2(-0.30, -0.34) * radius
|
||||
var distance_from_highlight := point.distance_to(highlight_center) / radius
|
||||
return smoothstep(0.12, 0.30, distance_from_highlight)
|
||||
|
||||
|
||||
func setup(cn: String, c: Color, home: Vector2, left: Vector2, r: float) -> void:
|
||||
func setup(cn: String, color: Color, home: Vector2, left: Vector2, r: float) -> void:
|
||||
color_name = cn
|
||||
ball_color = c
|
||||
ball_color = color
|
||||
radius = r
|
||||
_rng.seed = hash(cn) # venature uniche ma stabili per ogni colore
|
||||
veins = _generate_veins()
|
||||
texture = ART[cn]
|
||||
surface_marks = _make_surface_marks(cn)
|
||||
home_pos = home
|
||||
left_pos = left
|
||||
position = home
|
||||
var shape := CollisionShape2D.new()
|
||||
var cs := CircleShape2D.new()
|
||||
cs.radius = r * 1.35 # area di tocco ampliata (ma l'input è via distanza in main)
|
||||
shape.shape = cs
|
||||
var circle := CircleShape2D.new()
|
||||
circle.radius = r * 1.35
|
||||
shape.shape = circle
|
||||
add_child(shape)
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _make_surface_marks(seed_name: String) -> Array[Dictionary]:
|
||||
var rng := RandomNumberGenerator.new()
|
||||
rng.seed = hash(seed_name)
|
||||
var marks: Array[Dictionary] = []
|
||||
for index in range(10):
|
||||
marks.append({
|
||||
"angle": rng.randf_range(-PI, PI),
|
||||
"span": rng.randf_range(0.22, 0.62),
|
||||
"latitude": rng.randf_range(-0.62, 0.62),
|
||||
"wobble": rng.randf_range(0.025, 0.11),
|
||||
"waves": rng.randf_range(0.7, 2.1),
|
||||
"phase": rng.randf_range(0.0, TAU),
|
||||
"weight": rng.randf_range(0.4, 1.5),
|
||||
"highlight": index % 3 == 0,
|
||||
})
|
||||
return marks
|
||||
|
||||
|
||||
func is_home() -> bool:
|
||||
return not in_motion
|
||||
|
||||
@@ -132,25 +137,17 @@ func reset() -> void:
|
||||
roll_angle = 0.0
|
||||
|
||||
|
||||
## Lancia la pallina verso sinistra e la fa tornare a casa. `mt` è il tempo
|
||||
## totale (andata + ritorno) in secondi.
|
||||
func launch(mt: float) -> void:
|
||||
func launch(total_time: float) -> void:
|
||||
in_motion = true
|
||||
if _tween and _tween.is_valid():
|
||||
_tween.kill()
|
||||
var half: float = maxf(mt * 0.5, 0.01)
|
||||
# rotazione proporzionale alla distanza percorsa (rotolamento)
|
||||
var dist: float = home_pos.x - left_pos.x
|
||||
var total_angle: float = dist / radius
|
||||
var half := maxf(total_time * 0.5, 0.01)
|
||||
var total_angle := (home_pos.x - left_pos.x) / radius
|
||||
_tween = create_tween()
|
||||
_tween.tween_property(self, "position", left_pos, half) \
|
||||
.set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN_OUT)
|
||||
_tween.parallel().tween_property(self, "roll_angle", total_angle, half) \
|
||||
.set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN_OUT)
|
||||
_tween.tween_property(self, "position", home_pos, half) \
|
||||
.set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN_OUT)
|
||||
_tween.parallel().tween_property(self, "roll_angle", 0.0, half) \
|
||||
.set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN_OUT)
|
||||
_tween.tween_property(self, "position", left_pos, half).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN_OUT)
|
||||
_tween.parallel().tween_property(self, "roll_angle", total_angle, half).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN_OUT)
|
||||
_tween.tween_property(self, "position", home_pos, half).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN_OUT)
|
||||
_tween.parallel().tween_property(self, "roll_angle", 0.0, half).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN_OUT)
|
||||
_tween.finished.connect(_on_motion_done)
|
||||
|
||||
|
||||
|
||||
@@ -1,88 +1,16 @@
|
||||
extends Node2D
|
||||
## Plancia Juggle Board in legno: binari a TUTTA larghezza con larghezza
|
||||
## variabile (svasati alle estremità, stretti al centro), alloggiamenti
|
||||
## circolari incassati e indicatori numerici 1-5, come nel mockup.
|
||||
## Fondale P2B: plancia illustrata, pannello laterale e cinque corsie.
|
||||
|
||||
const VIEW := Vector2(1280, 720)
|
||||
const LANE_TOP := 210.0
|
||||
const LANE_GAP := 98.0
|
||||
const LANE_LEFT := 100.0 # centro alloggiamento sinistro (dentro i bordi)
|
||||
const LANE_RIGHT := 1180.0 # centro alloggiamento destro (dentro i bordi)
|
||||
const SOCKET_R := 46.0 # raggio alloggiamento (ampio alle estremità)
|
||||
const H_NARROW := 24.0 # semi-altezza canale al centro (stretto)
|
||||
const CENTER_X := (LANE_LEFT + LANE_RIGHT) / 2.0
|
||||
const HALF_SPAN := (LANE_RIGHT - LANE_LEFT) / 2.0
|
||||
|
||||
const LANE_FILL := Color("#c9ac7c")
|
||||
const LANE_EDGE := Color(0.22, 0.15, 0.08, 0.9)
|
||||
|
||||
var wood_tex: Texture2D
|
||||
var circus_tex: Texture2D
|
||||
const P2B_BACKGROUND := preload("res://games/juggleboard/assets/p2b/board_background.png")
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
z_index = -10
|
||||
wood_tex = load("res://shared/assets/textures/wood_board_rounded.png") # angoli arrotondati coerenti con la cornice
|
||||
circus_tex = load("res://games/juggleboard/assets/textures/circus_arena_bg.png")
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
# Arena circense quieta ai margini; la plancia copre il centro più ricco.
|
||||
if circus_tex:
|
||||
draw_texture_rect(circus_tex, Rect2(Vector2.ZERO, VIEW), false)
|
||||
# Ombra e corpo smussato del tabellone.
|
||||
var shadow := StyleBoxFlat.new()
|
||||
shadow.bg_color = Color(0.12, 0.055, 0.02, 0.5)
|
||||
shadow.corner_radius_top_left = 28
|
||||
shadow.corner_radius_top_right = 28
|
||||
shadow.corner_radius_bottom_left = 28
|
||||
shadow.corner_radius_bottom_right = 28
|
||||
draw_style_box(shadow, Rect2(32, 151, 1216, 560))
|
||||
if wood_tex:
|
||||
draw_texture_rect(wood_tex, Rect2(36, 142, 1208, 558), false)
|
||||
var frame := StyleBoxFlat.new()
|
||||
frame.bg_color = Color(0, 0, 0, 0)
|
||||
frame.border_color = Color("#81542f")
|
||||
frame.set_border_width_all(8)
|
||||
frame.corner_radius_top_left = 27
|
||||
frame.corner_radius_top_right = 27
|
||||
frame.corner_radius_bottom_left = 27
|
||||
frame.corner_radius_bottom_right = 27
|
||||
draw_style_box(frame, Rect2(32, 138, 1216, 566))
|
||||
for i in range(5):
|
||||
_draw_lane(LANE_TOP + i * LANE_GAP)
|
||||
|
||||
|
||||
func _lane_half_width(x: float) -> float:
|
||||
# larghezza corsia: ampia alle estremità (d=1), stretta al centro (d=0)
|
||||
var d := absf(x - CENTER_X) / HALF_SPAN
|
||||
d = clampf(d, 0.0, 1.0)
|
||||
return H_NARROW + (SOCKET_R - H_NARROW) * d * d
|
||||
|
||||
|
||||
func _draw_lane(y: float) -> void:
|
||||
var pts := PackedVector2Array()
|
||||
var steps := 48
|
||||
for k in range(steps + 1):
|
||||
var x := lerpf(LANE_LEFT, LANE_RIGHT, float(k) / steps)
|
||||
var hw := _lane_half_width(x)
|
||||
pts.append(Vector2(x, y - hw))
|
||||
for k in range(steps, -1, -1):
|
||||
var x := lerpf(LANE_LEFT, LANE_RIGHT, float(k) / steps)
|
||||
var hw := _lane_half_width(x)
|
||||
pts.append(Vector2(x, y + hw))
|
||||
draw_polygon(pts, [LANE_FILL])
|
||||
var closed := pts.duplicate()
|
||||
closed.append(pts[0])
|
||||
draw_polyline(closed, LANE_EDGE, 3.0, true)
|
||||
# Alloggiamenti a calotta: anelli concentrici e luce/ombra danno profondità.
|
||||
for cx in [LANE_LEFT, LANE_RIGHT]:
|
||||
var c := Vector2(cx, y)
|
||||
draw_circle(c + Vector2(0, 4), SOCKET_R + 3.0, Color(0.12, 0.06, 0.02, 0.38))
|
||||
draw_circle(c, SOCKET_R, Color("#8b6841"))
|
||||
draw_circle(c + Vector2(0, 3), SOCKET_R - 6.0, Color("#6d4b2d"))
|
||||
draw_circle(c + Vector2(-2, -4), SOCKET_R - 9.0, Color("#c5a476"))
|
||||
draw_circle(c + Vector2(3, 5), SOCKET_R - 13.0, Color(0.2, 0.1, 0.04, 0.16))
|
||||
draw_arc(c, SOCKET_R, 0.0, TAU, 48, Color("#4a301d"), 3.0)
|
||||
draw_arc(c - Vector2(1, 2), SOCKET_R - 8.0, PI, TAU, 24, Color(1, 0.92, 0.72, 0.28), 2.0)
|
||||
# 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)
|
||||
|
||||
@@ -1,82 +1,29 @@
|
||||
extends Node2D
|
||||
## Bersaglio P2B: stessa famiglia di asset delle palline interattive.
|
||||
|
||||
## Pallina di colore che mostra il bersaglio attuale nella barra in alto.
|
||||
## Stile coerente con le biglie della plancia: glossy con venature marmorizzate.
|
||||
const R := 48.0
|
||||
const ART := {
|
||||
Color("de6b7e"): preload("res://games/juggleboard/assets/p2b/2b_pallina_150__rossa.png"),
|
||||
Color("1852de"): preload("res://games/juggleboard/assets/p2b/2b_pallina_150__blu.png"),
|
||||
Color("88ce96"): preload("res://games/juggleboard/assets/p2b/2b_pallina_150__verde.png"),
|
||||
Color("e8b84b"): preload("res://games/juggleboard/assets/p2b/2b_pallina_150__gialla.png"),
|
||||
Color("9b7ede"): preload("res://games/juggleboard/assets/p2b/2b_pallina_150__rosa.png"),
|
||||
}
|
||||
|
||||
var dot_color := Color.WHITE
|
||||
const R := 36.0
|
||||
var veins: Array[PackedVector2Array] = []
|
||||
var _rng := RandomNumberGenerator.new()
|
||||
var texture: Texture2D
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
texture = ART[Color("de6b7e")]
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
# ombra
|
||||
draw_circle(Vector2(0, 4), R, Color(0, 0, 0, 0.25))
|
||||
# corpo
|
||||
draw_circle(Vector2.ZERO, R, dot_color)
|
||||
# ombreggiatura inferiore-destra e luce superiore-sinistra
|
||||
draw_circle(Vector2(6.5, 8.0), R * 0.92, Color(0, 0, 0, 0.16))
|
||||
draw_circle(Vector2(-6.5, -8.0), R * 0.92, Color(1, 1, 1, 0.2))
|
||||
# venature marmorizzate (come le biglie della plancia, statiche in HUD)
|
||||
_draw_marbling()
|
||||
# bordo nero stile china
|
||||
draw_arc(Vector2.ZERO, R, 0.0, TAU, 40, Color(0.1, 0.1, 0.1, 0.9), 4.0)
|
||||
# riflesso speculare fisso
|
||||
draw_circle(Vector2(-11.0, -12.0), 6.0, Color(1, 1, 1, 0.85))
|
||||
if texture:
|
||||
draw_texture_rect(texture, Rect2(-R, -R, R * 2.0, R * 2.0), false)
|
||||
|
||||
|
||||
func _draw_marbling() -> void:
|
||||
var vein_color: Color = dot_color.darkened(0.42)
|
||||
vein_color.a = 0.2
|
||||
var soft_color: Color = dot_color.darkened(0.28)
|
||||
soft_color.a = 0.12
|
||||
for i: int in range(veins.size()):
|
||||
var pts := PackedVector2Array()
|
||||
for point: Vector2 in veins[i]:
|
||||
# visibile solo la metà frontale (longitudine cos>0): copertura 360°
|
||||
var theta: float = asin(clampf(point.x / R, -1.0, 1.0))
|
||||
if cos(theta) <= 0.0:
|
||||
continue
|
||||
pts.append(point)
|
||||
if pts.size() < 2:
|
||||
continue
|
||||
var width: float = R * (0.075 if i == 0 else 0.045)
|
||||
draw_polyline(pts, soft_color, width * 1.9, true)
|
||||
draw_polyline(pts, vein_color, width, true)
|
||||
|
||||
|
||||
func _generate_veins() -> Array[PackedVector2Array]:
|
||||
# 7 venature distribuite uniformemente su tutta la longitudine (360°)
|
||||
var result: Array[PackedVector2Array] = []
|
||||
var count := 7
|
||||
for i in count:
|
||||
var center: float = -PI + TAU * (float(i) + _rng.randf_range(0.0, 0.9)) / count
|
||||
var spread := _rng.randf_range(0.25, 0.7)
|
||||
var yc := _rng.randf_range(-0.5, 0.5)
|
||||
var y0 := clampf(yc - _rng.randf_range(0.0, 0.3), -0.6, 0.6)
|
||||
var y1 := clampf(yc + _rng.randf_range(0.0, 0.3), -0.6, 0.6)
|
||||
var bend := _rng.randf_range(0.05, 0.22)
|
||||
var ripple := _rng.randf_range(0.03, 0.11)
|
||||
result.append(_make_vein(sin(center - spread), y0, sin(center + spread), y1, bend, ripple))
|
||||
return result
|
||||
|
||||
|
||||
func _make_vein(x0: float, y0: float, x1: float, y1: float,
|
||||
bend: float, ripple: float) -> PackedVector2Array:
|
||||
var points := PackedVector2Array()
|
||||
var direction := Vector2(x1 - x0, y1 - y0)
|
||||
var normal := direction.normalized().orthogonal()
|
||||
for step: int in range(17):
|
||||
var t: float = float(step) / 16.0
|
||||
var base := Vector2(lerpf(x0, x1, t), lerpf(y0, y1, t))
|
||||
var curve: float = sin(t * PI) * bend
|
||||
var wave: float = sin(t * TAU * 1.35 + x0 * 4.0) * ripple
|
||||
points.append((base + normal * (curve + wave)) * R)
|
||||
return points
|
||||
|
||||
|
||||
func set_color(c: Color) -> void:
|
||||
dot_color = c
|
||||
_rng.seed = hash(c) # pattern unico ma stabile per ogni colore
|
||||
veins = _generate_veins()
|
||||
func set_color(color: Color) -> void:
|
||||
# I colori vengono dalle costanti JuggleBoardConfig, quindi la lookup è esatta.
|
||||
texture = ART[color]
|
||||
queue_redraw()
|
||||
|
||||
@@ -2,6 +2,8 @@ class_name JuggleBoardGame
|
||||
extends Node2D
|
||||
## Controller del gameplay JuggleBoard. Menu, HUD e audio sono moduli separati.
|
||||
|
||||
signal return_requested
|
||||
|
||||
const BOARD_SCRIPT := preload("res://games/juggleboard/scripts/components/board.gd")
|
||||
const BALL_SCRIPT := preload("res://games/juggleboard/scripts/components/ball.gd")
|
||||
const HUD_SCRIPT := preload("res://games/juggleboard/scripts/ui/game_hud.gd")
|
||||
@@ -76,6 +78,7 @@ func _build_balls() -> void:
|
||||
func _build_services() -> void:
|
||||
hud = HUD_SCRIPT.new()
|
||||
hud.options_requested.connect(_request_options)
|
||||
hud.back_requested.connect(func(): return_requested.emit())
|
||||
add_child(hud)
|
||||
audio = AUDIO_SCRIPT.new()
|
||||
add_child(audio)
|
||||
|
||||
@@ -3,12 +3,17 @@ extends RefCounted
|
||||
## Costanti di layout, colori e note del JuggleBoard.
|
||||
|
||||
const VIEW := Vector2(1280, 720)
|
||||
const LANE_TOP := 210.0
|
||||
const LANE_GAP := 98.0
|
||||
const BALL_RADIUS := 40.0
|
||||
const HOME_X := 1180.0
|
||||
const LEFT_X := 100.0
|
||||
const HIT_RADIUS := BALL_RADIUS * 1.4
|
||||
# Coordinate della plancia P2B, ricavate dall'illustrazione board_background.
|
||||
const LANE_TOP := 132.0
|
||||
const LANE_GAP := 115.0
|
||||
const BALL_RADIUS := 48.0
|
||||
const HOME_X := 1100.0
|
||||
const LEFT_X := 405.0
|
||||
const HIT_RADIUS := BALL_RADIUS * 1.35
|
||||
|
||||
# Centro della cornice bersaglio misurato nello sfondo P2B (2796×1290) e
|
||||
# convertito nel viewport 1280×720 usato da board.gd.
|
||||
const TARGET_CENTER := Vector2(222.4, 360.5)
|
||||
|
||||
const COLORS := {
|
||||
"rosso": Color("de6b7e"),
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
class_name JuggleBoardHud
|
||||
extends CanvasLayer
|
||||
## HUD del JuggleBoard: costruzione e aggiornamenti visuali.
|
||||
## HUD P2B: bersaglio e cartigli sul pannello laterale illustrato.
|
||||
|
||||
signal options_requested
|
||||
signal back_requested
|
||||
|
||||
const DISPLAY_FONT := preload("res://shared/assets/fonts/NotoSerifDisplay-CondensedExtraBold.ttf")
|
||||
const CARTOUCHE_SCRIPT := preload("res://games/juggleboard/scripts/ui/hud_cartouche.gd")
|
||||
const TARGET_SCRIPT := preload("res://games/juggleboard/scripts/components/target_dot.gd")
|
||||
const HEARTS_SCRIPT := preload("res://games/juggleboard/scripts/ui/hearts_display.gd")
|
||||
const BACK_BUTTON_ART := preload("res://menus/assets/common/pulsante_indietro.png")
|
||||
|
||||
var target_dot: Node2D
|
||||
var score_label: Label
|
||||
@@ -16,6 +17,7 @@ var help_label: Label
|
||||
var milestone_label: Label
|
||||
var flash: ColorRect
|
||||
var hearts: Node2D
|
||||
var back_button: TextureButton
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
@@ -23,56 +25,41 @@ func _ready() -> void:
|
||||
|
||||
|
||||
func _build() -> void:
|
||||
var ink := Color("#5b2f19")
|
||||
var cartouche: Node2D = CARTOUCHE_SCRIPT.new()
|
||||
add_child(cartouche)
|
||||
|
||||
var target_label := _make_label("BERSAGLIO", Vector2(94, 44), Vector2(270, 54), 34, ink)
|
||||
target_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
var ink := Color("#251b17")
|
||||
|
||||
# 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 = Vector2(430, 78)
|
||||
target_dot.position = JuggleBoardConfig.TARGET_CENTER
|
||||
add_child(target_dot)
|
||||
|
||||
help_label = _make_label(
|
||||
"Tocca la pallina corrispondente al bersaglio per lanciarla!",
|
||||
Vector2(JuggleBoardConfig.VIEW.x / 2 - 350, 154), Vector2(700, 30), 17, ink
|
||||
)
|
||||
help_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
back_button = _make_back_button()
|
||||
add_child(back_button)
|
||||
|
||||
score_label = _make_label("", Vector2(925, 34), Vector2(245, 48), 36, ink)
|
||||
score_label = _make_label("", Vector2(112, 520), Vector2(220, 45), 25, ink)
|
||||
score_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
combo_label = _make_label("", Vector2(925, 80), Vector2(245, 34), 21, ink)
|
||||
score_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
combo_label = _make_label("", Vector2(112, 606), Vector2(220, 45), 24, ink)
|
||||
combo_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
combo_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
|
||||
hearts = HEARTS_SCRIPT.new()
|
||||
hearts.position = Vector2(121, 99)
|
||||
hearts.position = Vector2(84, 655)
|
||||
hearts.visible = false
|
||||
add_child(hearts)
|
||||
|
||||
var options_button := Button.new()
|
||||
options_button.text = "Opzioni"
|
||||
options_button.position = Vector2(580, 49)
|
||||
options_button.size = Vector2(120, 57)
|
||||
options_button.add_theme_font_override("font", DISPLAY_FONT)
|
||||
options_button.add_theme_font_size_override("font_size", 25)
|
||||
options_button.add_theme_color_override("font_color", ink)
|
||||
options_button.add_theme_color_override("font_hover_color", Color("#7b421f"))
|
||||
var option_style := _button_style(Color("#ddb983"))
|
||||
options_button.add_theme_stylebox_override("normal", option_style)
|
||||
var option_hover := _button_style(Color("#ebcb96"))
|
||||
options_button.add_theme_stylebox_override("hover", option_hover)
|
||||
options_button.add_theme_stylebox_override("pressed", option_style)
|
||||
options_button.pressed.connect(func(): options_requested.emit())
|
||||
add_child(options_button)
|
||||
# Le opzioni restano accessibili da Esc per non introdurre elementi estranei
|
||||
# al mockup P2B; il lock adulto e la persistenza delle impostazioni non cambiano.
|
||||
help_label = _make_label("", Vector2.ZERO, Vector2.ZERO, 1, ink)
|
||||
help_label.visible = false
|
||||
|
||||
milestone_label = _make_label("", Vector2(0, 160), Vector2(JuggleBoardConfig.VIEW.x, 120), 56, Color(0.8, 0.55, 0.1))
|
||||
milestone_label = _make_label("", Vector2(350, 20), Vector2(820, 80), 46, Color("#9e5420"))
|
||||
milestone_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
milestone_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
milestone_label.visible = false
|
||||
|
||||
flash = ColorRect.new()
|
||||
flash.color = Color(1, 1, 0.7)
|
||||
flash.color = Color(1, 0.95, 0.72)
|
||||
flash.position = Vector2.ZERO
|
||||
flash.size = JuggleBoardConfig.VIEW
|
||||
flash.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
@@ -81,16 +68,41 @@ func _build() -> void:
|
||||
add_child(flash)
|
||||
|
||||
|
||||
func _make_back_button() -> TextureButton:
|
||||
var button := TextureButton.new()
|
||||
button.texture_normal = BACK_BUTTON_ART
|
||||
button.texture_hover = BACK_BUTTON_ART
|
||||
button.texture_pressed = BACK_BUTTON_ART
|
||||
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)
|
||||
button.pressed.connect(func(): back_requested.emit())
|
||||
return button
|
||||
|
||||
|
||||
func reset(score: int, combo: float, lives: int, survival: bool) -> void:
|
||||
set_score(score, combo)
|
||||
hearts.set_lives(lives)
|
||||
hearts.visible = survival
|
||||
help_label.visible = true
|
||||
|
||||
|
||||
func set_score(score: int, combo: float) -> void:
|
||||
score_label.text = "PUNTI %d" % score
|
||||
combo_label.text = "Combo x%d" % int(combo)
|
||||
score_label.text = "punteggio %03d" % score
|
||||
combo_label.text = "livello %d" % (1 + int(score / 1000))
|
||||
|
||||
|
||||
func set_target(color: Color, shown: bool = true) -> void:
|
||||
@@ -103,12 +115,10 @@ func celebrate(amount: int) -> void:
|
||||
milestone_label.text = "%d PUNTI!" % amount
|
||||
milestone_label.visible = true
|
||||
milestone_label.modulate.a = 1.0
|
||||
milestone_label.scale = Vector2(0.6, 0.6)
|
||||
flash.visible = true
|
||||
flash.modulate.a = 0.18
|
||||
flash.modulate.a = 0.16
|
||||
var tween := create_tween()
|
||||
tween.set_parallel(true)
|
||||
tween.tween_property(milestone_label, "scale", Vector2(1.3, 1.3), 0.4).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
|
||||
tween.tween_property(milestone_label, "modulate:a", 0.0, 0.8).set_delay(0.6)
|
||||
tween.tween_property(flash, "modulate:a", 0.0, 0.4)
|
||||
tween.chain().tween_callback(func():
|
||||
@@ -125,14 +135,6 @@ func _make_label(text: String, position: Vector2, size: Vector2, font_size: int,
|
||||
label.add_theme_font_override("font", DISPLAY_FONT)
|
||||
label.add_theme_font_size_override("font_size", font_size)
|
||||
label.add_theme_color_override("font_color", color)
|
||||
label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
add_child(label)
|
||||
return label
|
||||
|
||||
|
||||
func _button_style(color: Color) -> StyleBoxFlat:
|
||||
var style := StyleBoxFlat.new()
|
||||
style.bg_color = color
|
||||
style.border_color = Color("#9b6738")
|
||||
style.set_border_width_all(3)
|
||||
style.set_corner_radius_all(9)
|
||||
return style
|
||||
|
||||
Reference in New Issue
Block a user