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.
584 lines
22 KiB
GDScript
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
|