Peril_Dice/Controller_MessageHandler.lsl
RebeccaNod1 03c5d81bb0 v2.8.6: Winner Glow Celebration System
- Winner Glow Effects: Bright green glow on both scoreboard and floaters for winners
- Extended Victory Display: 24-second celebration timer before game reset
- Enhanced Visual Feedback: Color-coded glow system (green/yellow/red)
- Victory Text Display: Winner floaters show 'ULTIMATE VICTORY!' and 'ULTIMATE SURVIVOR'
- Timer System Optimization: Dedicated victory timer mode, removed unused timeout system
- Memory Efficiency: Cleaned up unused components for better performance
- Synchronized Celebration: Both scoreboard and floater systems celebrate winner simultaneously
- Visual Priority: Winner glow overrides peril glow during victory celebration

Professional winner recognition system makes victory feel rewarding and celebrated!
2025-09-16 13:10:41 -05:00

498 lines
19 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

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.

// === Controller Message Handler Helper Script ===
// Handles non-critical message processing for the Main Controller
// Reduces Main Controller size and improves modularity
// Verbose logging control
integer VERBOSE_LOGGING = TRUE; // Global flag for verbose debug logs
integer MSG_TOGGLE_VERBOSE_LOGS = 9998; // Message to toggle verbose logging
// 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)");
}
// Message constants for communication with main controller
integer MSG_REQUEST_PLAYER_DATA = 8001;
integer MSG_REQUEST_PICK_DATA = 8002;
integer MSG_REQUEST_LIFE_DATA = 8003;
integer MSG_PICK_ACTION = 8004;
integer MSG_DICE_TYPE_REQUEST = 8005;
integer MSG_LEAVE_GAME_REQUEST = 8006;
integer MSG_OWNER_MESSAGE = 9030; // Send formatted owner message
integer MSG_PUBLIC_MESSAGE = 9031; // Send formatted public message
integer MSG_REGION_MESSAGE = 9032; // Send formatted region message
integer MSG_DIALOG_REQUEST = 9033; // Send formatted dialog
// Game state data synchronized from Main Controller
list players = [];
list names = [];
list lives = [];
list picksData = [];
list readyPlayers = [];
integer diceType = 6;
string perilPlayer = ""; // Track who the peril player is to determine if game is active
integer roundStarted = FALSE; // Track if the game round has started
// Message constants from Main Controller (synchronized)
integer MSG_SYNC_GAME_STATE = 107;
integer MSG_SYNC_PICKQUEUE = 2001;
// Channel constants for scoreboard updates
integer SCOREBOARD_CHANNEL_1;
integer SCOREBOARD_CHANNEL_2;
integer FLOATER_BASE_CHANNEL;
integer MSG_CLEANUP_FLOAT = 104;
integer MSG_UPDATE_FLOAT = 103;
// Dynamic channel calculation (matches Main Controller)
integer CHANNEL_BASE = -77000;
integer calculateChannel(integer offset) {
// Use BOTH owner's key AND object's key to make channels unique per game instance
string ownerStr = (string)llGetOwner();
string objectStr = (string)llGetKey();
string combinedStr = ownerStr + objectStr;
// Create a more unique hash using both keys
string hashStr = llMD5String(combinedStr, 0);
integer hash1 = llSubStringIndex("0123456789abcdef", llGetSubString(hashStr, 0, 0));
integer hash2 = llSubStringIndex("0123456789abcdef", llGetSubString(hashStr, 1, 1));
integer combinedHash = hash1 * 16 + hash2; // Creates 0-255 range
return CHANNEL_BASE - (offset * 1000) - combinedHash;
}
initializeChannels() {
SCOREBOARD_CHANNEL_1 = calculateChannel(6); // ~-77600 range
SCOREBOARD_CHANNEL_2 = calculateChannel(7); // ~-77700 range
FLOATER_BASE_CHANNEL = calculateChannel(9); // ~-77900 range
}
// Handle player list requests
handlePlayerListRequest(key requester) {
string namesCSV = llList2CSV(names);
llMessageLinked(LINK_SET, 203, namesCSV, requester);
}
// Handle pick data requests
handlePickDataRequest(string targetName, key requester) {
integer i;
for (i = 0; i < llGetListLength(picksData); i++) {
string rawEntry = llList2String(picksData, i);
list parts = llParseString2List(rawEntry, ["|"], []);
if (llList2String(parts, 0) == targetName) {
llMessageLinked(LINK_SET, 205, targetName + "|" + llList2String(parts, 1), requester);
return;
}
}
llMessageLinked(LINK_SET, 205, targetName + "|", requester);
}
// Handle life data requests
handleLifeDataRequest(string playerName, key requester) {
integer idx = llListFindList(names, [playerName]);
if (idx != -1) {
string lifeVal = llList2String(lives, idx);
llMessageLinked(LINK_SET, 208, playerName + "|" + lifeVal, requester);
}
}
// Handle pick actions (ADD_PICK/REMOVE_PICK)
handlePickAction(string actionData) {
list parts = llParseString2List(actionData, ["~"], []);
string action = llList2String(parts, 0);
list args = llParseString2List(llList2String(parts, 1), ["|"], []);
string name = llList2String(args, 0);
string pick = llList2String(args, 1);
integer i;
for (i = 0; i < llGetListLength(picksData); i++) {
string entry = llList2String(picksData, i);
list pdParts = llParseString2List(entry, ["|"], []);
if (llList2String(pdParts, 0) == name) {
list pickList = [];
string rawPicks = llList2String(pdParts, 1);
if (rawPicks != "") {
pickList = llParseString2List(rawPicks, [","], []);
}
if (action == "ADD_PICK") {
if (llListFindList(pickList, [pick]) == -1) {
pickList += [pick];
if (VERBOSE_LOGGING) {
llOwnerSay("➕ [Message Handler] Added " + pick + " to " + name);
}
}
} else if (action == "REMOVE_PICK") {
integer idx = llListFindList(pickList, [pick]);
if (idx != -1) {
pickList = llDeleteSubList(pickList, idx, idx);
if (VERBOSE_LOGGING) {
llOwnerSay("➖ [Message Handler] Removed " + pick + " from " + name);
}
}
}
// Update local picks data
picksData = llListReplaceList(picksData, [name + "|" + llList2CSV(pickList)], i, i);
// Notify Main Controller of the change
llMessageLinked(LINK_SET, 204, actionData, NULL_KEY);
return;
}
}
}
// Handle dice type requests
handleDiceTypeRequest(key requester) {
if (VERBOSE_LOGGING) {
llOwnerSay("🎲 [Message Handler] Sending dice type: " + (string)diceType);
}
llMessageLinked(LINK_SET, 102, (string)diceType, requester); // MSG_ROLL_RESULT
}
// Handle leave game and kick player requests
handleLeaveGameRequest(string requestData) {
list parts = llParseString2List(requestData, ["|"], []);
string action = llList2String(parts, 0);
if (action == "LEAVE_GAME" || action == "KICK_PLAYER") {
string leavingName = llList2String(parts, 1);
key requestKey = (key)llList2String(parts, 2); // This is the requester's key, not the player's key
integer idx = llListFindList(names, [leavingName]); // Find by name instead of key
if (idx != -1) {
// Update local lists FIRST to prevent race conditions
players = llDeleteSubList(players, idx, idx);
names = llDeleteSubList(names, idx, idx);
lives = llDeleteSubList(lives, idx, idx);
picksData = llDeleteSubList(picksData, idx, idx);
// THEN send floater cleanup using the previously calculated channel
// Note: We DON'T send MSG_CLEANUP_FLOAT here anymore to avoid double-cleanup
// The Main Controller will handle floater cleanup after list synchronization
if (VERBOSE_LOGGING) {
llOwnerSay("📝 [Message Handler] Updated local lists for " + leavingName + ", deferring floater cleanup to Main Controller");
}
// Remove from ready list if present
integer readyIdx = llListFindList(readyPlayers, [leavingName]);
if (readyIdx != -1) {
readyPlayers = llDeleteSubList(readyPlayers, readyIdx, readyIdx);
}
// Different messages for voluntary leave vs kick
if (action == "LEAVE_GAME") {
llOwnerSay("👋 [Message Handler] " + leavingName + " left the game");
llSay(0, "👋 " + leavingName + " has left the deadly game! 👋");
} else if (action == "KICK_PLAYER") {
llOwnerSay("👢 [Message Handler] " + leavingName + " was kicked from the game");
llSay(0, "👢 " + leavingName + " has been kicked from the deadly game by the owner! 👢");
}
// Notify Main Controller of the removal
llMessageLinked(LINK_SET, MSG_LEAVE_GAME_REQUEST, requestData, NULL_KEY);
// Check if game should end (0 players = reset, 1 player = victory)
if (llGetListLength(names) == 0) {
if (VERBOSE_LOGGING) {
llOwnerSay("🔄 [Message Handler] All players left - game will reset");
}
// Main Controller will handle the reset
} else if (llGetListLength(names) == 1 && roundStarted && perilPlayer != "" && perilPlayer != "NONE") {
// Only declare victory if the game has actually started (round active and peril player assigned)
string winner = llList2String(names, 0);
llSay(0, "✨ ULTIMATE VICTORY! " + winner + " is the Ultimate Survivor!");
// Notify Main Controller of victory
llMessageLinked(LINK_SET, 998, "GAME_WON|" + winner, NULL_KEY);
} else if (llGetListLength(names) == 1) {
// Game not started yet, just inform but don't declare victory
string remainingPlayer = llList2String(names, 0);
if (VERBOSE_LOGGING) {
llOwnerSay("📝 [Message Handler] Only " + remainingPlayer + " remains, but game hasn't started yet");
}
}
}
}
}
// Synchronize game state from Main Controller
syncGameState(string stateData) {
list parts = llParseString2List(stateData, ["~"], []);
// Handle special RESET sync message
if (llGetListLength(parts) >= 5 && llList2String(parts, 0) == "RESET") {
if (VERBOSE_LOGGING) {
llOwnerSay("🔄 [Message Handler] Received reset sync - clearing all state");
}
// Clear all state during reset
players = [];
names = [];
lives = [];
picksData = [];
readyPlayers = [];
perilPlayer = "";
roundStarted = FALSE;
return;
}
if (llGetListLength(parts) >= 4) {
lives = llCSV2List(llList2String(parts, 0));
// Decode picks data
string encodedPicksDataStr = llList2String(parts, 1);
picksData = [];
if (encodedPicksDataStr != "" && encodedPicksDataStr != "EMPTY") {
list encodedEntries = llParseString2List(encodedPicksDataStr, ["^"], []);
integer i;
for (i = 0; i < llGetListLength(encodedEntries); i++) {
string entry = llList2String(encodedEntries, i);
list entryParts = llParseString2List(entry, ["|"], []);
if (llGetListLength(entryParts) >= 2) {
string playerName = llList2String(entryParts, 0);
string picks = llList2String(entryParts, 1);
// Convert semicolons back to commas
picks = llDumpList2String(llParseString2List(picks, [";"], []), ",");
picksData += [playerName + "|" + picks];
} else {
picksData += [entry];
}
}
}
// Track peril player and round state for victory detection
string receivedPeril = llList2String(parts, 2);
if (receivedPeril == "NONE" || receivedPeril == "") {
perilPlayer = "";
roundStarted = FALSE;
} else {
perilPlayer = receivedPeril;
roundStarted = TRUE; // Game is active if there's a peril player
}
names = llCSV2List(llList2String(parts, 3));
// SAFETY: Validate peril player exists in current names list AFTER updating names
if (perilPlayer != "" && perilPlayer != "NONE") {
integer perilIdx = llListFindList(names, [perilPlayer]);
if (perilIdx == -1) {
// Only warn if we actually have players - during reset this is expected
if (llGetListLength(names) > 0 && VERBOSE_LOGGING) {
llOwnerSay("⚠️ [Message Handler] WARNING: Peril player '" + perilPlayer + "' not found in names list - clearing peril status");
}
perilPlayer = "";
roundStarted = FALSE;
}
}
}
}
// Memory-efficient message handling functions
// Handle owner messages
handleOwnerMessage(string messageData) {
// Format: "simple_message" or "type|param1|param2"
list parts = llParseString2List(messageData, ["|"], []);
if (llGetListLength(parts) == 1) {
// Simple message
llOwnerSay(messageData);
} else {
// Formatted message
string msgType = llList2String(parts, 0);
if (msgType == "JOIN") {
string playerName = llList2String(parts, 1);
llOwnerSay("👋 " + playerName + " joined the game!");
llSay(0, "🔔 Added player: " + playerName);
} else if (msgType == "LEAVE") {
string playerName = llList2String(parts, 1);
llOwnerSay("👋 " + playerName + " left the game.");
} else if (msgType == "ERROR") {
string error = llList2String(parts, 1);
llOwnerSay("⚠️ " + error);
} else if (msgType == "SUCCESS") {
string msg = llList2String(parts, 1);
llOwnerSay("✅ " + msg);
} else if (msgType == "DEBUG") {
string component = llList2String(parts, 1);
string msg = llList2String(parts, 2);
llOwnerSay("🔧 [" + component + "] " + msg);
}
}
}
// Handle public messages
handlePublicMessage(string messageData) {
list parts = llParseString2List(messageData, ["|"], []);
if (llGetListLength(parts) == 1) {
llSay(0, messageData);
} else {
string msgType = llList2String(parts, 0);
if (msgType == "GAME") {
string stateVal = llList2String(parts, 1);
llSay(0, "🎮 " + stateVal);
}
}
}
// Handle region messages to specific players
handleRegionMessage(string messageData) {
// Format: "player_key|message" or "player_key|type|param1|param2"
list parts = llParseString2List(messageData, ["|"], []);
if (llGetListLength(parts) >= 2) {
key playerKey = llList2Key(parts, 0);
string message = llList2String(parts, 1);
if (llGetListLength(parts) == 2) {
// Simple message - use llSay since we can't use llRegionSayTo on channel 0
llSay(0, "[To " + llKey2Name(playerKey) + "] " + message);
} else {
// Formatted message
string msgType = llList2String(parts, 1);
if (msgType == "WELCOME") {
string msg = llList2String(parts, 2);
llSay(0, "[To " + llKey2Name(playerKey) + "] 🔄 " + msg);
}
}
}
}
// Handle dialog requests
handleDialogRequest(string dialogData) {
// Format: "player_key|dialog_text|option1,option2,option3|channel"
list parts = llParseString2List(dialogData, ["|"], []);
if (llGetListLength(parts) >= 4) {
key playerKey = llList2Key(parts, 0);
string dialogText = llList2String(parts, 1);
string optionsStr = llList2String(parts, 2);
integer channel = llList2Integer(parts, 3);
list options = llParseString2List(optionsStr, [","], []);
llDialog(playerKey, dialogText, options, channel);
}
}
default {
state_entry() {
reportMemoryUsage("📨 Message Handler");
llOwnerSay("📨 [Message Handler] Helper script ready!");
initializeChannels();
}
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("🔊 [Message Handler] Verbose logging ENABLED");
} else {
llOwnerSay("🔊 [Message Handler] Verbose logging DISABLED");
}
return;
}
// Handle player list requests (for pick management only)
if (num == 202 && str == "REQUEST_PLAYER_LIST") {
handlePlayerListRequest(id);
return;
}
// Handle kick player list requests (separate message ID to avoid conflicts)
if (num == 8009 && str == "REQUEST_PLAYER_LIST_KICK") {
// Send back kick-specific message ID to avoid conflict with pick management
string namesCSV = llList2CSV(names);
if (VERBOSE_LOGGING) {
llOwnerSay("🔍 [Message Handler] Received request: '" + str + "'");
llOwnerSay("🔍 [Message Handler] Names list: " + llDumpList2String(names, ", "));
llOwnerSay("🔍 [Message Handler] Sending kick player list: '" + namesCSV + "'");
}
llMessageLinked(LINK_SET, 8009, namesCSV, id);
return;
}
// Handle pick data requests
if (num == 206) {
handlePickDataRequest(str, id);
return;
}
// Handle life data requests
if (num == 208) {
handleLifeDataRequest(str, id);
return;
}
// Handle pick actions
if (num == 204) {
handlePickAction(str);
return;
}
// Handle dice type requests
if (num == 996) {
if (str == "GET_DICE_TYPE") {
handleDiceTypeRequest(id);
}
return;
}
// Handle leave game requests (use proper message ID, not 107 which is for sync)
if (num == 8007) { // Different from MSG_SYNC_GAME_STATE = 107
handleLeaveGameRequest(str);
return;
}
// Sync game state from Main Controller
if (num == MSG_SYNC_GAME_STATE) {
syncGameState(str);
return;
}
// Sync player list for reference
if (num == MSG_SYNC_PICKQUEUE) {
// Update players list reference (if needed for future functionality)
return;
}
// Handle memory-heavy message operations
if (num == MSG_OWNER_MESSAGE) {
handleOwnerMessage(str);
return;
}
if (num == MSG_PUBLIC_MESSAGE) {
handlePublicMessage(str);
return;
}
if (num == MSG_REGION_MESSAGE) {
handleRegionMessage(str);
return;
}
if (num == MSG_DIALOG_REQUEST) {
handleDialogRequest(str);
return;
}
// Handle full reset
if (num == -99999 && str == "FULL_RESET") {
players = [];
names = [];
lives = [];
picksData = [];
readyPlayers = [];
diceType = 6;
perilPlayer = "";
roundStarted = FALSE;
llOwnerSay("📨 [Message Handler] Reset complete");
}
}
}