#include "Peril_Constants.lsl" // Game Scoreboard Manager - Linkset Version // Shows current game players in grid layout // Each player gets a prim showing profile picture + heart texture (lives) // --- DYNAMIC LINK DISCOVERY --- integer BACKGROUND_PRIM = -1; integer ACTIONS_PRIM = -1; // Grid link mappings for player grid (Stride 3: Profile, Hearts, Overlay) list gridLinks = [ 0,0,0, 0,0,0, 0,0,0, 0,0,0, 0,0,0, 0,0,0, 0,0,0, 0,0,0, 0,0,0, 0,0,0 ]; // Discover all relevant prims by name at startup discoverLinks() { integer i; integer total = llGetNumberOfPrims(); // Reset list (30 elements for 10 slots * 3 prims) gridLinks = [ 0,0,0, 0,0,0, 0,0,0, 0,0,0, 0,0,0, 0,0,0, 0,0,0, 0,0,0, 0,0,0, 0,0,0 ]; for (i = 1; i <= total; i++) { string name = llGetLinkName(i); if (name == "backboard:0:0") BACKGROUND_PRIM = i; else if (name == "TitleAction:0:0") ACTIONS_PRIM = i; else if (name == "Scoreboard:0:0") LINK_SCOREBOARD = i; else { // Pattern match: "type:row:col" list parts = llParseString2List(name, [":"], []); if (llGetListLength(parts) == 3) { string type = llList2String(parts, 0); integer row = (integer)llList2String(parts, 1); integer col = (integer)llList2String(parts, 2); integer index = (row * 2) + col; if (index >= 0 && index < 10) { integer gridBase = index * 3; if (type == "profile") gridLinks = llListReplaceList(gridLinks, [i], gridBase, gridBase); else if (type == "life") gridLinks = llListReplaceList(gridLinks, [i], gridBase + 1, gridBase + 1); else if (type == "overlay") gridLinks = llListReplaceList(gridLinks, [i], gridBase + 2, gridBase + 2); } } } } dbg("Discovery done"); if (BACKGROUND_PRIM == -1) { dbg("No backboard"); } // Safety Fallback: Use Root if Scoreboard prim is missing if (LINK_SCOREBOARD == -1) LINK_SCOREBOARD = 1; } // --- Leaderboard Configuration --- #define LEADERBOARD_WIDTH 32 // Standard width for 4x8 character Furware grids (4 columns of 8) #define MAX_LEADERBOARD_ENTRIES 11 // Fits within 11 remaining rows (after 1-row header) // Heart texture UUIDs - REPLACE WITH YOUR ACTUAL TEXTURE UUIDs #define TEXTURE_0_HEARTS "7d8ae121-e171-12ae-f5b6-7cc3c0395c7b" // 0 hearts (dead) #define TEXTURE_1_HEARTS "6605d25f-8e2d-2870-eb87-77c58cd47fa9" // 1 heart #define TEXTURE_2_HEARTS "7ba6cb1e-f384-25a5-8e88-a90bbd7cc041" // 2 hearts #define TEXTURE_3_HEARTS "a5d16715-4648-6526-5582-e8068293f792" // 3 hearts // Default textures - REPLACE WITH YOUR ACTUAL TEXTURE UUIDs #define TEXTURE_DEFAULT_PROFILE "1ce89375-6c3c-3845-26b1-1dc666bc9169" // Default avatar #define TEXTURE_BOT_PROFILE "62f31722-04c1-8c29-c236-398543f2a6ae" // Bot avatar picture #define BLANK_TEXTURE "5748decc-f629-461c-9a36-a35a221fe21f" // White texture UUID #define TEXTURE_BACKGROUND "5748decc-f629-461c-9a36-a35a221fe21f" // Background texture (blank + black color) #define TEXTURE_ACTIONS "6aac8dce-1b83-f931-abe3-286f4f2faa29" // Punishment texture // Status textures - REPLACE WITH YOUR ACTUAL TEXTURE UUIDs #define TEXTURE_PERIL "c5676fec-0c85-5567-3dd8-f939234e21d9" // Elimination texture #define TEXTURE_PERIL_SELECTED "a53ff601-3c8a-e312-9e0e-f6fa76f6773a" // Peril Selected texture #define TEXTURE_VICTORY "ec5bf10e-4970-fb63-e7bf-751e1dc27a8d" // Victory texture #define TEXTURE_PUNISHMENT "acfabed0-84ad-bfd1-cdfc-2ada0aeeaa2f" // Punishment texture (fallback) #define TEXTURE_DIRECT_HIT "ecd2dba2-3969-6c39-ad59-319747307f55" // Direct Hit texture #define TEXTURE_NO_SHIELD "2440174f-e385-44e2-8016-ac34934f11f5" // No Shield texture #define TEXTURE_PLOT_TWIST "ec533379-4f7f-8183-e877-e68af703dcce" // Plot Twist texture #define TEXTURE_TITLE "624bb7a7-e856-965c-bae8-94d75226c1bc" // Title texture #define TEXTURE_ELIMINATED_X "90524092-03b0-1b3c-bcea-3ea5118c6dba" // Red X overlay for eliminated players // Player data - used for BOTH current game display AND leaderboard list activePlayers = []; // Current game state (Stride 3: Name, Lives, UUID) // Peril player tracking string currentPerilPlayer = ""; // Track current peril player for glow effects string currentWinner = ""; // Track current winner for green glow effects // Leaderboard data (persistent - stored in linkset data) - Stride of 3 [Name, Wins, Losses] list leaderboardData = []; // HTTP tracking list profileRequests = []; // Track which profile requests belong to which player list httpRequests = []; // Track HTTP requests for profile pictures // KVP tracking key kvpReadReq; key kvpWriteReq; string pendingSerialize; // Profile picture extraction constants // Profile picture extraction constants #define PROFILE_KEY_PREFIX ", <0,0,0>, 0.0, PRIM_COLOR, ALL_SIDES, <0.0, 0.0, 0.0>, 1.0, // Black color PRIM_TEXT, "", <0,0,0>, 0.0 // Remove any text ]); dbg("Backboard Reset"); return 0; } // Reset the scoreboard manager cube (link 12) to neutral state integer resetManagerCube() { llSetLinkPrimitiveParamsFast(LINK_SCOREBOARD, [ PRIM_TEXTURE, ALL_SIDES, BLANK_TEXTURE, <1,1,0>, <0,0,0>, 0.0, PRIM_COLOR, ALL_SIDES, <0.0, 0.0, 0.0>, 1.0, // Black color to blend in PRIM_TEXT, "", <0,0,0>, 0.0 // Remove any text ]); dbg("Manager Reset"); return 0; } // Update actions prim with appropriate status texture integer updateActionsPrim(string status) { string textureToUse = ""; if (status == "Elimination") { textureToUse = TEXTURE_PERIL; } else if (status == "Victory") { textureToUse = TEXTURE_VICTORY; } else if (status == "Punishment") { textureToUse = TEXTURE_PUNISHMENT; } else if (status == "Direct Hit") { textureToUse = TEXTURE_DIRECT_HIT; } else if (status == "No Shield") { textureToUse = TEXTURE_NO_SHIELD; } else if (status == "Peril Selected") { textureToUse = TEXTURE_PERIL_SELECTED; } else if (status == "Plot Twist") { textureToUse = TEXTURE_PLOT_TWIST; } else if (status == "Title") { textureToUse = TEXTURE_TITLE; } else { return 0; // Don't change texture for unrecognized statuses } // Replace with action prim texture update if (ACTIONS_PRIM != -1) { llSetLinkPrimitiveParamsFast(ACTIONS_PRIM, [ PRIM_TEXTURE, ALL_SIDES, textureToUse, <1,1,0>, <0,0,0>, 0.0, PRIM_TEXT, "", <0,0,0>, 0.0 // Remove existing floating text ]); } return 0; } // Load leaderboard data from Experience KVP database integer loadLeaderboardData() { kvpReadReq = llReadKeyValue("Peril_LB_Top50"); dbg("LB Requesting"); return 0; } // Save leaderboard data to Experience KVP database integer saveLeaderboardData() { string serialized = ""; integer totalEntries = llGetListLength(leaderboardData) / 3; integer saveCount = totalEntries; integer saveI = 0; integer base = 0; string save_entry = ""; if (saveCount > 50) saveCount = 50; // Cap at Top 50 for (saveI = 0; saveI < saveCount; saveI++) { base = saveI * 3; save_entry = llList2String(leaderboardData, base) + ":" + (string)llList2Integer(leaderboardData, base + 1) + ":" + (string)llList2Integer(leaderboardData, base + 2); if (serialized != "") serialized += "|"; serialized += save_entry; } pendingSerialize = serialized; kvpWriteReq = llUpdateKeyValue("Peril_LB_Top50", serialized, FALSE, ""); dbg("LB Saving"); return 0; } // Handle game won - update leaderboard integer handleGameWon(string winnerName) { integer idx = llListFindList(leaderboardData, [winnerName]); if (idx == -1) { // New player leaderboardData += [winnerName, 1, 0]; } else { // Existing player integer currentWins = llList2Integer(leaderboardData, idx + 1); leaderboardData = llListReplaceList(leaderboardData, [currentWins + 1], idx + 1, idx + 1); } saveLeaderboardData(); generateLeaderboardText(); return 0; } // Handle game lost - update leaderboard integer handleGameLost(string loserName) { integer lostIdx = llListFindList(leaderboardData, [loserName]); if (lostIdx == -1) { // New player leaderboardData += [loserName, 0, 1]; } else { // Existing player integer currentLosses = llList2Integer(leaderboardData, lostIdx + 2); leaderboardData = llListReplaceList(leaderboardData, [currentLosses + 1], lostIdx + 2, lostIdx + 2); } saveLeaderboardData(); generateLeaderboardText(); return 0; } list getSortedLeaderboard() { list sortList = []; integer i = 0; integer totalEntries = llGetListLength(leaderboardData) / 3; integer base = 0; integer wins = 0; integer losses = 0; integer sortKey = 0; list sortedData = []; integer origIdx = 0; for (i = 0; i < totalEntries; i++) { base = i * 3; wins = llList2Integer(leaderboardData, base + 1); losses = llList2Integer(leaderboardData, base + 2); if (wins > 65535) wins = 65535; if (losses > 65535) losses = 65535; sortKey = (wins << 16) | (65535 - losses); sortList += [sortKey, i]; } // 2. Sort by key (stride of 2) in descending order (highest score first) sortList = llListSort(sortList, 2, FALSE); // 3. Rebuild the leaderboard data in sorted order (Stided list: Name, Wins, Losses) for (i = 0; i < llGetListLength(sortList); i += 2) { origIdx = llList2Integer(sortList, i + 1); base = origIdx * 3; sortedData += [ llList2String(leaderboardData, base), llList2Integer(leaderboardData, base + 1), llList2Integer(leaderboardData, base + 2) ]; } return sortedData; } // Generate leaderboard text and send to separate leaderboard object integer generateLeaderboardText() { list sortedData = getSortedLeaderboard(); string SPACES = " "; string title = "TOP BATTLE RECORDS"; string rankCol = ""; string nameCol = ""; string statsCol = ""; integer numSorted = llGetListLength(sortedData) / 3; integer genI; integer base; string playerName; integer genWins; integer genLosses; string rank; string sWins; string sLosses; // 1. Send Title to Parent Box (Row 0) integer titleMargin = (LEADERBOARD_WIDTH - llStringLength(title)) / 2; if (titleMargin < 0) titleMargin = 0; string titleText = llGetSubString(SPACES, 0, titleMargin - 1) + title; llMessageLinked(LINK_SET, MSG_DISPLAY_LEADERBOARD, "FORMATTED_TEXT|" + titleText, NULL_KEY); // Add actual player data column-by-column for (genI = 0; genI < numSorted && genI < MAX_LEADERBOARD_ENTRIES; genI++) { base = genI * 3; playerName = llList2String(sortedData, base); genWins = llList2Integer(sortedData, base + 1); genLosses = llList2Integer(sortedData, base + 2); if (llStringLength(playerName) > 19) playerName = llGetSubString(playerName, 0, 16) + "..."; rank = (string)(genI + 1) + "."; if (genI < 9) rank = "0" + rank; sWins = (string)genWins; sLosses = (string)genLosses; rankCol += rank + "\n"; nameCol += playerName + "\n"; statsCol += "W:" + sWins + "/L:" + sLosses + "\n"; } // Fill remaining positions with placeholders for (genI = numSorted; genI < MAX_LEADERBOARD_ENTRIES; genI++) { rank = (string)(genI + 1) + "."; if (genI < 9) rank = "0" + rank; rankCol += rank + "\n"; nameCol += "----------\n"; statsCol += "W:0/L:0\n"; } // Send 3-column data to bridge llMessageLinked(LINK_SET, MSG_DISPLAY_LEADERBOARD, "COLUMNS|" + rankCol + "|" + nameCol + "|" + statsCol, NULL_KEY); return 0; } // Reset leaderboard data (SECURED for Creator Only) integer resetLeaderboard() { // PROTECT THE GLOBAL DATABASE! if (llGetOwner() != llGetCreator()) { llOwnerSay("❌ Access Denied: Only the creator of Peril Dice can wipe the Global Scoreboard!"); return 0; } leaderboardData = []; pendingSerialize = ""; kvpWriteReq = llUpdateKeyValue("Peril_LB_Top50", "", FALSE, ""); generateLeaderboardText(); // Send empty leaderboard immediately return 0; } // Clear all current players from the scoreboard integer clearAllPlayers() { activePlayers = []; // Clear any pending HTTP requests httpRequests = []; profileRequests = []; // Variable declarations for the loop integer clearI; integer clearProfilePrimIndex; integer clearHeartsPrimIndex; integer clearOverlayPrimIndex; // Reset all player prims to default state for (clearI = 0; clearI < 10; clearI++) { clearProfilePrimIndex = getProfilePrimLink(clearI); clearHeartsPrimIndex = getHeartsPrimLink(clearI); clearOverlayPrimIndex = getOverlayPrimLink(clearI); // SAFETY CHECK - use if block instead of continue for maximum compatibility if (clearProfilePrimIndex > 0) { // Reset profile prim to default and hide it llSetLinkPrimitiveParamsFast(clearProfilePrimIndex, [ PRIM_TEXTURE, ALL_SIDES, TEXTURE_DEFAULT_PROFILE, <1,1,0>, <0,0,0>, 0.0, PRIM_COLOR, ALL_SIDES, <1.0, 1.0, 1.0>, 0.0, // Hidden by default PRIM_TEXT, "", <1,1,1>, 0.0, PRIM_GLOW, ALL_SIDES, 0.0 ]); // Reset hearts prim to 3 hearts and hide it llSetLinkPrimitiveParamsFast(clearHeartsPrimIndex, [ PRIM_TEXTURE, ALL_SIDES, TEXTURE_3_HEARTS, <1,1,0>, <0,0,0>, 0.0, PRIM_COLOR, ALL_SIDES, <1.0, 1.0, 1.0>, 0.0, // Hidden by default PRIM_TEXT, "", <1,1,1>, 0.0, PRIM_GLOW, ALL_SIDES, 0.0 ]); // Reset overlay prim to transparent llSetLinkPrimitiveParamsFast(clearOverlayPrimIndex, [ PRIM_TEXTURE, ALL_SIDES, BLANK_TEXTURE, <1,1,0>, <0,0,0>, 0.0, PRIM_COLOR, ALL_SIDES, <1.0, 1.0, 1.0>, 0.0, // Fully transparent PRIM_TEXT, "", <1,1,1>, 0.0 ]); } } return 0; } // Remove a single player from the scoreboard display integer removePlayer(string playerName) { integer idx = llListFindList(activePlayers, [playerName]); if (idx != -1) { activePlayers = llDeleteSubList(activePlayers, idx, idx + 2); refreshPlayerDisplay(); dbg("📈 Player removed: " + playerName); } return 0; } integer refreshPlayerDisplay() { integer numActivePlayers = llGetListLength(activePlayers) / 3; integer refreshI; integer refreshProfilePrimIndex = 0; integer refreshHeartsPrimIndex = 0; integer refreshOverlayPrimIndex = 0; // Position-specific variables string r_name; integer r_lives; string r_profileTexture; integer r_profileIdx; integer r_heartsIdx; integer r_overlayIdx; // Only reset prims for unused slots (avoid flickering for active players) for (refreshI = numActivePlayers; refreshI < 10; refreshI++) { refreshProfilePrimIndex = getProfilePrimLink(refreshI); refreshHeartsPrimIndex = getHeartsPrimLink(refreshI); refreshOverlayPrimIndex = getOverlayPrimLink(refreshI); if (refreshProfilePrimIndex > 0) { // Unused slots: Reset and HIDE llSetLinkPrimitiveParamsFast(refreshProfilePrimIndex, [ PRIM_TEXTURE, ALL_SIDES, TEXTURE_DEFAULT_PROFILE, <1,1,0>, <0,0,0>, 0.0, PRIM_COLOR, ALL_SIDES, <1.0, 1.0, 1.0>, 0.0, // Invisible PRIM_TEXT, "", <1,1,1>, 0.0 ]); llSetLinkPrimitiveParamsFast(refreshHeartsPrimIndex, [ PRIM_TEXTURE, ALL_SIDES, TEXTURE_3_HEARTS, <1,1,0>, <0,0,0>, 0.0, PRIM_COLOR, ALL_SIDES, <1.0, 1.0, 1.0>, 0.0, // Invisible PRIM_TEXT, "", <1,1,1>, 0.0 ]); // Hide overlay prim (make transparent) llSetLinkPrimitiveParamsFast(refreshOverlayPrimIndex, [ PRIM_TEXTURE, ALL_SIDES, BLANK_TEXTURE, <1,1,0>, <0,0,0>, 0.0, PRIM_COLOR, ALL_SIDES, <1.0, 1.0, 1.0>, 0.0, // Fully transparent PRIM_TEXT, "", <1,1,1>, 0.0 ]); } } // Now display all current players in their new positions for (refreshI = 0; refreshI < numActivePlayers; refreshI++) { integer base = refreshI * 3; r_name = llList2String(activePlayers, base); r_lives = llList2Integer(activePlayers, base + 1); r_profileTexture = llList2String(activePlayers, base + 2); // Calculate prim indices for this position r_profileIdx = getProfilePrimLink(refreshI); r_heartsIdx = getHeartsPrimLink(refreshI); r_overlayIdx = getOverlayPrimLink(refreshI); if (r_profileIdx > 0) { // Determine profile texture to use if (isBot(r_name)) { r_profileTexture = TEXTURE_BOT_PROFILE; } else if (r_profileTexture == "" || r_profileTexture == "00000000-0000-0000-0000-000000000000") { r_profileTexture = TEXTURE_DEFAULT_PROFILE; } // Set profile texture llSetLinkPrimitiveParamsFast(r_profileIdx, [ PRIM_TEXTURE, ALL_SIDES, r_profileTexture, <1,1,0>, <0,0,0>, 0.0, PRIM_COLOR, ALL_SIDES, <1.0, 1.0, 1.0>, 1.0, PRIM_TEXT, "", <1,1,1>, 0.0 ]); // Set hearts texture string heartTexture = getHeartTexture(r_lives); llSetLinkPrimitiveParamsFast(r_heartsIdx, [ PRIM_TEXTURE, ALL_SIDES, heartTexture, <1,1,0>, <0,0,0>, 0.0, PRIM_COLOR, ALL_SIDES, <1.0, 1.0, 1.0>, 1.0, PRIM_TEXT, "", <1,1,1>, 0.0 ]); // Control overlay prim - show red X if eliminated, hide if alive if (r_lives <= 0) { llSetLinkPrimitiveParamsFast(r_overlayIdx, [ PRIM_TEXTURE, ALL_SIDES, TEXTURE_ELIMINATED_X, <1,1,0>, <0,0,0>, 0.0, PRIM_COLOR, ALL_SIDES, <1.0, 1.0, 1.0>, 1.0, PRIM_TEXT, "", <1,1,1>, 0.0 ]); } else { llSetLinkPrimitiveParamsFast(r_overlayIdx, [ PRIM_TEXTURE, ALL_SIDES, BLANK_TEXTURE, <1,1,0>, <0,0,0>, 0.0, PRIM_COLOR, ALL_SIDES, <1.0, 1.0, 1.0>, 0.0, PRIM_TEXT, "", <1,1,1>, 0.0 ]); } } } // Update player glow effects after refreshing all players updatePlayerGlowEffects(); return 0; } // Check if a player name indicates it's a bot integer isBot(string playerName) { return (llSubStringIndex(playerName, "Bot") == 0); } // Map player index to profile prim link number // Functions relocated to avoid hardcoded math integer getProfilePrimLink(integer playerIndex) { return llList2Integer(gridLinks, playerIndex * 3); } integer getHeartsPrimLink(integer playerIndex) { return llList2Integer(gridLinks, (playerIndex * 3) + 1); } integer getOverlayPrimLink(integer playerIndex) { return llList2Integer(gridLinks, (playerIndex * 3) + 2); } string getHeartTexture(integer lives) { if (lives <= 0) return TEXTURE_0_HEARTS; else if (lives == 1) return TEXTURE_1_HEARTS; else if (lives == 2) return TEXTURE_2_HEARTS; else return TEXTURE_3_HEARTS; // 3 or more } // Helper to set glow and color on a prim setGlow(integer link, float glow, vector color) { if (link > 0) { llSetLinkPrimitiveParamsFast(link, [PRIM_GLOW, ALL_SIDES, glow, PRIM_COLOR, ALL_SIDES, color, 1.0]); } } // Update all glow effects (peril and winner) integer updatePlayerGlowEffects() { integer numPlayers = llGetListLength(activePlayers) / 3; integer glowI; for (glowI = 0; glowI < numPlayers; glowI++) { setGlow(getProfilePrimLink(glowI), 0.0, <1,1,1>); setGlow(getHeartsPrimLink(glowI), 0.0, <1,1,1>); } integer idx = -1; vector col = <1,1,1>; float glow = 0.0; if (currentWinner != "" && currentWinner != "NONE") { idx = llListFindList(activePlayers, [currentWinner]); if (idx != -1) { idx = idx / 3; col = <0,1,0>; glow = 0.3; } } else if (currentPerilPlayer != "" && currentPerilPlayer != "NONE") { idx = llListFindList(activePlayers, [currentPerilPlayer]); if (idx != -1) { idx = idx / 3; col = <1,1,0>; glow = 0.2; } } if (idx != -1) { setGlow(getProfilePrimLink(idx), glow, col); setGlow(getHeartsPrimLink(idx), glow, col); } return 0; } // Backward compatibility wrapper integer updatePerilPlayerGlow() { updatePlayerGlowEffects(); return 0; } // Update player display on the grid integer updatePlayerDisplay(string playerName, integer lives, string profileUUID) { integer playerIndex = llListFindList(activePlayers, [playerName]); if (playerIndex != -1) playerIndex = playerIndex / 3; if (playerIndex == -1) { playerIndex = llGetListLength(activePlayers) / 3; if (playerIndex >= 10) { dbg("📈 MAX players reached: " + playerName); return 0; } string activeUUID = profileUUID; if (isBot(playerName)) activeUUID = TEXTURE_BOT_PROFILE; activePlayers += [playerName, lives, activeUUID]; // DEBUG: Show prim mapping - DISABLED to save memory // if (VERBOSE_LOGGING) { // integer debugProfile = getProfilePrimLink(playerIndex); // integer debugHearts = getHeartsPrimLink(playerIndex); // integer debugOverlay = getOverlayPrimLink(playerIndex); // llOwnerSay("🔧 [Scoreboard] Player " + (string)playerIndex + " (" + playerName + "): Profile=" + (string)debugProfile + ", Hearts=" + (string)debugHearts + ", Overlay=" + (string)debugOverlay); // llOwnerSay("🔍 [Scoreboard] Prim ranges: FIRST_PROFILE=" + (string)FIRST_PROFILE_PRIM + ", LAST_PROFILE=" + (string)LAST_PROFILE_PRIM); // // // Test if the calculated prim is actually valid // if (debugProfile < FIRST_PROFILE_PRIM || debugProfile > LAST_PROFILE_PRIM) { // llOwnerSay("⚠️ [Scoreboard] WARNING: Calculated profile prim " + (string)debugProfile + " is outside valid range!"); // } // } } else { activePlayers = llListReplaceList(activePlayers, [lives], (playerIndex * 3) + 1, (playerIndex * 3) + 1); } // Update the physical prims for this player integer updateProfilePrimIndex = getProfilePrimLink(playerIndex); integer updateHeartsPrimIndex = getHeartsPrimLink(playerIndex); integer updateOverlayPrimIndex = getOverlayPrimLink(playerIndex); if (updateProfilePrimIndex <= 0) return 0; // Update profile prim texture - show red X if eliminated, otherwise show profile string updateProfileTexture; if (lives <= 0) { // Player is eliminated - keep profile picture but show red X on overlay prim dbg("📈 [Scoreboard Manager] 💀 Scoreboard: Showing elimination X overlay for " + playerName); // CRITICAL: Still need to set updateProfileTexture for eliminated players if (isBot(playerName)) { updateProfileTexture = TEXTURE_BOT_PROFILE; } else { string updateCachedTexture = llList2String(activePlayers, (playerIndex * 3) + 2); if (updateCachedTexture != "" && updateCachedTexture != profileUUID && updateCachedTexture != TEXTURE_DEFAULT_PROFILE && updateCachedTexture != TEXTURE_BOT_PROFILE && updateCachedTexture != TEXTURE_ELIMINATED_X) { updateProfileTexture = updateCachedTexture; } else { updateProfileTexture = TEXTURE_DEFAULT_PROFILE; } } } else { // Player is alive - determine profile texture to use updateProfileTexture = profileUUID; // Check if we already have a cached profile texture for this player string liveCachedTexture = llList2String(activePlayers, (playerIndex * 3) + 2); if (liveCachedTexture != "" && liveCachedTexture != profileUUID && liveCachedTexture != TEXTURE_DEFAULT_PROFILE && liveCachedTexture != TEXTURE_BOT_PROFILE && liveCachedTexture != TEXTURE_ELIMINATED_X) { updateProfileTexture = liveCachedTexture; } // If no cached texture or using original UUID, determine what to use // if (VERBOSE_LOGGING) { // llOwnerSay("🔍 [DEBUG] " + playerName + " - profileTexture='" + profileTexture + "', profileUUID='" + profileUUID + "', isBot=" + (string)isBot(playerName)); // } if (updateProfileTexture == profileUUID) { if (isBot(playerName)) { updateProfileTexture = TEXTURE_BOT_PROFILE; // if (VERBOSE_LOGGING) { // llOwnerSay("🤖 [DEBUG] " + playerName + " is a bot, using bot texture"); // } } else if (updateProfileTexture == "" || updateProfileTexture == "00000000-0000-0000-0000-000000000000") { updateProfileTexture = TEXTURE_DEFAULT_PROFILE; dbg("Default texture"); } else { // CRITICAL: Set default texture FIRST, then request profile picture updateProfileTexture = TEXTURE_DEFAULT_PROFILE; // if (VERBOSE_LOGGING) { // llOwnerSay("🔄 [DEBUG] " + playerName + " making HTTP request for UUID: " + profileUUID); // } // Request profile picture via HTTP (will update later) #define URL_RESIDENT "https://world.secondlife.com/resident/" key httpRequestID = llHTTPRequest(URL_RESIDENT + profileUUID, [HTTP_METHOD, "GET"], ""); // Store the HTTP request mapping: requestID -> playerIndex httpRequests += [httpRequestID, playerIndex]; } } } // Enhanced visual effects for eliminated vs alive players vector primColor; if (lives <= 0) { // Eliminated player - red tint to make X more prominent primColor = <1.0, 0.3, 0.3>; // Red tinted } else { // Alive player - normal coloring primColor = <1.0, 1.0, 1.0>; // White/normal } // AGGRESSIVE VISUAL UPDATE: Force immediate rendering with glow llSetLinkPrimitiveParamsFast(updateProfilePrimIndex, [ PRIM_TEXTURE, ALL_SIDES, updateProfileTexture, <1,1,0>, <0,0,0>, 0.0, PRIM_COLOR, ALL_SIDES, primColor, 1.0, PRIM_TEXT, "", <1,1,1>, 0.0, // Remove player name text PRIM_GLOW, ALL_SIDES, 0.01 // Tiny glow to force re-render ]); // Remove the glow after a moment to make it normal llSleep(0.1); llSetLinkPrimitiveParamsFast(updateProfilePrimIndex, [ PRIM_GLOW, ALL_SIDES, 0.0 // Remove glow ]); // if (VERBOSE_LOGGING) { // llOwnerSay("🖼️ [Scoreboard] Applied texture " + profileTexture + " to prim " + (string)profilePrimIndex + " with forced refresh"); // } // Update hearts prim texture string updateHeartTexture = getHeartTexture(lives); llSetLinkPrimitiveParamsFast(updateHeartsPrimIndex, [ PRIM_TEXTURE, ALL_SIDES, updateHeartTexture, <1,1,0>, <0,0,0>, 0.0, PRIM_COLOR, ALL_SIDES, <1.0, 1.0, 1.0>, 1.0, PRIM_TEXT, "", <1,0,0>, 0.0 // Remove lives text ]); // Update overlay prim - show red X if eliminated, hide if alive if (lives <= 0) { // Show red X overlay for eliminated player llSetLinkPrimitiveParamsFast(updateOverlayPrimIndex, [ PRIM_TEXTURE, ALL_SIDES, TEXTURE_ELIMINATED_X, <1,1,0>, <0,0,0>, 0.0, PRIM_COLOR, ALL_SIDES, <1.0, 1.0, 1.0>, 1.0, // Fully visible PRIM_TEXT, "", <1,1,1>, 0.0 ]); } else { // Hide overlay for alive player (make it transparent) llSetLinkPrimitiveParamsFast(updateOverlayPrimIndex, [ PRIM_TEXTURE, ALL_SIDES, BLANK_TEXTURE, <1,1,0>, <0,0,0>, 0.0, PRIM_COLOR, ALL_SIDES, <1.0, 1.0, 1.0>, 0.0, // Fully transparent PRIM_TEXT, "", <1,1,1>, 0.0 ]); } // Update player glow effects after display changes updatePlayerGlowEffects(); return 0; } default { on_rez(integer start_param) { discoverLinks(); llResetScript(); } state_entry() { discoverLinks(); llListen(1, "", llGetOwner(), ""); REPORT_MEMORY(); dbg("Ready"); // Reset background prim to proper state resetBackgroundPrim(); // Reset manager cube to neutral state resetManagerCube(); // Load persistent leaderboard data from KVP Experience database loadLeaderboardData(); // Set a timer to passively auto-refresh the global scoreboard every 5 minutes llSetTimerEvent(300.0); // NOTE: generateLeaderboardText() will be triggered by dataserver automatically when data arrives // No need to generate it randomly before data arrives // Ensure no glow effects remain from previous sessions currentPerilPlayer = ""; currentWinner = ""; updatePlayerGlowEffects(); dbg("Init complete"); } link_message(integer sender, integer num, string str, key id) { // Handle messages from the game logic and bridges if (num == MSG_GAME_STATUS) { updateActionsPrim(str); } else if (num == MSG_PLAYER_UPDATE) { list parts = llParseString2List(str, ["|"], []); if (llGetListLength(parts) >= 3) { string playerName = llList2String(parts, 0); integer lives = (integer)llList2String(parts, 1); string profileUUID = llList2String(parts, 2); // Debug: Show what data scoreboard received - DISABLED to save memory dbg("Update: " + playerName); // } updatePlayerDisplay(playerName, lives, profileUUID); } } else if (num == MSG_CLEAR_GAME) { // Reset to default state resetBackgroundPrim(); // Reset background to black resetManagerCube(); // Reset manager cube to neutral clearAllPlayers(); // Clear all current players updateActionsPrim("Title"); // Reset to title // Clear peril player and winner tracking and remove any remaining glow currentPerilPlayer = ""; currentWinner = ""; updatePlayerGlowEffects(); // This will remove glow from all players generateLeaderboardText(); // Refresh leaderboard display } else if (num == MSG_GAME_WON) { handleGameWon(str); } else if (num == MSG_GAME_LOST) { handleGameLost(str); } else if (num == MSG_RESET_LEADERBOARD) { resetLeaderboard(); } else if (num == MSG_REMOVE_PLAYER) { removePlayer(str); } else if (num == MSG_UPDATE_PERIL_PLAYER) { // Update peril player and refresh glow effects string oldPeril = currentPerilPlayer; currentPerilPlayer = str; if (currentPerilPlayer == "NONE") { currentPerilPlayer = ""; } dbg("Peril: " + currentPerilPlayer); // Update glow effects updatePlayerGlowEffects(); } else if (num == MSG_UPDATE_WINNER) { // Update winner and refresh glow effects string oldWinner = currentWinner; currentWinner = str; if (currentWinner == "NONE") { currentWinner = ""; } dbg("Winner: " + currentWinner); // Update glow effects - winner glow overrides peril glow updatePlayerGlowEffects(); } else if (num == MSG_DISPLAY_LEADERBOARD && str == "REFRESH_STARTUP") { // Startup handshake from leaderboard bridge - re-send text now that boxes are ready dbg("📋 [Scoreboard] Handshake received! Refreshing virtual columns..."); generateLeaderboardText(); } } http_response(key request_id, integer status, list metadata, string body) { // Handle profile picture HTTP responses integer requestIndex = llListFindList(httpRequests, [request_id]); if (requestIndex != -1) { integer playerIndex = llList2Integer(httpRequests, requestIndex + 1); // Remove this request from tracking httpRequests = llDeleteSubList(httpRequests, requestIndex, requestIndex + 1); if (status == 200 && playerIndex < llGetListLength(activePlayers) / 3) { // Parse the HTML to extract profile image UUID string profileUUID = ""; // Try the meta tag method first integer metaStart = llSubStringIndex(body, PROFILE_KEY_PREFIX); if (metaStart != -1) { metaStart += PROFILE_KEY_PREFIX_LENGTH; string remainingBody = llGetSubString(body, metaStart, -1); integer metaEnd = llSubStringIndex(remainingBody, "\""); if (metaEnd != -1) { profileUUID = llGetSubString(remainingBody, 0, metaEnd - 1); } } // If meta tag failed, try img src method if (profileUUID == "") { integer imgStart = llSubStringIndex(body, PROFILE_IMG_PREFIX); if (imgStart != -1) { imgStart += PROFILE_IMG_PREFIX_LENGTH; string remainingBody = llGetSubString(body, imgStart, -1); integer imgEnd = llSubStringIndex(remainingBody, "/"); if (imgEnd != -1) { profileUUID = llGetSubString(remainingBody, 0, imgEnd - 1); } } } // If we got a valid UUID, update the profile prim and cache it if (profileUUID != "" && profileUUID != "00000000-0000-0000-0000-000000000000") { // Update the cached profile texture in stride activePlayers = llListReplaceList(activePlayers, [profileUUID], (playerIndex * 3) + 2, (playerIndex * 3) + 2); // Update the physical prim using correct mapping integer profilePrimIndex = getProfilePrimLink(playerIndex); // Safety check: Ensure the prim link was found if (profilePrimIndex > 0) { // AGGRESSIVE VISUAL UPDATE: Force immediate rendering llSetLinkPrimitiveParamsFast(profilePrimIndex, [ PRIM_TEXTURE, ALL_SIDES, profileUUID, <1,1,0>, <0,0,0>, 0.0, PRIM_COLOR, ALL_SIDES, <1.0, 1.0, 1.0>, 1.0, PRIM_GLOW, ALL_SIDES, 0.01 // Tiny glow to force re-render ]); // Remove the glow after a moment to make it normal llSleep(0.1); llSetLinkPrimitiveParamsFast(profilePrimIndex, [ PRIM_GLOW, ALL_SIDES, 0.0 // Remove glow ]); string playerName = llList2String(activePlayers, playerIndex * 3); dbg("Profile updated: " + playerName); } else { dbg("Invalid prim: " + (string)profilePrimIndex); } } } } } dataserver(key queryid, string data) { integer comma; if (queryid == kvpReadReq) { comma = llSubStringIndex(data, ","); if (comma != -1) { string status = llGetSubString(data, 0, comma - 1); string val = llGetSubString(data, comma + 1, -1); if (status == "1") { leaderboardData = []; list entries = llParseString2List(val, ["|"], []); integer i; for (i = 0; i < llGetListLength(entries); i++) { string data_entry = llList2String(entries, i); list parts = llParseString2List(data_entry, [":"], []); if (llGetListLength(parts) >= 3) { leaderboardData += [ llList2String(parts, 0), (integer)llList2String(parts, 1), (integer)llList2String(parts, 2) ]; } } dbg("LB Loaded"); } else if (status == "3") { leaderboardData = []; dbg("LB Empty"); } else { dbg("LB Error: " + status); } } // Generate board with downloaded or empty data generateLeaderboardText(); } else if (queryid == kvpWriteReq) { comma = llSubStringIndex(data, ","); if (comma != -1) { string status = llGetSubString(data, 0, comma - 1); if (status == "3") { // Update failed because key didn't exist yet! Create it. llCreateKeyValue("Peril_LB_Top50", pendingSerialize); dbg("LB Saved"); pendingSerialize = ""; } } } } timer() { dbg("Timer"); loadLeaderboardData(); } listen(integer channel, string name, key id, string message) { string msg = llToLower(llStringTrim(message, STRING_TRIM)); if (msg == "testx") { llOwnerSay("🛠️ [Scoreboard] Testing FULL LAYOUT on all 10 slots (Forced Alpha Reveal)..."); integer i; for (i = 0; i < 10; i++) { integer pIdx = getProfilePrimLink(i); integer hIdx = getHeartsPrimLink(i); integer oIdx = getOverlayPrimLink(i); if (pIdx > 0) { llSetLinkPrimitiveParamsFast(pIdx, [PRIM_COLOR, ALL_SIDES, <1,1,1>, 1.0]); llSetLinkPrimitiveParamsFast(hIdx, [PRIM_COLOR, ALL_SIDES, <1,1,1>, 1.0]); llSetLinkPrimitiveParamsFast(oIdx, [ PRIM_TEXTURE, ALL_SIDES, TEXTURE_ELIMINATED_X, <1,1,0>, <0,0,0>, 0.0, PRIM_COLOR, ALL_SIDES, <1.0, 1.0, 1.0>, 1.0 ]); } } } else if (msg == "clearx") { refreshPlayerDisplay(); } } }