feat: fix gatcha, and login flow connection
This commit is contained in:
+5
-4
@@ -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
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 6.2 KiB |
@@ -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
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 5.5 KiB |
@@ -1,42 +0,0 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://d0egs6j3sg0me"
|
||||
path.s3tc="res://.godot/imported/oldpop_cloth_white_pant.png-a367f6f4f091bba7e63c3dcc90b20b36.s3tc.ctex"
|
||||
path.etc2="res://.godot/imported/oldpop_cloth_white_pant.png-a367f6f4f091bba7e63c3dcc90b20b36.etc2.ctex"
|
||||
metadata={
|
||||
"imported_formats": ["s3tc_bptc", "etc2_astc"],
|
||||
"vram_texture": true
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/characters/skins/clothing/oldpop_cloth_white_pant.png"
|
||||
dest_files=["res://.godot/imported/oldpop_cloth_white_pant.png-a367f6f4f091bba7e63c3dcc90b20b36.s3tc.ctex", "res://.godot/imported/oldpop_cloth_white_pant.png-a367f6f4f091bba7e63c3dcc90b20b36.etc2.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=2
|
||||
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=true
|
||||
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=0
|
||||
File diff suppressed because one or more lines are too long
@@ -11,20 +11,20 @@
|
||||
"img_scale": 1.0,
|
||||
"masks": [
|
||||
[
|
||||
"(0.016667, 0.663542)",
|
||||
"(0.147917, 0.665625)",
|
||||
"(0.266667, 0.621875)",
|
||||
"(0.370833, 0.615625)",
|
||||
"(0.439583, 0.590625)",
|
||||
"(0.441667, 0.948958)",
|
||||
"(0.014583, 0.948958)",
|
||||
"(0.016667, 0.665625)"
|
||||
"(0.0125, 0.665625)",
|
||||
"(0.139583, 0.671875)",
|
||||
"(0.270833, 0.621875)",
|
||||
"(0.360417, 0.615625)",
|
||||
"(0.435417, 0.588542)",
|
||||
"(0.435417, 0.942708)",
|
||||
"(0.010417, 0.946875)",
|
||||
"(0.0125, 0.665625)"
|
||||
]
|
||||
],
|
||||
"ref_path": ""
|
||||
},
|
||||
"oldpop-bottom1": {
|
||||
"category": -1,
|
||||
"category": 2,
|
||||
"fill_color": "ffffffff",
|
||||
"img_path": "",
|
||||
"img_pos": [
|
||||
@@ -33,7 +33,60 @@
|
||||
],
|
||||
"img_rot": 0.0,
|
||||
"img_scale": 1.0,
|
||||
"masks": [],
|
||||
"masks": [
|
||||
[
|
||||
"(0.445833, 0.728125)",
|
||||
"(0.44375, 0.992708)",
|
||||
"(0.995833, 0.996875)",
|
||||
"(0.997917, 0.728125)",
|
||||
"(0.445833, 0.730208)"
|
||||
]
|
||||
],
|
||||
"ref_path": ""
|
||||
},
|
||||
"oldpop-bottom2": {
|
||||
"category": 2,
|
||||
"fill_color": "ffffffff",
|
||||
"img_path": "",
|
||||
"img_pos": [
|
||||
0.5,
|
||||
0.5
|
||||
],
|
||||
"img_rot": 0.0,
|
||||
"img_scale": 1.0,
|
||||
"masks": [
|
||||
[
|
||||
"(0.447917, 0.728125)",
|
||||
"(1.0, 0.728125)",
|
||||
"(1.0, 1.0)",
|
||||
"(0.447917, 1.0)",
|
||||
"(0.447917, 0.728125)"
|
||||
]
|
||||
],
|
||||
"ref_path": ""
|
||||
},
|
||||
"oldpop-bottom3": {
|
||||
"category": -1,
|
||||
"fill_color": "ffffffff",
|
||||
"img_path": "",
|
||||
"img_pos": [
|
||||
0.5,
|
||||
0.5
|
||||
],
|
||||
"img_rot": 0.0,
|
||||
"img_scale": 1.0,
|
||||
"masks": [
|
||||
[
|
||||
"(0.0125, 0.665625)",
|
||||
"(0.139583, 0.671875)",
|
||||
"(0.270833, 0.621875)",
|
||||
"(0.360417, 0.615625)",
|
||||
"(0.435417, 0.588542)",
|
||||
"(0.435417, 0.942708)",
|
||||
"(0.010417, 0.946875)",
|
||||
"(0.0125, 0.665625)"
|
||||
]
|
||||
],
|
||||
"ref_path": ""
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 6.7 KiB |
@@ -233,4 +233,4 @@
|
||||
"star": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ extends Control
|
||||
@onready var leaderboard_btn = %LeaderboardBtn
|
||||
@onready var shop_btn = %CartBtn
|
||||
@onready var top_right_profile_btn = %ProfileBtn
|
||||
@onready var banner1_btn = %Banner1
|
||||
|
||||
# UI References - Room List
|
||||
@onready var room_list_panel = $RoomListPanel
|
||||
@@ -161,6 +162,12 @@ func _ready():
|
||||
# Shop Button
|
||||
if shop_btn:
|
||||
shop_btn.pressed.connect(_on_shop_pressed)
|
||||
|
||||
# Banner1 → Gacha
|
||||
if banner1_btn:
|
||||
banner1_btn.disabled = false
|
||||
banner1_btn.text = "✨ Gacha"
|
||||
banner1_btn.pressed.connect(_on_banner1_pressed)
|
||||
|
||||
if leaderboard_btn:
|
||||
leaderboard_btn.pressed.connect(_on_leaderboard_pressed)
|
||||
@@ -788,6 +795,31 @@ func _on_shop_pressed() -> 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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+272
-35
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user