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 lever_button: TextureButton var reel_states: 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) var cell := 66.0 reel_states.clear() for _column_index in range(3): var clip := Control.new() clip.clip_contents = true clip.custom_minimum_size = Vector2(cell, cell * 3) reels_grid.add_child(clip) var strip := VBoxContainer.new() strip.add_theme_constant_override("separation", 0) clip.add_child(strip) var icons: Array = [] for _row_index in range(4): var icon := TextureRect.new() icon.texture = REEL_SYMBOLS[randi() % REEL_SYMBOLS.size()] icon.custom_minimum_size = Vector2(cell, cell) icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED strip.add_child(icon) icons.append(icon) reel_states.append({ "strip": strip, "icons": icons, "step": cell, "speed": 620.0, "spinning": false, "stop_requested": false, "target_texture": null, "recycles_after_target": -1, "done": true, }) lever_button = TextureButton.new() lever_button.texture_normal = LEVER_TEXTURE lever_button.ignore_texture_size = true lever_button.stretch_mode = TextureButton.STRETCH_KEEP_ASPECT_CENTERED lever_button.anchor_left = 0.80 lever_button.anchor_right = 1.06 lever_button.anchor_top = 0.24 lever_button.anchor_bottom = 0.88 lever_button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND lever_button.tooltip_text = "Pull to spin" lever_button.resized.connect(func(): lever_button.pivot_offset = Vector2(lever_button.size.x * 0.5, lever_button.size.y * 0.92)) lever_button.pressed.connect(_on_lever_pulled) machine_root.add_child(lever_button) 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: var disabled := _spinning or CreditsService.credits < CreditsService.spin_cost spin_button.disabled = disabled lever_button.disabled = disabled func _on_lever_pulled() -> void: _play_lever_pull_animation() _on_spin_pressed() func _play_lever_pull_animation() -> void: var tween := create_tween() tween.tween_property(lever_button, "rotation_degrees", -20.0, 0.1).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT) tween.tween_property(lever_button, "rotation_degrees", 0.0, 0.3).set_trans(Tween.TRANS_ELASTIC).set_ease(Tween.EASE_OUT) 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: for state in reel_states: state["stop_requested"] = false state["target_texture"] = null state["recycles_after_target"] = -1 state["done"] = false _spin_reel(state) var result := await _await_spin_result() if not result.get("ok", false): for state in reel_states: state["target_texture"] = REEL_SYMBOLS[randi() % REEL_SYMBOLS.size()] state["stop_requested"] = true await _await_reels_done() 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_key := str(result.get("reward_key", "")) var reward_texture := _icon_for_reward(reward_key) for index in range(reel_states.size()): var state: Dictionary = reel_states[index] get_tree().create_timer(index * 0.25).timeout.connect(func(): state["target_texture"] = reward_texture state["stop_requested"] = true) await _await_reels_done() 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 _icon_for_reward(key: String) -> Texture2D: var spec: Dictionary = REWARD_ICONS.get(key, {}) var texture: Texture2D = spec.get("texture", SLOT_COIN) return texture if texture != null else SLOT_COIN func _await_reels_done() -> void: while true: var all_done := true for state in reel_states: if not state.get("done", false): all_done = false break if all_done: return await get_tree().process_frame 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 _spin_reel(state: Dictionary) -> void: state["spinning"] = true var strip: VBoxContainer = state["strip"] var icons: Array = state["icons"] var step: float = state["step"] var speed: float = state["speed"] while true: var current_speed: float = speed if state["recycles_after_target"] == -1 else speed * 0.45 strip.position.y -= current_speed * get_process_delta_time() if strip.position.y <= -step: strip.position.y += step var front: TextureRect = icons.pop_front() strip.move_child(front, strip.get_child_count() - 1) icons.append(front) if state["stop_requested"] and state["recycles_after_target"] == -1: front.texture = state["target_texture"] state["recycles_after_target"] = 0 else: front.texture = REEL_SYMBOLS[randi() % REEL_SYMBOLS.size()] if state["recycles_after_target"] >= 0: state["recycles_after_target"] += 1 if state["recycles_after_target"] == 2: strip.position.y = 0.0 break await get_tree().process_frame state["spinning"] = false state["done"] = true 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