Peril_Dice/Game_Scoreboard_Manager_Linkset.lsl

1145 lines
48 KiB
Text
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#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;
// Link mappings for player grid (Rows 0-4, Columns 0-1)
list profileLinks = [0,0,0,0,0,0,0,0,0,0];
list heartsLinks = [0,0,0,0,0,0,0,0,0,0];
list overlayLinks = [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 lists
profileLinks = [0,0,0,0,0,0,0,0,0,0];
heartsLinks = [0,0,0,0,0,0,0,0,0,0];
overlayLinks = [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) {
if (type == "profile") profileLinks = llListReplaceList(profileLinks, [i], index, index);
else if (type == "life") heartsLinks = llListReplaceList(heartsLinks, [i], index, index);
else if (type == "overlay") overlayLinks = llListReplaceList(overlayLinks, [i], index, index);
}
}
}
}
dbg("📈 [Scoreboard Manager] 🔍 Dynamic Discovery complete!");
if (BACKGROUND_PRIM == -1) dbg("⚠️ WARNING: Background prim ('backboard:0:0') not found!");
}
// --- Leaderboard Configuration ---
#define LEADERBOARD_WIDTH 32 // Standard width for 4x8 character Furware grids (4 columns of 8)
#define MAX_LEADERBOARD_ENTRIES 11 // Fits within 12-row board (+1 for title)
// 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
// KVP tracking
key kvpReadReq;
key kvpWriteReq;
string pendingSerialize;
// Memory reporting function
integer reportMemoryUsage(string scriptName) {
integer used = llGetUsedMemory();
integer free = llGetFreeMemory();
integer total = used + free;
float percentUsed = ((float)used / (float)total) * 100.0;
dbg("🧠 [" + scriptName + "] Memory: " +
(string)used + " used, " +
(string)free + " free (" +
llGetSubString((string)percentUsed, 0, 4) + "% used)");
return 0;
}
// Profile picture extraction constants
string profile_key_prefix = "<meta name=\"imageid\" content=\"";
string profile_img_prefix = "<img alt=\"profile image\" src=\"http://secondlife.com/app/image/";
integer profile_key_prefix_length;
integer profile_img_prefix_length;
// Reset background prim to proper black background
integer resetBackgroundPrim() {
// Reset main background prim (link 13)
llSetLinkPrimitiveParamsFast(BACKGROUND_PRIM, [
PRIM_TEXTURE, ALL_SIDES, TEXTURE_BACKGROUND, <1,1,0>, <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("📈 [Scoreboard Manager] 🖤 Reset background prim " + (string)BACKGROUND_PRIM + " to black");
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("📈 [Scoreboard Manager] 📦 Reset manager cube (link 12) to black");
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("📈 [Scoreboard Manager] 🔍 Requesting global scoreboard from Experience database...");
return 0;
}
// Save leaderboard data to Experience KVP database
integer saveLeaderboardData() {
string serialized = "";
integer saveCount = llGetListLength(leaderboardNames);
if (saveCount > 50) saveCount = 50; // Cap at Top 50 to fit safely in KVP 4Kb limit
integer saveI;
for (saveI = 0; saveI < saveCount; saveI++) {
string save_entry = llList2String(leaderboardNames, saveI) + ":" +
(string)llList2Integer(leaderboardWins, saveI) + ":" +
(string)llList2Integer(leaderboardLosses, saveI);
if (serialized != "") serialized += "|";
serialized += save_entry;
}
pendingSerialize = serialized;
kvpWriteReq = llUpdateKeyValue("Peril_LB_Top50", serialized, FALSE, "");
dbg("📈 [Scoreboard Manager] 💾 Synced " + (string)saveCount + " players to global scoreboard database.");
return 0;
}
// Handle game won - update leaderboard
integer 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();
return 0;
}
// Handle game lost - update leaderboard
integer 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();
return 0;
}
// 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 sorted_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;
sorted_entry = sortKey + ":" + sortName + ":" + (string)sortWins + ":" + (string)sortLosses;
sortedLbData += [sorted_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++) {
sorted_entry = llList2String(sortedLbData, sortI);
parts = llParseString2List(sorted_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
integer generateLeaderboardText() {
list sortedData = getSortedLeaderboard();
// 1. Build Title Row (Centered)
string title = "TOP BATTLE RECORDS";
integer titleLen = llStringLength(title);
integer titleMargin = (LEADERBOARD_WIDTH - titleLen) / 2;
if (titleMargin < 0) titleMargin = 0;
string titleSpaces = "";
integer ts;
for (ts = 0; ts < titleMargin; ts++) titleSpaces += " ";
string leaderboardText = titleSpaces + title + "\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 MAX_LEADERBOARD_ENTRIES)
for (genI = 0; genI < llGetListLength(sortedData) && genI < MAX_LEADERBOARD_ENTRIES; genI++) {
playerData = llList2String(sortedData, genI);
playerParts = llParseString2List(playerData, [":"], []);
if (llGetListLength(playerParts) >= 3) {
playerName = llList2String(playerParts, 0);
genWins = llList2String(playerParts, 1);
genLosses = llList2String(playerParts, 2);
// [Rank(4) + Name(14) + Gaps + Stats(10) = 32 chars total]
if (llStringLength(playerName) > 12) {
playerName = llGetSubString(playerName, 0, 9) + "...";
}
// Format rank string: "01. " through "10. " (always 4 chars)
rankNumber = genI + 1;
rank = (string)rankNumber + ". ";
if (rankNumber < 10) rank = "0" + rank;
// Format stats with fixed-width padding to align columns
// Standardizing to 10 characters: "W: 99/L: 9"
string sWins = (string)genWins;
while (llStringLength(sWins) < 3) sWins = " " + sWins;
string sLosses = (string)genLosses;
while (llStringLength(sLosses) < 2) sLosses = " " + sLosses;
leftPart = rank + playerName;
rightPart = "W:" + sWins + "/L:" + sLosses; // Exactly 10 characters
// Push rightPart to column 32
spaceNeeded = LEADERBOARD_WIDTH - llStringLength(leftPart) - llStringLength(rightPart);
if (spaceNeeded < 1) spaceNeeded = 1;
spacer = "";
for (s = 0; s < spaceNeeded; s++) spacer += " ";
line = leftPart + spacer + rightPart;
leaderboardText += line + "\n";
}
}
// Fill remaining positions with placeholders
actualPlayers = llGetListLength(sortedData);
for (genI = actualPlayers; genI < MAX_LEADERBOARD_ENTRIES; genI++) {
rankNumber = genI + 1;
rank = (string)rankNumber + ". ";
if (rankNumber < 10) rank = "0" + rank;
// Use EXACT same formatting as players so columns align!
leftPart = rank + "----------";
rightPart = "W: 0/L: 0"; // Matches 10-char fixed width
spaceNeeded = LEADERBOARD_WIDTH - llStringLength(leftPart) - llStringLength(rightPart);
if (spaceNeeded < 1) spaceNeeded = 1;
spacer = "";
for (s = 0; s < spaceNeeded; s++) spacer += " ";
line = leftPart + spacer + rightPart;
leaderboardText += line + "\n";
}
// Send formatted text to leaderboard bridge
llMessageLinked(LINK_LEADERBOARD_BRIDGE, MSG_RESET_LEADERBOARD, "FORMATTED_TEXT|" + leaderboardText, 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;
}
leaderboardNames = [];
leaderboardWins = [];
leaderboardLosses = [];
pendingSerialize = "";
kvpWriteReq = llUpdateKeyValue("Peril_LB_Top50", "", FALSE, "");
generateLeaderboardText(); // Send empty leaderboard immediately
return 0;
}
// Clear all current players from the scoreboard
integer clearAllPlayers() {
// Clear ONLY current game player data lists (NOT leaderboard data)
playerNames = [];
playerLives = [];
playerProfiles = [];
// 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 = llList2Integer(profileLinks, clearI);
clearHeartsPrimIndex = llList2Integer(heartsLinks, clearI);
clearOverlayPrimIndex = llList2Integer(overlayLinks, clearI);
// SAFETY CHECK - use if block instead of continue for maximum compatibility
if (clearProfilePrimIndex > 0) {
// 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
]);
}
}
return 0;
}
// Remove a single player from the scoreboard display
integer 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();
dbg("📈 [Scoreboard Manager] 📊 Removed " + playerName + " from the scoreboard and shifted remaining players.");
}
return 0;
}
// Refresh the entire player display - shows all current players in order
integer refreshPlayerDisplay() {
integer numActivePlayers = llGetListLength(playerNames);
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 = llList2Integer(profileLinks, refreshI);
refreshHeartsPrimIndex = llList2Integer(heartsLinks, refreshI);
refreshOverlayPrimIndex = llList2Integer(overlayLinks, refreshI);
if (refreshProfilePrimIndex > 0) {
// 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 < numActivePlayers; refreshI++) {
r_name = llList2String(playerNames, refreshI);
r_lives = llList2Integer(playerLives, refreshI);
r_profileTexture = llList2String(playerProfiles, refreshI);
// Calculate prim indices for this position
r_profileIdx = llList2Integer(profileLinks, refreshI);
r_heartsIdx = llList2Integer(heartsLinks, refreshI);
r_overlayIdx = llList2Integer(overlayLinks, 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(profileLinks, playerIndex); }
integer getHeartsPrimLink(integer playerIndex) { return llList2Integer(heartsLinks, playerIndex); }
integer getOverlayPrimLink(integer playerIndex) { return llList2Integer(overlayLinks, playerIndex); }
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)
integer updatePlayerGlowEffects() {
// First, remove glow and reset tint from all players
integer glowI;
integer glowProfilePrimIndex;
integer glowHeartsPrimIndex;
for (glowI = 0; glowI < llGetListLength(playerNames); glowI++) {
glowProfilePrimIndex = llList2Integer(profileLinks, glowI);
glowHeartsPrimIndex = llList2Integer(heartsLinks, glowI);
if (glowProfilePrimIndex > 0) {
llSetLinkPrimitiveParamsFast(glowProfilePrimIndex, [
PRIM_GLOW, ALL_SIDES, 0.0,
PRIM_COLOR, ALL_SIDES, <1.0, 1.0, 1.0>, 1.0
]);
}
if (glowHeartsPrimIndex > 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);
// Check if the prim exists (discovered index > 0)
if (winnerProfilePrimIndex > 0) {
// 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
]);
// Also add glow to hearts if found
if (winnerHeartsPrimIndex > 0) {
llSetLinkPrimitiveParamsFast(winnerHeartsPrimIndex, [
PRIM_GLOW, ALL_SIDES, 0.3,
PRIM_COLOR, ALL_SIDES, <0.0, 1.0, 0.0>, 1.0
]);
}
dbg("📈 [Scoreboard Manager] 🏆 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);
// Check if the prim exists (discovered index > 0)
if (perilProfilePrimIndex > 0) {
// 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
]);
// Also add glow to hearts if found
if (perilHeartsPrimIndex > 0) {
llSetLinkPrimitiveParamsFast(perilHeartsPrimIndex, [
PRIM_GLOW, ALL_SIDES, 0.2,
PRIM_COLOR, ALL_SIDES, <1.0, 1.0, 0.0>, 1.0
]);
}
dbg("📈 [Scoreboard Manager] ✨ Added yellow glow to peril player: " + currentPerilPlayer);
}
}
}
return 0;
}
// Backward compatibility wrapper
integer updatePerilPlayerGlow() {
updatePlayerGlowEffects();
return 0;
}
// Update player display on the grid
integer 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) {
dbg("📈 [Scoreboard Manager] ⚠️ WARNING: Maximum 10 players supported, ignoring: " + playerName);
return 0;
}
// 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 = llList2Integer(profileLinks, playerIndex);
integer updateHeartsPrimIndex = llList2Integer(heartsLinks, playerIndex);
integer updateOverlayPrimIndex = llList2Integer(overlayLinks, 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 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;
dbg("📈 [Scoreboard Manager] ❓ [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");
// }
}
}
}
// 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(), "");
reportMemoryUsage("Scoreboard Manager");
dbg("📈 [Scoreboard Manager] 📈 Managing profile and heart prims via dynamic discovery.");
// 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 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("📈 [Scoreboard Manager] ✅ Linkset communication active - no discovery needed!");
dbg("📈 [Scoreboard Manager] 📊 Loaded " + (string)llGetListLength(leaderboardNames) + " players from leaderboard data");
}
link_message(integer sender, integer num, string str, key id) {
// Only listen to messages from the main controller (link 1)
if (sender != LINK_CONTROLLER) 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
dbg("📈 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 = "";
}
dbg("📈 [Scoreboard Manager] 🎯 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 = "";
}
dbg("📈 [Scoreboard Manager] 🏆 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: 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(playerNames, playerIndex);
dbg("📈 [Scoreboard Manager] 🖼️ Updated profile picture for " + playerName + " on prim " + (string)profilePrimIndex + " with forced refresh");
} else {
dbg("📈 [Scoreboard Manager] ❌ ERROR: HTTP response tried to update invalid prim " + (string)profilePrimIndex + " for player " + (string)playerIndex);
}
}
}
}
}
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") {
leaderboardNames = [];
leaderboardWins = [];
leaderboardLosses = [];
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) {
leaderboardNames += [llList2String(parts, 0)];
leaderboardWins += [(integer)llList2String(parts, 1)];
leaderboardLosses += [(integer)llList2String(parts, 2)];
}
}
dbg("📈 [Scoreboard Manager] ✅ Loaded global scoreboard containing " + (string)llGetListLength(leaderboardNames) + " players!");
} else if (status == "3") { // XP_ERROR_KEY_NOT_FOUND
leaderboardNames = [];
leaderboardWins = [];
leaderboardLosses = [];
dbg("📈 [Scoreboard Manager] ℹ️ Global scoreboard is currently empty (database fresh).");
} else {
dbg("📈 [Scoreboard Manager] ⚠️ Failed to load global scoreboard (Status: " + 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);
} else if (status == "1") {
dbg("📈 [Scoreboard Manager] ✅ Global scoreboard synced successfully.");
}
}
}
}
timer() {
dbg("📈 [Scoreboard Manager] ⏱️ Auto-refreshing Global Scoreboard Data...");
loadLeaderboardData();
}
listen(integer channel, string name, key id, string message) {
string msg = llToLower(llStringTrim(message, STRING_TRIM));
if (msg == "testx") {
llOwnerSay("Testing X overlays on all 10 player slots...");
integer i;
integer overlayIdx;
for (i = 0; i < 10; i++) {
overlayIdx = getOverlayPrimLink(i);
if (overlayIdx > 0) {
llSetLinkPrimitiveParamsFast(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
]);
}
}
}
else if (msg == "clearx") {
dbg("Refreshing display to clear test X overlays...");
refreshPlayerDisplay();
}
}
}