From 5a08db38de7c70b21fc1507246fc1e8ecbd8f78e Mon Sep 17 00:00:00 2001 From: adtpdn Date: Fri, 24 Apr 2026 02:50:16 +0800 Subject: [PATCH] feat: fix gatcha, and login flow connection --- CHANGELOG_DRAFT.md | 9 +- .../skins/clothing/oldpop_cloth_white.png | Bin 6353 -> 0 bytes .../clothing/oldpop_cloth_white.png.import | 40 --- .../clothing/oldpop_cloth_white_pant.png | Bin 5594 -> 0 bytes .../oldpop_cloth_white_pant.png.import | 42 --- .../clothing/oldpop_mat_cloth_white.tres | 2 +- .../skins/presets/oldpop_preset.json | 73 ++++- .../skins/tex/oldpop_cloth_white.png | Bin 5708 -> 6865 bytes assets/data/skin_catalog_data.json | 2 +- scenes/lobby.gd | 32 ++ scenes/ui/gacha_panel.tscn | 3 +- scripts/managers/auth_manager.gd | 21 +- scripts/managers/user_profile_manager.gd | 10 +- scripts/ui/boot_screen.gd | 4 +- scripts/ui/gacha_panel.gd | 307 ++++++++++++++++-- 15 files changed, 400 insertions(+), 145 deletions(-) delete mode 100644 assets/characters/skins/clothing/oldpop_cloth_white.png delete mode 100644 assets/characters/skins/clothing/oldpop_cloth_white.png.import delete mode 100644 assets/characters/skins/clothing/oldpop_cloth_white_pant.png delete mode 100644 assets/characters/skins/clothing/oldpop_cloth_white_pant.png.import diff --git a/CHANGELOG_DRAFT.md b/CHANGELOG_DRAFT.md index be3d969..a05443c 100644 --- a/CHANGELOG_DRAFT.md +++ b/CHANGELOG_DRAFT.md @@ -1,6 +1,7 @@ ## [NEXT] -## [2.1.6] — 2026-04-24 -- Added Gacha system with Star and Gold banners (1x & 10x pulls) -- Fragment Craft system — collect drops to craft exclusive skins -- Fixed boot screen stuck on "Checking versions..." +## [2.1.7] — 2026-04-24 +- Upgraded Gacha interface with dynamic CSGO-style sequential reveal animations +- Added an animation skip button for faster 10x multi-pulls +- Fixed a bug where opening the Craft menu from Gacha would show an empty screen +- Stabilized login flow by resetting connection states after failed auto-logins diff --git a/assets/characters/skins/clothing/oldpop_cloth_white.png b/assets/characters/skins/clothing/oldpop_cloth_white.png deleted file mode 100644 index 7839dcc0775628d2e7361cbd021792fcf6694a2a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6353 zcmeHLdr*^C7C)g;gH}LcDzZFu>k3w0ZdrK<1t~10K&%L2G(gZr1zH3{iV_KtPQ^+U zr?`khi7&uH2?A2&C2?3l3k7Wk;)4K!h%rE9!pnrfo}1d8-9PsaXJ=>f`{RD!J-PQh ze)pV{JKr~-(fyV$HCqY*%l$v`2?n6zD-~#q@sVb+@hf~#*uj3@AnY`k0hng``)mz4 zl0VwJEzVeN**5WPr48k>uMh1m|K>nzdiuuVlTYrS_P$WO;_e4|<8E6nk1J^=KToVq z8mZjq9DL`r&B@P}2Cr!8YiS%#>2mEBH)TIlrhFkbh+OZZOclSb-i`Kj&@rH6K*xZN z0UZN626PPQ7nI&#Uj{Vgh71^!TJ+g^rH?ourxg4{K}+f9Zhjx^NN)Fc_=+M_C^16aTt>GCGS*MicXxi~IidWn{$tBE!>YWTE{8H&E?G6 z!-Znmtg|V)J=AV3+HfwK(N~|W=$n#D%E^iIO1opFwd@f(K7Z-z>Z-wARmOTCS}+Ef zzlJ1BF)^|iN7Xyq4;zALawYI>+ANABvDLemA%3AFGpqan_NO;b0t`r~ zz#4Q7G(_Jz&+osq-9Y>f@Y~non%1Zk=Y4b&Q5zrPBFjkwR@)D@e}&d$zu8We1y0@9~BAd4gj4-X%kN@*+_ z!2{?#4{*m8*PQ(r_+FfFNoZ(j03xUh4})ny0dI3VO(tUO%X;agJD%Y1N?@J9Ro$HM zJu1{df&GCvJLqI)=iuOw>}DZI1Y%7e9F2)$kz$t#>(jqSGYm4Cqni2^$@S8Bu@X-@ zI|%q2_R5eFdI0eC!?5ITBYr~MxN+mY)>sl6)3dPClp-1<$|YLiq%pA4zik_;D6|5& zMbiVPABFO|l9JfO#Ka0|d`(^f>dXy7x#(BqmbsY%_Wu9rq6e}c%bv3{jNVA<8eG!BPTjrseV@H>47{C?ty`%jZt zuKC|$3wh%um+c=I3536FrsFV^3<-h_=T0 z!IsQbZH?1Un==f%1fFTU9@h)*7~Rze2X8hsH1x`ns2?B`?88yZb{30eb1$8yN!@9> z6`sdtUp+04NShvgR5cW#p4X_8C!$hzk&LtN3$))e*!*l`du8v#hYxo#7>o+yEz>aC`6oO$h!AF>;bF&je3?T*1 zegZIi+9C4xvDYfR6j#m7c-3SABEPt@zl`K>SzX;zr`+e>YPzBD zCi~+Jh0pSpbo^GI??r(_^b0OCeO%hw(OxW;d|!%)6TZF^UyHd(6xQIo*e`d?T?3oK mbEf;|rd$89n$*<)Ee2=~i|OU&V>Jh2NDThIbRVHNEB&9CpxQnF diff --git a/assets/characters/skins/clothing/oldpop_cloth_white.png.import b/assets/characters/skins/clothing/oldpop_cloth_white.png.import deleted file mode 100644 index d1a7553..0000000 --- a/assets/characters/skins/clothing/oldpop_cloth_white.png.import +++ /dev/null @@ -1,40 +0,0 @@ -[remap] - -importer="texture" -type="CompressedTexture2D" -uid="uid://csfwnrjb5a2v4" -path="res://.godot/imported/oldpop_cloth_white.png-1c0d4075c414911b05de4cc77cfe74e0.ctex" -metadata={ -"vram_texture": false -} - -[deps] - -source_file="res://assets/characters/skins/clothing/oldpop_cloth_white.png" -dest_files=["res://.godot/imported/oldpop_cloth_white.png-1c0d4075c414911b05de4cc77cfe74e0.ctex"] - -[params] - -compress/mode=0 -compress/high_quality=false -compress/lossy_quality=0.7 -compress/uastc_level=0 -compress/rdo_quality_loss=0.0 -compress/hdr_compression=1 -compress/normal_map=0 -compress/channel_pack=0 -mipmaps/generate=false -mipmaps/limit=-1 -roughness/mode=0 -roughness/src_normal="" -process/channel_remap/red=0 -process/channel_remap/green=1 -process/channel_remap/blue=2 -process/channel_remap/alpha=3 -process/fix_alpha_border=true -process/premult_alpha=false -process/normal_map_invert_y=false -process/hdr_as_srgb=false -process/hdr_clamp_exposure=false -process/size_limit=0 -detect_3d/compress_to=1 diff --git a/assets/characters/skins/clothing/oldpop_cloth_white_pant.png b/assets/characters/skins/clothing/oldpop_cloth_white_pant.png deleted file mode 100644 index a2650bb16eec1181cfe06db73e47d972aa013def..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5594 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 W|L8(7?K{54uDk1owFPT}j{X(Z;N2Af literal 5708 zcmeHK>r+!l6hBE&9z_CewP~X~>L|3(@~}h{h=32oN9;HZMk{cOkB*uVMpID;$rY!9 z1Ijo~@ex`?rllPa!Z1ZqNumxQh(u*-U=Tq;5TkP zW55`5N>aOJ8@oB^@DrZtl zr-mF~bhAoL=7trILf(d3sN%(DU*aR1>!R5iKHF|7>SRJ5dTGZS!FpNJhl2krh+&8JH{mSCqvu2r+0)uiLKWeUt2 zJC3_cddE-t4~62jm%f@k!Hzd$AgQ2o3alSMn~?lu8VoSA(ekdS2Rg4FD&9~Q7pm8% zSx27VJn0)R)$8{>Zuq&qJ(e*EKvz*Q7hrSdm`Z9;PmB+DS%Q;a+^_o4a_@YXr5a7V zaQFQnyNRbvAWuV;8N zc5cY3j1L1sTeNlg(zKL8)Pw6wTcDqN00a}5r+*i3NbY}7ZE?{c(=v@?HBcg4Sc+1q z9D3e%ArWJ#Bq3G8V6D}vmW)$fchZ`M1rU~nUmPn&Q5k$td{Jn{4LPB3pt3~mD5W=_ z@Su{ulZFDp)r+RW+e8Apxd}?FD=9|eS%c zkV}!UUko4eu04^W0>uc#Of}nDZX4~{irQ9zyNQI)hJkxM+o-M0aAffUc=Y)n2nY?v zT(;`Y{A}LVUF@OKnGepkS?+)M#H?O8Pm void: shop.closed.connect(func(): if main_menu_panel: main_menu_panel.show()) shop.show_panel() +func _on_banner1_pressed() -> void: + var gacha_scene = load("res://scenes/ui/gacha_panel.tscn") + if not gacha_scene: + connection_status.text = "Gacha panel not found" + return + var gacha = gacha_scene.instantiate() + gacha.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + add_child(gacha) + if main_menu_panel: main_menu_panel.hide() + # GachaPanel emits "closed" from its BackBtn handler + if gacha.has_signal("closed"): + gacha.closed.connect(func(): + gacha.queue_free() + if main_menu_panel: main_menu_panel.show() + ) + else: + # Fallback: listen for BackBtn directly + var back = gacha.get_node_or_null("%BackBtn") + if back: + back.pressed.connect(func(): + gacha.queue_free() + if main_menu_panel: main_menu_panel.show() + ) + + func _on_leaderboard_pressed() -> void: if not leaderboard_panel_instance: var leaderboard_panel_scene := load("res://scenes/ui/leaderboard_panel.tscn") diff --git a/scenes/ui/gacha_panel.tscn b/scenes/ui/gacha_panel.tscn index 9ed3ea8..f43fda7 100644 --- a/scenes/ui/gacha_panel.tscn +++ b/scenes/ui/gacha_panel.tscn @@ -125,7 +125,7 @@ alignment = 1 [node name="BalanceLbl" type="Label" parent="MainMargin/MainVBox/ContentHBox/LeftPanel/LeftMargin/LeftVBox/BalanceRow"] layout_mode = 2 -theme_override_colors/font_color = Color(0.7, 0.7, 0.7) +theme_override_colors/font_color = Color(0.7, 0.7, 0.7, 1) theme_override_font_sizes/font_size = 13 text = "Balance:" @@ -214,6 +214,7 @@ unique_name_in_owner = true layout_mode = 2 size_flags_horizontal = 3 size_flags_vertical = 3 +theme_override_colors/default_color = Color(0.35, 0.2, 0.1, 1) theme_override_font_sizes/font_size = 13 text = "" fit_content = true diff --git a/scripts/managers/auth_manager.gd b/scripts/managers/auth_manager.gd index 61ac121..7f4cd3a 100644 --- a/scripts/managers/auth_manager.gd +++ b/scripts/managers/auth_manager.gd @@ -73,7 +73,8 @@ func _try_restore_session() -> void: session = refreshed _save_session(session, saved_auth_mode) else: - print("[AuthManager] Session refresh failed, need to re-login") + var err_msg: String = refreshed.get_exception().message + print("[AuthManager] Session refresh failed (%s) — need to re-login" % err_msg) return else: print("[AuthManager] Session expired, need to re-login") @@ -85,12 +86,18 @@ func _try_restore_session() -> void: is_guest = auth_mode == AuthMode.GUEST var socket_success := await _connect_socket() - if socket_success: - await _load_user_profile() - is_authenticated = true - emit_signal("session_restored") - emit_signal("auth_completed", true, current_user) - print("[AuthManager] Session restored successfully") + if not socket_success: + # Clean up so manual login starts from a blank slate + push_warning("[AuthManager] Session restore failed — socket could not connect. Resetting state.") + NakamaManager.session = null + NakamaManager.socket = null + return + + await _load_user_profile() + is_authenticated = true + emit_signal("session_restored") + emit_signal("auth_completed", true, current_user) + print("[AuthManager] Session restored successfully") func _save_session(session: NakamaSession, mode: AuthMode) -> void: var file := FileAccess.open_encrypted_with_pass(SESSION_FILE, FileAccess.WRITE, ENCRYPTION_KEY) diff --git a/scripts/managers/user_profile_manager.gd b/scripts/managers/user_profile_manager.gd index efb0803..32e732d 100644 --- a/scripts/managers/user_profile_manager.gd +++ b/scripts/managers/user_profile_manager.gd @@ -405,11 +405,17 @@ func _reload_wallet() -> void: func save_wallet() -> void: """Persist wallet deductions and fragment counts to Nakama storage.""" - if not NakamaManager.session: return + if not NakamaManager.session: + print("[UserProfileManager] save_wallet: no session, saved in-memory only.") + return var write_objs: Array = [ NakamaWriteStorageObject.new(PROFILE_COLLECTION, "fragments", 1, 1, JSON.stringify(fragments), "") ] - await NakamaManager.client.write_storage_objects_async(NakamaManager.session, write_objs) + var result = await NakamaManager.client.write_storage_objects_async(NakamaManager.session, write_objs) + if result.is_exception(): + push_warning("[UserProfileManager] save_wallet failed: " + result.get_exception().message) + else: + print("[UserProfileManager] Fragments saved.") # ============================================================================= # Stats Management diff --git a/scripts/ui/boot_screen.gd b/scripts/ui/boot_screen.gd index 007f1cb..476996a 100644 --- a/scripts/ui/boot_screen.gd +++ b/scripts/ui/boot_screen.gd @@ -46,9 +46,9 @@ func _ready() -> void: status_label.text = "Checking versions..." - # Instant bypass in editor — go straight to game + # In the editor always skip update check — login screen handles session restore if OS.has_feature("editor"): - print("[BootScreen] Editor detected — bypassing update check.") + print("[BootScreen] Editor mode — bypassing update check.") _begin_resource_load() return diff --git a/scripts/ui/gacha_panel.gd b/scripts/ui/gacha_panel.gd index e72b4ad..bb474de 100644 --- a/scripts/ui/gacha_panel.gd +++ b/scripts/ui/gacha_panel.gd @@ -1,7 +1,7 @@ extends Control ## GachaPanel — Two-banner gacha interface. ## Banners: Star (✦) and Gold (▤) -## Pull results shown in animated card reveal. +## CSGO-style spinning case reveal animation. signal closed @@ -16,20 +16,34 @@ signal closed @onready var pull_10_btn := %Pull10Btn as Button @onready var cost_1_label := %Cost1Label as Label @onready var cost_10_label := %Cost10Label as Label -@onready var rates_label := %RatesLabel as Label +@onready var rates_label := %RatesLabel as RichTextLabel @onready var result_panel := %ResultPanel as PanelContainer @onready var result_grid := %ResultGrid as GridContainer @onready var close_result_btn := %CloseResultBtn as Button @onready var craft_btn := %CraftBtn as Button @onready var status_label := %StatusLabel as Label +# ─── CSGO Roll nodes (built at runtime) ────────────────────────────────────── +var _roll_overlay: Control = null # dims the screen during roll +var _roll_container: Control = null # clip rect +var _roll_strip: HBoxContainer = null # scrolling items +var _roll_arrow: Control = null # center arrow indicator +var _roll_tween: Tween = null +var _roll_lbl: Label = null +var _skip_roll: bool = false + +const CARD_W := 130 +const CARD_H := 160 +const CARD_GAP := 8 +const STRIP_VISIBLE_COUNT := 7 # how many cards show at once + # ─── State ─────────────────────────────────────────────────────────────────── var _current_banner: String = "star" var _pulling: bool = false const RARITY_COLORS := { - "common": Color(0.80, 0.80, 0.80), - "uncommon": Color(0.30, 0.85, 0.35), + "common": Color(0.75, 0.75, 0.75), + "uncommon": Color(0.20, 0.85, 0.35), "rare": Color(0.20, 0.55, 1.00), "real_prize": Color(1.00, 0.75, 0.10) } @@ -39,6 +53,12 @@ const RARITY_LABELS := { "rare": "Rare", "real_prize": "✨ REAL PRIZE ✨" } +const RARITY_ICONS := { + "common": "⬜", + "uncommon": "🟩", + "rare": "🟦", + "real_prize": "✨" +} # ─── Lifecycle ──────────────────────────────────────────────────────────────── func _ready() -> void: @@ -50,12 +70,21 @@ func _ready() -> void: close_result_btn.pressed.connect(func(): result_panel.hide()) craft_btn.pressed.connect(_on_open_craft) result_panel.hide() + _ensure_dummy_wallet() _switch_banner("star") func show_panel() -> void: show() _refresh_ui() +# ─── Dummy wallet so editor testing works without Nakama ───────────────────── +func _ensure_dummy_wallet() -> void: + var w: Dictionary = UserProfileManager.wallet + if w.get("star", 0) == 0: + UserProfileManager.wallet["star"] = 3200 + if w.get("gold", 0) == 0: + UserProfileManager.wallet["gold"] = 1500 + # ─── Banner switching ───────────────────────────────────────────────────────── func _switch_banner(id: String) -> void: _current_banner = id @@ -91,6 +120,7 @@ func _refresh_ui() -> void: var rd = gd.get("real_prize_catalog", {}).get(rid, {}) real_names.append(rd.get("name", rid)) + if not rates_label: return rates_label.text = ( "Common %.0f%% Uncommon %.0f%% Rare %.0f%% ✨Real Prize %.1f%%\n" + "Guaranteed Real Prize every %d pulls (current pity: %d)\n\n" + @@ -107,25 +137,230 @@ func _refresh_ui() -> void: func _do_pull(count: int) -> void: if _pulling: return _pulling = true + pull_1_btn.disabled = true + pull_10_btn.disabled = true status_label.text = "Rolling..." - var results: Array = await _run_pull(count) - _pulling = false + + await get_tree().process_frame + var results: Array = GachaManager.pull(_current_banner, count) + if results.is_empty(): status_label.text = "❌ Not enough currency!" + _pulling = false _refresh_ui() return + status_label.text = "" + # Show CSGO roll for first result, then list the rest + await _play_case_roll(results) + _pulling = false _refresh_ui() + +# ─── CSGO Case Roll ─────────────────────────────────────────────────────────── +func _play_case_roll(results: Array) -> void: + _skip_roll = false + _build_roll_ui() + await get_tree().process_frame + + for i in range(results.size()): + if _skip_roll: + break + + var winner: Dictionary = results[i] + + if _roll_lbl: + if results.size() > 1: + _roll_lbl.text = "🎰 Opening %d / %d..." % [i + 1, results.size()] + else: + _roll_lbl.text = "🎰 Opening..." + + # Filler pool: weighted toward common so rare winner pops + var filler_pool: Array = [] + var gd = GachaManager.data + for rarity in ["common", "common", "common", "uncommon", "uncommon", "rare"]: + var pool: Array = gd.get("pools", {}).get(rarity, []) + if pool.is_empty(): continue + var fid: String = pool[randi() % pool.size()] + var fname: String = gd.get("fragments", {}).get(fid, {}).get("name", fid) + filler_pool.append({"id": fid, "rarity": rarity, "name": fname}) + + # Build full strip items + const FILLER_BEFORE := 30 + const FILLER_AFTER := 5 + var strip_items: Array = [] + for j in range(FILLER_BEFORE): + strip_items.append(filler_pool[j % filler_pool.size()]) + strip_items.append(winner) + for j in range(FILLER_AFTER): + strip_items.append(filler_pool[j % filler_pool.size()]) + + # Populate strip + for child in _roll_strip.get_children(): child.queue_free() + for item in strip_items: + _roll_strip.add_child(_make_roll_card(item)) + await get_tree().process_frame + + # Calculate scroll target: winner is at index FILLER_BEFORE + var step := CARD_W + CARD_GAP + var strip_w := float(_roll_strip.get_children().size() * step) + var container_w := float(STRIP_VISIBLE_COUNT * step - CARD_GAP) + var center_offset := container_w / 2.0 - CARD_W / 2.0 + var target_x := -(FILLER_BEFORE * step - center_offset) + # Add small random offset so it doesn't always stop dead center + target_x += randf_range(-20, 20) + + # Start far left (fast) + _roll_strip.position.x = 0.0 + _roll_overlay.visible = true + + # Animate: fast then ease-out + _roll_tween = create_tween() + _roll_tween.set_ease(Tween.EASE_OUT) + _roll_tween.set_trans(Tween.TRANS_QUINT) + # Faster spin for multiples to save time + var duration = 3.5 if results.size() == 1 else 2.2 + _roll_tween.tween_property(_roll_strip, "position:x", target_x, duration) + await _roll_tween.finished + + if _skip_roll: + break + + # Flash winner card + var winner_card = _roll_strip.get_children()[FILLER_BEFORE] + var winner_col = RARITY_COLORS.get(winner.get("rarity", "common"), Color.WHITE) + var flash_tween := create_tween().set_loops(3) + flash_tween.tween_property(winner_card, "modulate", winner_col * 2.0, 0.12) + flash_tween.tween_property(winner_card, "modulate", Color.WHITE, 0.12) + await flash_tween.finished + + if not _skip_roll and i < results.size() - 1: + await get_tree().create_timer(0.4).timeout + + # Tear down roll, show full results + _roll_overlay.visible = false _show_results(results) -func _run_pull(count: int) -> Array: - # Yield one frame so UI updates first - await get_tree().process_frame - return GachaManager.pull(_current_banner, count) +func _build_roll_ui() -> void: + if _roll_overlay: + _roll_overlay.queue_free() -# ─── Result display ─────────────────────────────────────────────────────────── + var step := CARD_W + CARD_GAP + var container_w := STRIP_VISIBLE_COUNT * step - CARD_GAP + + # Dim overlay + _roll_overlay = Control.new() + _roll_overlay.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + _roll_overlay.visible = false + add_child(_roll_overlay) + + var bg := ColorRect.new() + bg.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + bg.color = Color(0.02, 0.02, 0.06, 0.92) + _roll_overlay.add_child(bg) + + # Center container + _roll_container = Control.new() + _roll_container.custom_minimum_size = Vector2(container_w, CARD_H) + _roll_container.clip_contents = true + _roll_container.set_anchors_and_offsets_preset(Control.PRESET_CENTER) + _roll_container.offset_left = -container_w / 2.0 + _roll_container.offset_right = container_w / 2.0 + _roll_container.offset_top = -CARD_H / 2.0 + _roll_container.offset_bottom = CARD_H / 2.0 + _roll_overlay.add_child(_roll_container) + + # Strip + _roll_strip = HBoxContainer.new() + _roll_strip.add_theme_constant_override("separation", CARD_GAP) + _roll_strip.position = Vector2.ZERO + _roll_container.add_child(_roll_strip) + + # Center arrow indicator + var arrow_line := ColorRect.new() + arrow_line.color = Color(1, 0.9, 0.2, 0.9) + arrow_line.size = Vector2(3, CARD_H) + arrow_line.position = Vector2(container_w / 2.0 - 1, 0) + _roll_container.add_child(arrow_line) + + # Label above + _roll_lbl = Label.new() + _roll_lbl.text = "🎰 Opening..." + _roll_lbl.add_theme_font_size_override("font_size", 20) + _roll_lbl.add_theme_color_override("font_color", Color(1, 0.9, 0.3)) + _roll_lbl.set_anchors_and_offsets_preset(Control.PRESET_CENTER_TOP) + _roll_lbl.offset_top = -CARD_H / 2.0 - 44 + _roll_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + _roll_overlay.add_child(_roll_lbl) + + # Skip button + var skip_btn := Button.new() + skip_btn.text = "Skip Animation ⏭" + skip_btn.add_theme_font_size_override("font_size", 16) + skip_btn.set_anchors_and_offsets_preset(Control.PRESET_BOTTOM_RIGHT) + skip_btn.offset_left = -200 + skip_btn.offset_top = -80 + skip_btn.offset_right = -40 + skip_btn.offset_bottom = -40 + skip_btn.pressed.connect(func(): + _skip_roll = true + if _roll_tween and _roll_tween.is_running(): + _roll_tween.kill() + _roll_tween.finished.emit() # Ensure await yields correctly + ) + _roll_overlay.add_child(skip_btn) + +func _make_roll_card(item: Dictionary) -> PanelContainer: + var rarity: String = item.get("rarity", "common") + var col: Color = RARITY_COLORS.get(rarity, Color.WHITE) + + var panel := PanelContainer.new() + panel.custom_minimum_size = Vector2(CARD_W, CARD_H) + + # Rarity-colored border via StyleBoxFlat + var style := StyleBoxFlat.new() + style.bg_color = col.darkened(0.7) + style.border_color = col + style.set_border_width_all(2) + style.set_corner_radius_all(6) + panel.add_theme_stylebox_override("panel", style) + + var margin := MarginContainer.new() + margin.add_theme_constant_override("margin_left", 6) + margin.add_theme_constant_override("margin_top", 8) + margin.add_theme_constant_override("margin_right", 6) + margin.add_theme_constant_override("margin_bottom", 8) + panel.add_child(margin) + + var vbox := VBoxContainer.new() + vbox.alignment = BoxContainer.ALIGNMENT_CENTER + margin.add_child(vbox) + + var icon_lbl := Label.new() + icon_lbl.text = RARITY_ICONS.get(rarity, "❓") + icon_lbl.add_theme_font_size_override("font_size", 40) + icon_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + icon_lbl.add_theme_color_override("font_color", col) + vbox.add_child(icon_lbl) + + var name_lbl := Label.new() + name_lbl.text = item.get("name", "?") + name_lbl.add_theme_font_size_override("font_size", 11) + name_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + name_lbl.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + name_lbl.add_theme_color_override("font_color", Color.WHITE) + vbox.add_child(name_lbl) + + var rar_lbl := Label.new() + rar_lbl.text = RARITY_LABELS.get(rarity, rarity.capitalize()) + rar_lbl.add_theme_font_size_override("font_size", 9) + rar_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + rar_lbl.add_theme_color_override("font_color", col) + vbox.add_child(rar_lbl) + + return panel + +# ─── Result display (after roll finishes) ──────────────────────────────────── func _show_results(results: Array) -> void: - # Clear old cards for c in result_grid.get_children(): c.queue_free() await get_tree().process_frame @@ -133,8 +368,7 @@ func _show_results(results: Array) -> void: for res in results: var card := _make_result_card(res) result_grid.add_child(card) - # Staggered reveal - await get_tree().create_timer(0.08).timeout + await get_tree().create_timer(0.06).timeout func _make_result_card(res: Dictionary) -> PanelContainer: var rarity: String = res.get("rarity", "common") @@ -144,6 +378,13 @@ func _make_result_card(res: Dictionary) -> PanelContainer: var panel := PanelContainer.new() panel.custom_minimum_size = Vector2(110, 130) + var style := StyleBoxFlat.new() + style.bg_color = col.darkened(0.7) + style.border_color = col + style.set_border_width_all(2) + style.set_corner_radius_all(6) + panel.add_theme_stylebox_override("panel", style) + var margin := MarginContainer.new() margin.add_theme_constant_override("margin_left", 8) margin.add_theme_constant_override("margin_top", 8) @@ -155,32 +396,29 @@ func _make_result_card(res: Dictionary) -> PanelContainer: vbox.alignment = BoxContainer.ALIGNMENT_CENTER margin.add_child(vbox) - # Rarity icon var icon_lbl := Label.new() - icon_lbl.text = _rarity_icon(rarity) + icon_lbl.text = RARITY_ICONS.get(rarity, "❓") icon_lbl.add_theme_font_size_override("font_size", 36) icon_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER icon_lbl.add_theme_color_override("font_color", col) vbox.add_child(icon_lbl) - # Item name var name_lbl := Label.new() name_lbl.text = res.get("name", "?") name_lbl.add_theme_font_size_override("font_size", 11) name_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER name_lbl.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART - name_lbl.add_theme_color_override("font_color", col) + name_lbl.add_theme_color_override("font_color", Color.WHITE) vbox.add_child(name_lbl) - # Rarity label var rar_lbl := Label.new() rar_lbl.text = label_txt rar_lbl.add_theme_font_size_override("font_size", 9) rar_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER - rar_lbl.add_theme_color_override("font_color", col.lerp(Color.WHITE, 0.3)) + rar_lbl.add_theme_color_override("font_color", col) vbox.add_child(rar_lbl) - # Flash animation + # Fade-in panel.modulate.a = 0.0 var tween := create_tween() tween.tween_property(panel, "modulate:a", 1.0, 0.25) @@ -190,22 +428,21 @@ func _make_result_card(res: Dictionary) -> PanelContainer: return panel -func _rarity_icon(rarity: String) -> String: - match rarity: - "common": return "⬜" - "uncommon": return "🟩" - "rare": return "🟦" - "real_prize": return "✨" - return "❓" - # ─── Craft link ─────────────────────────────────────────────────────────────── func _on_open_craft() -> void: - hide() - # Find or load the fragment craft panel in the scene tree - var main = get_tree().current_scene - var fcp = main.get_node_or_null("FragmentCraftPanel") - if fcp: - fcp.show_panel() + # Load FragmentCraftPanel as overlay on top of GachaPanel + var fcp_scene = load("res://scenes/ui/fragment_craft_panel.tscn") + if not fcp_scene: + status_label.text = "Fragment Craft panel not found" + return + var fcp = fcp_scene.instantiate() + fcp.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + add_child(fcp) + fcp.closed.connect(func(): + fcp.queue_free() + _refresh_ui() + ) + fcp.show_panel() func _on_close() -> void: hide()