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.
667 lines
25 KiB
GDScript
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
|