Add an RZ Coin slot machine
Adds a spend-coins-for-a-random-reward mini-game reachable from the shop. Backend mirrors the store's existing atomic/idempotent purchase pattern (SpinAsync in CreditStore.cs) with a server-authoritative, weighted reward roll and a rolling per-user spin-rate guard; odds are served from GET /api/credits/spin-config so the client never hardcodes numbers that could drift from the real payout table. The mockup's "Streak Protector" reward was swapped for a real Exam Simulation Token since streaks aren't currently a persisted, protectable concept. Also fixes two latent bugs surfaced while building this: - credits_service.gd's operation-id builder referenced a nonexistent AuthState.user_id (should be .username), throwing on every credit action. - _post() silently no-op'd with no signal when signed out, which combined with a GDScript closure gotcha (lambdas capture outer locals by value, so reassigning inside one never reached the caller) meant a spin attempted while unauthenticated could hang forever instead of surfacing an error.
This commit is contained in:
@@ -28,6 +28,10 @@ var balance_value_label: Label
|
||||
var coin_caption_label: Label
|
||||
var inventory_label: Label
|
||||
var tab_controls: Array = []
|
||||
var banner_card: PanelContainer
|
||||
var banner_icon_panel: PanelContainer
|
||||
var banner_heading: Label
|
||||
var banner_description: Label
|
||||
var section_heading: Label
|
||||
var section_subtitle: Label
|
||||
var products_grid: HFlowContainer
|
||||
@@ -49,6 +53,7 @@ func _ready() -> void:
|
||||
_build_header()
|
||||
_build_summary_card()
|
||||
_build_tabs()
|
||||
_build_slot_machine_banner()
|
||||
|
||||
section_heading = Label.new()
|
||||
section_heading.text = "Featured"
|
||||
@@ -217,6 +222,63 @@ func _apply_tab_theme(dark: bool) -> void:
|
||||
(entry["underline"] as ColorRect).color = Brand.ROYAL_BLUE if active else Color(0, 0, 0, 0)
|
||||
|
||||
|
||||
func _build_slot_machine_banner() -> void:
|
||||
var dark := UIThemeService.is_dark_mode()
|
||||
banner_card = PanelContainer.new()
|
||||
banner_card.add_theme_stylebox_override("panel", Brand.content_card_box(dark, true, Brand.ROYAL_BLUE))
|
||||
add_child(banner_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)
|
||||
banner_card.add_child(margin)
|
||||
|
||||
var row := HBoxContainer.new()
|
||||
row.add_theme_constant_override("separation", 14)
|
||||
margin.add_child(row)
|
||||
|
||||
banner_icon_panel = PanelContainer.new()
|
||||
banner_icon_panel.custom_minimum_size = Vector2(40, 40)
|
||||
banner_icon_panel.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||
banner_icon_panel.add_theme_stylebox_override("panel", Brand.icon_circle_box(dark, Brand.ROYAL_BLUE, 20))
|
||||
var icon := TextureRect.new()
|
||||
icon.texture = load("res://assets/icons/lucide/star.svg")
|
||||
icon.custom_minimum_size = Vector2(20, 20)
|
||||
icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||
icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||
icon.modulate = Brand.ROYAL_BLUE
|
||||
banner_icon_panel.add_child(icon)
|
||||
row.add_child(banner_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)
|
||||
banner_heading = Label.new()
|
||||
banner_heading.text = "RZ Coin Slot Machine"
|
||||
banner_heading.add_theme_font_override("font", Brand.font)
|
||||
banner_heading.add_theme_font_size_override("font_size", 15)
|
||||
banner_heading.add_theme_color_override("font_color", Brand.text_color(dark))
|
||||
copy.add_child(banner_heading)
|
||||
banner_description = Label.new()
|
||||
banner_description.text = "Spend RZ Coins for a chance at bigger rewards."
|
||||
banner_description.add_theme_font_override("font", Brand.font)
|
||||
banner_description.add_theme_font_size_override("font_size", 12)
|
||||
banner_description.add_theme_color_override("font_color", Brand.muted_color(dark))
|
||||
copy.add_child(banner_description)
|
||||
row.add_child(copy)
|
||||
|
||||
var play_button := Button.new()
|
||||
play_button.text = "Play Now"
|
||||
play_button.custom_minimum_size = Vector2(96, 36)
|
||||
play_button.add_theme_font_override("font", Brand.font)
|
||||
_style_buy_button(play_button)
|
||||
play_button.pressed.connect(func(): main.show_screen("slot_machine"))
|
||||
row.add_child(play_button)
|
||||
|
||||
|
||||
func _build_footer() -> void:
|
||||
var dark := UIThemeService.is_dark_mode()
|
||||
footer_card = PanelContainer.new()
|
||||
@@ -276,6 +338,10 @@ func _refresh_theme_controls() -> void:
|
||||
coin_caption_label.add_theme_color_override("font_color", Brand.muted_color(dark))
|
||||
inventory_label.add_theme_color_override("font_color", Brand.muted_color(dark))
|
||||
_apply_tab_theme(dark)
|
||||
banner_card.add_theme_stylebox_override("panel", Brand.content_card_box(dark, true, Brand.ROYAL_BLUE))
|
||||
banner_icon_panel.add_theme_stylebox_override("panel", Brand.icon_circle_box(dark, Brand.ROYAL_BLUE, 20))
|
||||
banner_heading.add_theme_color_override("font_color", Brand.text_color(dark))
|
||||
banner_description.add_theme_color_override("font_color", Brand.muted_color(dark))
|
||||
section_subtitle.add_theme_color_override("font_color", Brand.muted_color(dark))
|
||||
footer_card.add_theme_stylebox_override("panel", Brand.content_card_box(dark, false))
|
||||
footer_icon_panel.add_theme_stylebox_override("panel", Brand.icon_circle_box(dark, Brand.ROYAL_BLUE, 18))
|
||||
|
||||
Reference in New Issue
Block a user