The shield icon's dark/mushy rendering was caused by disabled mipmaps on all 6 slot-machine icon textures (inherited from the flat lucide icon convention, wrong for these gradient-heavy assets) - at small display sizes this caused aliasing that hit the shield's fine detail (RZ lettering, metallic bevels) hardest since the other icons are bolder/simpler shapes. Enabling mipmaps/generate on all 6 fixes it; confirmed by rendering the actual in-game presentation size before and after, not just eyeballing the source file. Also converts the slot machine from a full standalone screen (sidebar nav entry, main.gd-managed) into a movable/draggable floating panel opened from the question-player toolbar, mirroring the existing calculator panel exactly: same top_level+custom title-bar-drag+ viewport-clamp mechanics, same open/close/toggle API, instantiated once alongside the calculator and closed on session hide. Shrunk the cabinet and restacked the rewards list below it (was beside it) to fit a toolbox-sized floating panel instead of a full page, wrapped in a ScrollContainer for any overflow. Removed the sidebar entry and screens[] registration from main.gd and the now-dead routing case from navigation_routes.gd.
778 lines
28 KiB
GDScript
778 lines
28 KiB
GDScript
extends PanelContainer
|
||
|
||
const Brand := preload("res://scripts/ui/medmind_style.gd")
|
||
const STAR_ICON := preload("res://assets/icons/lucide/star.svg")
|
||
const CLOCK_ICON := preload("res://assets/icons/lucide/clock-3.svg")
|
||
const CABINET_FRAME := preload("res://assets/slot_machine/cabinet_frame.png")
|
||
const LEVER_TEXTURE := preload("res://assets/slot_machine/lever.png")
|
||
const SLOT_COIN := preload("res://assets/slot_machine/slot_coin.png")
|
||
const SLOT_GEM := preload("res://assets/slot_machine/slot_gem.png")
|
||
const SLOT_POTION := preload("res://assets/slot_machine/slot_potion.png")
|
||
const SLOT_SHIELD := preload("res://assets/slot_machine/slot_shield.png")
|
||
const SLOT_COIN_STACK := preload("res://assets/slot_machine/slot_coin_stack.png")
|
||
const SLOT_GIFT := preload("res://assets/slot_machine/slot_gift.png")
|
||
const REEL_SYMBOLS := [SLOT_COIN, SLOT_GEM, SLOT_POTION, SLOT_SHIELD, SLOT_COIN_STACK, SLOT_GIFT]
|
||
# fraction-of-image rect of the cabinet's empty reel window, measured against cabinet_frame.png
|
||
const WINDOW_RECT := Rect2(0.203, 0.246, 0.591, 0.604)
|
||
const REWARD_ICONS := {
|
||
"jackpot": {"texture": SLOT_COIN, "modulate": false},
|
||
"epic": {"texture": SLOT_GEM, "modulate": false},
|
||
"great": {"texture": SLOT_SHIELD, "modulate": false},
|
||
"exam_token": {"texture": CLOCK_ICON, "modulate": true},
|
||
"qbank_reset": {"texture": SLOT_GIFT, "modulate": false},
|
||
"nice": {"texture": SLOT_COIN_STACK, "modulate": false},
|
||
"good_luck": {"texture": SLOT_COIN, "modulate": false, "tint": true},
|
||
}
|
||
const MACHINE_SIZE := 380.0
|
||
const CELL := 44.0
|
||
|
||
var main: Control
|
||
|
||
var drag_active := false
|
||
var drag_offset := Vector2.ZERO
|
||
var has_been_positioned := false
|
||
|
||
var subtitle_label: Label
|
||
var machine_root: Control
|
||
var lever_button: TextureButton
|
||
var reel_states: Array = []
|
||
var win_up_to_card: PanelContainer
|
||
var win_up_to_label: Label
|
||
var cost_card: PanelContainer
|
||
var cost_value_label: Label
|
||
var auto_card: PanelContainer
|
||
var spin_button: Button
|
||
var auto_spin_toggle: CheckButton
|
||
var status_label: Label
|
||
var rewards_list: VBoxContainer
|
||
var footer_card: PanelContainer
|
||
var footer_icon_panel: PanelContainer
|
||
var footer_icon: TextureRect
|
||
var footer_heading: Label
|
||
var footer_description: Label
|
||
|
||
var _spinning := false
|
||
|
||
|
||
func _ready() -> void:
|
||
name = "RzSlotMachinePanel"
|
||
visible = false
|
||
top_level = true
|
||
mouse_filter = Control.MOUSE_FILTER_STOP
|
||
custom_minimum_size = Vector2(460, 680)
|
||
size = custom_minimum_size
|
||
|
||
_build_ui()
|
||
|
||
if not get_viewport().size_changed.is_connected(_on_viewport_size_changed):
|
||
get_viewport().size_changed.connect(_on_viewport_size_changed)
|
||
_on_viewport_size_changed()
|
||
|
||
CreditsService.credits_changed.connect(_refresh_spin_button)
|
||
CreditsService.spin_config_loaded.connect(_refresh_rewards)
|
||
UIThemeService.theme_changed.connect(func(_dark: bool): refresh_theme_controls())
|
||
refresh_theme_controls()
|
||
_refresh_rewards()
|
||
_refresh_spin_button()
|
||
|
||
|
||
func open_panel() -> void:
|
||
visible = true
|
||
move_to_front()
|
||
if not has_been_positioned:
|
||
var viewport_size := get_viewport_rect().size
|
||
position = Vector2(maxf(12.0, viewport_size.x - size.x - 24.0), 72.0)
|
||
has_been_positioned = true
|
||
_clamp_to_viewport()
|
||
CreditsService.refresh()
|
||
CreditsService.fetch_spin_config()
|
||
_refresh_rewards()
|
||
_refresh_spin_button()
|
||
|
||
|
||
func close_panel() -> void:
|
||
visible = false
|
||
drag_active = false
|
||
|
||
|
||
func toggle_panel() -> void:
|
||
if visible:
|
||
close_panel()
|
||
else:
|
||
open_panel()
|
||
|
||
|
||
func _build_ui() -> void:
|
||
var margin := MarginContainer.new()
|
||
margin.add_theme_constant_override("margin_left", 12)
|
||
margin.add_theme_constant_override("margin_right", 12)
|
||
margin.add_theme_constant_override("margin_top", 12)
|
||
margin.add_theme_constant_override("margin_bottom", 14)
|
||
add_child(margin)
|
||
|
||
var content := VBoxContainer.new()
|
||
content.add_theme_constant_override("separation", 10)
|
||
margin.add_child(content)
|
||
|
||
_build_title_bar(content)
|
||
|
||
var scroll := ScrollContainer.new()
|
||
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||
content.add_child(scroll)
|
||
|
||
var scroll_content := VBoxContainer.new()
|
||
scroll_content.add_theme_constant_override("separation", 14)
|
||
scroll_content.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
scroll.add_child(scroll_content)
|
||
|
||
subtitle_label = Label.new()
|
||
subtitle_label.text = "Test your luck and win exciting rewards!"
|
||
subtitle_label.add_theme_font_override("font", Brand.font)
|
||
subtitle_label.add_theme_font_size_override("font_size", 13)
|
||
subtitle_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||
scroll_content.add_child(subtitle_label)
|
||
|
||
_build_machine(scroll_content)
|
||
_build_controls(scroll_content)
|
||
_build_rewards_section(scroll_content)
|
||
_build_footer(scroll_content)
|
||
|
||
status_label = Label.new()
|
||
status_label.add_theme_font_override("font", Brand.font)
|
||
status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||
status_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||
scroll_content.add_child(status_label)
|
||
|
||
|
||
func _build_title_bar(content: VBoxContainer) -> void:
|
||
var title_bar := HBoxContainer.new()
|
||
title_bar.name = "SlotMachineTitleBar"
|
||
title_bar.mouse_filter = Control.MOUSE_FILTER_STOP
|
||
title_bar.custom_minimum_size.y = 34
|
||
title_bar.gui_input.connect(_on_title_bar_gui_input)
|
||
content.add_child(title_bar)
|
||
|
||
var title := Label.new()
|
||
title.text = "RZ Slot Machine"
|
||
title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
title.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
title.add_theme_font_override("font", Brand.font)
|
||
title.add_theme_font_size_override("font_size", 18)
|
||
title_bar.add_child(title)
|
||
|
||
var close := Button.new()
|
||
close.name = "SlotMachineClose"
|
||
close.text = "×"
|
||
close.tooltip_text = "Close slot machine"
|
||
close.custom_minimum_size = Vector2(36, 32)
|
||
close.focus_mode = Control.FOCUS_NONE
|
||
close.pressed.connect(close_panel)
|
||
title_bar.add_child(close)
|
||
|
||
|
||
func _build_machine(parent: Control) -> void:
|
||
var center := CenterContainer.new()
|
||
parent.add_child(center)
|
||
|
||
machine_root = Control.new()
|
||
machine_root.custom_minimum_size = Vector2(MACHINE_SIZE, MACHINE_SIZE)
|
||
center.add_child(machine_root)
|
||
|
||
var frame := TextureRect.new()
|
||
frame.texture = CABINET_FRAME
|
||
frame.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||
frame.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||
frame.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT
|
||
frame.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
machine_root.add_child(frame)
|
||
|
||
var window := Control.new()
|
||
window.anchor_left = WINDOW_RECT.position.x
|
||
window.anchor_top = WINDOW_RECT.position.y
|
||
window.anchor_right = WINDOW_RECT.position.x + WINDOW_RECT.size.x
|
||
window.anchor_bottom = WINDOW_RECT.position.y + WINDOW_RECT.size.y
|
||
window.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
machine_root.add_child(window)
|
||
|
||
var reel_center := CenterContainer.new()
|
||
reel_center.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||
window.add_child(reel_center)
|
||
|
||
var reels_grid := HBoxContainer.new()
|
||
reels_grid.add_theme_constant_override("separation", 10)
|
||
reel_center.add_child(reels_grid)
|
||
|
||
reel_states.clear()
|
||
for _column_index in range(3):
|
||
var clip := Control.new()
|
||
clip.clip_contents = true
|
||
clip.custom_minimum_size = Vector2(CELL, CELL * 3)
|
||
reels_grid.add_child(clip)
|
||
|
||
var strip := VBoxContainer.new()
|
||
strip.add_theme_constant_override("separation", 0)
|
||
clip.add_child(strip)
|
||
|
||
var icons: Array = []
|
||
for _row_index in range(4):
|
||
var icon := TextureRect.new()
|
||
icon.texture = REEL_SYMBOLS[randi() % REEL_SYMBOLS.size()]
|
||
icon.custom_minimum_size = Vector2(CELL, CELL)
|
||
icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||
icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||
strip.add_child(icon)
|
||
icons.append(icon)
|
||
|
||
reel_states.append({
|
||
"strip": strip,
|
||
"icons": icons,
|
||
"step": CELL,
|
||
"speed": 420.0,
|
||
"spinning": false,
|
||
"stop_requested": false,
|
||
"target_texture": null,
|
||
"recycles_after_target": -1,
|
||
"done": true,
|
||
})
|
||
|
||
lever_button = TextureButton.new()
|
||
lever_button.texture_normal = LEVER_TEXTURE
|
||
lever_button.ignore_texture_size = true
|
||
lever_button.stretch_mode = TextureButton.STRETCH_KEEP_ASPECT_CENTERED
|
||
lever_button.anchor_left = 0.80
|
||
lever_button.anchor_right = 1.06
|
||
lever_button.anchor_top = 0.24
|
||
lever_button.anchor_bottom = 0.88
|
||
lever_button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
|
||
lever_button.tooltip_text = "Pull to spin"
|
||
lever_button.resized.connect(func(): lever_button.pivot_offset = Vector2(lever_button.size.x * 0.5, lever_button.size.y * 0.92))
|
||
lever_button.pressed.connect(_on_lever_pulled)
|
||
machine_root.add_child(lever_button)
|
||
|
||
|
||
func _build_controls(parent: Control) -> void:
|
||
var dark := UIThemeService.is_dark_mode()
|
||
|
||
win_up_to_card = PanelContainer.new()
|
||
win_up_to_card.add_theme_stylebox_override("panel", Brand.content_card_box(dark, false))
|
||
parent.add_child(win_up_to_card)
|
||
var win_margin := MarginContainer.new()
|
||
win_margin.add_theme_constant_override("margin_left", 14)
|
||
win_margin.add_theme_constant_override("margin_right", 14)
|
||
win_margin.add_theme_constant_override("margin_top", 10)
|
||
win_margin.add_theme_constant_override("margin_bottom", 10)
|
||
win_up_to_card.add_child(win_margin)
|
||
win_up_to_label = Label.new()
|
||
win_up_to_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||
win_up_to_label.add_theme_font_override("font", Brand.font)
|
||
win_up_to_label.add_theme_font_size_override("font_size", 14)
|
||
win_up_to_label.add_theme_color_override("font_color", Brand.text_color(dark))
|
||
win_margin.add_child(win_up_to_label)
|
||
|
||
var controls_row := HBoxContainer.new()
|
||
controls_row.add_theme_constant_override("separation", 10)
|
||
parent.add_child(controls_row)
|
||
|
||
cost_card = PanelContainer.new()
|
||
cost_card.add_theme_stylebox_override("panel", Brand.content_card_box(dark, false))
|
||
controls_row.add_child(cost_card)
|
||
var cost_margin := MarginContainer.new()
|
||
cost_margin.add_theme_constant_override("margin_left", 10)
|
||
cost_margin.add_theme_constant_override("margin_right", 10)
|
||
cost_margin.add_theme_constant_override("margin_top", 6)
|
||
cost_margin.add_theme_constant_override("margin_bottom", 6)
|
||
cost_card.add_child(cost_margin)
|
||
var cost_box := VBoxContainer.new()
|
||
cost_box.add_theme_constant_override("separation", 0)
|
||
cost_margin.add_child(cost_box)
|
||
var cost_caption := Label.new()
|
||
cost_caption.text = "Cost per spin"
|
||
cost_caption.add_theme_font_override("font", Brand.font)
|
||
cost_caption.add_theme_font_size_override("font_size", 10)
|
||
cost_caption.add_theme_color_override("font_color", Brand.muted_color(dark))
|
||
cost_box.add_child(cost_caption)
|
||
cost_value_label = Label.new()
|
||
cost_value_label.add_theme_font_override("font", Brand.font)
|
||
cost_value_label.add_theme_font_size_override("font_size", 14)
|
||
cost_value_label.add_theme_color_override("font_color", Brand.text_color(dark))
|
||
cost_box.add_child(cost_value_label)
|
||
|
||
spin_button = Button.new()
|
||
spin_button.text = "Spin"
|
||
spin_button.custom_minimum_size = Vector2(110, 52)
|
||
spin_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
spin_button.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||
spin_button.add_theme_font_override("font", Brand.font)
|
||
spin_button.add_theme_font_size_override("font_size", 16)
|
||
_style_spin_button()
|
||
spin_button.pressed.connect(_on_spin_pressed)
|
||
controls_row.add_child(spin_button)
|
||
|
||
auto_card = PanelContainer.new()
|
||
auto_card.add_theme_stylebox_override("panel", Brand.content_card_box(dark, false))
|
||
controls_row.add_child(auto_card)
|
||
var auto_margin := MarginContainer.new()
|
||
auto_margin.add_theme_constant_override("margin_left", 10)
|
||
auto_margin.add_theme_constant_override("margin_right", 10)
|
||
auto_margin.add_theme_constant_override("margin_top", 6)
|
||
auto_margin.add_theme_constant_override("margin_bottom", 6)
|
||
auto_card.add_child(auto_margin)
|
||
var auto_box := VBoxContainer.new()
|
||
auto_box.add_theme_constant_override("separation", 0)
|
||
auto_margin.add_child(auto_box)
|
||
auto_spin_toggle = CheckButton.new()
|
||
auto_spin_toggle.text = "Auto Spin"
|
||
auto_spin_toggle.add_theme_font_override("font", Brand.font)
|
||
auto_box.add_child(auto_spin_toggle)
|
||
var auto_caption := Label.new()
|
||
auto_caption.text = "10 spins"
|
||
auto_caption.add_theme_font_override("font", Brand.font)
|
||
auto_caption.add_theme_font_size_override("font_size", 10)
|
||
auto_caption.add_theme_color_override("font_color", Brand.muted_color(dark))
|
||
auto_box.add_child(auto_caption)
|
||
|
||
var info_label := Label.new()
|
||
info_label.text = "All rewards are instant and sent to your account."
|
||
info_label.add_theme_font_override("font", Brand.font)
|
||
info_label.add_theme_font_size_override("font_size", 10)
|
||
info_label.add_theme_color_override("font_color", Brand.muted_color(dark))
|
||
info_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||
parent.add_child(info_label)
|
||
|
||
|
||
func _build_rewards_section(parent: Control) -> void:
|
||
var dark := UIThemeService.is_dark_mode()
|
||
var panel := PanelContainer.new()
|
||
panel.add_theme_stylebox_override("panel", Brand.content_card_box(dark, false))
|
||
parent.add_child(panel)
|
||
|
||
var margin := MarginContainer.new()
|
||
margin.add_theme_constant_override("margin_left", 14)
|
||
margin.add_theme_constant_override("margin_right", 14)
|
||
margin.add_theme_constant_override("margin_top", 12)
|
||
margin.add_theme_constant_override("margin_bottom", 12)
|
||
panel.add_child(margin)
|
||
|
||
var column := VBoxContainer.new()
|
||
column.add_theme_constant_override("separation", 10)
|
||
margin.add_child(column)
|
||
|
||
var heading := Label.new()
|
||
heading.text = "Possible Rewards"
|
||
heading.add_theme_font_override("font", Brand.font)
|
||
heading.add_theme_font_size_override("font_size", 15)
|
||
heading.add_theme_color_override("font_color", Brand.text_color(dark))
|
||
column.add_child(heading)
|
||
|
||
rewards_list = VBoxContainer.new()
|
||
rewards_list.add_theme_constant_override("separation", 8)
|
||
column.add_child(rewards_list)
|
||
|
||
|
||
func _build_footer(parent: Control) -> void:
|
||
var dark := UIThemeService.is_dark_mode()
|
||
footer_card = PanelContainer.new()
|
||
footer_card.add_theme_stylebox_override("panel", Brand.content_card_box(dark, false))
|
||
parent.add_child(footer_card)
|
||
|
||
var margin := MarginContainer.new()
|
||
margin.add_theme_constant_override("margin_left", 14)
|
||
margin.add_theme_constant_override("margin_right", 14)
|
||
margin.add_theme_constant_override("margin_top", 10)
|
||
margin.add_theme_constant_override("margin_bottom", 10)
|
||
footer_card.add_child(margin)
|
||
|
||
var row := HBoxContainer.new()
|
||
row.add_theme_constant_override("separation", 12)
|
||
margin.add_child(row)
|
||
|
||
footer_icon_panel = PanelContainer.new()
|
||
footer_icon_panel.custom_minimum_size = Vector2(32, 32)
|
||
footer_icon_panel.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||
footer_icon_panel.add_theme_stylebox_override("panel", Brand.icon_circle_box(dark, Brand.ROYAL_BLUE, 16))
|
||
footer_icon = TextureRect.new()
|
||
footer_icon.texture = STAR_ICON
|
||
footer_icon.custom_minimum_size = Vector2(16, 16)
|
||
footer_icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||
footer_icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||
footer_icon.modulate = Brand.ROYAL_BLUE
|
||
footer_icon_panel.add_child(footer_icon)
|
||
row.add_child(footer_icon_panel)
|
||
|
||
var copy := VBoxContainer.new()
|
||
copy.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
copy.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||
copy.add_theme_constant_override("separation", 2)
|
||
footer_heading = Label.new()
|
||
footer_heading.text = "How it works"
|
||
footer_heading.add_theme_font_override("font", Brand.font)
|
||
footer_heading.add_theme_font_size_override("font_size", 14)
|
||
footer_heading.add_theme_color_override("font_color", Brand.text_color(dark))
|
||
copy.add_child(footer_heading)
|
||
footer_description = Label.new()
|
||
footer_description.text = "Spend RZ Coins to spin the slot machine. Win coins, boosts, tokens, and more!"
|
||
footer_description.add_theme_font_override("font", Brand.font)
|
||
footer_description.add_theme_font_size_override("font_size", 11)
|
||
footer_description.add_theme_color_override("font_color", Brand.muted_color(dark))
|
||
footer_description.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||
copy.add_child(footer_description)
|
||
row.add_child(copy)
|
||
|
||
|
||
func refresh_theme_controls() -> void:
|
||
var dark := UIThemeService.is_dark_mode()
|
||
add_theme_stylebox_override("panel", Brand.content_card_box(dark, true, Brand.ROYAL_BLUE))
|
||
subtitle_label.add_theme_color_override("font_color", Brand.muted_color(dark))
|
||
win_up_to_card.add_theme_stylebox_override("panel", Brand.content_card_box(dark, false))
|
||
win_up_to_label.add_theme_color_override("font_color", Brand.text_color(dark))
|
||
cost_card.add_theme_stylebox_override("panel", Brand.content_card_box(dark, false))
|
||
cost_value_label.add_theme_color_override("font_color", Brand.text_color(dark))
|
||
auto_card.add_theme_stylebox_override("panel", Brand.content_card_box(dark, false))
|
||
footer_card.add_theme_stylebox_override("panel", Brand.content_card_box(dark, false))
|
||
footer_icon_panel.add_theme_stylebox_override("panel", Brand.icon_circle_box(dark, Brand.ROYAL_BLUE, 16))
|
||
footer_heading.add_theme_color_override("font_color", Brand.text_color(dark))
|
||
footer_description.add_theme_color_override("font_color", Brand.muted_color(dark))
|
||
_refresh_rewards()
|
||
|
||
|
||
func _reward_color(key: String) -> Color:
|
||
match key:
|
||
"jackpot": return Brand.AMBER
|
||
"epic": return Brand.PURPLE
|
||
"great": return Brand.ROYAL_BLUE
|
||
"exam_token": return Brand.ROYAL_BLUE
|
||
"qbank_reset": return Brand.SUCCESS
|
||
"nice": return Brand.SKY_BLUE
|
||
"good_luck": return Brand.SLATE
|
||
_: return Brand.ROYAL_BLUE
|
||
|
||
|
||
func _refresh_rewards() -> void:
|
||
if cost_value_label == null:
|
||
return
|
||
var dark := UIThemeService.is_dark_mode()
|
||
cost_value_label.text = "%d RZ Coins" % CreditsService.spin_cost
|
||
|
||
var max_coins := 0
|
||
for reward_variant in CreditsService.spin_rewards:
|
||
if typeof(reward_variant) != TYPE_DICTIONARY:
|
||
continue
|
||
max_coins = maxi(max_coins, int((reward_variant as Dictionary).get("coinAmount", 0)))
|
||
win_up_to_label.text = "Win up to %s RZ Coins!" % _format_thousands(max_coins) if max_coins > 0 else ""
|
||
|
||
for child in rewards_list.get_children():
|
||
child.queue_free()
|
||
for reward_variant in CreditsService.spin_rewards:
|
||
if typeof(reward_variant) != TYPE_DICTIONARY:
|
||
continue
|
||
var reward: Dictionary = reward_variant
|
||
rewards_list.add_child(_build_reward_row(dark, reward))
|
||
|
||
|
||
func _build_reward_row(dark: bool, reward: Dictionary) -> HBoxContainer:
|
||
var key := str(reward.get("key", ""))
|
||
var accent := _reward_color(key)
|
||
var row := HBoxContainer.new()
|
||
row.add_theme_constant_override("separation", 10)
|
||
|
||
var icon_panel := PanelContainer.new()
|
||
icon_panel.custom_minimum_size = Vector2(34, 34)
|
||
icon_panel.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||
icon_panel.add_theme_stylebox_override("panel", Brand.icon_circle_box(dark, accent, 17))
|
||
var icon := TextureRect.new()
|
||
var spec: Dictionary = REWARD_ICONS.get(key, {"texture": SLOT_COIN, "modulate": false})
|
||
icon.texture = spec.get("texture")
|
||
icon.custom_minimum_size = Vector2(20, 20)
|
||
icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||
icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||
if spec.get("modulate", false):
|
||
icon.modulate = accent
|
||
elif spec.get("tint", false):
|
||
icon.modulate = Color("94a3b8")
|
||
icon_panel.add_child(icon)
|
||
row.add_child(icon_panel)
|
||
|
||
var copy := VBoxContainer.new()
|
||
copy.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||
copy.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||
copy.add_theme_constant_override("separation", 0)
|
||
var label := Label.new()
|
||
label.text = str(reward.get("label", ""))
|
||
label.add_theme_font_override("font", Brand.font)
|
||
label.add_theme_font_size_override("font_size", 12)
|
||
label.add_theme_color_override("font_color", Brand.text_color(dark))
|
||
copy.add_child(label)
|
||
var description := Label.new()
|
||
description.text = str(reward.get("description", ""))
|
||
description.add_theme_font_override("font", Brand.font)
|
||
description.add_theme_font_size_override("font_size", 10)
|
||
description.add_theme_color_override("font_color", Brand.muted_color(dark))
|
||
copy.add_child(description)
|
||
row.add_child(copy)
|
||
|
||
var percent_label := Label.new()
|
||
percent_label.text = "%s%%" % _format_percent(float(reward.get("probability", 0.0)) * 100.0)
|
||
percent_label.add_theme_font_override("font", Brand.font)
|
||
percent_label.add_theme_font_size_override("font_size", 12)
|
||
percent_label.add_theme_color_override("font_color", accent)
|
||
row.add_child(percent_label)
|
||
|
||
return row
|
||
|
||
|
||
func _refresh_spin_button() -> void:
|
||
if spin_button == null:
|
||
return
|
||
var disabled := _spinning or CreditsService.credits < CreditsService.spin_cost
|
||
spin_button.disabled = disabled
|
||
lever_button.disabled = disabled
|
||
|
||
|
||
func _on_lever_pulled() -> void:
|
||
_play_lever_pull_animation()
|
||
_on_spin_pressed()
|
||
|
||
|
||
func _play_lever_pull_animation() -> void:
|
||
var tween := create_tween()
|
||
tween.tween_property(lever_button, "rotation_degrees", -20.0, 0.1).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
|
||
tween.tween_property(lever_button, "rotation_degrees", 0.0, 0.3).set_trans(Tween.TRANS_ELASTIC).set_ease(Tween.EASE_OUT)
|
||
|
||
|
||
func _on_spin_pressed() -> void:
|
||
if _spinning:
|
||
return
|
||
_spinning = true
|
||
_refresh_spin_button()
|
||
var spin_count := 10 if auto_spin_toggle.button_pressed else 1
|
||
for _i in range(spin_count):
|
||
if CreditsService.credits < CreditsService.spin_cost:
|
||
break
|
||
var should_continue := await _do_one_spin()
|
||
if not should_continue:
|
||
break
|
||
_spinning = false
|
||
_refresh_spin_button()
|
||
|
||
|
||
func _do_one_spin() -> bool:
|
||
for state in reel_states:
|
||
state["stop_requested"] = false
|
||
state["target_texture"] = null
|
||
state["recycles_after_target"] = -1
|
||
state["done"] = false
|
||
_spin_reel(state)
|
||
|
||
var result := await _await_spin_result()
|
||
|
||
if not result.get("ok", false):
|
||
for state in reel_states:
|
||
state["target_texture"] = REEL_SYMBOLS[randi() % REEL_SYMBOLS.size()]
|
||
state["stop_requested"] = true
|
||
await _await_reels_done()
|
||
status_label.text = str(result.get("message", "The spin could not be completed."))
|
||
status_label.add_theme_color_override("font_color", Brand.MAGENTA)
|
||
return false
|
||
|
||
var reward_key := str(result.get("reward_key", ""))
|
||
var reward_texture := _icon_for_reward(reward_key)
|
||
for index in range(reel_states.size()):
|
||
var state: Dictionary = reel_states[index]
|
||
get_tree().create_timer(index * 0.25).timeout.connect(func():
|
||
state["target_texture"] = reward_texture
|
||
state["stop_requested"] = true)
|
||
await _await_reels_done()
|
||
|
||
var reward_label := str(result.get("reward_label", ""))
|
||
var coins_won := int(result.get("coins_won", 0))
|
||
status_label.text = "You won: %s" % reward_label
|
||
status_label.add_theme_color_override("font_color", Brand.SUCCESS)
|
||
if coins_won > 0:
|
||
_play_coin_burst(coins_won)
|
||
elif main != null:
|
||
var credits_badge: Object = main.get("credits_badge")
|
||
if credits_badge != null:
|
||
credits_badge.bump()
|
||
return true
|
||
|
||
|
||
func _icon_for_reward(key: String) -> Texture2D:
|
||
var spec: Dictionary = REWARD_ICONS.get(key, {})
|
||
var texture: Texture2D = spec.get("texture", SLOT_COIN)
|
||
return texture if texture != null else SLOT_COIN
|
||
|
||
|
||
func _await_reels_done() -> void:
|
||
while true:
|
||
var all_done := true
|
||
for state in reel_states:
|
||
if not state.get("done", false):
|
||
all_done = false
|
||
break
|
||
if all_done:
|
||
return
|
||
await get_tree().process_frame
|
||
|
||
|
||
func _await_spin_result() -> Dictionary:
|
||
var result := {}
|
||
var on_resolved := func(reward_key: String, reward_label: String, coins_won: int):
|
||
result.merge({"ok": true, "reward_key": reward_key, "reward_label": reward_label, "coins_won": coins_won}, true)
|
||
var on_failed := func(message: String):
|
||
result.merge({"ok": false, "message": message}, true)
|
||
CreditsService.spin_resolved.connect(on_resolved, CONNECT_ONE_SHOT)
|
||
CreditsService.credits_failed.connect(on_failed, CONNECT_ONE_SHOT)
|
||
CreditsService.spin()
|
||
var deadline := Time.get_ticks_msec() + 20000
|
||
while result.is_empty() and Time.get_ticks_msec() < deadline:
|
||
await get_tree().process_frame
|
||
if result.is_empty():
|
||
result = {"ok": false, "message": "The spin timed out. Check your connection and try again."}
|
||
if CreditsService.spin_resolved.is_connected(on_resolved):
|
||
CreditsService.spin_resolved.disconnect(on_resolved)
|
||
if CreditsService.credits_failed.is_connected(on_failed):
|
||
CreditsService.credits_failed.disconnect(on_failed)
|
||
return result
|
||
|
||
|
||
func _spin_reel(state: Dictionary) -> void:
|
||
state["spinning"] = true
|
||
var strip: VBoxContainer = state["strip"]
|
||
var icons: Array = state["icons"]
|
||
var step: float = state["step"]
|
||
var speed: float = state["speed"]
|
||
while true:
|
||
var current_speed: float = speed if state["recycles_after_target"] == -1 else speed * 0.45
|
||
strip.position.y -= current_speed * get_process_delta_time()
|
||
if strip.position.y <= -step:
|
||
strip.position.y += step
|
||
var front: TextureRect = icons.pop_front()
|
||
strip.move_child(front, strip.get_child_count() - 1)
|
||
icons.append(front)
|
||
if state["stop_requested"] and state["recycles_after_target"] == -1:
|
||
front.texture = state["target_texture"]
|
||
state["recycles_after_target"] = 0
|
||
else:
|
||
front.texture = REEL_SYMBOLS[randi() % REEL_SYMBOLS.size()]
|
||
if state["recycles_after_target"] >= 0:
|
||
state["recycles_after_target"] += 1
|
||
if state["recycles_after_target"] == 2:
|
||
strip.position.y = 0.0
|
||
break
|
||
await get_tree().process_frame
|
||
state["spinning"] = false
|
||
state["done"] = true
|
||
|
||
|
||
func _play_coin_burst(coins_won: int) -> void:
|
||
if main == null:
|
||
return
|
||
var credits_badge: Control = main.get("credits_badge")
|
||
if credits_badge == null or not is_instance_valid(credits_badge):
|
||
return
|
||
var origin := machine_root.get_global_rect().get_center()
|
||
var target := credits_badge.get_global_rect().get_center()
|
||
var coin_count := clampi(3 + coins_won / 500, 4, 10)
|
||
var flight_time := 0.55
|
||
var stagger := 0.035
|
||
for index in range(coin_count):
|
||
var coin := TextureRect.new()
|
||
coin.texture = SLOT_COIN
|
||
coin.custom_minimum_size = Vector2(16, 16)
|
||
coin.size = Vector2(16, 16)
|
||
coin.top_level = true
|
||
coin.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||
coin.pivot_offset = Vector2(8, 8)
|
||
var jitter := Vector2(randf_range(-24.0, 24.0), randf_range(-16.0, 16.0))
|
||
coin.global_position = origin + jitter - coin.size * 0.5
|
||
add_child(coin)
|
||
var tween := create_tween()
|
||
tween.tween_interval(index * stagger)
|
||
tween.set_parallel(true)
|
||
tween.tween_property(coin, "global_position", target - coin.size * 0.5, flight_time).set_trans(Tween.TRANS_CUBIC).set_ease(Tween.EASE_IN)
|
||
tween.tween_property(coin, "scale", Vector2(0.6, 0.6), flight_time).set_trans(Tween.TRANS_CUBIC).set_ease(Tween.EASE_IN)
|
||
tween.set_parallel(false)
|
||
tween.tween_callback(coin.queue_free)
|
||
var settle_time := (coin_count - 1) * stagger + flight_time
|
||
get_tree().create_timer(settle_time).timeout.connect(func():
|
||
if is_instance_valid(credits_badge):
|
||
credits_badge.bump())
|
||
|
||
|
||
func _style_spin_button() -> void:
|
||
var normal := StyleBoxFlat.new()
|
||
normal.bg_color = Brand.ROYAL_BLUE
|
||
normal.set_corner_radius_all(8)
|
||
var hover := StyleBoxFlat.new()
|
||
hover.bg_color = Brand.SKY_BLUE
|
||
hover.set_corner_radius_all(8)
|
||
var disabled := StyleBoxFlat.new()
|
||
disabled.bg_color = Color(Brand.ROYAL_BLUE, 0.35)
|
||
disabled.set_corner_radius_all(8)
|
||
spin_button.add_theme_stylebox_override("normal", normal)
|
||
spin_button.add_theme_stylebox_override("hover", hover)
|
||
spin_button.add_theme_stylebox_override("pressed", normal)
|
||
spin_button.add_theme_stylebox_override("disabled", disabled)
|
||
spin_button.add_theme_color_override("font_color", Brand.DARK_TEXT)
|
||
spin_button.add_theme_color_override("font_hover_color", Brand.DARK_TEXT)
|
||
spin_button.add_theme_color_override("font_disabled_color", Color(Brand.DARK_TEXT, 0.7))
|
||
|
||
|
||
func _on_title_bar_gui_input(event: InputEvent) -> void:
|
||
var touch_event := event as InputEventScreenTouch
|
||
if touch_event != null:
|
||
drag_active = touch_event.pressed
|
||
if drag_active:
|
||
drag_offset = touch_event.position
|
||
get_viewport().set_input_as_handled()
|
||
return
|
||
var touch_drag := event as InputEventScreenDrag
|
||
if touch_drag != null and drag_active:
|
||
global_position += touch_drag.relative
|
||
_clamp_to_viewport()
|
||
get_viewport().set_input_as_handled()
|
||
return
|
||
var mouse_event := event as InputEventMouseButton
|
||
if mouse_event != null and mouse_event.button_index == MOUSE_BUTTON_LEFT:
|
||
drag_active = mouse_event.pressed
|
||
if drag_active:
|
||
drag_offset = mouse_event.global_position - global_position
|
||
get_viewport().set_input_as_handled()
|
||
return
|
||
var motion := event as InputEventMouseMotion
|
||
if motion != null and drag_active:
|
||
global_position = motion.global_position - drag_offset
|
||
_clamp_to_viewport()
|
||
get_viewport().set_input_as_handled()
|
||
|
||
|
||
func _on_viewport_size_changed() -> void:
|
||
_clamp_to_viewport()
|
||
|
||
|
||
func _clamp_to_viewport() -> void:
|
||
if not is_inside_tree():
|
||
return
|
||
var viewport_size := get_viewport_rect().size
|
||
position.x = clampf(position.x, 12.0, maxf(12.0, viewport_size.x - size.x - 12.0))
|
||
position.y = clampf(position.y, 12.0, maxf(12.0, viewport_size.y - size.y - 12.0))
|
||
|
||
|
||
func _format_thousands(value: int) -> String:
|
||
var digits := str(absi(value))
|
||
var grouped := ""
|
||
var count := 0
|
||
for index in range(digits.length() - 1, -1, -1):
|
||
grouped = digits[index] + grouped
|
||
count += 1
|
||
if count % 3 == 0 and index != 0:
|
||
grouped = "," + grouped
|
||
return ("-" if value < 0 else "") + grouped
|
||
|
||
|
||
func _format_percent(value: float) -> String:
|
||
if absf(value - roundf(value)) < 0.001:
|
||
return str(int(roundf(value)))
|
||
return "%.1f" % value
|