feat: fix gatcha, and login flow connection
This commit is contained in:
+5
-4
@@ -1,6 +1,7 @@
|
|||||||
## [NEXT]
|
## [NEXT]
|
||||||
|
|
||||||
## [2.1.6] — 2026-04-24
|
## [2.1.7] — 2026-04-24
|
||||||
- Added Gacha system with Star and Gold banners (1x & 10x pulls)
|
- Upgraded Gacha interface with dynamic CSGO-style sequential reveal animations
|
||||||
- Fragment Craft system — collect drops to craft exclusive skins
|
- Added an animation skip button for faster 10x multi-pulls
|
||||||
- Fixed boot screen stuck on "Checking versions..."
|
- 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,
|
"img_scale": 1.0,
|
||||||
"masks": [
|
"masks": [
|
||||||
[
|
[
|
||||||
"(0.016667, 0.663542)",
|
"(0.0125, 0.665625)",
|
||||||
"(0.147917, 0.665625)",
|
"(0.139583, 0.671875)",
|
||||||
"(0.266667, 0.621875)",
|
"(0.270833, 0.621875)",
|
||||||
"(0.370833, 0.615625)",
|
"(0.360417, 0.615625)",
|
||||||
"(0.439583, 0.590625)",
|
"(0.435417, 0.588542)",
|
||||||
"(0.441667, 0.948958)",
|
"(0.435417, 0.942708)",
|
||||||
"(0.014583, 0.948958)",
|
"(0.010417, 0.946875)",
|
||||||
"(0.016667, 0.665625)"
|
"(0.0125, 0.665625)"
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"ref_path": ""
|
"ref_path": ""
|
||||||
},
|
},
|
||||||
"oldpop-bottom1": {
|
"oldpop-bottom1": {
|
||||||
"category": -1,
|
"category": 2,
|
||||||
"fill_color": "ffffffff",
|
"fill_color": "ffffffff",
|
||||||
"img_path": "",
|
"img_path": "",
|
||||||
"img_pos": [
|
"img_pos": [
|
||||||
@@ -33,7 +33,60 @@
|
|||||||
],
|
],
|
||||||
"img_rot": 0.0,
|
"img_rot": 0.0,
|
||||||
"img_scale": 1.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": ""
|
"ref_path": ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 6.7 KiB |
@@ -29,6 +29,7 @@ extends Control
|
|||||||
@onready var leaderboard_btn = %LeaderboardBtn
|
@onready var leaderboard_btn = %LeaderboardBtn
|
||||||
@onready var shop_btn = %CartBtn
|
@onready var shop_btn = %CartBtn
|
||||||
@onready var top_right_profile_btn = %ProfileBtn
|
@onready var top_right_profile_btn = %ProfileBtn
|
||||||
|
@onready var banner1_btn = %Banner1
|
||||||
|
|
||||||
# UI References - Room List
|
# UI References - Room List
|
||||||
@onready var room_list_panel = $RoomListPanel
|
@onready var room_list_panel = $RoomListPanel
|
||||||
@@ -162,6 +163,12 @@ func _ready():
|
|||||||
if shop_btn:
|
if shop_btn:
|
||||||
shop_btn.pressed.connect(_on_shop_pressed)
|
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:
|
if leaderboard_btn:
|
||||||
leaderboard_btn.pressed.connect(_on_leaderboard_pressed)
|
leaderboard_btn.pressed.connect(_on_leaderboard_pressed)
|
||||||
if quit_btn:
|
if quit_btn:
|
||||||
@@ -788,6 +795,31 @@ func _on_shop_pressed() -> void:
|
|||||||
shop.closed.connect(func(): if main_menu_panel: main_menu_panel.show())
|
shop.closed.connect(func(): if main_menu_panel: main_menu_panel.show())
|
||||||
shop.show_panel()
|
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:
|
func _on_leaderboard_pressed() -> void:
|
||||||
if not leaderboard_panel_instance:
|
if not leaderboard_panel_instance:
|
||||||
var leaderboard_panel_scene := load("res://scenes/ui/leaderboard_panel.tscn")
|
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"]
|
[node name="BalanceLbl" type="Label" parent="MainMargin/MainVBox/ContentHBox/LeftPanel/LeftMargin/LeftVBox/BalanceRow"]
|
||||||
layout_mode = 2
|
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
|
theme_override_font_sizes/font_size = 13
|
||||||
text = "Balance:"
|
text = "Balance:"
|
||||||
|
|
||||||
@@ -214,6 +214,7 @@ unique_name_in_owner = true
|
|||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
size_flags_horizontal = 3
|
size_flags_horizontal = 3
|
||||||
size_flags_vertical = 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
|
theme_override_font_sizes/font_size = 13
|
||||||
text = ""
|
text = ""
|
||||||
fit_content = true
|
fit_content = true
|
||||||
|
|||||||
@@ -73,7 +73,8 @@ func _try_restore_session() -> void:
|
|||||||
session = refreshed
|
session = refreshed
|
||||||
_save_session(session, saved_auth_mode)
|
_save_session(session, saved_auth_mode)
|
||||||
else:
|
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
|
return
|
||||||
else:
|
else:
|
||||||
print("[AuthManager] Session expired, need to re-login")
|
print("[AuthManager] Session expired, need to re-login")
|
||||||
@@ -85,12 +86,18 @@ func _try_restore_session() -> void:
|
|||||||
is_guest = auth_mode == AuthMode.GUEST
|
is_guest = auth_mode == AuthMode.GUEST
|
||||||
|
|
||||||
var socket_success := await _connect_socket()
|
var socket_success := await _connect_socket()
|
||||||
if socket_success:
|
if not socket_success:
|
||||||
await _load_user_profile()
|
# Clean up so manual login starts from a blank slate
|
||||||
is_authenticated = true
|
push_warning("[AuthManager] Session restore failed — socket could not connect. Resetting state.")
|
||||||
emit_signal("session_restored")
|
NakamaManager.session = null
|
||||||
emit_signal("auth_completed", true, current_user)
|
NakamaManager.socket = null
|
||||||
print("[AuthManager] Session restored successfully")
|
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:
|
func _save_session(session: NakamaSession, mode: AuthMode) -> void:
|
||||||
var file := FileAccess.open_encrypted_with_pass(SESSION_FILE, FileAccess.WRITE, ENCRYPTION_KEY)
|
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:
|
func save_wallet() -> void:
|
||||||
"""Persist wallet deductions and fragment counts to Nakama storage."""
|
"""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 = [
|
var write_objs: Array = [
|
||||||
NakamaWriteStorageObject.new(PROFILE_COLLECTION, "fragments", 1, 1, JSON.stringify(fragments), "")
|
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
|
# Stats Management
|
||||||
|
|||||||
@@ -46,9 +46,9 @@ func _ready() -> void:
|
|||||||
|
|
||||||
status_label.text = "Checking versions..."
|
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"):
|
if OS.has_feature("editor"):
|
||||||
print("[BootScreen] Editor detected — bypassing update check.")
|
print("[BootScreen] Editor mode — bypassing update check.")
|
||||||
_begin_resource_load()
|
_begin_resource_load()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
+272
-35
@@ -1,7 +1,7 @@
|
|||||||
extends Control
|
extends Control
|
||||||
## GachaPanel — Two-banner gacha interface.
|
## GachaPanel — Two-banner gacha interface.
|
||||||
## Banners: Star (✦) and Gold (▤)
|
## Banners: Star (✦) and Gold (▤)
|
||||||
## Pull results shown in animated card reveal.
|
## CSGO-style spinning case reveal animation.
|
||||||
|
|
||||||
signal closed
|
signal closed
|
||||||
|
|
||||||
@@ -16,20 +16,34 @@ signal closed
|
|||||||
@onready var pull_10_btn := %Pull10Btn as Button
|
@onready var pull_10_btn := %Pull10Btn as Button
|
||||||
@onready var cost_1_label := %Cost1Label as Label
|
@onready var cost_1_label := %Cost1Label as Label
|
||||||
@onready var cost_10_label := %Cost10Label 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_panel := %ResultPanel as PanelContainer
|
||||||
@onready var result_grid := %ResultGrid as GridContainer
|
@onready var result_grid := %ResultGrid as GridContainer
|
||||||
@onready var close_result_btn := %CloseResultBtn as Button
|
@onready var close_result_btn := %CloseResultBtn as Button
|
||||||
@onready var craft_btn := %CraftBtn as Button
|
@onready var craft_btn := %CraftBtn as Button
|
||||||
@onready var status_label := %StatusLabel as Label
|
@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 ───────────────────────────────────────────────────────────────────
|
# ─── State ───────────────────────────────────────────────────────────────────
|
||||||
var _current_banner: String = "star"
|
var _current_banner: String = "star"
|
||||||
var _pulling: bool = false
|
var _pulling: bool = false
|
||||||
|
|
||||||
const RARITY_COLORS := {
|
const RARITY_COLORS := {
|
||||||
"common": Color(0.80, 0.80, 0.80),
|
"common": Color(0.75, 0.75, 0.75),
|
||||||
"uncommon": Color(0.30, 0.85, 0.35),
|
"uncommon": Color(0.20, 0.85, 0.35),
|
||||||
"rare": Color(0.20, 0.55, 1.00),
|
"rare": Color(0.20, 0.55, 1.00),
|
||||||
"real_prize": Color(1.00, 0.75, 0.10)
|
"real_prize": Color(1.00, 0.75, 0.10)
|
||||||
}
|
}
|
||||||
@@ -39,6 +53,12 @@ const RARITY_LABELS := {
|
|||||||
"rare": "Rare",
|
"rare": "Rare",
|
||||||
"real_prize": "✨ REAL PRIZE ✨"
|
"real_prize": "✨ REAL PRIZE ✨"
|
||||||
}
|
}
|
||||||
|
const RARITY_ICONS := {
|
||||||
|
"common": "⬜",
|
||||||
|
"uncommon": "🟩",
|
||||||
|
"rare": "🟦",
|
||||||
|
"real_prize": "✨"
|
||||||
|
}
|
||||||
|
|
||||||
# ─── Lifecycle ────────────────────────────────────────────────────────────────
|
# ─── Lifecycle ────────────────────────────────────────────────────────────────
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
@@ -50,12 +70,21 @@ func _ready() -> void:
|
|||||||
close_result_btn.pressed.connect(func(): result_panel.hide())
|
close_result_btn.pressed.connect(func(): result_panel.hide())
|
||||||
craft_btn.pressed.connect(_on_open_craft)
|
craft_btn.pressed.connect(_on_open_craft)
|
||||||
result_panel.hide()
|
result_panel.hide()
|
||||||
|
_ensure_dummy_wallet()
|
||||||
_switch_banner("star")
|
_switch_banner("star")
|
||||||
|
|
||||||
func show_panel() -> void:
|
func show_panel() -> void:
|
||||||
show()
|
show()
|
||||||
_refresh_ui()
|
_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 ─────────────────────────────────────────────────────────
|
# ─── Banner switching ─────────────────────────────────────────────────────────
|
||||||
func _switch_banner(id: String) -> void:
|
func _switch_banner(id: String) -> void:
|
||||||
_current_banner = id
|
_current_banner = id
|
||||||
@@ -91,6 +120,7 @@ func _refresh_ui() -> void:
|
|||||||
var rd = gd.get("real_prize_catalog", {}).get(rid, {})
|
var rd = gd.get("real_prize_catalog", {}).get(rid, {})
|
||||||
real_names.append(rd.get("name", rid))
|
real_names.append(rd.get("name", rid))
|
||||||
|
|
||||||
|
if not rates_label: return
|
||||||
rates_label.text = (
|
rates_label.text = (
|
||||||
"Common %.0f%% Uncommon %.0f%% Rare %.0f%% ✨Real Prize %.1f%%\n" +
|
"Common %.0f%% Uncommon %.0f%% Rare %.0f%% ✨Real Prize %.1f%%\n" +
|
||||||
"Guaranteed Real Prize every %d pulls (current pity: %d)\n\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:
|
func _do_pull(count: int) -> void:
|
||||||
if _pulling: return
|
if _pulling: return
|
||||||
_pulling = true
|
_pulling = true
|
||||||
|
pull_1_btn.disabled = true
|
||||||
|
pull_10_btn.disabled = true
|
||||||
status_label.text = "Rolling..."
|
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():
|
if results.is_empty():
|
||||||
status_label.text = "❌ Not enough currency!"
|
status_label.text = "❌ Not enough currency!"
|
||||||
|
_pulling = false
|
||||||
_refresh_ui()
|
_refresh_ui()
|
||||||
return
|
return
|
||||||
|
|
||||||
status_label.text = ""
|
status_label.text = ""
|
||||||
|
# Show CSGO roll for first result, then list the rest
|
||||||
|
await _play_case_roll(results)
|
||||||
|
_pulling = false
|
||||||
_refresh_ui()
|
_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)
|
_show_results(results)
|
||||||
|
|
||||||
func _run_pull(count: int) -> Array:
|
func _build_roll_ui() -> void:
|
||||||
# Yield one frame so UI updates first
|
if _roll_overlay:
|
||||||
await get_tree().process_frame
|
_roll_overlay.queue_free()
|
||||||
return GachaManager.pull(_current_banner, count)
|
|
||||||
|
|
||||||
# ─── 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:
|
func _show_results(results: Array) -> void:
|
||||||
# Clear old cards
|
|
||||||
for c in result_grid.get_children(): c.queue_free()
|
for c in result_grid.get_children(): c.queue_free()
|
||||||
await get_tree().process_frame
|
await get_tree().process_frame
|
||||||
|
|
||||||
@@ -133,8 +368,7 @@ func _show_results(results: Array) -> void:
|
|||||||
for res in results:
|
for res in results:
|
||||||
var card := _make_result_card(res)
|
var card := _make_result_card(res)
|
||||||
result_grid.add_child(card)
|
result_grid.add_child(card)
|
||||||
# Staggered reveal
|
await get_tree().create_timer(0.06).timeout
|
||||||
await get_tree().create_timer(0.08).timeout
|
|
||||||
|
|
||||||
func _make_result_card(res: Dictionary) -> PanelContainer:
|
func _make_result_card(res: Dictionary) -> PanelContainer:
|
||||||
var rarity: String = res.get("rarity", "common")
|
var rarity: String = res.get("rarity", "common")
|
||||||
@@ -144,6 +378,13 @@ func _make_result_card(res: Dictionary) -> PanelContainer:
|
|||||||
var panel := PanelContainer.new()
|
var panel := PanelContainer.new()
|
||||||
panel.custom_minimum_size = Vector2(110, 130)
|
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()
|
var margin := MarginContainer.new()
|
||||||
margin.add_theme_constant_override("margin_left", 8)
|
margin.add_theme_constant_override("margin_left", 8)
|
||||||
margin.add_theme_constant_override("margin_top", 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
|
vbox.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||||
margin.add_child(vbox)
|
margin.add_child(vbox)
|
||||||
|
|
||||||
# Rarity icon
|
|
||||||
var icon_lbl := Label.new()
|
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.add_theme_font_size_override("font_size", 36)
|
||||||
icon_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
icon_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
icon_lbl.add_theme_color_override("font_color", col)
|
icon_lbl.add_theme_color_override("font_color", col)
|
||||||
vbox.add_child(icon_lbl)
|
vbox.add_child(icon_lbl)
|
||||||
|
|
||||||
# Item name
|
|
||||||
var name_lbl := Label.new()
|
var name_lbl := Label.new()
|
||||||
name_lbl.text = res.get("name", "?")
|
name_lbl.text = res.get("name", "?")
|
||||||
name_lbl.add_theme_font_size_override("font_size", 11)
|
name_lbl.add_theme_font_size_override("font_size", 11)
|
||||||
name_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
name_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
name_lbl.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
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)
|
vbox.add_child(name_lbl)
|
||||||
|
|
||||||
# Rarity label
|
|
||||||
var rar_lbl := Label.new()
|
var rar_lbl := Label.new()
|
||||||
rar_lbl.text = label_txt
|
rar_lbl.text = label_txt
|
||||||
rar_lbl.add_theme_font_size_override("font_size", 9)
|
rar_lbl.add_theme_font_size_override("font_size", 9)
|
||||||
rar_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
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)
|
vbox.add_child(rar_lbl)
|
||||||
|
|
||||||
# Flash animation
|
# Fade-in
|
||||||
panel.modulate.a = 0.0
|
panel.modulate.a = 0.0
|
||||||
var tween := create_tween()
|
var tween := create_tween()
|
||||||
tween.tween_property(panel, "modulate:a", 1.0, 0.25)
|
tween.tween_property(panel, "modulate:a", 1.0, 0.25)
|
||||||
@@ -190,22 +428,21 @@ func _make_result_card(res: Dictionary) -> PanelContainer:
|
|||||||
|
|
||||||
return panel
|
return panel
|
||||||
|
|
||||||
func _rarity_icon(rarity: String) -> String:
|
|
||||||
match rarity:
|
|
||||||
"common": return "⬜"
|
|
||||||
"uncommon": return "🟩"
|
|
||||||
"rare": return "🟦"
|
|
||||||
"real_prize": return "✨"
|
|
||||||
return "❓"
|
|
||||||
|
|
||||||
# ─── Craft link ───────────────────────────────────────────────────────────────
|
# ─── Craft link ───────────────────────────────────────────────────────────────
|
||||||
func _on_open_craft() -> void:
|
func _on_open_craft() -> void:
|
||||||
hide()
|
# Load FragmentCraftPanel as overlay on top of GachaPanel
|
||||||
# Find or load the fragment craft panel in the scene tree
|
var fcp_scene = load("res://scenes/ui/fragment_craft_panel.tscn")
|
||||||
var main = get_tree().current_scene
|
if not fcp_scene:
|
||||||
var fcp = main.get_node_or_null("FragmentCraftPanel")
|
status_label.text = "Fragment Craft panel not found"
|
||||||
if fcp:
|
return
|
||||||
fcp.show_panel()
|
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:
|
func _on_close() -> void:
|
||||||
hide()
|
hide()
|
||||||
|
|||||||
Reference in New Issue
Block a user