feat: take_powerup VFX, rank fix, admin chat management
- Wire take_powerup AnimatedSprite3D on powerup pickup via add_powerup_from_item() - Make take_powerup animation one-shot (loop: false) - Fix rank Position label hidden at game start (visible = false, only shows when score > 0) - Competition ranking for tied scores in main.gd - Lobby Chat admin tab: system prefix, max messages, wipe, purge old, save config - Chat Storage admin tab: list/browse/delete individual channel messages - Backend RPCs: admin_get_chat_config, admin_set_chat_config, admin_purge_old_messages, admin_list_channel_messages, admin_delete_channel_message - Chat config applied on lobby join (max_messages, prefix injection)
This commit is contained in:
@@ -556,3 +556,28 @@ func _apply_loadout_character() -> void:
|
||||
if idx != -1:
|
||||
LobbyManager.local_character_index = idx
|
||||
print("[Lobby] Loadout character applied: ", saved_char)
|
||||
|
||||
# =============================================================================
|
||||
# Admin Chat Actions (called from Admin Panel)
|
||||
# =============================================================================
|
||||
func admin_wipe_chat() -> void:
|
||||
"""Wipe the entire global lobby chat. Called by admin panel."""
|
||||
if not chat or not chat._chat_channel:
|
||||
push_warning("[Lobby] admin_wipe_chat: chat not connected.")
|
||||
return
|
||||
var payload = JSON.stringify({"channel_id": chat._chat_channel.id})
|
||||
var result = await BackendService.admin_clear_global_chat(payload)
|
||||
if result.get("success", false):
|
||||
chat._chat_messages.clear()
|
||||
chat._refresh_chat_display()
|
||||
chat._inject_local_message("[SYSTEM] : Global chat cleared by admin.")
|
||||
else:
|
||||
push_warning("[Lobby] admin_wipe_chat failed: " + str(result.get("message", "")))
|
||||
|
||||
func admin_purge_chat(max_age_days: int) -> int:
|
||||
"""Purge messages older than max_age_days. Returns count deleted. Called by admin panel."""
|
||||
if not chat or not chat._chat_channel:
|
||||
push_warning("[Lobby] admin_purge_chat: chat not connected.")
|
||||
return 0
|
||||
var result = await BackendService.admin_purge_old_messages(chat._chat_channel.id, max_age_days)
|
||||
return result.get("deleted", 0)
|
||||
|
||||
+12
-2
@@ -2262,12 +2262,22 @@ func _on_leaderboard_updated(sorted_scores: Array):
|
||||
else:
|
||||
sorted_players.sort_custom(func(a, b): return a.score > b.score)
|
||||
|
||||
# Assign rank
|
||||
# Assign rank. Players sharing a score share a rank (standard competition
|
||||
# ranking), and zero-score players get no rank at all — this prevents the
|
||||
# match from starting with everyone displaying a position.
|
||||
var prev_score = null
|
||||
var prev_rank = 0
|
||||
for i in range(sorted_players.size()):
|
||||
var p_node = sorted_players[i].node
|
||||
var p_score = sorted_players[i].score
|
||||
var rank = i + 1
|
||||
# Tie: reuse the rank of the player above with the same score.
|
||||
if prev_score != null and p_score == prev_score:
|
||||
rank = prev_rank
|
||||
prev_score = p_score
|
||||
prev_rank = rank
|
||||
if p_node.has_method("update_rank_visuals"):
|
||||
p_node.update_rank_visuals(rank)
|
||||
p_node.update_rank_visuals(rank, p_score)
|
||||
|
||||
func _on_global_timer_updated(time_remaining: float):
|
||||
"""Update the global match timer display."""
|
||||
|
||||
+8
-2
@@ -928,11 +928,17 @@ func _refresh_player_visuals():
|
||||
if active_character:
|
||||
apply_loadout(active_character)
|
||||
|
||||
func update_rank_visuals(rank: int):
|
||||
func update_rank_visuals(rank: int, score: int = -1):
|
||||
var pos_label = get_node_or_null("Position")
|
||||
if not pos_label:
|
||||
return
|
||||
|
||||
|
||||
# Hide rank until the player has actually scored, so the match doesn't
|
||||
# start with everyone showing a position (e.g. all "1st").
|
||||
if score == 0:
|
||||
pos_label.visible = false
|
||||
return
|
||||
|
||||
if rank <= 4:
|
||||
pos_label.visible = true
|
||||
if race_manager:
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
[ext_resource type="AnimationLibrary" path="res://assets/characters/animations/animation-pack.res" id="6_5oq5w"]
|
||||
[ext_resource type="Script" uid="uid://cwwwixc07jc86" path="res://scripts/bot_controller.gd" id="7_botctrl"]
|
||||
[ext_resource type="FontFile" uid="uid://xnjx058n4tsw" path="res://assets/fonts/Nougat-ExtraBlack.ttf" id="8_y4r1p"]
|
||||
[ext_resource type="SpriteFrames" uid="uid://bq8ifua64lag2" path="res://assets/graphics/vfx/effects/powerup.tres" id="10_d2wvv"]
|
||||
[ext_resource type="SpriteFrames" uid="uid://7r0qbbm88vfy" path="res://assets/graphics/vfx/effects/animation-head.tres" id="10_y4r1p"]
|
||||
|
||||
[sub_resource type="TorusMesh" id="TorusMesh_ur7pv"]
|
||||
@@ -81,6 +82,7 @@ autowrap_mode = 2
|
||||
|
||||
[node name="Position" type="Label3D" parent="." unique_id=482425681]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.537, 0)
|
||||
visible = false
|
||||
billboard = 1
|
||||
no_depth_test = true
|
||||
render_priority = 2
|
||||
@@ -116,6 +118,17 @@ width = 700.0
|
||||
[node name="BotController" type="Node" parent="." unique_id=723259755]
|
||||
script = ExtResource("7_botctrl")
|
||||
|
||||
[node name="take_powerup" type="AnimatedSprite3D" parent="." unique_id=1497442994]
|
||||
transform = Transform3D(0.54, 0, 0, 0, 0.54, 0, 0, 0, 0.54, 0, 0.21994019, 0)
|
||||
visible = false
|
||||
modulate = Color(1, 1, 1, 0.8)
|
||||
billboard = 2
|
||||
no_depth_test = true
|
||||
render_priority = 3
|
||||
sprite_frames = ExtResource("10_d2wvv")
|
||||
animation = &"take_powerup"
|
||||
frame_progress = 0.5033338
|
||||
|
||||
[node name="skill_freeze" type="AnimatedSprite3D" parent="." unique_id=674916570]
|
||||
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, -1, 0, 1, -4.371139e-08, 0, 1.5653763, 0)
|
||||
visible = false
|
||||
|
||||
@@ -544,6 +544,144 @@ text = "Load Current"
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(160, 36)
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Lobby Chat" type="VBoxContainer" parent="Margin/VBox/Tabs"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 10
|
||||
|
||||
[node name="PrefixRow" type="HBoxContainer" parent="Margin/VBox/Tabs/Lobby Chat"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 8
|
||||
|
||||
[node name="Label" type="Label" parent="Margin/VBox/Tabs/Lobby Chat/PrefixRow"]
|
||||
layout_mode = 2
|
||||
custom_minimum_size = Vector2(220, 0)
|
||||
text = "System Prefix:"
|
||||
|
||||
[node name="PrefixEdit" type="LineEdit" parent="Margin/VBox/Tabs/Lobby Chat/PrefixRow"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
placeholder_text = "[SERVER]"
|
||||
|
||||
[node name="MaxMsgRow" type="HBoxContainer" parent="Margin/VBox/Tabs/Lobby Chat"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 8
|
||||
|
||||
[node name="Label" type="Label" parent="Margin/VBox/Tabs/Lobby Chat/MaxMsgRow"]
|
||||
layout_mode = 2
|
||||
custom_minimum_size = Vector2(220, 0)
|
||||
text = "Max messages loaded:"
|
||||
|
||||
[node name="MaxMsgSpin" type="SpinBox" parent="Margin/VBox/Tabs/Lobby Chat/MaxMsgRow"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
custom_minimum_size = Vector2(120, 0)
|
||||
min_value = 10.0
|
||||
max_value = 200.0
|
||||
step = 10.0
|
||||
value = 50.0
|
||||
|
||||
[node name="MaxAgeRow" type="HBoxContainer" parent="Margin/VBox/Tabs/Lobby Chat"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 8
|
||||
|
||||
[node name="Label" type="Label" parent="Margin/VBox/Tabs/Lobby Chat/MaxAgeRow"]
|
||||
layout_mode = 2
|
||||
custom_minimum_size = Vector2(220, 0)
|
||||
text = "Delete messages older than (days):"
|
||||
|
||||
[node name="MaxAgeSpin" type="SpinBox" parent="Margin/VBox/Tabs/Lobby Chat/MaxAgeRow"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
custom_minimum_size = Vector2(120, 0)
|
||||
min_value = 0.0
|
||||
max_value = 365.0
|
||||
step = 1.0
|
||||
value = 0.0
|
||||
tooltip_text = "0 = don't auto-delete, use manual purge only"
|
||||
|
||||
[node name="ChatActions" type="HBoxContainer" parent="Margin/VBox/Tabs/Lobby Chat"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 8
|
||||
|
||||
[node name="WipeChatBtn" type="Button" parent="Margin/VBox/Tabs/Lobby Chat/ChatActions"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(140, 36)
|
||||
layout_mode = 2
|
||||
text = "Wipe Chat"
|
||||
|
||||
[node name="PurgeOldBtn" type="Button" parent="Margin/VBox/Tabs/Lobby Chat/ChatActions"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(140, 36)
|
||||
layout_mode = 2
|
||||
text = "Purge Old"
|
||||
|
||||
[node name="SaveConfigBtn" type="Button" parent="Margin/VBox/Tabs/Lobby Chat/ChatActions"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(140, 36)
|
||||
layout_mode = 2
|
||||
text = "Save Config"
|
||||
|
||||
[node name="ChatStatusLabel" type="Label" parent="Margin/VBox/Tabs/Lobby Chat"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = ""
|
||||
|
||||
[node name="Chat Storage" type="VBoxContainer" parent="Margin/VBox/Tabs"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 8
|
||||
|
||||
[node name="ChannelIdRow" type="HBoxContainer" parent="Margin/VBox/Tabs/Chat Storage"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 8
|
||||
|
||||
[node name="Label" type="Label" parent="Margin/VBox/Tabs/Chat Storage/ChannelIdRow"]
|
||||
layout_mode = 2
|
||||
text = "Channel ID:"
|
||||
|
||||
[node name="ChannelIdEdit" type="LineEdit" parent="Margin/VBox/Tabs/Chat Storage/ChannelIdRow"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
placeholder_text = "Enter channel ID..."
|
||||
|
||||
[node name="LoadMessagesBtn" type="Button" parent="Margin/VBox/Tabs/Chat Storage/ChannelIdRow"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(100, 0)
|
||||
layout_mode = 2
|
||||
text = "Load"
|
||||
|
||||
[node name="ChatTree" type="Tree" parent="Margin/VBox/Tabs/Chat Storage"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
columns = 4
|
||||
column_titles_visible = true
|
||||
allow_reselect = true
|
||||
hide_root = true
|
||||
select_mode = 1
|
||||
|
||||
[node name="ChatStorageActionBar" type="HBoxContainer" parent="Margin/VBox/Tabs/Chat Storage"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 8
|
||||
|
||||
[node name="RefreshChatBtn" type="Button" parent="Margin/VBox/Tabs/Chat Storage/ChatStorageActionBar"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(100, 36)
|
||||
layout_mode = 2
|
||||
text = "Refresh"
|
||||
|
||||
[node name="Spacer" type="Control" parent="Margin/VBox/Tabs/Chat Storage/ChatStorageActionBar"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="DeleteSelectedBtn" type="Button" parent="Margin/VBox/Tabs/Chat Storage/ChatStorageActionBar"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(120, 36)
|
||||
layout_mode = 2
|
||||
text = "Delete Selected"
|
||||
|
||||
[node name="HistoryDialog" type="AcceptDialog" parent="."]
|
||||
unique_name_in_owner = true
|
||||
title = "User History"
|
||||
|
||||
+21
-3
@@ -9,6 +9,7 @@ var _chat_messages: Array = []
|
||||
var _active_chat_context: String = "global"
|
||||
var _dm_tabs: Dictionary = {}
|
||||
var _dm_messages: Dictionary = {}
|
||||
var _chat_config: Dictionary = {"prefix": "", "max_messages": 50, "max_age_days": 0}
|
||||
|
||||
func _init(p_lobby: Control):
|
||||
lobby = p_lobby
|
||||
@@ -40,17 +41,34 @@ func join_global_chat() -> void:
|
||||
|
||||
_chat_channel = result
|
||||
print("[Chat] Joined global channel: ", _chat_channel.id)
|
||||
|
||||
|
||||
if not socket.received_channel_message.is_connected(_on_chat_message_received):
|
||||
socket.received_channel_message.connect(_on_chat_message_received)
|
||||
|
||||
|
||||
# Fetch admin chat config (prefix, max_messages, etc.)
|
||||
if BackendService.has_method("admin_get_chat_config"):
|
||||
var cfg_res = await BackendService.admin_get_chat_config()
|
||||
if cfg_res.has("config"):
|
||||
_chat_config = cfg_res["config"]
|
||||
|
||||
_chat_messages.clear()
|
||||
var history_result = await NakamaManager.client.list_channel_messages_async(NakamaManager.session, _chat_channel.id, 50, false)
|
||||
var limit: int = _chat_config.get("max_messages", 50)
|
||||
var history_result = await NakamaManager.client.list_channel_messages_async(NakamaManager.session, _chat_channel.id, limit, false)
|
||||
if not history_result.is_exception() and history_result.messages:
|
||||
var msgs = history_result.messages.duplicate()
|
||||
msgs.reverse()
|
||||
for msg in msgs:
|
||||
_add_chat_message(msg, false)
|
||||
|
||||
# Inject admin system prefix if configured
|
||||
var prefix: String = _chat_config.get("prefix", "")
|
||||
if not prefix.is_empty():
|
||||
_chat_messages.insert(0, {
|
||||
"sender": "SYSTEM",
|
||||
"content": prefix,
|
||||
"ts": _get_local_time(),
|
||||
"date": Time.get_date_string_from_system()
|
||||
})
|
||||
|
||||
_trim_old_messages()
|
||||
_refresh_chat_display()
|
||||
|
||||
Reference in New Issue
Block a user