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.
This commit is contained in:
@@ -28,7 +28,8 @@ var main: Control
|
||||
|
||||
var subtitle_label: Label
|
||||
var machine_root: Control
|
||||
var reel_columns: Array = []
|
||||
var lever_button: TextureButton
|
||||
var reel_states: Array = []
|
||||
var win_up_to_card: PanelContainer
|
||||
var win_up_to_label: Label
|
||||
var cost_card: PanelContainer
|
||||
@@ -134,32 +135,53 @@ func _build_machine(parent: Control) -> void:
|
||||
reels_grid.add_theme_constant_override("separation", 16)
|
||||
center.add_child(reels_grid)
|
||||
|
||||
reel_columns.clear()
|
||||
var cell := 66.0
|
||||
reel_states.clear()
|
||||
for _column_index in range(3):
|
||||
var reel_col := VBoxContainer.new()
|
||||
reel_col.add_theme_constant_override("separation", 10)
|
||||
reels_grid.add_child(reel_col)
|
||||
var cells: Array = []
|
||||
for _row_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(66, 66)
|
||||
icon.custom_minimum_size = Vector2(cell, cell)
|
||||
icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||
icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||
reel_col.add_child(icon)
|
||||
cells.append(icon)
|
||||
reel_columns.append(cells)
|
||||
strip.add_child(icon)
|
||||
icons.append(icon)
|
||||
|
||||
var lever := TextureRect.new()
|
||||
lever.texture = LEVER_TEXTURE
|
||||
lever.anchor_left = 0.80
|
||||
lever.anchor_right = 1.06
|
||||
lever.anchor_top = 0.24
|
||||
lever.anchor_bottom = 0.88
|
||||
lever.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||
lever.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT
|
||||
lever.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
machine_root.add_child(lever)
|
||||
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:
|
||||
@@ -431,7 +453,20 @@ func _build_reward_row(dark: bool, reward: Dictionary) -> HBoxContainer:
|
||||
|
||||
|
||||
func _refresh_spin_button() -> void:
|
||||
spin_button.disabled = _spinning or CreditsService.credits < CreditsService.spin_cost
|
||||
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:
|
||||
@@ -451,13 +486,33 @@ func _on_spin_pressed() -> void:
|
||||
|
||||
|
||||
func _do_one_spin() -> bool:
|
||||
_animate_reels()
|
||||
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
|
||||
@@ -471,6 +526,24 @@ func _do_one_spin() -> bool:
|
||||
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):
|
||||
@@ -492,23 +565,33 @@ func _await_spin_result() -> Dictionary:
|
||||
return result
|
||||
|
||||
|
||||
func _animate_reels() -> void:
|
||||
for index in range(reel_columns.size()):
|
||||
_spin_one_reel(reel_columns[index], 0.9 + index * 0.25)
|
||||
|
||||
|
||||
func _spin_one_reel(cells: Array, duration: float) -> void:
|
||||
var elapsed := 0.0
|
||||
var interval := 0.06
|
||||
while elapsed < duration:
|
||||
var symbol: Texture2D = REEL_SYMBOLS[randi() % REEL_SYMBOLS.size()]
|
||||
for cell in cells:
|
||||
cell.texture = symbol
|
||||
await get_tree().create_timer(interval).timeout
|
||||
elapsed += interval
|
||||
interval = minf(interval * 1.18, 0.22)
|
||||
for cell in cells:
|
||||
cell.texture = SLOT_COIN
|
||||
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:
|
||||
|
||||
Reference in New Issue
Block a user