Replace hand-coded cabinet/lever/icons with real generated art

The user generated actual 3D-rendered assets (cabinet frame, lever,
reel-symbol sprite sheet) via an image model - swaps out the
StyleBoxFlat-approximated cabinet, Tween-animated marquee lights/glow,
and gradient-SVG icons from the last pass for the real thing.

The generated PNGs had no real alpha channel (a baked checkerboard
pattern standing in for transparency, a known limitation of some
image-gen output) - added a flood-fill background remover (BFS from
the image border, matching near-white/grey checkerboard tones) to
produce genuinely transparent PNGs, then sliced the sprite sheet into
6 individual icons and measured the cabinet's empty reel-window
rect as a fraction of the frame image so the real 3x3 reel grid and
lever can be positioned precisely over the static art via anchors.

Verified with actual rendered screenshots at each step (window
alignment, then lever sizing/position) rather than just structural
checks, since this is exactly the kind of thing that looks fine in
code and wrong on screen.
This commit is contained in:
hz
2026-07-30 04:06:35 -05:00
parent 0bd74fcabf
commit 3fd670d111
25 changed files with 237 additions and 388 deletions
+61 -180
View File
@@ -3,13 +3,17 @@ 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 SLOT_COIN := preload("res://assets/icons/slots/slot_coin.svg")
const SLOT_GEM := preload("res://assets/icons/slots/slot_gem.svg")
const SLOT_POTION := preload("res://assets/icons/slots/slot_potion.svg")
const SLOT_SHIELD := preload("res://assets/icons/slots/slot_shield.svg")
const SLOT_COIN_STACK := preload("res://assets/icons/slots/slot_coin_stack.svg")
const SLOT_GIFT := preload("res://assets/icons/slots/slot_gift.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},
@@ -23,7 +27,7 @@ const REWARD_ICONS := {
var main: Control
var subtitle_label: Label
var machine_panel: PanelContainer
var machine_root: Control
var reel_columns: Array = []
var win_up_to_card: PanelContainer
var win_up_to_label: Label
@@ -55,7 +59,13 @@ func _ready() -> void:
body.size_flags_vertical = Control.SIZE_EXPAND_FILL
add_child(body)
_build_machine(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()
@@ -94,48 +104,35 @@ func _build_header() -> void:
func _build_machine(parent: Control) -> void:
var dark := UIThemeService.is_dark_mode()
machine_panel = PanelContainer.new()
machine_panel.custom_minimum_size = Vector2(560, 0)
machine_panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
machine_panel.size_flags_vertical = Control.SIZE_SHRINK_BEGIN
machine_panel.add_theme_stylebox_override("panel", _cabinet_box())
parent.add_child(machine_panel)
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 margin := MarginContainer.new()
margin.add_theme_constant_override("margin_left", 32)
margin.add_theme_constant_override("margin_right", 32)
margin.add_theme_constant_override("margin_top", 30)
margin.add_theme_constant_override("margin_bottom", 28)
machine_panel.add_child(margin)
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 column := VBoxContainer.new()
column.add_theme_constant_override("separation", 20)
margin.add_child(column)
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)
_build_marquee(column)
var reel_row := HBoxContainer.new()
reel_row.add_theme_constant_override("separation", 20)
reel_row.alignment = BoxContainer.ALIGNMENT_CENTER
column.add_child(reel_row)
reel_row.add_child(_build_light_strip(7))
var reel_screen := PanelContainer.new()
reel_screen.add_theme_stylebox_override("panel", _reel_screen_box())
reel_row.add_child(reel_screen)
var reel_margin := MarginContainer.new()
reel_margin.add_theme_constant_override("margin_left", 22)
reel_margin.add_theme_constant_override("margin_right", 22)
reel_margin.add_theme_constant_override("margin_top", 18)
reel_margin.add_theme_constant_override("margin_bottom", 18)
reel_screen.add_child(reel_margin)
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", 18)
reel_margin.add_child(reels_grid)
reels_grid.add_theme_constant_override("separation", 16)
center.add_child(reels_grid)
reel_columns.clear()
for _column_index in range(3):
@@ -146,19 +143,31 @@ func _build_machine(parent: Control) -> void:
for _row_index in range(3):
var icon := TextureRect.new()
icon.texture = REEL_SYMBOLS[randi() % REEL_SYMBOLS.size()]
icon.custom_minimum_size = Vector2(58, 58)
icon.custom_minimum_size = Vector2(66, 66)
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)
reel_row.add_child(_build_light_strip(7))
reel_row.add_child(_build_lever())
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)
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))
column.add_child(win_up_to_card)
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)
@@ -174,7 +183,7 @@ func _build_machine(parent: Control) -> void:
var controls_row := HBoxContainer.new()
controls_row.add_theme_constant_override("separation", 12)
column.add_child(controls_row)
parent.add_child(controls_row)
cost_card = PanelContainer.new()
cost_card.add_theme_stylebox_override("panel", Brand.content_card_box(dark, false))
@@ -240,135 +249,7 @@ func _build_machine(parent: Control) -> void:
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
column.add_child(info_label)
func _build_marquee(column: VBoxContainer) -> void:
var row := HBoxContainer.new()
row.alignment = BoxContainer.ALIGNMENT_CENTER
row.add_theme_constant_override("separation", 14)
column.add_child(row)
row.add_child(_marquee_star())
row.add_child(_marquee_glow_label())
row.add_child(_marquee_star())
func _marquee_star() -> TextureRect:
var star := TextureRect.new()
star.texture = STAR_ICON
star.modulate = Brand.SKY_BLUE
star.custom_minimum_size = Vector2(16, 16)
star.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
star.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
return star
func _marquee_glow_label() -> Control:
var wrap := Control.new()
wrap.custom_minimum_size = Vector2(150, 70)
var halo_specs := [
{"size": 74, "alpha": 0.22, "offset": Vector2(-4, -4)},
{"size": 74, "alpha": 0.22, "offset": Vector2(4, 4)},
{"size": 64, "alpha": 0.4, "offset": Vector2.ZERO},
]
for spec in halo_specs:
var glow := Label.new()
glow.text = "RZ"
glow.add_theme_font_override("font", Brand.font)
glow.add_theme_font_size_override("font_size", spec["size"])
glow.add_theme_color_override("font_color", Color(Brand.SKY_BLUE, spec["alpha"]))
glow.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
glow.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
glow.set_anchors_preset(Control.PRESET_FULL_RECT)
glow.position += spec["offset"]
wrap.add_child(glow)
var mark := Label.new()
mark.text = "RZ"
mark.add_theme_font_override("font", Brand.font)
mark.add_theme_font_size_override("font_size", 46)
mark.add_theme_color_override("font_color", Brand.SKY_BLUE)
mark.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
mark.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
mark.set_anchors_preset(Control.PRESET_FULL_RECT)
wrap.add_child(mark)
return wrap
func _build_light_strip(count: int) -> VBoxContainer:
var strip := VBoxContainer.new()
strip.add_theme_constant_override("separation", 14)
strip.alignment = BoxContainer.ALIGNMENT_CENTER
for index in range(count):
var bulb := PanelContainer.new()
bulb.custom_minimum_size = Vector2(11, 11)
var bulb_box := StyleBoxFlat.new()
bulb_box.bg_color = Color("fef3c7")
bulb_box.set_corner_radius_all(6)
bulb_box.corner_detail = 12
bulb_box.anti_aliasing = false
bulb.add_theme_stylebox_override("panel", bulb_box)
strip.add_child(bulb)
_animate_bulb(bulb, index)
return strip
func _animate_bulb(bulb: Control, index: int) -> void:
bulb.modulate.a = 0.3
var tween := create_tween()
tween.set_loops()
tween.tween_interval(index * 0.12)
tween.tween_property(bulb, "modulate:a", 1.0, 0.4).set_trans(Tween.TRANS_SINE)
tween.tween_property(bulb, "modulate:a", 0.3, 0.4).set_trans(Tween.TRANS_SINE)
tween.tween_interval(1.4)
func _build_lever() -> Control:
var wrap := VBoxContainer.new()
wrap.alignment = BoxContainer.ALIGNMENT_BEGIN
wrap.size_flags_vertical = Control.SIZE_EXPAND_FILL
wrap.add_theme_constant_override("separation", 0)
var knob := PanelContainer.new()
knob.custom_minimum_size = Vector2(30, 30)
knob.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
var knob_box := StyleBoxFlat.new()
knob_box.bg_color = Brand.ROYAL_BLUE
knob_box.set_corner_radius_all(15)
knob_box.corner_detail = 20
knob_box.anti_aliasing = false
knob.add_theme_stylebox_override("panel", knob_box)
wrap.add_child(knob)
var rod := ColorRect.new()
rod.color = Color("64748b")
rod.custom_minimum_size = Vector2(6, 140)
rod.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
wrap.add_child(rod)
return wrap
func _cabinet_box() -> StyleBoxFlat:
var box := StyleBoxFlat.new()
box.bg_color = Color("0b1220")
box.border_color = Brand.SKY_BLUE
box.set_border_width_all(4)
box.corner_radius_top_left = 90
box.corner_radius_top_right = 90
box.corner_radius_bottom_left = 20
box.corner_radius_bottom_right = 20
box.shadow_color = Color(Brand.SKY_BLUE, 0.45)
box.shadow_size = 18
return box
func _reel_screen_box() -> StyleBoxFlat:
var box := StyleBoxFlat.new()
box.bg_color = Color("e2e8f0")
box.border_color = Color("94a3b8")
box.set_border_width_all(2)
box.set_corner_radius_all(10)
return box
parent.add_child(info_label)
func _build_rewards_panel(parent: Control) -> void:
@@ -636,7 +517,7 @@ func _play_coin_burst(coins_won: int) -> void:
var credits_badge: Control = main.get("credits_badge")
if credits_badge == null or not is_instance_valid(credits_badge):
return
var origin := machine_panel.get_global_rect().get_center()
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