Files
step3/scripts/ui/rz_slot_machine_screen.gd
T
hz 3fd670d111 Replace hand-coded cabinet/lever/icons with real generated art
The user generated actual 3D-rendered assets (cabinet frame, lever,
reel-symbol sprite sheet) via an image model - swaps out the
StyleBoxFlat-approximated cabinet, Tween-animated marquee lights/glow,
and gradient-SVG icons from the last pass for the real thing.

The generated PNGs had no real alpha channel (a baked checkerboard
pattern standing in for transparency, a known limitation of some
image-gen output) - added a flood-fill background remover (BFS from
the image border, matching near-white/grey checkerboard tones) to
produce genuinely transparent PNGs, then sliced the sprite sheet into
6 individual icons and measured the cabinet's empty reel-window
rect as a fraction of the frame image so the real 3x3 reel grid and
lever can be positioned precisely over the static art via anchors.

Verified with actual rendered screenshots at each step (window
alignment, then lever sizing/position) rather than just structural
checks, since this is exactly the kind of thing that looks fine in
code and wrong on screen.
2026-07-30 04:06:35 -05:00

584 lines
22 KiB
GDScript

extends VBoxContainer
const Brand := preload("res://scripts/ui/medmind_style.gd")
const STAR_ICON := preload("res://assets/icons/lucide/star.svg")
const CLOCK_ICON := preload("res://assets/icons/lucide/clock-3.svg")
const CABINET_FRAME := preload("res://assets/slot_machine/cabinet_frame.png")
const LEVER_TEXTURE := preload("res://assets/slot_machine/lever.png")
const SLOT_COIN := preload("res://assets/slot_machine/slot_coin.png")
const SLOT_GEM := preload("res://assets/slot_machine/slot_gem.png")
const SLOT_POTION := preload("res://assets/slot_machine/slot_potion.png")
const SLOT_SHIELD := preload("res://assets/slot_machine/slot_shield.png")
const SLOT_COIN_STACK := preload("res://assets/slot_machine/slot_coin_stack.png")
const SLOT_GIFT := preload("res://assets/slot_machine/slot_gift.png")
const REEL_SYMBOLS := [SLOT_COIN, SLOT_GEM, SLOT_POTION, SLOT_SHIELD, SLOT_COIN_STACK, SLOT_GIFT]
# fraction-of-image rect of the cabinet's empty reel window, measured against cabinet_frame.png
const WINDOW_RECT := Rect2(0.203, 0.246, 0.591, 0.604)
const REWARD_ICONS := {
"jackpot": {"texture": SLOT_COIN, "modulate": false},
"epic": {"texture": SLOT_GEM, "modulate": false},
"great": {"texture": SLOT_SHIELD, "modulate": false},
"exam_token": {"texture": CLOCK_ICON, "modulate": true},
"qbank_reset": {"texture": SLOT_GIFT, "modulate": false},
"nice": {"texture": SLOT_COIN_STACK, "modulate": false},
"good_luck": {"texture": SLOT_COIN, "modulate": false, "tint": true},
}
var main: Control
var subtitle_label: Label
var machine_root: Control
var reel_columns: Array = []
var win_up_to_card: PanelContainer
var win_up_to_label: Label
var cost_card: PanelContainer
var cost_value_label: Label
var auto_card: PanelContainer
var spin_button: Button
var auto_spin_toggle: CheckButton
var status_label: Label
var rewards_list: VBoxContainer
var footer_card: PanelContainer
var footer_icon_panel: PanelContainer
var footer_icon: TextureRect
var footer_heading: Label
var footer_description: Label
var _spinning := false
func _ready() -> void:
size_flags_horizontal = Control.SIZE_EXPAND_FILL
size_flags_vertical = Control.SIZE_EXPAND_FILL
add_theme_constant_override("separation", 18)
_build_header()
var body := HBoxContainer.new()
body.add_theme_constant_override("separation", 18)
body.size_flags_vertical = Control.SIZE_EXPAND_FILL
add_child(body)
var left_column := VBoxContainer.new()
left_column.add_theme_constant_override("separation", 16)
left_column.size_flags_horizontal = Control.SIZE_EXPAND_FILL
body.add_child(left_column)
_build_machine(left_column)
_build_controls(left_column)
_build_rewards_panel(body)
status_label = Label.new()
status_label.add_theme_font_override("font", Brand.font)
status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
add_child(status_label)
_build_footer()
CreditsService.credits_changed.connect(_refresh_spin_button)
CreditsService.spin_config_loaded.connect(_refresh_rewards)
UIThemeService.theme_changed.connect(func(_dark: bool): _refresh_theme_controls())
_refresh_rewards()
_refresh_spin_button()
func on_show(_payload: Variant = null) -> void:
CreditsService.refresh()
CreditsService.fetch_spin_config()
_refresh_rewards()
_refresh_spin_button()
func _build_header() -> void:
var title := Label.new()
title.text = "RZ Coin Slot Machine"
title.add_theme_font_override("font", Brand.font)
title.add_theme_font_size_override("font_size", 30)
add_child(title)
subtitle_label = Label.new()
subtitle_label.text = "Test your luck and win exciting rewards!"
subtitle_label.add_theme_font_override("font", Brand.font)
subtitle_label.add_theme_font_size_override("font_size", 14)
subtitle_label.add_theme_color_override("font_color", Brand.muted_color(UIThemeService.is_dark_mode()))
add_child(subtitle_label)
func _build_machine(parent: Control) -> void:
machine_root = Control.new()
machine_root.custom_minimum_size = Vector2(560, 560)
machine_root.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
machine_root.size_flags_vertical = Control.SIZE_SHRINK_BEGIN
parent.add_child(machine_root)
var frame := TextureRect.new()
frame.texture = CABINET_FRAME
frame.set_anchors_preset(Control.PRESET_FULL_RECT)
frame.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
frame.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT
frame.mouse_filter = Control.MOUSE_FILTER_IGNORE
machine_root.add_child(frame)
var window := Control.new()
window.anchor_left = WINDOW_RECT.position.x
window.anchor_top = WINDOW_RECT.position.y
window.anchor_right = WINDOW_RECT.position.x + WINDOW_RECT.size.x
window.anchor_bottom = WINDOW_RECT.position.y + WINDOW_RECT.size.y
window.mouse_filter = Control.MOUSE_FILTER_IGNORE
machine_root.add_child(window)
var center := CenterContainer.new()
center.set_anchors_preset(Control.PRESET_FULL_RECT)
window.add_child(center)
var reels_grid := HBoxContainer.new()
reels_grid.add_theme_constant_override("separation", 16)
center.add_child(reels_grid)
reel_columns.clear()
for _column_index in range(3):
var reel_col := VBoxContainer.new()
reel_col.add_theme_constant_override("separation", 10)
reels_grid.add_child(reel_col)
var cells: Array = []
for _row_index in range(3):
var icon := TextureRect.new()
icon.texture = REEL_SYMBOLS[randi() % REEL_SYMBOLS.size()]
icon.custom_minimum_size = Vector2(66, 66)
icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
reel_col.add_child(icon)
cells.append(icon)
reel_columns.append(cells)
var lever := TextureRect.new()
lever.texture = LEVER_TEXTURE
lever.anchor_left = 0.80
lever.anchor_right = 1.06
lever.anchor_top = 0.24
lever.anchor_bottom = 0.88
lever.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
lever.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT
lever.mouse_filter = Control.MOUSE_FILTER_IGNORE
machine_root.add_child(lever)
func _build_controls(parent: Control) -> void:
var dark := UIThemeService.is_dark_mode()
win_up_to_card = PanelContainer.new()
win_up_to_card.add_theme_stylebox_override("panel", Brand.content_card_box(dark, false))
parent.add_child(win_up_to_card)
var win_margin := MarginContainer.new()
win_margin.add_theme_constant_override("margin_left", 14)
win_margin.add_theme_constant_override("margin_right", 14)
win_margin.add_theme_constant_override("margin_top", 10)
win_margin.add_theme_constant_override("margin_bottom", 10)
win_up_to_card.add_child(win_margin)
win_up_to_label = Label.new()
win_up_to_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
win_up_to_label.add_theme_font_override("font", Brand.font)
win_up_to_label.add_theme_font_size_override("font_size", 15)
win_up_to_label.add_theme_color_override("font_color", Brand.text_color(dark))
win_margin.add_child(win_up_to_label)
var controls_row := HBoxContainer.new()
controls_row.add_theme_constant_override("separation", 12)
parent.add_child(controls_row)
cost_card = PanelContainer.new()
cost_card.add_theme_stylebox_override("panel", Brand.content_card_box(dark, false))
controls_row.add_child(cost_card)
var cost_margin := MarginContainer.new()
cost_margin.add_theme_constant_override("margin_left", 14)
cost_margin.add_theme_constant_override("margin_right", 14)
cost_margin.add_theme_constant_override("margin_top", 8)
cost_margin.add_theme_constant_override("margin_bottom", 8)
cost_card.add_child(cost_margin)
var cost_box := VBoxContainer.new()
cost_box.add_theme_constant_override("separation", 0)
cost_margin.add_child(cost_box)
var cost_caption := Label.new()
cost_caption.text = "Cost per spin"
cost_caption.add_theme_font_override("font", Brand.font)
cost_caption.add_theme_font_size_override("font_size", 11)
cost_caption.add_theme_color_override("font_color", Brand.muted_color(dark))
cost_box.add_child(cost_caption)
cost_value_label = Label.new()
cost_value_label.add_theme_font_override("font", Brand.font)
cost_value_label.add_theme_font_size_override("font_size", 15)
cost_value_label.add_theme_color_override("font_color", Brand.text_color(dark))
cost_box.add_child(cost_value_label)
spin_button = Button.new()
spin_button.text = "Spin"
spin_button.custom_minimum_size = Vector2(140, 58)
spin_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
spin_button.size_flags_vertical = Control.SIZE_SHRINK_CENTER
spin_button.add_theme_font_override("font", Brand.font)
spin_button.add_theme_font_size_override("font_size", 17)
_style_spin_button()
spin_button.pressed.connect(_on_spin_pressed)
controls_row.add_child(spin_button)
auto_card = PanelContainer.new()
auto_card.add_theme_stylebox_override("panel", Brand.content_card_box(dark, false))
controls_row.add_child(auto_card)
var auto_margin := MarginContainer.new()
auto_margin.add_theme_constant_override("margin_left", 14)
auto_margin.add_theme_constant_override("margin_right", 14)
auto_margin.add_theme_constant_override("margin_top", 8)
auto_margin.add_theme_constant_override("margin_bottom", 8)
auto_card.add_child(auto_margin)
var auto_box := VBoxContainer.new()
auto_box.add_theme_constant_override("separation", 0)
auto_margin.add_child(auto_box)
auto_spin_toggle = CheckButton.new()
auto_spin_toggle.text = "Auto Spin"
auto_spin_toggle.add_theme_font_override("font", Brand.font)
auto_box.add_child(auto_spin_toggle)
var auto_caption := Label.new()
auto_caption.text = "10 spins"
auto_caption.add_theme_font_override("font", Brand.font)
auto_caption.add_theme_font_size_override("font_size", 11)
auto_caption.add_theme_color_override("font_color", Brand.muted_color(dark))
auto_box.add_child(auto_caption)
var info_label := Label.new()
info_label.text = "All rewards are instant and sent to your account."
info_label.add_theme_font_override("font", Brand.font)
info_label.add_theme_font_size_override("font_size", 11)
info_label.add_theme_color_override("font_color", Brand.muted_color(dark))
info_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
parent.add_child(info_label)
func _build_rewards_panel(parent: Control) -> void:
var dark := UIThemeService.is_dark_mode()
var panel := PanelContainer.new()
panel.custom_minimum_size = Vector2(280, 0)
panel.add_theme_stylebox_override("panel", Brand.content_card_box(dark, false))
parent.add_child(panel)
var margin := MarginContainer.new()
margin.add_theme_constant_override("margin_left", 16)
margin.add_theme_constant_override("margin_right", 16)
margin.add_theme_constant_override("margin_top", 14)
margin.add_theme_constant_override("margin_bottom", 14)
panel.add_child(margin)
var column := VBoxContainer.new()
column.add_theme_constant_override("separation", 10)
margin.add_child(column)
var heading := Label.new()
heading.text = "Possible Rewards"
heading.add_theme_font_override("font", Brand.font)
heading.add_theme_font_size_override("font_size", 16)
heading.add_theme_color_override("font_color", Brand.text_color(dark))
column.add_child(heading)
rewards_list = VBoxContainer.new()
rewards_list.add_theme_constant_override("separation", 10)
column.add_child(rewards_list)
func _build_footer() -> void:
var dark := UIThemeService.is_dark_mode()
footer_card = PanelContainer.new()
footer_card.add_theme_stylebox_override("panel", Brand.content_card_box(dark, false))
add_child(footer_card)
var margin := MarginContainer.new()
margin.add_theme_constant_override("margin_left", 16)
margin.add_theme_constant_override("margin_right", 16)
margin.add_theme_constant_override("margin_top", 12)
margin.add_theme_constant_override("margin_bottom", 12)
footer_card.add_child(margin)
var row := HBoxContainer.new()
row.add_theme_constant_override("separation", 14)
margin.add_child(row)
footer_icon_panel = PanelContainer.new()
footer_icon_panel.custom_minimum_size = Vector2(36, 36)
footer_icon_panel.size_flags_vertical = Control.SIZE_SHRINK_CENTER
footer_icon_panel.add_theme_stylebox_override("panel", Brand.icon_circle_box(dark, Brand.ROYAL_BLUE, 18))
footer_icon = TextureRect.new()
footer_icon.texture = STAR_ICON
footer_icon.custom_minimum_size = Vector2(18, 18)
footer_icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
footer_icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
footer_icon.modulate = Brand.ROYAL_BLUE
footer_icon_panel.add_child(footer_icon)
row.add_child(footer_icon_panel)
var copy := VBoxContainer.new()
copy.size_flags_horizontal = Control.SIZE_EXPAND_FILL
copy.size_flags_vertical = Control.SIZE_SHRINK_CENTER
copy.add_theme_constant_override("separation", 2)
footer_heading = Label.new()
footer_heading.text = "How it works"
footer_heading.add_theme_font_override("font", Brand.font)
footer_heading.add_theme_font_size_override("font_size", 15)
footer_heading.add_theme_color_override("font_color", Brand.text_color(dark))
copy.add_child(footer_heading)
footer_description = Label.new()
footer_description.text = "Spend RZ Coins to spin the slot machine. Win coins, boosts, tokens, and more!"
footer_description.add_theme_font_override("font", Brand.font)
footer_description.add_theme_font_size_override("font_size", 12)
footer_description.add_theme_color_override("font_color", Brand.muted_color(dark))
footer_description.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
copy.add_child(footer_description)
row.add_child(copy)
func _refresh_theme_controls() -> void:
var dark := UIThemeService.is_dark_mode()
subtitle_label.add_theme_color_override("font_color", Brand.muted_color(dark))
win_up_to_card.add_theme_stylebox_override("panel", Brand.content_card_box(dark, false))
win_up_to_label.add_theme_color_override("font_color", Brand.text_color(dark))
cost_card.add_theme_stylebox_override("panel", Brand.content_card_box(dark, false))
cost_value_label.add_theme_color_override("font_color", Brand.text_color(dark))
auto_card.add_theme_stylebox_override("panel", Brand.content_card_box(dark, false))
footer_card.add_theme_stylebox_override("panel", Brand.content_card_box(dark, false))
footer_icon_panel.add_theme_stylebox_override("panel", Brand.icon_circle_box(dark, Brand.ROYAL_BLUE, 18))
footer_heading.add_theme_color_override("font_color", Brand.text_color(dark))
footer_description.add_theme_color_override("font_color", Brand.muted_color(dark))
_refresh_rewards()
func _reward_color(key: String) -> Color:
match key:
"jackpot": return Brand.AMBER
"epic": return Brand.PURPLE
"great": return Brand.ROYAL_BLUE
"exam_token": return Brand.ROYAL_BLUE
"qbank_reset": return Brand.SUCCESS
"nice": return Brand.SKY_BLUE
"good_luck": return Brand.SLATE
_: return Brand.ROYAL_BLUE
func _refresh_rewards() -> void:
var dark := UIThemeService.is_dark_mode()
cost_value_label.text = "%d RZ Coins" % CreditsService.spin_cost
var max_coins := 0
for reward_variant in CreditsService.spin_rewards:
if typeof(reward_variant) != TYPE_DICTIONARY:
continue
max_coins = maxi(max_coins, int((reward_variant as Dictionary).get("coinAmount", 0)))
win_up_to_label.text = "Win up to %s RZ Coins!" % _format_thousands(max_coins) if max_coins > 0 else ""
for child in rewards_list.get_children():
child.queue_free()
for reward_variant in CreditsService.spin_rewards:
if typeof(reward_variant) != TYPE_DICTIONARY:
continue
var reward: Dictionary = reward_variant
rewards_list.add_child(_build_reward_row(dark, reward))
func _build_reward_row(dark: bool, reward: Dictionary) -> HBoxContainer:
var key := str(reward.get("key", ""))
var accent := _reward_color(key)
var row := HBoxContainer.new()
row.add_theme_constant_override("separation", 10)
var icon_panel := PanelContainer.new()
icon_panel.custom_minimum_size = Vector2(36, 36)
icon_panel.size_flags_vertical = Control.SIZE_SHRINK_CENTER
icon_panel.add_theme_stylebox_override("panel", Brand.icon_circle_box(dark, accent, 18))
var icon := TextureRect.new()
var spec: Dictionary = REWARD_ICONS.get(key, {"texture": SLOT_COIN, "modulate": false})
icon.texture = spec.get("texture")
icon.custom_minimum_size = Vector2(22, 22)
icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
if spec.get("modulate", false):
icon.modulate = accent
elif spec.get("tint", false):
icon.modulate = Color("94a3b8")
icon_panel.add_child(icon)
row.add_child(icon_panel)
var copy := VBoxContainer.new()
copy.size_flags_horizontal = Control.SIZE_EXPAND_FILL
copy.size_flags_vertical = Control.SIZE_SHRINK_CENTER
copy.add_theme_constant_override("separation", 0)
var label := Label.new()
label.text = str(reward.get("label", ""))
label.add_theme_font_override("font", Brand.font)
label.add_theme_font_size_override("font_size", 13)
label.add_theme_color_override("font_color", Brand.text_color(dark))
copy.add_child(label)
var description := Label.new()
description.text = str(reward.get("description", ""))
description.add_theme_font_override("font", Brand.font)
description.add_theme_font_size_override("font_size", 11)
description.add_theme_color_override("font_color", Brand.muted_color(dark))
copy.add_child(description)
row.add_child(copy)
var percent_label := Label.new()
percent_label.text = "%s%%" % _format_percent(float(reward.get("probability", 0.0)) * 100.0)
percent_label.add_theme_font_override("font", Brand.font)
percent_label.add_theme_font_size_override("font_size", 13)
percent_label.add_theme_color_override("font_color", accent)
row.add_child(percent_label)
return row
func _refresh_spin_button() -> void:
spin_button.disabled = _spinning or CreditsService.credits < CreditsService.spin_cost
func _on_spin_pressed() -> void:
if _spinning:
return
_spinning = true
_refresh_spin_button()
var spin_count := 10 if auto_spin_toggle.button_pressed else 1
for _i in range(spin_count):
if CreditsService.credits < CreditsService.spin_cost:
break
var should_continue := await _do_one_spin()
if not should_continue:
break
_spinning = false
_refresh_spin_button()
func _do_one_spin() -> bool:
_animate_reels()
var result := await _await_spin_result()
if not result.get("ok", false):
status_label.text = str(result.get("message", "The spin could not be completed."))
status_label.add_theme_color_override("font_color", Brand.MAGENTA)
return false
var reward_label := str(result.get("reward_label", ""))
var coins_won := int(result.get("coins_won", 0))
status_label.text = "You won: %s" % reward_label
status_label.add_theme_color_override("font_color", Brand.SUCCESS)
if coins_won > 0:
_play_coin_burst(coins_won)
elif main != null:
var credits_badge: Object = main.get("credits_badge")
if credits_badge != null:
credits_badge.bump()
return true
func _await_spin_result() -> Dictionary:
var result := {}
var on_resolved := func(reward_key: String, reward_label: String, coins_won: int):
result.merge({"ok": true, "reward_key": reward_key, "reward_label": reward_label, "coins_won": coins_won}, true)
var on_failed := func(message: String):
result.merge({"ok": false, "message": message}, true)
CreditsService.spin_resolved.connect(on_resolved, CONNECT_ONE_SHOT)
CreditsService.credits_failed.connect(on_failed, CONNECT_ONE_SHOT)
CreditsService.spin()
var deadline := Time.get_ticks_msec() + 20000
while result.is_empty() and Time.get_ticks_msec() < deadline:
await get_tree().process_frame
if result.is_empty():
result = {"ok": false, "message": "The spin timed out. Check your connection and try again."}
if CreditsService.spin_resolved.is_connected(on_resolved):
CreditsService.spin_resolved.disconnect(on_resolved)
if CreditsService.credits_failed.is_connected(on_failed):
CreditsService.credits_failed.disconnect(on_failed)
return result
func _animate_reels() -> void:
for index in range(reel_columns.size()):
_spin_one_reel(reel_columns[index], 0.9 + index * 0.25)
func _spin_one_reel(cells: Array, duration: float) -> void:
var elapsed := 0.0
var interval := 0.06
while elapsed < duration:
var symbol: Texture2D = REEL_SYMBOLS[randi() % REEL_SYMBOLS.size()]
for cell in cells:
cell.texture = symbol
await get_tree().create_timer(interval).timeout
elapsed += interval
interval = minf(interval * 1.18, 0.22)
for cell in cells:
cell.texture = SLOT_COIN
func _play_coin_burst(coins_won: int) -> void:
if main == null:
return
var credits_badge: Control = main.get("credits_badge")
if credits_badge == null or not is_instance_valid(credits_badge):
return
var origin := machine_root.get_global_rect().get_center()
var target := credits_badge.get_global_rect().get_center()
var coin_count := clampi(3 + coins_won / 500, 4, 10)
var flight_time := 0.55
var stagger := 0.035
for index in range(coin_count):
var coin := TextureRect.new()
coin.texture = SLOT_COIN
coin.custom_minimum_size = Vector2(16, 16)
coin.size = Vector2(16, 16)
coin.top_level = true
coin.mouse_filter = Control.MOUSE_FILTER_IGNORE
coin.pivot_offset = Vector2(8, 8)
var jitter := Vector2(randf_range(-24.0, 24.0), randf_range(-16.0, 16.0))
coin.global_position = origin + jitter - coin.size * 0.5
add_child(coin)
var tween := create_tween()
tween.tween_interval(index * stagger)
tween.set_parallel(true)
tween.tween_property(coin, "global_position", target - coin.size * 0.5, flight_time).set_trans(Tween.TRANS_CUBIC).set_ease(Tween.EASE_IN)
tween.tween_property(coin, "scale", Vector2(0.6, 0.6), flight_time).set_trans(Tween.TRANS_CUBIC).set_ease(Tween.EASE_IN)
tween.set_parallel(false)
tween.tween_callback(coin.queue_free)
var settle_time := (coin_count - 1) * stagger + flight_time
get_tree().create_timer(settle_time).timeout.connect(func():
if is_instance_valid(credits_badge):
credits_badge.bump())
func _style_spin_button() -> void:
var normal := StyleBoxFlat.new()
normal.bg_color = Brand.ROYAL_BLUE
normal.set_corner_radius_all(8)
var hover := StyleBoxFlat.new()
hover.bg_color = Brand.SKY_BLUE
hover.set_corner_radius_all(8)
var disabled := StyleBoxFlat.new()
disabled.bg_color = Color(Brand.ROYAL_BLUE, 0.35)
disabled.set_corner_radius_all(8)
spin_button.add_theme_stylebox_override("normal", normal)
spin_button.add_theme_stylebox_override("hover", hover)
spin_button.add_theme_stylebox_override("pressed", normal)
spin_button.add_theme_stylebox_override("disabled", disabled)
spin_button.add_theme_color_override("font_color", Brand.DARK_TEXT)
spin_button.add_theme_color_override("font_hover_color", Brand.DARK_TEXT)
spin_button.add_theme_color_override("font_disabled_color", Color(Brand.DARK_TEXT, 0.7))
func _format_thousands(value: int) -> String:
var digits := str(absi(value))
var grouped := ""
var count := 0
for index in range(digits.length() - 1, -1, -1):
grouped = digits[index] + grouped
count += 1
if count % 3 == 0 and index != 0:
grouped = "," + grouped
return ("-" if value < 0 else "") + grouped
func _format_percent(value: float) -> String:
if absf(value - roundf(value)) < 0.001:
return str(int(roundf(value)))
return "%.1f" % value