Files
step3/scripts/ui/rz_slot_machine_screen.gd
T
hz a58aa401c8 Fix slot machine: real spin animation, working lever, honest reels
Three real bugs from user testing:
- The lever was a plain TextureRect with mouse_filter set to IGNORE -
  it visually looked interactive but could never receive a click.
  Swapped it for a TextureButton wired to the same spin action, plus
  a quick pull-and-spring-back rotation for tactile feedback.
- The "reel animation" was just rapidly swapping each cell's texture
  in place - no actual motion. Reels are now real scrolling strips
  (a clipped viewport + a taller VBoxContainer strip that scrolls and
  recycles its front icon to the back), which is what an actual
  spinning reel look/feel requires.
- Every reel unconditionally settled on the coin icon regardless of
  what was actually won, so a jackpot and a QBank Reset Token looked
  identical - the two facts users need to trust the mechanic. Reels
  now settle with the real reward's icon landing in the middle
  (payline) row on all three columns, with random decorative icons
  above/below, so the visual result always matches the actual payout.

Verified the settle math directly (a reel-only probe confirming the
middle icon matches the requested target after recycling) and the
lever's click wiring (confirms _spinning toggles and the disabled
state round-trips), plus a real rendered screenshot of a settled
spin, before cleaning up the probes.
2026-07-30 04:26:42 -05:00

667 lines
25 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 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