feat: add battlepass slider and quest list tabs to lobby HUD

This commit is contained in:
2026-07-09 18:25:07 +08:00
parent 114748a54f
commit 933119ca56
99 changed files with 3861 additions and 417 deletions
+2 -10
View File
@@ -79,7 +79,6 @@ func _ready():
return
# Initialize strategic planner
var BotStrategicPlanner = load("res://scripts/bot_strategic_planner.gd")
strategic_planner = BotStrategicPlanner.new(actor, enhanced_gridmap)
# Disable input processing for bots
@@ -211,7 +210,7 @@ func _run_ai_tick():
return
# Priority 0.5: Gauntlet (#075) — use Ghost powerup if boxed in
if await _try_activate_ghost():
if _try_activate_ghost():
print("[BotController] Action Taken: Ghost (trapped)")
return
@@ -677,8 +676,6 @@ func _try_move() -> bool:
else:
print("[BotController] %s simple_move_to BLOCKED (others). Trying unstuck move." % actor.name)
return await _try_unstuck_move()
return false
func _should_freeze_for_stop_n_go() -> bool:
"""Check if the bot should intentionally skip its turn during STOP phase outside of safe zones."""
@@ -909,12 +906,7 @@ func _handle_goal_completion():
if goals_cycle_manager:
var time_remaining = goals_cycle_manager.get_time_remaining()
goals_cycle_manager.on_goal_completed(actor, time_remaining)
var powerup_manager = actor.get_node_or_null("PowerUpManager")
# if powerup_manager:
# powerup_manager.add_goal_completion_reward()
print("[BotController] %s COMPLETED GOAL!" % actor.name)
func _wait_with_variance(base_delay: float):
+25 -32
View File
@@ -204,9 +204,7 @@ func get_unneeded_tile_slot() -> int:
"""Find a slot containing a tile that is not needed for the goal."""
if not actor or actor.playerboard.size() == 0:
return -1
var needed_tiles = get_tiles_needed()
# Check center 3x3 for misplaced tiles
for i in range(3):
for j in range(3):
@@ -252,7 +250,6 @@ func get_unneeded_tile_slot() -> int:
var center_indices = [6, 7, 8, 11, 12, 13, 16, 17, 18]
for i in range(actor.playerboard.size()):
if not i in center_indices and actor.playerboard[i] != -1:
var item = actor.playerboard[i]
# Only keep if we strictly need it and can't find it easily?
# Actually, generally dump outer ring tiles to keep board clean
# unless we are about to move it to a valid spot.
@@ -263,8 +260,6 @@ func get_unneeded_tile_slot() -> int:
return -1
return -1
func get_unneeded_tile_slot_panic() -> int:
"""Aggressively find ANY tile that doesn't match a goal perfectly."""
if not actor or actor.playerboard.size() == 0:
@@ -439,33 +434,31 @@ func find_optimal_move_target() -> Vector2i:
var time_left = gc_manager.get_global_time_remaining() if gc_manager else 999.0
var is_match_running = gc_manager.is_match_running() if gc_manager else false
var is_late_game = is_sng and is_match_running and time_left > 0.0 and time_left <= 30.0
# 1. STOP N GO: Reach the finish line if goals are complete
if is_sng and main:
var sng_manager = main.get_node_or_null("StopNGoManager")
if sng_manager and sng_manager.is_mission_complete(actor.name.to_int()):
# MISSION COMPLETE: We CAN finish, but should we?
if not is_late_game:
# CHAOS MODE: Allow falling through to target holo tiles, but we'll limit the target X later
# print("[BotStrategicPlanner] %s mission complete (Chaos Phase %.1fs). Roaming field." % [actor.name, time_left])
pass
else:
# Late game: go to finish
var finish_target = Vector2i(21, actor.current_position.y)
# Ensure finish_target is walkable
if not _is_valid_move_target(finish_target):
for dy in [1, -1, 2, -2]:
var alt = Vector2i(21, actor.current_position.y + dy)
if _is_valid_move_target(alt):
finish_target = alt
break
print("[BotStrategicPlanner] %s mission complete (Late Game %.1fs)! Heading to finish: %s" % [actor.name, time_left, finish_target])
return finish_target
var sng_manager = main.get_node_or_null("StopNGoManager") if main else null
# 1. STOP N GO: Reach the finish line if goals are complete
if is_sng and sng_manager and sng_manager.is_mission_complete(actor.name.to_int()):
# MISSION COMPLETE: We CAN finish, but should we?
if not is_late_game:
# CHAOS MODE: Allow falling through to target holo tiles, but we'll limit the target X later
# print("[BotStrategicPlanner] %s mission complete (Chaos Phase %.1fs). Roaming field." % [actor.name, time_left])
pass
else:
# Late game: go to finish
var finish_target = Vector2i(21, actor.current_position.y)
# Ensure finish_target is walkable
if not _is_valid_move_target(finish_target):
for dy in [1, -1, 2, -2]:
var alt = Vector2i(21, actor.current_position.y + dy)
if _is_valid_move_target(alt):
finish_target = alt
break
print("[BotStrategicPlanner] %s mission complete (Late Game %.1fs)! Heading to finish: %s" % [actor.name, time_left, finish_target])
return finish_target
var is_mission_complete = sng_manager.is_mission_complete(actor.name.to_int()) if sng_manager else false
var needed_tiles = get_tiles_needed()
+4 -4
View File
@@ -69,9 +69,9 @@ func generate_guest_name() -> String:
_used_guest_names.append(fallback)
return fallback
func release_guest_name(name: String) -> void:
func release_guest_name(guest_name: String) -> void:
"""Call when a guest disconnects to free up their name for reuse."""
_used_guest_names.erase(name)
_used_guest_names.erase(guest_name)
# -----------------------------------------------------------------------
# Bot name generation
@@ -98,9 +98,9 @@ func generate_bot_name() -> String:
_used_bot_names.append(candidate)
return candidate
func release_bot_name(name: String) -> void:
func release_bot_name(bot_name: String) -> void:
"""Call when a bot slot is removed to free up its name."""
_used_bot_names.erase(name)
_used_bot_names.erase(bot_name)
func reset_all() -> void:
"""Reset both pools — useful on returning to main menu."""
+5 -5
View File
@@ -173,15 +173,15 @@ func send_lobby_invite(to_user_id: String, match_id: String) -> void:
if result.get("success", false) == false:
push_warning("[FriendManager] send_lobby_invite failed: " + str(result.get("error", "")))
func _on_notification_received(notification) -> void:
print("[FriendManager] _on_notification_received: code=%d sender=%s" % [notification.code, notification.sender_id])
match notification.code:
func _on_notification_received(notif) -> void:
print("[FriendManager] _on_notification_received: code=%d sender=%s" % [notif.code, notif.sender_id])
match notif.code:
NOTIF_LOBBY_INVITE:
var content = JSON.parse_string(notification.content)
var content = JSON.parse_string(notif.content)
if content:
var from_name: String = content.get("from_name", "Someone")
var match_id: String = content.get("match_id", "")
emit_signal("lobby_invite_received", notification.sender_id, from_name, match_id)
emit_signal("lobby_invite_received", notif.sender_id, from_name, match_id)
NOTIF_FRIEND_REQUEST:
# Refresh friends list so the incoming request appears immediately
print("[FriendManager] friend request notification received, reloading friends...")
-5
View File
@@ -32,11 +32,6 @@ func pull(banner_id: String, count: int) -> Array:
push_error("[GachaManager] Not authenticated")
return []
var payload = JSON.stringify({
"banner_id": banner_id,
"count": count
})
var result = await BackendService.perform_gacha_pull(banner_id, count)
if result.get("success", false) == false:
+1 -3
View File
@@ -44,7 +44,7 @@ func initialize(main: Node):
if LobbyManager:
LobbyManager.enable_cycle_timer_changed.connect(_on_enable_cycle_timer_changed)
func _on_enable_cycle_timer_changed(enabled: bool):
func _on_enable_cycle_timer_changed(_enabled: bool):
# If disabled mid-cycle, the timer will just freeze in _process
# If enabled mid-cycle, it will resume
pass
@@ -69,9 +69,7 @@ func _process(delta):
return
# Skip countdown if timer is disabled
var lobby_manager = get_tree().get_root().get_node_or_null("Main/LobbyManager")
# Note: LobbyManager is an Autoload, so we can access it directly via 'LobbyManager'
if LobbyManager and not LobbyManager.get_enable_cycle_timer():
# If timer is disabled, we just don't decrement.
# But we still keep is_cycle_active = true so the phase is "active" (allowing actions)
+1 -4
View File
@@ -2,15 +2,12 @@ extends Node
# PlayerManager - Handles player and bot lifecycle management
signal player_added(peer_id, player_node)
signal player_removed(peer_id)
signal bot_created(bot_id, bot_node)
signal bot_removed(bot_id)
var player_scene = preload("res://scenes/player.tscn")
var connected_peer_ids = []
func add_player_character(peer_id: int, position: Vector2i = Vector2i.ZERO) -> Node:
func add_player_character(peer_id: int, _position: Vector2i = Vector2i.ZERO) -> Node:
var player_character = player_scene.instantiate()
player_character.set_multiplayer_authority(peer_id)
player_character.name = str(peer_id)
-5
View File
@@ -282,11 +282,6 @@ func list_matches_async(mode_filter: String = "") -> Array:
# Batch-read room metadata from Nakama storage
var meta_map: Dictionary = {} # match_id -> {host_name, game_mode, ...}
if read_ids.size() > 0:
var storage_reads: Array = []
for mid in read_ids:
# We don't know the owner_id, so list objects by collection+key
# Use list_storage_objects per match or batch read
pass
# Read all match metas from our own storage (other users' objects are public read)
for mid in read_ids:
var list_result = await client.list_storage_objects_async(session, "match_meta", "", 100)
+2 -2
View File
@@ -87,13 +87,13 @@ func get_steamworks_manager() -> Node:
return steamworks_manager
## Unified RPC with Retry and Error Mapping
func api_rpc_async(rpc_id: String, payload: String = "{}") -> Dictionary:
func api_rpc_async(p_rpc_id: String, payload: String = "{}") -> Dictionary:
if not nakama_backend or not nakama_backend.client or not nakama_backend.session:
return { "success": false, "error": ErrorCode.UNAUTHORIZED, "message": "Not authenticated" }
var retries := 0
while retries <= MAX_RETRIES:
var result = await nakama_backend.client.rpc_async(nakama_backend.session, rpc_id, payload)
var result = await nakama_backend.client.rpc_async(nakama_backend.session, p_rpc_id, payload)
# NakamaAPI.ApiRpc has is_exception()
if result.is_exception():
+1 -1
View File
@@ -95,7 +95,7 @@ func _on_update_pressed() -> void:
status_label.text = "Downloading update..."
update_manager.download_update()
func _on_download_started(total_size: int) -> void:
func _on_download_started(_total_size: int) -> void:
progress_bar.visible = true
progress_label.visible = true
progress_bar.value = 0