Peril_Dice/Main_Controller_Linkset.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

1290 lines
58 KiB
Text

// === Main Controller - Peril Dice Game (Linkset Version) ===
// Helper function to get display name with fallback to username
string getPlayerName(key id) {
string displayName = llGetDisplayName(id);
if (displayName == "") {
// Fallback to legacy username if display name is unavailable
displayName = llKey2Name(id);
}
return displayName;
}
//
// LINKSET VERSION - Uses llMessageLinked() instead of llRegionSay()
// All discovery and channel management code removed
//
// =============================================================================
// LINKSET COMMUNICATION CONSTANTS
// =============================================================================
// Target link numbers (UPDATED after overlay prim insertion)
integer SCOREBOARD_LINK = 12; // Scoreboard manager cube
integer LEADERBOARD_LINK = 35; // Leaderboard manager (first XyzzyText prim)
integer DICE_LINK = 83; // Dice display manager (first dice prim)
// Message constants for link communication
// Scoreboard messages (to link 12)
integer MSG_GAME_STATUS = 3001;
integer MSG_PLAYER_UPDATE = 3002;
integer MSG_CLEAR_GAME = 3003;
integer MSG_REMOVE_PLAYER = 3004;
integer MSG_UPDATE_PERIL_PLAYER = 3005;
integer MSG_UPDATE_WINNER = 3006;
// Leaderboard messages (to link 35)
integer MSG_GAME_WON = 3010;
integer MSG_GAME_LOST = 3011;
integer MSG_RESET_LEADERBOARD = 3012;
// Dice messages (to link 83)
integer MSG_DICE_ROLL = 3020;
integer MSG_CLEAR_DICE = 3021;
// Legacy message constants (keep for internal controller communication)
integer MSG_SHOW_DIALOG = 101;
integer MSG_ROLL_RESULT = 102;
integer MSG_UPDATE_FLOAT = 103;
integer MSG_CLEANUP_FLOAT = 104;
integer MSG_REZ_FLOAT = 105;
integer MSG_REGISTER_PLAYER = 106;
integer MSG_REGISTER_PLAYER_REQUEST = 9050; // New dedicated message to Player_RegistrationManager
integer MSG_SYNC_GAME_STATE = 107;
integer MSG_SHOW_MENU = 201;
integer MSG_TOGGLE_READY = 202;
integer MSG_QUERY_READY_STATE = 210;
integer MSG_READY_STATE_RESULT = 211;
integer MSG_CLEANUP_ALL_FLOATERS = 212;
integer MSG_QUERY_OWNER_STATUS = 213;
integer MSG_OWNER_STATUS_RESULT = 214;
integer MSG_SHOW_ROLL_DIALOG = 301;
integer MSG_GET_CURRENT_DIALOG = 302;
integer MSG_PLAYER_WON = 551;
integer MSG_GET_PICKS_REQUIRED = 1002;
integer MSG_GET_PICKER_INDEX = 1003;
// Memory monitoring handled by Controller_Memory.lsl helper script
integer MSG_MEMORY_CHECK = 6001;
integer MSG_MEMORY_STATS = 6002;
integer MSG_MEMORY_CLEANUP = 6003;
integer MSG_MEMORY_REPORT = 6004;
integer MSG_EMERGENCY_CLEANUP = 6005;
integer MSG_MEMORY_STATS_REQUEST = 6006;
// Unified Timer System - prevents conflicts between multiple timer needs
integer TIMER_IDLE = 0;
integer TIMER_STATUS = 1;
integer TIMER_VICTORY_DELAY = 2; // Timer mode for victory glow display
integer currentTimerMode = 0; // Track what the timer is currently doing
float timerInterval = 1.0; // How often timer() is called for checks
integer victoryDelayTimer = 0; // Track victory delay timing
// Debug control - set to TRUE for verbose pick debugging, FALSE for normal operation
integer DEBUG_PICKS = FALSE;
// Verbose logging moved to dedicated Verbose_Logger.lsl script to save memory
// Message constants for verbose logging
integer MSG_VERBOSE_LOG = 9020;
integer MSG_VERBOSE_TOGGLE = 9010;
// Script ID for Main Controller (used by Verbose_Logger for prefixes)
integer SCRIPT_ID_MAIN = 0;
// Message handling constants (delegate to Controller_MessageHandler)
integer MSG_OWNER_MESSAGE = 9030;
integer MSG_PUBLIC_MESSAGE = 9031;
integer MSG_REGION_MESSAGE = 9032;
integer MSG_DIALOG_REQUEST = 9033;
// 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)");
}
// REMOVED: debugMsg() - too memory intensive for Main Controller
// REMOVED: Heavy message forwarding functions - too memory intensive
// Status message timing
float STATUS_DISPLAY_TIME = 8.0; // How long to show status messages on scoreboard
integer statusTimer = 0; // Track when status messages were sent
string lastStatus = ""; // Track last status sent
// Game state variables
list players = [];
list names = [];
list lives = [];
string perilPlayer = "";
list globalPickedNumbers = [];
list picksData = [];
list readyPlayers = [];
list floaterChannels = []; // Track actual floater channels for cleanup
// Victory state tracking
integer victoryInProgress = FALSE;
// Lockout system variables
integer isLocked = FALSE;
key gameOwner;
list pickQueue = [];
integer currentPickerIdx = 0;
integer roundStarted = FALSE;
integer gameStarting = FALSE; // Track when game is in startup sequence
// Prevent duplicate registration by tracking pending registrations
list pendingRegistrations = []; // Keys of players who have registration in progress
integer REGISTRATION_TIMEOUT = 10; // Seconds to keep pending registration
// Display configuration
integer CONTROLLER_FACE = 1; // Face to display start image on (front face)
string TEXTURE_START = "title_start"; // Start image texture
string TEXTURE_GAME_ACTIVE = "game_active"; // Optional: different texture during game
// Maximum number of players allowed (including test players)
integer MAX_PLAYERS = 10;
// Timeout system removed - using owner kick functionality instead
key currentPicker;
// Game timing settings
float BOT_PICK_DELAY = 2.0;
float HUMAN_PICK_DELAY = 1.0;
float DIALOG_DELAY = 1.5;
integer gameTimer = 0;
// Dynamic channel system for floaters and dialogs (still needed for external communication)
integer CHANNEL_BASE = -77000;
integer calculateChannel(integer offset) {
string ownerStr = (string)llGetOwner();
string objectStr = (string)llGetKey();
string combinedStr = ownerStr + objectStr;
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;
return CHANNEL_BASE - (offset * 1000) - combinedHash;
}
// Dynamic channels still needed for external communication (floaters, dialogs)
integer SYNC_CHANNEL;
integer NUMBERPICK_CHANNEL;
integer ROLLDIALOG_CHANNEL;
integer MAIN_DIALOG_CHANNEL;
integer BOT_COMMAND_CHANNEL;
integer FLOATER_BASE_CHANNEL;
// Legacy channel variables for backward compatibility
integer syncChannel;
integer numberPickChannel;
integer rollDialogChannel;
integer DIALOG_CHANNEL;
// Listen handle management
integer dialogHandle = -1;
integer botHandle = -1;
integer rollHandle = -1;
// Channel initialization function
initializeChannels() {
SYNC_CHANNEL = calculateChannel(1);
NUMBERPICK_CHANNEL = calculateChannel(2);
ROLLDIALOG_CHANNEL = calculateChannel(3);
MAIN_DIALOG_CHANNEL = calculateChannel(4);
BOT_COMMAND_CHANNEL = calculateChannel(5);
FLOATER_BASE_CHANNEL = calculateChannel(9);
// Set legacy variables for backward compatibility
syncChannel = SYNC_CHANNEL;
numberPickChannel = NUMBERPICK_CHANNEL;
rollDialogChannel = ROLLDIALOG_CHANNEL;
DIALOG_CHANNEL = MAIN_DIALOG_CHANNEL;
}
// Clean up listen handles
cleanupListeners() {
if (dialogHandle != -1) {
llListenRemove(dialogHandle);
dialogHandle = -1;
}
if (botHandle != -1) {
llListenRemove(botHandle);
botHandle = -1;
}
if (rollHandle != -1) {
llListenRemove(rollHandle);
rollHandle = -1;
}
}
// Re-initialize listen handles
initListeners() {
dialogHandle = llListen(DIALOG_CHANNEL, "", NULL_KEY, "");
botHandle = llListen(BOT_COMMAND_CHANNEL, "", NULL_KEY, "");
rollHandle = llListen(rollDialogChannel, "", NULL_KEY, "");
}
// Memory monitoring functions delegated to Controller_Memory.lsl helper script
checkMemoryUsage(string context) {
llMessageLinked(LINK_SET, MSG_MEMORY_CHECK, context, NULL_KEY);
}
// REMOVED: emergencyMemoryCleanup() - delegated to Controller_Memory.lsl
// REMOVED: reportMemoryStats() - delegated to Controller_Memory.lsl
// Send status message to scoreboard using link messages
sendStatusMessage(string status) {
statusTimer = llGetUnixTime();
lastStatus = status;
// CHANGED: Use link message instead of llRegionSay
llMessageLinked(SCOREBOARD_LINK, MSG_GAME_STATUS, status, NULL_KEY);
// Removed ownerMsg call to save memory
currentTimerMode = TIMER_STATUS;
llSetTimerEvent(STATUS_DISPLAY_TIME + 1.0);
}
// Direct scoreboard update for most recently added player
updateHelpers() {
// EMERGENCY memory check - much stricter threshold
integer freeMem = llGetFreeMemory();
if (victoryInProgress || freeMem < 4000) {
// Emergency: Don't even create strings if memory is critically low
return;
}
// MINIMAL: Only send update for the most recently added player
// No CSV operations, no string building - just direct message
integer nameCount = llGetListLength(names);
if (nameCount > 0) {
integer lastIdx = nameCount - 1;
// Minimal string operations - direct message
llMessageLinked(SCOREBOARD_LINK, MSG_PLAYER_UPDATE,
llList2String(names, lastIdx) + "|" +
(string)llList2Integer(lives, lastIdx) + "|" +
(string)llList2Key(players, lastIdx), NULL_KEY);
}
}
// Request helper-calculated values
requestPicksRequired(integer idx) {
llMessageLinked(LINK_SET, MSG_GET_PICKS_REQUIRED, llList2String(names, idx), NULL_KEY);
}
requestPickerIndex(string name) {
llMessageLinked(LINK_SET, MSG_GET_PICKER_INDEX, name, NULL_KEY);
}
string generateSerializedState() {
// Simplified to avoid memory-intensive CSV operations
return "MINIMAL_STATE~" + perilPlayer + "~" + (string)llGetListLength(names);
}
// Update floating text based on lock status
updateFloatingText() {
if (isLocked) {
llSetText("🔒 GAME LOCKED\nOwner access only", <1.0, 0.5, 0.0>, 1.0);
} else {
if (roundStarted || gameStarting) {
integer actualPlayerCount = llGetListLength(names);
llSetText("🎮 GAME IN PROGRESS\n" + (string)actualPlayerCount + " players", <1.0, 0.2, 0.2>, 1.0);
} else {
llSetText("🎮 PERIL DICE GAME\nTouch to play!", <1.0, 1.0, 0.0>, 1.0);
}
}
}
resetGame() {
cleanupListeners();
// Let Floater Manager handle cleanup intelligently
// Only request cleanup if we actually have game data that might have floaters
if (llGetListLength(names) > 0 || llGetListLength(floaterChannels) > 0) {
llMessageLinked(LINK_SET, MSG_CLEANUP_ALL_FLOATERS, "RESET", NULL_KEY);
llSleep(1.0);
}
llSleep(0.5);
players = names = lives = picksData = globalPickedNumbers = readyPlayers = [];
floaterChannels = [];
pendingRegistrations = [];
perilPlayer = "";
pickQueue = [];
currentPickerIdx = 0;
roundStarted = FALSE;
gameStarting = FALSE;
victoryInProgress = FALSE;
currentPicker = NULL_KEY;
llMessageLinked(LINK_SET, -99999, "FULL_RESET", NULL_KEY);
llSay(syncChannel, "RESET");
// CHANGED: Use link messages instead of llRegionSay for display cleanup
llMessageLinked(SCOREBOARD_LINK, MSG_CLEAR_GAME, "", NULL_KEY);
// Clear winner glow from scoreboard (floaters will clear naturally on reset)
llMessageLinked(SCOREBOARD_LINK, MSG_UPDATE_WINNER, "", NULL_KEY); // Clear winner glow
llMessageLinked(DICE_LINK, MSG_CLEAR_DICE, "", NULL_KEY);
llOwnerSay("🎮 Game reset! All state cleared (including scoreboard).");
statusTimer = 0;
lastStatus = "";
llSetTexture(TEXTURE_START, CONTROLLER_FACE);
llSetText("🎮 PERIL DICE GAME\nTouch to play!", <1.0, 1.0, 0.0>, 1.0);
llSleep(0.5);
llSetTimerEvent(0);
initListeners();
// SAFETY: Only call updateHelpers if we have actual game data to prevent startup issues
if (llGetListLength(names) > 0) {
updateHelpers();
}
}
default {
state_entry() {
reportMemoryUsage("Main Controller");
llOwnerSay("🎮 Main Controller ready! (Linkset Version)");
// Initialize lockout system
gameOwner = llGetOwner();
isLocked = FALSE; // Default to unlocked
// Initialize channels for external communication
initializeChannels();
// Reset the game on startup to ensure clean state
resetGame();
llOwnerSay("✅ Main Controller initialization complete!");
}
on_rez(integer start_param) {
reportMemoryUsage("Main Controller");
llOwnerSay("🔄 Main Controller rezzed - resetting game state...");
// Re-initialize lockout system
gameOwner = llGetOwner();
isLocked = FALSE;
// Re-initialize channels for external communication
initializeChannels();
// Reset the game to ensure clean state when rezzed
resetGame();
llOwnerSay("✅ Main Controller reset complete after rez!");
}
touch_start(integer total_number) {
key toucher = llDetectedKey(0);
integer idx = llListFindList(players, [toucher]);
// Enhanced disconnect/reconnect handling - check if this player should be picking
if (idx != -1 && toucher != llGetOwner() && roundStarted) {
string playerName = llList2String(names, idx);
// ENHANCED: Check if this player is supposed to be the current picker based on game state
// This handles cases where currentPicker got corrupted due to disconnect
integer shouldBePicking = FALSE;
if (currentPickerIdx < llGetListLength(pickQueue)) {
string expectedPickerName = llList2String(pickQueue, currentPickerIdx);
// Removed debugMsg call to save memory
if (playerName == expectedPickerName) {
shouldBePicking = TRUE;
// Fix corrupted currentPicker state
if (currentPicker != toucher) {
// Removed ownerMsg call to save memory
currentPicker = toucher;
}
}
} else {
// Removed debugMsg call to save memory
}
// FALLBACK: If game is stuck and no one is set as current picker, but we're in pick phase,
// let any registered player attempt to resume (with owner confirmation)
if (!shouldBePicking && currentPicker == NULL_KEY && currentPickerIdx < llGetListLength(pickQueue)) {
if (toucher == llGetOwner()) {
// Removed ownerMsg calls to save memory
// Game stuck error and recovery logging removed
currentPicker = toucher;
shouldBePicking = TRUE;
}
}
// Check for dialog recovery (original logic OR enhanced detection)
if (currentPicker == toucher || shouldBePicking) {
llSay(0, "🔄 " + playerName + " - Welcome back! Restoring your number picking dialog...");
llMessageLinked(LINK_SET, MSG_GET_CURRENT_DIALOG, playerName, toucher);
return;
}
else if (playerName == perilPlayer && currentPickerIdx >= llGetListLength(pickQueue)) {
llSay(0, "🔄 " + playerName + " - Welcome back! Restoring your roll dialog...");
llMessageLinked(LINK_SET, MSG_SHOW_ROLL_DIALOG, perilPlayer, toucher);
return;
}
}
// Special handling for owner during gameplay
if (toucher == llGetOwner() && roundStarted) {
string ownerName = getPlayerName(toucher);
list options = [];
string menuText = "Game in progress. What would you like to do?";
integer hasActiveDialog = FALSE;
if (currentPicker == toucher) {
options += ["🔄 Recover Pick Dialog"];
hasActiveDialog = TRUE;
}
if (ownerName == perilPlayer && currentPickerIdx >= llGetListLength(pickQueue)) {
options += ["🔄 Recover Roll Dialog"];
hasActiveDialog = TRUE;
}
options += ["🔧 Admin Menu"];
if (!hasActiveDialog) {
llMessageLinked(LINK_SET, MSG_SHOW_MENU, "owner|0", toucher);
return;
}
llDialog(toucher, menuText, options, DIALOG_CHANNEL);
return;
}
if (toucher == llGetOwner()) {
llOwnerSay("🔍 [DEBUG] Owner touched - idx: " + (string)idx + ", players list: " + llList2CSV(players));
if (idx == -1) {
// Owner is not in game - show join/admin choice
if (llListFindList(pendingRegistrations, [toucher]) != -1) {
llOwnerSay("⏳ Registration already in progress for " + getPlayerName(toucher));
return;
}
// Owner is not in game - determine menu type based on player count
if (llGetListLength(players) >= 2) {
// Enough players for game - show starter menu
llMessageLinked(LINK_SET, MSG_SHOW_MENU, "unregistered_owner_starter|1", toucher);
} else {
// Not enough players - show basic menu
llMessageLinked(LINK_SET, MSG_SHOW_MENU, "unregistered_owner|0", toucher);
}
} else {
// Owner is already in game - show player menu with appropriate starter status
integer isStarter = TRUE;
integer k;
for (k = 0; k < idx && isStarter; k++) {
string existingName = llList2String(names, k);
if (llSubStringIndex(existingName, "Bot") != 0) {
isStarter = FALSE;
}
}
if (idx == 0) {
isStarter = TRUE;
}
llMessageLinked(LINK_SET, MSG_SHOW_MENU, "owner|" + (string)isStarter, toucher);
}
} else if (idx != -1) {
// Non-owner player who is already registered
integer isStarter = TRUE;
integer m;
for (m = 0; m < idx && isStarter; m++) {
string existingName = llList2String(names, m);
if (llSubStringIndex(existingName, "Bot") != 0) {
isStarter = FALSE;
}
}
llMessageLinked(LINK_SET, MSG_SHOW_MENU, "player|" + (string)isStarter, toucher);
} else {
string playerName = getPlayerName(toucher);
if (playerName != "") {
if (llListFindList(pendingRegistrations, [toucher]) != -1) {
llOwnerSay("⏳ Registration already in progress for " + playerName);
return;
}
pendingRegistrations += [toucher];
llMessageLinked(LINK_SET, MSG_REGISTER_PLAYER_REQUEST, playerName + "|" + (string)toucher, NULL_KEY);
integer isStarter = TRUE;
integer n;
for (n = 0; n < llGetListLength(names) && isStarter; n++) {
string existingName = llList2String(names, n);
if (llSubStringIndex(existingName, "Bot") != 0) {
isStarter = FALSE;
}
}
llMessageLinked(LINK_SET, MSG_SHOW_MENU, "player|" + (string)isStarter, toucher);
} else {
llOwnerSay("⚠️ Could not get name for toucher: " + (string)toucher);
}
}
}
link_message(integer sender, integer num, string str, key id) {
// Dialog forwarding now handled by Player_RegistrationManager
// Handle player registration from other scripts (NOT delegated, this is for other internal communication)
if (num == MSG_REGISTER_PLAYER) {
// This is for other scripts that might still use the old MSG_REGISTER_PLAYER message
// Convert it to the new message for the Player_RegistrationManager
llMessageLinked(LINK_SET, MSG_REGISTER_PLAYER_REQUEST, str, id);
return;
}
// Handle registration updates from Player_RegistrationManager
if (num == 9040) { // MSG_UPDATE_MAIN_LISTS
// ULTRA-OPTIMIZED: Player_RegistrationManager now handles all heavy processing
// Main Controller just adds the player to its master lists
list parts = llParseString2List(str, ["~"], []);
if (llGetListLength(parts) >= 2) {
integer ch = llList2Integer(parts, 0);
string newName = llList2String(parts, 1);
// CHECK FOR DUPLICATES: Only check players list (names list may be populated by sync messages)
integer existingPlayerIdx = llListFindList(players, [id]);
if (existingPlayerIdx != -1) {
llOwnerSay("⚠️ [Main Controller] DUPLICATE PLAYER KEY DETECTED - Player: " + newName + " (" + (string)id + ") already exists!");
llOwnerSay("⚠️ [Main Controller] Players list length: " + (string)llGetListLength(players) + ", Names list length: " + (string)llGetListLength(names));
return; // Skip adding duplicate
}
// Add new player directly to master lists (minimal operations)
players += [id];
names += [newName];
lives += [3];
floaterChannels += [ch];
// Clean up pending registrations
integer pendingIdx = llListFindList(pendingRegistrations, [id]);
if (pendingIdx != -1) {
pendingRegistrations = llDeleteSubList(pendingRegistrations, pendingIdx, pendingIdx);
}
// That's it! Player_RegistrationManager already sent sync and scoreboard updates
llOwnerSay("✅ Player added to master lists: " + newName + " (" + (string)llGetFreeMemory() + " bytes free)");
llOwnerSay("🔍 [DEBUG] Updated lists - Players: " + (string)llGetListLength(players) + ", Names: " + (string)llGetListLength(names));
}
return;
}
// Handle elimination requests
if (num == 999) {
list parts = llParseString2List(str, ["|"], []);
if (llList2String(parts, 0) == "ELIMINATE_PLAYER") {
string eliminatedPlayer = llList2String(parts, 1);
integer idx = llListFindList(names, [eliminatedPlayer]);
if (idx != -1) {
// FIRST: Set the player's lives to 0 and show 0 hearts
lives = llListReplaceList(lives, [0], idx, idx);
// Send ONLY a direct scoreboard update to show 0 hearts
llMessageLinked(SCOREBOARD_LINK, MSG_PLAYER_UPDATE,
eliminatedPlayer + "|0|" + (string)llList2Key(players, idx), NULL_KEY);
// CRITICAL: Check for victory condition BEFORE assigning new peril player
integer willCauseVictory = (llGetListLength(names) <= 2); // After removing one player
string potentialWinner = "";
if (willCauseVictory) {
// Find the winner (the remaining non-eliminated player)
integer w;
for (w = 0; w < llGetListLength(names); w++) {
string candidateName = llList2String(names, w);
if (candidateName != eliminatedPlayer) {
potentialWinner = candidateName;
jump found_winner;
}
}
@found_winner;
if (potentialWinner != "") {
// SEND WINNER GLOW to scoreboard and trigger floater update
llMessageLinked(SCOREBOARD_LINK, MSG_UPDATE_WINNER, potentialWinner, NULL_KEY);
llMessageLinked(LINK_SET, MSG_UPDATE_FLOAT, potentialWinner, NULL_KEY); // Update winner's floater
llOwnerSay("🏆 [Main Controller] Pre-victory winner glow sent for: " + potentialWinner);
}
}
// Only assign new peril player if NOT causing victory
if (!willCauseVictory && (perilPlayer == eliminatedPlayer || perilPlayer == "")) {
// Find first remaining alive player to be new peril player (before removing eliminated player)
string newPerilPlayer = "";
integer k;
for (k = 0; k < llGetListLength(names) && newPerilPlayer == ""; k++) {
string candidateName = llList2String(names, k);
if (candidateName != eliminatedPlayer) { // Skip the player being eliminated
integer candidateIdx = llListFindList(names, [candidateName]);
if (candidateIdx != -1 && candidateIdx < llGetListLength(lives)) {
integer candidateLives = llList2Integer(lives, candidateIdx);
if (candidateLives > 0) {
newPerilPlayer = candidateName;
}
}
}
}
if (newPerilPlayer != "") {
perilPlayer = newPerilPlayer;
// Send peril player update to scoreboard for glow effect
llMessageLinked(SCOREBOARD_LINK, MSG_UPDATE_PERIL_PLAYER, newPerilPlayer, NULL_KEY);
} else {
perilPlayer = "NONE";
// Clear peril player glow on scoreboard
llMessageLinked(SCOREBOARD_LINK, MSG_UPDATE_PERIL_PLAYER, "", NULL_KEY);
}
}
// CRITICAL: Send sync message with 0 lives so floater shows elimination state
// Build sync message BEFORE any list modifications to prevent corruption
string livesStr = llList2CSV(lives);
string picksStr = llDumpList2String(picksData, "^");
if (picksStr == "") picksStr = "EMPTY"; // Prevent empty string
string perilStr = perilPlayer;
if (perilStr == "") perilStr = "NONE"; // Prevent empty string
string namesStr = llList2CSV(names);
string playersStr = llList2CSV(players);
string eliminationSync = livesStr + "~" + picksStr + "~" + perilStr + "~" + namesStr + "~" + playersStr;
llMessageLinked(LINK_SET, MSG_SYNC_GAME_STATE, eliminationSync, NULL_KEY);
// Direct floater update to ensure eliminated status is displayed
llMessageLinked(LINK_SET, MSG_UPDATE_FLOAT, eliminatedPlayer, llList2Key(players, idx));
llSleep(2.0); // Give floater time to show eliminated status with red display
if (willCauseVictory) {
// If this elimination will cause victory, give extra time for 0 hearts display
llSleep(1.5); // Additional time for victory scenarios
}
// CHANGED: Don't cleanup floaters during elimination - let them show eliminated status
// The floater will show red glow + "ELIMINATED!" and stay visible until game end
// integer ch = llList2Integer(floaterChannels, idx);
// llMessageLinked(LINK_SET, MSG_CLEANUP_FLOAT, (string)ch, NULL_KEY);
// llSleep(0.3); // Give floater time to cleanup
// CRITICAL: Remove eliminated player's picks data by searching for their entry
// NOT by index, since picksData format is "PlayerName|picks" strings
integer pickIdx = -1;
integer p;
for (p = 0; p < llGetListLength(picksData); p++) {
string entry = llList2String(picksData, p);
if (llSubStringIndex(entry, eliminatedPlayer + "|") == 0) {
pickIdx = p;
jump found_pick_entry;
}
}
@found_pick_entry;
if (pickIdx != -1) {
picksData = llDeleteSubList(picksData, pickIdx, pickIdx);
}
// Now remove from indexed lists (players, names, lives, floaterChannels)
players = llDeleteSubList(players, idx, idx);
names = llDeleteSubList(names, idx, idx);
lives = llDeleteSubList(lives, idx, idx);
floaterChannels = llDeleteSubList(floaterChannels, idx, idx);
// Peril player assignment already handled before sync message was sent
// CHANGED: Send to scoreboard, which will handle leaderboard updates
llMessageLinked(SCOREBOARD_LINK, MSG_GAME_LOST, eliminatedPlayer, NULL_KEY);
// Status message now sent directly by Roll Module
// Check for victory condition BEFORE calling updateHelpers()
if (llGetListLength(names) <= 1) {
if (llGetListLength(names) == 1) {
string winner = llList2String(names, 0);
// ENHANCED: Give eliminated player's floater time to show eliminated status
// before declaring victory and cleaning up
llOwnerSay("⏳ [Main Controller] Final elimination detected - allowing display time before victory");
llSleep(2.0); // Give final eliminated player time to show red eliminated status
// CRITICAL: Set victory flag immediately to prevent sync processing
victoryInProgress = TRUE;
// CRITICAL: Clear all game state immediately before any sync messages
// This prevents broadcasting stale winner data to other scripts
players = [];
names = [];
lives = [];
picksData = [];
perilPlayer = "";
pickQueue = [];
currentPickerIdx = 0;
roundStarted = FALSE;
gameStarting = FALSE;
currentPicker = NULL_KEY;
llSay(0, "✨ ULTIMATE VICTORY! " + winner + " is the Ultimate Survivor!");
llMessageLinked(LINK_SET, MSG_PLAYER_WON, winner, NULL_KEY);
llMessageLinked(LINK_SET, 995, "VICTORY_CONFETTI", NULL_KEY);
// IMPORTANT: Send winner update to scoreboard and trigger floater update
llMessageLinked(SCOREBOARD_LINK, MSG_UPDATE_WINNER, winner, NULL_KEY);
llMessageLinked(LINK_SET, MSG_UPDATE_FLOAT, winner, NULL_KEY); // Update winner's floater
// CHANGED: Send to scoreboard, which will handle leaderboard updates
llMessageLinked(SCOREBOARD_LINK, MSG_GAME_WON, winner, NULL_KEY);
// Use timer instead of llSleep to avoid immediate reset
llOwnerSay("🏆 [Main Controller] Winner glow applied - starting victory delay timer");
victoryDelayTimer = llGetUnixTime();
currentTimerMode = TIMER_VICTORY_DELAY;
llSetTimerEvent(1.0); // Check every second
return; // Exit here - timer will handle reset
} else {
// CRITICAL: Set victory flag and clear state for no survivors scenario too
victoryInProgress = TRUE;
players = [];
names = [];
lives = [];
picksData = [];
perilPlayer = "";
pickQueue = [];
currentPickerIdx = 0;
roundStarted = FALSE;
gameStarting = FALSE;
currentPicker = NULL_KEY;
llSay(0, "💀 DESPAIR WINS! No Ultimate Survivors remain!");
// Use timer for no survivors scenario too
victoryDelayTimer = llGetUnixTime();
currentTimerMode = TIMER_VICTORY_DELAY;
llSetTimerEvent(1.0);
return;
}
}
// CRITICAL: After elimination, sync state and continue game with new peril player
if (perilPlayer != "" && perilPlayer != "NONE") {
// Sync the updated game state to all modules (5-part format)
string picksDataStr = "EMPTY"; // Empty picks for new round
string gameSync = llList2CSV(lives) + "~" + picksDataStr + "~" + perilPlayer + "~" + llList2CSV(names) + "~" + llList2CSV(players);
llMessageLinked(LINK_SET, MSG_SYNC_GAME_STATE, gameSync, NULL_KEY);
llSleep(0.5); // Give sync time to propagate
// Continue the game with the new peril player - delegate to Game Manager
llMessageLinked(LINK_SET, 998, perilPlayer, NULL_KEY);
}
// MINIMAL: Only send direct scoreboard update for remaining players
// Skip heavy updateHelpers() calls to prevent memory crashes
}
}
return;
}
// Handle emergency reset for stuck games
if (num == -99998 && str == "EMERGENCY_RESET") {
llOwnerSay("🚨 [Main Controller] Emergency reset triggered - sending emergency reset to all scripts");
// Signal all scripts to emergency reset
llMessageLinked(LINK_SET, -99998, "EMERGENCY_RESET", NULL_KEY);
llSleep(0.5);
// Then do full reset
resetGame();
return;
}
// Handle game won
if (num == 998) {
list parts = llParseString2List(str, ["|"], []);
if (llList2String(parts, 0) == "GAME_WON") {
string winner = llList2String(parts, 1);
// CHANGED: Use link message instead of llRegionSay
llMessageLinked(DICE_LINK, MSG_DICE_ROLL, winner + "|WON", NULL_KEY);
llSleep(2.0);
resetGame();
}
return;
}
// Forward sync updates to Game Manager for processing
if (num == MSG_SYNC_GAME_STATE) {
// Skip forwarding during victory sequence
if (victoryInProgress) {
return;
}
// DON'T forward sync messages - this creates loops!
// Game Manager should receive the original message directly
// Main Controller just ignores sync messages now
return;
}
// Receive lightweight updates from Game Manager
if (num == 9070) {
list parts = llParseString2List(str, ["|"], []);
if (llGetListLength(parts) >= 3 && llList2String(parts, 0) == "PERIL_UPDATE") {
perilPlayer = llList2String(parts, 1);
integer playerCount = (integer)llList2String(parts, 2);
// Update floating text if needed
if (roundStarted || gameStarting) {
llSetText("🎮 GAME IN PROGRESS\n" + (string)playerCount + " players", <1.0, 0.2, 0.2>, 1.0);
}
}
return;
}
// Legacy 997 message handler removed - now using direct communication to Game Manager
// Handle ready state toggle
if (num == MSG_TOGGLE_READY) {
string playerName = str;
integer idx = llListFindList(names, [playerName]);
if (idx != -1) {
if (llSubStringIndex(playerName, "Bot") == 0) {
llOwnerSay("🤖 Bots are always ready and cannot change state");
return;
}
integer readyIdx = llListFindList(readyPlayers, [playerName]);
if (readyIdx == -1) {
readyPlayers += [playerName];
llSay(0, "⚔️ " + playerName + " steels themselves for the deadly challenge ahead! ⚔️");
} else {
readyPlayers = llDeleteSubList(readyPlayers, readyIdx, readyIdx);
llSay(0, "🏃 " + playerName + " loses their nerve and backs away from the challenge! 🏃");
}
}
return;
}
// Handle ready state queries
if (num == MSG_QUERY_READY_STATE) {
list queryParts = llParseString2List(str, ["|"], []);
string playerName;
integer requestID = 0;
if (llGetListLength(queryParts) >= 2) {
playerName = llList2String(queryParts, 0);
requestID = (integer)llList2String(queryParts, 1);
} else {
playerName = str;
}
integer isReady = llListFindList(readyPlayers, [playerName]) != -1;
integer isBot = llSubStringIndex(playerName, "Bot") == 0;
string result = playerName + "|" + (string)isReady + "|" + (string)isBot + "|" + (string)requestID;
llMessageLinked(LINK_SET, MSG_READY_STATE_RESULT, result, id);
return;
}
// Handle owner status queries
if (num == MSG_QUERY_OWNER_STATUS) {
list queryParts = llParseString2List(str, ["|"], []);
string ownerName;
integer requestID = 0;
if (llGetListLength(queryParts) >= 2) {
ownerName = llList2String(queryParts, 0);
requestID = (integer)llList2String(queryParts, 1);
} else {
ownerName = str;
}
// Check if owner is registered
integer ownerIdx = llListFindList(players, [id]);
integer isRegistered = (ownerIdx != -1);
integer isPending = (llListFindList(pendingRegistrations, [id]) != -1);
integer isStarter = FALSE;
if (isRegistered) {
// Calculate starter status if registered
isStarter = TRUE;
integer k;
for (k = 0; k < ownerIdx && isStarter; k++) {
string existingName = llList2String(names, k);
if (llSubStringIndex(existingName, "Bot") != 0) {
isStarter = FALSE;
}
}
if (ownerIdx == 0) {
isStarter = TRUE;
}
}
string result = ownerName + "|" + (string)isRegistered + "|" + (string)isPending + "|" + (string)isStarter + "|" + (string)requestID;
llMessageLinked(LINK_SET, MSG_OWNER_STATUS_RESULT, result, id);
return;
}
// MSG_CLEANUP_ALL_FLOATERS is handled by Floater Manager only
// Handle leave game requests processed by Controller_MessageHandler
if (num == 8006) { // MSG_LEAVE_GAME_REQUEST
// This message comes FROM the Controller_MessageHandler after it has processed the removal
// We need to update the main controller's lists to match
list parts = llParseString2List(str, ["|"], []);
string action = llList2String(parts, 0);
if (action == "LEAVE_GAME" || action == "KICK_PLAYER") {
string leavingName = llList2String(parts, 1);
// Find and remove the player from main controller lists
integer idx = llListFindList(names, [leavingName]);
if (idx != -1) {
// CHANGED: Remove player from scoreboard BEFORE updating lists
llMessageLinked(SCOREBOARD_LINK, MSG_REMOVE_PLAYER, leavingName, NULL_KEY);
// Clean up the leaving player's floater using their current channel
integer ch = FLOATER_BASE_CHANNEL + idx;
llMessageLinked(LINK_SET, MSG_CLEANUP_FLOAT, (string)ch, NULL_KEY);
// CRITICAL: If the leaving player was the peril player, update before updateHelpers()
if (perilPlayer == leavingName) {
// Find first remaining alive player to be new peril player
string newPerilPlayer = "";
integer k;
for (k = 0; k < llGetListLength(names) && newPerilPlayer == ""; k++) {
string candidateName = llList2String(names, k);
if (candidateName != leavingName) { // Skip the player being removed
integer candidateIdx = llListFindList(names, [candidateName]);
if (candidateIdx != -1 && candidateIdx < llGetListLength(lives)) {
integer candidateLives = llList2Integer(lives, candidateIdx);
if (candidateLives > 0) {
newPerilPlayer = candidateName;
}
}
}
}
if (newPerilPlayer != "") {
llOwnerSay("🎯 [Main Controller] Peril player left - assigning new peril player: " + newPerilPlayer);
perilPlayer = newPerilPlayer;
// Send peril player update to scoreboard for glow effect
llMessageLinked(SCOREBOARD_LINK, 3005, newPerilPlayer, NULL_KEY); // MSG_UPDATE_PERIL_PLAYER
} else {
llOwnerSay("⚠️ [Main Controller] No valid peril player candidates found after player left!");
perilPlayer = "NONE";
// Clear peril player glow on scoreboard
llMessageLinked(SCOREBOARD_LINK, MSG_UPDATE_PERIL_PLAYER, "", NULL_KEY);
}
}
// Update main controller's lists to match the message handler
players = llDeleteSubList(players, idx, idx);
names = llDeleteSubList(names, idx, idx);
lives = llDeleteSubList(lives, idx, idx);
// Remove any picks data entry for this player
integer j;
for (j = 0; j < llGetListLength(picksData); j++) {
list pickParts = llParseString2List(llList2String(picksData, j), ["|"], []);
if (llList2String(pickParts, 0) == leavingName) {
picksData = llDeleteSubList(picksData, j, j);
j = llGetListLength(picksData); // Exit loop LSL-style
}
}
floaterChannels = llDeleteSubList(floaterChannels, idx, idx);
// Remove from ready list if present
integer readyIdx = llListFindList(readyPlayers, [leavingName]);
if (readyIdx != -1) {
readyPlayers = llDeleteSubList(readyPlayers, readyIdx, readyIdx);
}
// Check if we should reset the game (no players left)
if (llGetListLength(names) == 0) {
llOwnerSay("🔄 [Main Controller] All players left - resetting game");
// Force clear any remaining game state before reset
roundStarted = FALSE;
gameStarting = FALSE;
perilPlayer = "";
currentPicker = NULL_KEY;
resetGame();
return;
}
// Skip updateHelpers to prevent memory crashes during player removal
// Scoreboard was already updated with MSG_REMOVE_PLAYER above
llOwnerSay("✅ [Main Controller] Synchronized removal of " + leavingName);
}
}
return;
}
// Handle lock/unlock messages from Dialog Handler
if (num == 9001) { // Lock game message
if (id == gameOwner && str == "LOCK_GAME") {
isLocked = TRUE;
updateFloatingText();
llOwnerSay("🔒 [Main Controller] Game has been LOCKED - Floating text updated");
}
return;
}
if (num == 9002) { // Unlock game message
if (id == gameOwner && str == "UNLOCK_GAME") {
isLocked = FALSE;
updateFloatingText();
llOwnerSay("🔓 [Main Controller] Game has been UNLOCKED - Floating text updated");
}
return;
}
// Handle verbose logging toggle - redirect to dedicated Verbose_Logger script
if (num == MSG_VERBOSE_TOGGLE && str == "TOGGLE_VERBOSE_LOGS") {
// Forward to dedicated verbose logger script, but exclude ourselves from LINK_SET
// Send to each script individually to avoid the forwarding loop
integer i;
integer linkCount = llGetNumberOfPrims();
for (i = 2; i <= linkCount; i++) { // Start from 2 to skip ourselves (link 1)
llMessageLinked(i, MSG_VERBOSE_TOGGLE, "TOGGLE_VERBOSE_LOGS", id);
}
return;
}
// Handle memory monitor messages
if (num == MSG_EMERGENCY_CLEANUP) {
// Removed emergencyMemoryCleanup call to save memory
llOwnerSay("Emergency cleanup disabled to prevent crashes");
return;
}
// Handle reset requests from other scripts (like Game_Manager)
if (num == -99998 && str == "REQUEST_GAME_RESET") {
llOwnerSay("🔄 [Main Controller] Reset requested by " + (string)sender + ", executing...");
resetGame();
return;
}
}
listen(integer channel, string name, key id, string msg) {
if (channel == rollDialogChannel) {
if (msg == "Start Next Round" || msg == "BEGIN KILLING GAME") {
if (roundStarted) {
llOwnerSay("⚠️ Round already in progress, ignoring duplicate round start from roll dialog");
return;
}
llSay(0, "⚡ THE KILLING GAME CONTINUES! " + perilPlayer + " begins the next deadly round!");
llMessageLinked(LINK_SET, 998, perilPlayer, NULL_KEY);
return;
}
}
if (channel == DIALOG_CHANNEL) {
// Handle owner choice dialog responses during gameplay
if (id == llGetOwner() && roundStarted) {
if (msg == "🔄 Recover Pick Dialog") {
string ownerName = getPlayerName(id);
llSay(0, "🔄 " + ownerName + " - Restoring your number picking dialog...");
llMessageLinked(LINK_SET, MSG_GET_CURRENT_DIALOG, ownerName, id);
return;
}
else if (msg == "🔄 Recover Roll Dialog") {
llSay(0, "🔄 " + getPlayerName(id) + " - Restoring your roll dialog...");
llMessageLinked(LINK_SET, MSG_SHOW_ROLL_DIALOG, perilPlayer, id);
return;
}
else if (msg == "🔧 Admin Menu") {
llMessageLinked(LINK_SET, MSG_SHOW_MENU, "owner|0", id);
return;
}
}
// Handle owner choice menu - "Join Game" is handled by Player_DialogHandler
if (id == llGetOwner()) {
if (msg == "Owner Menu") {
// Owner wants admin functions without joining - use admin targetType
llMessageLinked(LINK_SET, MSG_SHOW_MENU, "admin|0", id);
return;
}
}
// Owner-specific commands (when they've accessed admin menu)
if (id == llGetOwner()) {
if (msg == "Reset Game") {
resetGame();
return;
}
if (msg == "Reset Leaderboard") {
// CHANGED: Send to scoreboard, which will handle leaderboard reset
llMessageLinked(SCOREBOARD_LINK, MSG_RESET_LEADERBOARD, "", NULL_KEY);
llOwnerSay("🏆 Leaderboard scores reset - game wins cleared!");
return;
}
if (msg == "Reset All") {
resetGame();
llMessageLinked(SCOREBOARD_LINK, MSG_RESET_LEADERBOARD, "", NULL_KEY);
llOwnerSay("🔄 Complete reset - game and leaderboard cleared!");
return;
}
if (msg == "Memory Stats") {
// Removed reportMemoryStats call to save memory
llOwnerSay("Memory stats reporting disabled to prevent crashes");
return;
}
if (msg == "Add Test Player") {
if (llGetListLength(players) >= MAX_PLAYERS) {
llOwnerSay("⚠️ Cannot add test player; the game is full (max " + (string)MAX_PLAYERS + ").");
return;
}
// Create shorter bot names to avoid dialog button length issues
// Count existing bots to determine next bot number
integer existingBots = 0;
integer i;
for (i = 0; i < llGetListLength(names); i++) {
string playerName = llList2String(names, i);
if (llSubStringIndex(playerName, "Bot") == 0) {
existingBots++;
}
}
integer botNum = existingBots + 1;
string testName = "Bot" + (string)botNum;
key fake = llGenerateKey();
llMessageLinked(LINK_SET, MSG_REGISTER_PLAYER_REQUEST, testName + "|" + (string)fake, NULL_KEY);
// Don't show menu immediately - let the player touch the board again
// This avoids timing issues where the menu is shown before registration completes
llOwnerSay("✅ " + testName + " registration sent - touch the board again to refresh menu");
return;
}
if (msg == "Force Floaters") {
llOwnerSay("🔧 Forcing floater creation for all " + (string)llGetListLength(names) + " players...");
integer i;
for (i = 0; i < llGetListLength(names); i++) {
string playerName = llList2String(names, i);
key playerKey = llList2Key(players, i);
llMessageLinked(LINK_SET, MSG_REZ_FLOAT, playerName, playerKey);
llSleep(0.3);
}
llOwnerSay("✅ Floater creation requests sent for all players!");
// After forcing floaters, show appropriate menu based on owner registration status
integer ownerIdx = llListFindList(players, [llGetOwner()]);
if (ownerIdx != -1) {
// Owner is registered - show registered owner menu
integer ownerIsStarter = TRUE;
integer n;
for (n = 0; n < ownerIdx && ownerIsStarter; n++) {
string existingName = llList2String(names, n);
if (llSubStringIndex(existingName, "Bot") != 0) {
ownerIsStarter = FALSE;
}
}
llMessageLinked(LINK_SET, MSG_SHOW_MENU, "owner|" + (string)ownerIsStarter, llGetOwner());
} else {
// Owner is not registered - show unregistered owner menu
if (llGetListLength(players) >= 2) {
llMessageLinked(LINK_SET, MSG_SHOW_MENU, "unregistered_owner_starter|1", llGetOwner());
} else {
llMessageLinked(LINK_SET, MSG_SHOW_MENU, "unregistered_owner|0", llGetOwner());
}
}
return;
}
}
// Allow starter to start the game
if (msg == "Start Game") {
integer playerCount = llGetListLength(players);
integer nameCount = llGetListLength(names);
if (playerCount < 2) {
llOwnerSay("⚠️ Need at least 2 players to start the game.");
return;
}
string starterName = "";
if (id == llGetOwner()) {
starterName = getPlayerName(llGetOwner());
} else if (llGetListLength(players) > 0) {
key firstPlayer = llList2Key(players, 0);
if (id == firstPlayer) {
starterName = getPlayerName(firstPlayer);
}
}
if (starterName == "") {
llOwnerSay("⚠️ Only the game starter can start the game.");
return;
}
list notReadyPlayers = [];
integer i;
for (i = 0; i < llGetListLength(names); i++) {
string playerName = llList2String(names, i);
if (playerName != starterName && llSubStringIndex(playerName, "Bot") != 0) {
if (llListFindList(readyPlayers, [playerName]) == -1) {
notReadyPlayers += [playerName];
}
}
}
if (llGetListLength(notReadyPlayers) > 0) {
llOwnerSay("⚠️ Cannot start game. These players are not ready: " + llList2CSV(notReadyPlayers));
return;
}
gameStarting = TRUE;
llSay(0, "⚡ ALL PARTICIPANTS READY! THE DEADLY PERIL DICE GAME BEGINS! ⚡");
integer actualPlayerCount = llGetListLength(names);
llSetText("🎮 GAME IN PROGRESS\n" + (string)actualPlayerCount + " players", <1.0, 0.2, 0.2>, 1.0);
sendStatusMessage("Title");
// For game start, use empty peril player - Game Manager will select one
llMessageLinked(LINK_SET, 998, "", NULL_KEY);
}
if (llListFindList(names, [msg]) != -1) {
llMessageLinked(LINK_SET, 206, msg, id);
}
}
}
timer() {
if (currentTimerMode == TIMER_STATUS) {
if (statusTimer > 0 && (llGetUnixTime() - statusTimer) >= STATUS_DISPLAY_TIME) {
statusTimer = 0;
lastStatus = "";
// CHANGED: Use link message instead of llRegionSay
llMessageLinked(SCOREBOARD_LINK, MSG_GAME_STATUS, "Title", NULL_KEY);
llOwnerSay("📢 Status cleared - reverted to Title");
currentTimerMode = TIMER_IDLE;
llSetTimerEvent(0);
}
}
else if (currentTimerMode == TIMER_VICTORY_DELAY) {
if (victoryDelayTimer > 0) {
integer elapsed = llGetUnixTime() - victoryDelayTimer;
if (elapsed >= (integer)(STATUS_DISPLAY_TIME * 3.0)) { // 24 seconds
llOwnerSay("🏆 [Main Controller] Victory display time complete - executing game reset");
victoryDelayTimer = 0;
currentTimerMode = TIMER_IDLE;
llSetTimerEvent(0);
resetGame(); // Now reset after the delay
}
}
}
}
}