extends VBoxContainer const Brand := preload("res://scripts/ui/medmind_style.gd") const STAR_ICON := preload("res://assets/icons/lucide/star.svg") const SHIELD_ICON := preload("res://assets/icons/lucide/shield.svg") const CLOCK_ICON := preload("res://assets/icons/lucide/clock-3.svg") const RESET_ICON := preload("res://assets/icons/lucide/rotate-ccw.svg") const REEL_SYMBOLS := [STAR_ICON, SHIELD_ICON, CLOCK_ICON, RESET_ICON] const ICON_BY_REWARD_KEY := { "jackpot": "star", "epic": "star", "great": "star", "exam_token": "clock-3", "qbank_reset": "rotate-ccw", "nice": "star", "good_luck": "star", } var main: Control var subtitle_label: Label var back_button: Button var machine_panel: PanelContainer var reel_icons: Array = [] var win_up_to_label: Label var cost_value_label: Label 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) _build_machine(body) _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: back_button = Button.new() back_button.text = "← Back to Shop" back_button.add_theme_font_override("font", Brand.font) back_button.flat = true back_button.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN back_button.pressed.connect(func(): main.show_screen("rz_store")) add_child(back_button) 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: var dark := UIThemeService.is_dark_mode() machine_panel = PanelContainer.new() machine_panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL machine_panel.add_theme_stylebox_override("panel", Brand.content_card_box(dark, true, Brand.ROYAL_BLUE)) parent.add_child(machine_panel) var margin := MarginContainer.new() margin.add_theme_constant_override("margin_left", 20) margin.add_theme_constant_override("margin_right", 20) margin.add_theme_constant_override("margin_top", 18) margin.add_theme_constant_override("margin_bottom", 18) machine_panel.add_child(margin) var column := VBoxContainer.new() column.add_theme_constant_override("separation", 14) margin.add_child(column) var reels_row := HBoxContainer.new() reels_row.add_theme_constant_override("separation", 12) reels_row.alignment = BoxContainer.ALIGNMENT_CENTER column.add_child(reels_row) reel_icons.clear() for _i in range(3): var reel_panel := PanelContainer.new() reel_panel.custom_minimum_size = Vector2(84, 84) reel_panel.add_theme_stylebox_override("panel", Brand.icon_circle_box(dark, Brand.ROYAL_BLUE, 12)) var icon := TextureRect.new() icon.texture = STAR_ICON icon.modulate = Brand.AMBER icon.custom_minimum_size = Vector2(40, 40) icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED icon.size_flags_horizontal = Control.SIZE_SHRINK_CENTER icon.size_flags_vertical = Control.SIZE_SHRINK_CENTER reel_panel.add_child(icon) reels_row.add_child(reel_panel) reel_icons.append(icon) 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)) column.add_child(win_up_to_label) var controls_row := HBoxContainer.new() controls_row.add_theme_constant_override("separation", 14) column.add_child(controls_row) var cost_box := VBoxContainer.new() cost_box.add_theme_constant_override("separation", 0) 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) controls_row.add_child(cost_box) spin_button = Button.new() spin_button.text = "Spin" spin_button.custom_minimum_size = Vector2(120, 44) spin_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL spin_button.add_theme_font_override("font", Brand.font) _style_spin_button() spin_button.pressed.connect(_on_spin_pressed) controls_row.add_child(spin_button) var auto_box := VBoxContainer.new() auto_box.add_theme_constant_override("separation", 0) 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) controls_row.add_child(auto_box) 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 column.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)) machine_panel.add_theme_stylebox_override("panel", Brand.content_card_box(dark, true, Brand.ROYAL_BLUE)) win_up_to_label.add_theme_color_override("font_color", Brand.text_color(dark)) cost_value_label.add_theme_color_override("font_color", Brand.text_color(dark)) 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(32, 32) icon_panel.size_flags_vertical = Control.SIZE_SHRINK_CENTER icon_panel.add_theme_stylebox_override("panel", Brand.icon_circle_box(dark, accent, 16)) var icon := TextureRect.new() icon.texture = load("res://assets/icons/lucide/%s.svg" % str(ICON_BY_REWARD_KEY.get(key, "star"))) icon.custom_minimum_size = Vector2(16, 16) icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED icon.modulate = accent 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_icons.size()): _spin_one_reel(reel_icons[index], 0.9 + index * 0.25) func _spin_one_reel(icon: TextureRect, duration: float) -> void: var elapsed := 0.0 var interval := 0.06 while elapsed < duration: icon.texture = REEL_SYMBOLS[randi() % REEL_SYMBOLS.size()] await get_tree().create_timer(interval).timeout elapsed += interval interval = minf(interval * 1.18, 0.22) icon.texture = STAR_ICON icon.modulate = Brand.AMBER 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_panel.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 = STAR_ICON coin.modulate = Brand.AMBER 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