// Game Scoreboard Manager - Linkset Version // Shows current game players in grid layout // Each player gets a prim showing profile picture + heart texture (lives) // ============================================================================= // LINKSET COMMUNICATION - NO DISCOVERY NEEDED // ============================================================================= // Verbose logging control integer VERBOSE_LOGGING = FALSE; // Global flag for verbose debug logs - DISABLED to save memory #define MSG_TOGGLE_VERBOSE_LOGS 9998 // Message to toggle verbose logging // Message constants for link communication // Scoreboard messages (from link 1 - controller) #define MSG_GAME_STATUS 3001 #define MSG_PLAYER_UPDATE 3002 #define MSG_CLEAR_GAME 3003 #define MSG_REMOVE_PLAYER 3004 #define MSG_UPDATE_PERIL_PLAYER 3005 #define MSG_UPDATE_WINNER 3006 // Leaderboard messages (to link 35) #define MSG_GAME_WON 3010 #define MSG_GAME_LOST 3011 #define MSG_RESET_LEADERBOARD 3012 // Dice messages (to link 83) #define MSG_DICE_ROLL 3020 #define MSG_CLEAR_DICE 3021 // Prim indices (UPDATED after overlay prim insertion - overlays are links 2-11) #define BACKGROUND_PRIM 13 // Now link 13 (backboard) #define ACTIONS_PRIM 14 // Now link 14 (title/action) #define FIRST_PROFILE_PRIM 15 // Profile prims start at 15 #define LAST_PROFILE_PRIM 34 // Profile/life prims end at 34 #define FIRST_OVERLAY_PRIM 2 // Overlay prims start at 2 #define LAST_OVERLAY_PRIM 11 // Overlay prims end at 11 // 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 playerNames = []; // Current game players for display list playerLives = []; // Current game player lives list playerProfiles = []; // Maps to profile texture UUIDs // 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) list leaderboardNames = []; // Loaded from linkset data at startup list leaderboardWins = []; // Loaded from linkset data at startup list leaderboardLosses = []; // Loaded from linkset data at startup // HTTP tracking list profileRequests = []; // Track which profile requests belong to which player list httpRequests = []; // Track HTTP requests for profile pictures // Memory reporting function reportMemoryUsage(string scriptName) { integer used = llGetUsedMemory(); integer free = llGetFreeMemory(); integer total = used + free; float percentUsed = ((float)used / (float)total) * 100.0; llOwnerSay("🧠 [" + scriptName + "] Memory: " + (string)used + " used, " + (string)free + " free (" + llGetSubString((string)percentUsed, 0, 4) + "% used)"); } // Profile picture extraction constants string 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 ]); if (VERBOSE_LOGGING) { llOwnerSay("🖤 Reset background prim " + (string)BACKGROUND_PRIM + " to black"); } } // Reset the scoreboard manager cube (link 12) to neutral state resetManagerCube() { llSetLinkPrimitiveParamsFast(12, [ 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 ]); if (VERBOSE_LOGGING) { llOwnerSay("📦 Reset manager cube (link 12) to black"); } } // Update actions prim with appropriate status texture 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; // Don't change texture for unrecognized statuses } // SAFETY CHECK - prevent overflow into leaderboard prims if (ACTIONS_PRIM > LAST_PROFILE_PRIM) { if (VERBOSE_LOGGING) { llOwnerSay("❌ ERROR: ACTIONS_PRIM (" + (string)ACTIONS_PRIM + ") exceeds safety boundary (" + (string)LAST_PROFILE_PRIM + ")"); } return; } 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 ]); } // Load leaderboard data from linkset data loadLeaderboardData() { string countStr = llLinksetDataRead("lb_count"); integer loadCount = (integer)countStr; leaderboardNames = []; leaderboardWins = []; leaderboardLosses = []; if (loadCount > 0) { integer loadI; for (loadI = 0; loadI < loadCount; loadI++) { string name = llLinksetDataRead("lb_player_" + (string)loadI); string wins = llLinksetDataRead("lb_wins_" + (string)loadI); string losses = llLinksetDataRead("lb_losses_" + (string)loadI); if (name != "") { leaderboardNames += [name]; leaderboardWins += [(integer)wins]; leaderboardLosses += [(integer)losses]; } } } } // Save leaderboard data to linkset data saveLeaderboardData() { integer saveCount = llGetListLength(leaderboardNames); llLinksetDataWrite("lb_count", (string)saveCount); integer saveI; for (saveI = 0; saveI < saveCount; saveI++) { llLinksetDataWrite("lb_player_" + (string)saveI, llList2String(leaderboardNames, saveI)); llLinksetDataWrite("lb_wins_" + (string)saveI, (string)llList2Integer(leaderboardWins, saveI)); llLinksetDataWrite("lb_losses_" + (string)saveI, (string)llList2Integer(leaderboardLosses, saveI)); } } // Handle game won - update leaderboard handleGameWon(string winnerName) { integer idx = llListFindList(leaderboardNames, [winnerName]); if (idx == -1) { // New player leaderboardNames += [winnerName]; leaderboardWins += [1]; leaderboardLosses += [0]; } else { // Existing player integer currentWins = llList2Integer(leaderboardWins, idx); leaderboardWins = llListReplaceList(leaderboardWins, [currentWins + 1], idx, idx); } saveLeaderboardData(); generateLeaderboardText(); } // Handle game lost - update leaderboard handleGameLost(string loserName) { integer lostIdx = llListFindList(leaderboardNames, [loserName]); if (lostIdx == -1) { // New player leaderboardNames += [loserName]; leaderboardWins += [0]; leaderboardLosses += [1]; } else { // Existing player integer currentLosses = llList2Integer(leaderboardLosses, lostIdx); leaderboardLosses = llListReplaceList(leaderboardLosses, [currentLosses + 1], lostIdx, lostIdx); } saveLeaderboardData(); generateLeaderboardText(); } // Get sorted leaderboard data list getSortedLeaderboard() { list sortedLbData = []; integer sortI; string sortName; integer sortWins; integer sortLosses; string paddedWins; string paddedLosses; integer invertedLosses; string sortKey; string entry; list parts; list cleanedData = []; string winsStr; string lossesStr; // Create combined data for sorting with padded wins and losses for proper multi-level sorting for (sortI = 0; sortI < llGetListLength(leaderboardNames); sortI++) { sortName = llList2String(leaderboardNames, sortI); sortWins = llList2Integer(leaderboardWins, sortI); sortLosses = llList2Integer(leaderboardLosses, sortI); // Pad wins to 4 digits (higher wins = better rank) paddedWins = (string)(10000 + sortWins); paddedWins = llGetSubString(paddedWins, 1, -1); // Remove the "1" prefix // Pad losses to 4 digits, but INVERTED for tiebreaker (lower losses = better rank) // Use 9999 - losses so lower losses get higher sort value invertedLosses = 9999 - sortLosses; paddedLosses = (string)(10000 + invertedLosses); paddedLosses = llGetSubString(paddedLosses, 1, -1); // Remove the "1" prefix // Format: "PaddedWins-PaddedInvertedLosses:Name:ActualWins:ActualLosses" // This sorts first by wins (descending), then by losses (ascending) for tiebreaker sortKey = paddedWins + "-" + paddedLosses; entry = sortKey + ":" + sortName + ":" + (string)sortWins + ":" + (string)sortLosses; sortedLbData += [entry]; } // Sort by combined key (wins descending, losses ascending) - FALSE = descending order sortedLbData = llListSort(sortedLbData, 1, FALSE); // Clean up the format back to "Name:Wins:Losses" (skip padded wins in position 0) cleanedData = []; for (sortI = 0; sortI < llGetListLength(sortedLbData); sortI++) { entry = llList2String(sortedLbData, sortI); parts = llParseString2List(entry, [":"], []); if (llGetListLength(parts) >= 4) { // parts[0] = paddedWins (skip), parts[1] = name, parts[2] = actualWins, parts[3] = losses sortName = llList2String(parts, 1); winsStr = llList2String(parts, 2); lossesStr = llList2String(parts, 3); cleanedData += [sortName + ":" + winsStr + ":" + lossesStr]; } } return cleanedData; } // Generate leaderboard text and send to separate leaderboard object generateLeaderboardText() { list sortedData = getSortedLeaderboard(); // Build formatted leaderboard text (40 chars per line) string leaderboardText = " TOP BATTLE RECORDS \n"; integer genI; string playerData; list playerParts; string playerName; string genWins; string genLosses; integer rankNumber; string rank; string leftPart; string rightPart; integer spaceNeeded; string spacer; integer s; string line; integer actualPlayers; // Add actual player data (up to 11 players) for (genI = 0; genI < llGetListLength(sortedData) && genI < 11; genI++) { playerData = llList2String(sortedData, genI); playerParts = llParseString2List(playerData, [":"], []); if (llGetListLength(playerParts) >= 3) { playerName = llList2String(playerParts, 0); genWins = llList2String(playerParts, 1); genLosses = llList2String(playerParts, 2); // Truncate long names to fit better spacing if (llStringLength(playerName) > 12) { playerName = llGetSubString(playerName, 0, 9) + "..."; } // Format rank rankNumber = genI + 1; rank = (string)rankNumber; if (rankNumber < 10) rank = " " + rank; // Pad single digits // Create well-spaced line to fill full 40-character width leftPart = rank + ". " + playerName; rightPart = "W:" + genWins + "/L:" + genLosses; // Calculate spaces needed to fill exactly 40 characters spaceNeeded = 40 - llStringLength(leftPart) - llStringLength(rightPart); if (spaceNeeded < 1) spaceNeeded = 1; // At least 1 space spacer = ""; for (s = 0; s < spaceNeeded; s++) { spacer += " "; } line = leftPart + spacer + rightPart; leaderboardText += line + "\n"; } } // Fill remaining positions with placeholders to always show 11 positions actualPlayers = llGetListLength(sortedData); for (genI = actualPlayers; genI < 11; genI++) { rankNumber = genI + 1; rank = (string)rankNumber; if (rankNumber < 10) rank = " " + rank; // Pad single digits // Create placeholder line to fill full 40-character width leftPart = rank + ". --------"; rightPart = "W:0/L:0"; // Calculate spaces needed to fill exactly 40 characters spaceNeeded = 40 - llStringLength(leftPart) - llStringLength(rightPart); if (spaceNeeded < 1) spaceNeeded = 1; // At least 1 space spacer = ""; for (s = 0; s < spaceNeeded; s++) { spacer += " "; } line = leftPart + spacer + rightPart; leaderboardText += line + "\n"; } // Send formatted text to leaderboard bridge llMessageLinked(35, MSG_RESET_LEADERBOARD, "FORMATTED_TEXT|" + leaderboardText, NULL_KEY); } // Reset leaderboard data resetLeaderboard() { leaderboardNames = []; leaderboardWins = []; leaderboardLosses = []; integer resetI; for (resetI = 0; resetI < 100; resetI++) { llLinksetDataDelete("lb_player_" + (string)resetI); llLinksetDataDelete("lb_wins_" + (string)resetI); llLinksetDataDelete("lb_losses_" + (string)resetI); } llLinksetDataDelete("lb_count"); generateLeaderboardText(); // Send empty leaderboard } // Clear all current players from the scoreboard clearAllPlayers() { // Clear ONLY current game player data lists (NOT leaderboard data) playerNames = []; playerLives = []; playerProfiles = []; // Clear any pending HTTP requests httpRequests = []; profileRequests = []; // Reset all player prims to default state integer clearI; for (clearI = 0; clearI < 10; clearI++) { integer clearProfilePrimIndex = getProfilePrimLink(clearI); integer clearHeartsPrimIndex = getHeartsPrimLink(clearI); integer clearOverlayPrimIndex = getOverlayPrimLink(clearI); // SAFETY CHECK - prevent overflow into leaderboard prims if (clearProfilePrimIndex < FIRST_PROFILE_PRIM || clearProfilePrimIndex > LAST_PROFILE_PRIM) { if (VERBOSE_LOGGING) { llOwnerSay("❌ ERROR: Player prim index out of range! Profile: " + (string)clearProfilePrimIndex); } return; // Don't modify prims outside our range! } // Reset profile prim to default and clear any glow 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>, 1.0, PRIM_TEXT, "", <1,1,1>, 0.0, PRIM_GLOW, ALL_SIDES, 0.0 // Explicitly clear glow ]); // Reset hearts prim to 3 hearts and clear any glow 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>, 1.0, PRIM_TEXT, "", <1,1,1>, 0.0, PRIM_GLOW, ALL_SIDES, 0.0 // Explicitly clear glow ]); // 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 ]); } } // Remove a single player from the scoreboard display removePlayer(string playerName) { integer removeIdx = llListFindList(playerNames, [playerName]); if (removeIdx != -1) { // Remove player from internal lists playerNames = llDeleteSubList(playerNames, removeIdx, removeIdx); playerLives = llDeleteSubList(playerLives, removeIdx, removeIdx); playerProfiles = llDeleteSubList(playerProfiles, removeIdx, removeIdx); // Refresh the entire display to shift remaining players up refreshPlayerDisplay(); if (VERBOSE_LOGGING) { llOwnerSay("📊 Removed " + playerName + " from the scoreboard and shifted remaining players."); } } } // Refresh the entire player display - shows all current players in order refreshPlayerDisplay() { // Clear only unused player slots (beyond current player count) integer numActivePlayers = llGetListLength(playerNames); integer refreshI; // Only reset prims for unused slots (avoid flickering for active players) for (refreshI = numActivePlayers; refreshI < 10; refreshI++) { integer refreshProfilePrimIndex = getProfilePrimLink(refreshI); integer refreshHeartsPrimIndex = getHeartsPrimLink(refreshI); integer refreshOverlayPrimIndex = getOverlayPrimLink(refreshI); // SAFETY CHECK if (refreshProfilePrimIndex < FIRST_PROFILE_PRIM || refreshProfilePrimIndex > LAST_PROFILE_PRIM) { return; // Stop if we'd go out of bounds } // Reset to default only for unused slots 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>, 1.0, 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>, 1.0, 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 < llGetListLength(playerNames); refreshI++) { string name = llList2String(playerNames, refreshI); integer lives = llList2Integer(playerLives, refreshI); string profileTexture = llList2String(playerProfiles, refreshI); // Calculate prim indices for this position integer refreshProfileIdx = getProfilePrimLink(refreshI); integer refreshHeartsIdx = getHeartsPrimLink(refreshI); integer refreshOverlayIdx = getOverlayPrimLink(refreshI); // SAFETY CHECK if (refreshProfileIdx < FIRST_PROFILE_PRIM || refreshProfileIdx > LAST_PROFILE_PRIM) { if (VERBOSE_LOGGING) { llOwnerSay("⚠️ Warning: Player " + name + " cannot be displayed - out of prim range"); } return; } // Determine profile texture to use - don't replace with X, keep profile if (isBot(name)) { profileTexture = TEXTURE_BOT_PROFILE; } else if (profileTexture == "" || profileTexture == "00000000-0000-0000-0000-000000000000") { profileTexture = TEXTURE_DEFAULT_PROFILE; } // Note: If we have a cached profile texture, use it; otherwise use default for now // HTTP requests for new profile pictures will update later // Set profile texture (always normal coloring now) llSetLinkPrimitiveParamsFast(refreshProfileIdx, [ PRIM_TEXTURE, ALL_SIDES, 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(lives); llSetLinkPrimitiveParamsFast(refreshHeartsIdx, [ 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 (lives <= 0) { // Show red X overlay for eliminated player llSetLinkPrimitiveParamsFast(refreshOverlayIdx, [ 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 llSetLinkPrimitiveParamsFast(refreshOverlayIdx, [ 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 refreshing all players updatePlayerGlowEffects(); } // 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 integer getProfilePrimLink(integer playerIndex) { // From link scan: Row 1 (top) has profile 31,33; Row 2 has 27,29; etc. // Player index 0,1 = row 1; 2,3 = row 2; 4,5 = row 3; etc. // Each row has 4 prims: profile col1, hearts col1, profile col2, hearts col2 integer profileRow = playerIndex / 2; // Which row (0=top, 1=second, etc) integer profileCol = playerIndex % 2; // Which column (0=left, 1=right) // Base link for each row (profile col1): 31, 27, 23, 19, 15 integer profileBaseLink = 31 - (profileRow * 4); if (profileCol == 0) { // Column 1 (left): profile prim return profileBaseLink; } else { // Column 2 (right): profile prim (skip hearts prim) return profileBaseLink + 2; } } // Map player index to hearts prim link number integer getHeartsPrimLink(integer playerIndex) { // Hearts prims are adjacent to profile prims // Row 1: profile 31->hearts 32, profile 33->hearts 34 // Row 2: profile 27->hearts 28, profile 29->hearts 30, etc. integer heartsRow = playerIndex / 2; // Which row (0=top, 1=second, etc) integer heartsCol = playerIndex % 2; // Which column (0=left, 1=right) // Base link for each row (hearts col1): 32, 28, 24, 20, 16 integer heartsBaseLink = 32 - (heartsRow * 4); if (heartsCol == 0) { // Column 1 (left): hearts prim return heartsBaseLink; } else { // Column 2 (right): hearts prim (skip profile prim) return heartsBaseLink + 2; } } // Map player index to overlay prim link number integer getOverlayPrimLink(integer playerIndex) { // Overlay prims arrangement: row 1 col 1=3, row 1 col 2=2, row 2 col 1=5, row 2 col 2=4, etc. // Pattern: odd rows (0,2,4,6,8) start at 3,7,11 for col1 and 2,6,10 for col2 // Wait, let me check the actual pattern from the scan... // Link 2: overlay row 1 col 2, Link 3: overlay row 1 col 1 // Link 4: overlay row 2 col 2, Link 5: overlay row 2 col 1 // So pattern is: row N col 1 = 3 + (row * 2), row N col 2 = 2 + (row * 2) integer overlayRow = playerIndex / 2; integer overlayCol = playerIndex % 2; if (overlayCol == 0) { // Column 1: 3, 5, 7, 9, 11 return 3 + (overlayRow * 2); } else { // Column 2: 2, 4, 6, 8, 10 return 2 + (overlayRow * 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 } // Update all glow effects (peril and winner) updatePlayerGlowEffects() { // First, remove glow and reset tint from all players integer glowI; for (glowI = 0; glowI < llGetListLength(playerNames); glowI++) { integer glowProfilePrimIndex = getProfilePrimLink(glowI); integer glowHeartsPrimIndex = getHeartsPrimLink(glowI); // SAFETY CHECK if (glowProfilePrimIndex >= FIRST_PROFILE_PRIM && glowProfilePrimIndex <= LAST_PROFILE_PRIM) { llSetLinkPrimitiveParamsFast(glowProfilePrimIndex, [ PRIM_GLOW, ALL_SIDES, 0.0, PRIM_COLOR, ALL_SIDES, <1.0, 1.0, 1.0>, 1.0 ]); llSetLinkPrimitiveParamsFast(glowHeartsPrimIndex, [ PRIM_GLOW, ALL_SIDES, 0.0, PRIM_COLOR, ALL_SIDES, <1.0, 1.0, 1.0>, 1.0 ]); } } // Priority 1: Green glow for winner (overrides peril glow) if (currentWinner != "" && currentWinner != "NONE") { integer winnerIndex = llListFindList(playerNames, [currentWinner]); if (winnerIndex != -1) { integer winnerProfilePrimIndex = getProfilePrimLink(winnerIndex); integer winnerHeartsPrimIndex = getHeartsPrimLink(winnerIndex); // SAFETY CHECK if (winnerProfilePrimIndex >= FIRST_PROFILE_PRIM && winnerProfilePrimIndex <= LAST_PROFILE_PRIM) { // Add green glow + green tint to winner llSetLinkPrimitiveParamsFast(winnerProfilePrimIndex, [ PRIM_GLOW, ALL_SIDES, 0.3, PRIM_COLOR, ALL_SIDES, <0.0, 1.0, 0.0>, 1.0 ]); llSetLinkPrimitiveParamsFast(winnerHeartsPrimIndex, [ PRIM_GLOW, ALL_SIDES, 0.3, PRIM_COLOR, ALL_SIDES, <0.0, 1.0, 0.0>, 1.0 ]); if (VERBOSE_LOGGING) { llOwnerSay("🏆 Added green glow to winner: " + currentWinner); } } } } // Priority 2: Yellow glow for peril player (only if not the winner) else if (currentPerilPlayer != "" && currentPerilPlayer != "NONE") { integer perilIndex = llListFindList(playerNames, [currentPerilPlayer]); if (perilIndex != -1) { integer perilProfilePrimIndex = getProfilePrimLink(perilIndex); integer perilHeartsPrimIndex = getHeartsPrimLink(perilIndex); // SAFETY CHECK if (perilProfilePrimIndex >= FIRST_PROFILE_PRIM && perilProfilePrimIndex <= LAST_PROFILE_PRIM) { // Add yellow glow + yellow tint to peril player llSetLinkPrimitiveParamsFast(perilProfilePrimIndex, [ PRIM_GLOW, ALL_SIDES, 0.2, PRIM_COLOR, ALL_SIDES, <1.0, 1.0, 0.0>, 1.0 ]); llSetLinkPrimitiveParamsFast(perilHeartsPrimIndex, [ PRIM_GLOW, ALL_SIDES, 0.2, PRIM_COLOR, ALL_SIDES, <1.0, 1.0, 0.0>, 1.0 ]); if (VERBOSE_LOGGING) { llOwnerSay("✨ Added yellow glow to peril player: " + currentPerilPlayer); } } } } } // Backward compatibility wrapper updatePerilPlayerGlow() { updatePlayerGlowEffects(); } // Update player display on the grid updatePlayerDisplay(string playerName, integer lives, string profileUUID) { // Find existing player or add new one integer playerIndex = llListFindList(playerNames, [playerName]); if (playerIndex == -1) { // New player - find next available slot playerIndex = llGetListLength(playerNames); if (playerIndex >= 10) { if (VERBOSE_LOGGING) { llOwnerSay("WARNING: Maximum 10 players supported, ignoring: " + playerName); } return; } // Add to tracking lists playerNames += [playerName]; playerLives += [lives]; // For bots, always store the bot texture UUID to prevent HTTP requests if (isBot(playerName)) { playerProfiles += [TEXTURE_BOT_PROFILE]; } else { playerProfiles += [profileUUID]; } // 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 { // Update existing player playerLives = llListReplaceList(playerLives, [lives], playerIndex, playerIndex); // Don't overwrite cached profile texture with original UUID on updates } // Update the physical prims for this player integer updateProfilePrimIndex = getProfilePrimLink(playerIndex); integer updateHeartsPrimIndex = getHeartsPrimLink(playerIndex); integer updateOverlayPrimIndex = getOverlayPrimLink(playerIndex); // SAFETY CHECK - prevent overflow if (updateProfilePrimIndex < FIRST_PROFILE_PRIM || updateProfilePrimIndex > LAST_PROFILE_PRIM || updateOverlayPrimIndex < FIRST_OVERLAY_PRIM || updateOverlayPrimIndex > LAST_OVERLAY_PRIM) { if (VERBOSE_LOGGING) { llOwnerSay("❌ ERROR: Player prim index out of range! Profile: " + (string)updateProfilePrimIndex + ", Overlay: " + (string)updateOverlayPrimIndex + ", Player: " + (string)playerIndex); } return; // Don't modify prims outside our range! } // 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 if (VERBOSE_LOGGING) { llOwnerSay("💀 Scoreboard: Showing elimination X overlay for " + playerName); } // CRITICAL: Still need to set updateProfileTexture for eliminated players if (isBot(playerName)) { updateProfileTexture = TEXTURE_BOT_PROFILE; } else if (playerIndex < llGetListLength(playerProfiles)) { string updateCachedTexture = llList2String(playerProfiles, playerIndex); if (updateCachedTexture != "" && updateCachedTexture != profileUUID && updateCachedTexture != TEXTURE_DEFAULT_PROFILE && updateCachedTexture != TEXTURE_BOT_PROFILE && updateCachedTexture != TEXTURE_ELIMINATED_X) { // We have a previously fetched profile picture, use it updateProfileTexture = updateCachedTexture; } else { updateProfileTexture = TEXTURE_DEFAULT_PROFILE; } } 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 if (playerIndex < llGetListLength(playerProfiles)) { string liveCachedTexture = llList2String(playerProfiles, playerIndex); if (liveCachedTexture != "" && liveCachedTexture != profileUUID && liveCachedTexture != TEXTURE_DEFAULT_PROFILE && liveCachedTexture != TEXTURE_BOT_PROFILE && liveCachedTexture != TEXTURE_ELIMINATED_X) { // We have a previously fetched profile picture, use it 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; if (VERBOSE_LOGGING) { llOwnerSay("❓ [DEBUG] " + playerName + " has empty/null UUID, using 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]; // if (VERBOSE_LOGGING) { // llOwnerSay("🔄 [Scoreboard] Requesting profile pic for " + playerName + ", showing default for now"); // } } } else { // if (VERBOSE_LOGGING) { // llOwnerSay("🔍 [DEBUG] " + playerName + " - profileTexture != profileUUID, not making HTTP request"); // } } } // 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(); } default { state_entry() { reportMemoryUsage("Scoreboard Manager"); if (VERBOSE_LOGGING) { llOwnerSay("📈 Scoreboard Manager ready! (Linkset Version)"); llOwnerSay("📈 Managing prims " + (string)FIRST_PROFILE_PRIM + "-" + (string)LAST_PROFILE_PRIM); } // Initialize profile picture extraction constants profile_key_prefix_length = llStringLength(profile_key_prefix); profile_img_prefix_length = llStringLength(profile_img_prefix); // Reset background prim to proper state resetBackgroundPrim(); // Reset manager cube to neutral state resetManagerCube(); // Load persistent leaderboard data loadLeaderboardData(); // Generate initial leaderboard generateLeaderboardText(); // Ensure no glow effects remain from previous sessions currentPerilPlayer = ""; currentWinner = ""; updatePlayerGlowEffects(); if (VERBOSE_LOGGING) { llOwnerSay("✅ Linkset communication active - no discovery needed!"); llOwnerSay("📊 Loaded " + (string)llGetListLength(leaderboardNames) + " players from leaderboard data"); } } link_message(integer sender, integer num, string str, key id) { // Handle verbose logging toggle if (num == MSG_TOGGLE_VERBOSE_LOGS) { VERBOSE_LOGGING = !VERBOSE_LOGGING; if (VERBOSE_LOGGING) { llOwnerSay("🔊 [Scoreboard] Verbose logging ENABLED"); } else { llOwnerSay("🔊 [Scoreboard] Verbose logging DISABLED"); } return; } // Only listen to messages from the main controller (link 1) if (sender != 1) return; 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 // if (VERBOSE_LOGGING) { // llOwnerSay("📈 Scoreboard received update: " + playerName + " has " + (string)lives + " hearts"); // } 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 = ""; } if (VERBOSE_LOGGING && oldPeril != currentPerilPlayer) { llOwnerSay("🎯 Peril player changed from '" + oldPeril + "' to '" + 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 = ""; } if (VERBOSE_LOGGING && oldWinner != currentWinner) { llOwnerSay("🏆 Winner changed from '" + oldWinner + "' to '" + currentWinner + "'"); } // Update glow effects - winner glow overrides peril glow updatePlayerGlowEffects(); } } 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(playerNames)) { // 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 playerProfiles = llListReplaceList(playerProfiles, [profileUUID], playerIndex, playerIndex); // Update the physical prim using correct mapping integer profilePrimIndex = getProfilePrimLink(playerIndex); // SAFETY CHECK if (profilePrimIndex >= FIRST_PROFILE_PRIM && profilePrimIndex <= LAST_PROFILE_PRIM) { // 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 ]); if (VERBOSE_LOGGING) { string playerName = llList2String(playerNames, playerIndex); llOwnerSay("🖼️ Updated profile picture for " + playerName + " on prim " + (string)profilePrimIndex + " with forced refresh"); } } else { if (VERBOSE_LOGGING) { llOwnerSay("❌ ERROR: HTTP response tried to update invalid prim " + (string)profilePrimIndex + " for player " + (string)playerIndex); } } } } } } touch_start(integer total_number) { // Owner can still access some debug functions by touching scoreboard if (llDetectedKey(0) == llGetOwner()) { llOwnerSay("📊 Scoreboard Status:"); llOwnerSay(" Active players: " + (string)llGetListLength(playerNames)); llOwnerSay(" Leaderboard entries: " + (string)llGetListLength(leaderboardNames)); llOwnerSay(" HTTP requests pending: " + (string)(llGetListLength(httpRequests) / 2)); } } }