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:
hz
2026-07-30 00:29:49 -05:00
parent 4909cf0b71
commit 1860e6ede6
8 changed files with 816 additions and 3 deletions
+5
View File
@@ -361,6 +361,11 @@ func _build_ui() -> void:
rz_store.main = self
screens_container.add_child(rz_store)
screens["rz_store"] = rz_store
var slot_machine := preload("res://scripts/ui/rz_slot_machine_screen.gd").new()
slot_machine.name = "RzSlotMachine"
slot_machine.main = self
screens_container.add_child(slot_machine)
screens["slot_machine"] = slot_machine
feedback_dialog = preload("res://scripts/ui/app_feedback_dialog.gd").new()
feedback_dialog.name = "AppFeedbackDialog"