extends PanelContainer 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}, } const MACHINE_SIZE := 380.0 const CELL := 44.0 var main: Control var drag_active := false var drag_offset := Vector2.ZERO var has_been_positioned := false 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: name = "RzSlotMachinePanel" visible = false top_level = true mouse_filter = Control.MOUSE_FILTER_STOP custom_minimum_size = Vector2(460, 680) size = custom_minimum_size _build_ui() if not get_viewport().size_changed.is_connected(_on_viewport_size_changed): get_viewport().size_changed.connect(_on_viewport_size_changed) _on_viewport_size_changed() 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_theme_controls() _refresh_rewards() _refresh_spin_button() func open_panel() -> void: visible = true move_to_front() if not has_been_positioned: var viewport_size := get_viewport_rect().size position = Vector2(maxf(12.0, viewport_size.x - size.x - 24.0), 72.0) has_been_positioned = true _clamp_to_viewport() CreditsService.refresh() CreditsService.fetch_spin_config() _refresh_rewards() _refresh_spin_button() func close_panel() -> void: visible = false drag_active = false func toggle_panel() -> void: if visible: close_panel() else: open_panel() func _build_ui() -> void: var margin := MarginContainer.new() margin.add_theme_constant_override("margin_left", 12) margin.add_theme_constant_override("margin_right", 12) margin.add_theme_constant_override("margin_top", 12) margin.add_theme_constant_override("margin_bottom", 14) add_child(margin) var content := VBoxContainer.new() content.add_theme_constant_override("separation", 10) margin.add_child(content) _build_title_bar(content) var scroll := ScrollContainer.new() scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED content.add_child(scroll) var scroll_content := VBoxContainer.new() scroll_content.add_theme_constant_override("separation", 14) scroll_content.size_flags_horizontal = Control.SIZE_EXPAND_FILL scroll.add_child(scroll_content) 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", 13) subtitle_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER scroll_content.add_child(subtitle_label) _build_machine(scroll_content) _build_controls(scroll_content) _build_rewards_section(scroll_content) _build_footer(scroll_content) status_label = Label.new() status_label.add_theme_font_override("font", Brand.font) status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART status_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER scroll_content.add_child(status_label) func _build_title_bar(content: VBoxContainer) -> void: var title_bar := HBoxContainer.new() title_bar.name = "SlotMachineTitleBar" title_bar.mouse_filter = Control.MOUSE_FILTER_STOP title_bar.custom_minimum_size.y = 34 title_bar.gui_input.connect(_on_title_bar_gui_input) content.add_child(title_bar) var title := Label.new() title.text = "RZ Slot Machine" title.size_flags_horizontal = Control.SIZE_EXPAND_FILL title.mouse_filter = Control.MOUSE_FILTER_IGNORE title.add_theme_font_override("font", Brand.font) title.add_theme_font_size_override("font_size", 18) title_bar.add_child(title) var close := Button.new() close.name = "SlotMachineClose" close.text = "×" close.tooltip_text = "Close slot machine" close.custom_minimum_size = Vector2(36, 32) close.focus_mode = Control.FOCUS_NONE close.pressed.connect(close_panel) title_bar.add_child(close) func _build_machine(parent: Control) -> void: var center := CenterContainer.new() parent.add_child(center) machine_root = Control.new() machine_root.custom_minimum_size = Vector2(MACHINE_SIZE, MACHINE_SIZE) center.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 reel_center := CenterContainer.new() reel_center.set_anchors_preset(Control.PRESET_FULL_RECT) window.add_child(reel_center) var reels_grid := HBoxContainer.new() reels_grid.add_theme_constant_override("separation", 10) reel_center.add_child(reels_grid) 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": 420.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", 14) 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", 10) 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", 10) cost_margin.add_theme_constant_override("margin_right", 10) cost_margin.add_theme_constant_override("margin_top", 6) cost_margin.add_theme_constant_override("margin_bottom", 6) 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", 10) 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", 14) 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(110, 52) 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", 16) _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", 10) auto_margin.add_theme_constant_override("margin_right", 10) auto_margin.add_theme_constant_override("margin_top", 6) auto_margin.add_theme_constant_override("margin_bottom", 6) 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", 10) 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", 10) 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_section(parent: Control) -> void: var dark := UIThemeService.is_dark_mode() var panel := PanelContainer.new() 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", 14) margin.add_theme_constant_override("margin_right", 14) margin.add_theme_constant_override("margin_top", 12) margin.add_theme_constant_override("margin_bottom", 12) 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", 15) 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", 8) column.add_child(rewards_list) func _build_footer(parent: Control) -> void: var dark := UIThemeService.is_dark_mode() footer_card = PanelContainer.new() footer_card.add_theme_stylebox_override("panel", Brand.content_card_box(dark, false)) parent.add_child(footer_card) var margin := MarginContainer.new() margin.add_theme_constant_override("margin_left", 14) margin.add_theme_constant_override("margin_right", 14) margin.add_theme_constant_override("margin_top", 10) margin.add_theme_constant_override("margin_bottom", 10) footer_card.add_child(margin) var row := HBoxContainer.new() row.add_theme_constant_override("separation", 12) margin.add_child(row) footer_icon_panel = PanelContainer.new() footer_icon_panel.custom_minimum_size = Vector2(32, 32) 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, 16)) footer_icon = TextureRect.new() footer_icon.texture = STAR_ICON footer_icon.custom_minimum_size = Vector2(16, 16) 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", 14) 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", 11) 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() add_theme_stylebox_override("panel", Brand.content_card_box(dark, true, Brand.ROYAL_BLUE)) 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, 16)) 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: if cost_value_label == null: return 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(34, 34) icon_panel.size_flags_vertical = Control.SIZE_SHRINK_CENTER icon_panel.add_theme_stylebox_override("panel", Brand.icon_circle_box(dark, accent, 17)) 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(20, 20) 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", 12) 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", 10) 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", 12) percent_label.add_theme_color_override("font_color", accent) row.add_child(percent_label) return row func _refresh_spin_button() -> void: if spin_button == null: return 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 _on_title_bar_gui_input(event: InputEvent) -> void: var touch_event := event as InputEventScreenTouch if touch_event != null: drag_active = touch_event.pressed if drag_active: drag_offset = touch_event.position get_viewport().set_input_as_handled() return var touch_drag := event as InputEventScreenDrag if touch_drag != null and drag_active: global_position += touch_drag.relative _clamp_to_viewport() get_viewport().set_input_as_handled() return var mouse_event := event as InputEventMouseButton if mouse_event != null and mouse_event.button_index == MOUSE_BUTTON_LEFT: drag_active = mouse_event.pressed if drag_active: drag_offset = mouse_event.global_position - global_position get_viewport().set_input_as_handled() return var motion := event as InputEventMouseMotion if motion != null and drag_active: global_position = motion.global_position - drag_offset _clamp_to_viewport() get_viewport().set_input_as_handled() func _on_viewport_size_changed() -> void: _clamp_to_viewport() func _clamp_to_viewport() -> void: if not is_inside_tree(): return var viewport_size := get_viewport_rect().size position.x = clampf(position.x, 12.0, maxf(12.0, viewport_size.x - size.x - 12.0)) position.y = clampf(position.y, 12.0, maxf(12.0, viewport_size.y - size.y - 12.0)) 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