diff --git a/CHANGELOG.md b/CHANGELOG.md index 428e0a6..7bf7b41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +**Peril Dice Game System - Created by Rebecca Nod and Noose the Bunny** + All notable changes to Peril Dice will be documented in this file. ## [2.6.0] - 2025-08-09 diff --git a/GAME_RULES.md b/GAME_RULES.md index 4a3ef71..ca725b9 100644 --- a/GAME_RULES.md +++ b/GAME_RULES.md @@ -1,5 +1,7 @@ # ๐ŸŽฒ **PERIL DICE GAME - OFFICIAL RULES** +**Created by Rebecca Nod and Noose the Bunny** + *Original concept by **Noose the Bunny** (djmusica28) in Second Life* --- diff --git a/Game_Calculator.lsl b/Game_Calculator.lsl index adc1d34..0c737b6 100644 --- a/Game_Calculator.lsl +++ b/Game_Calculator.lsl @@ -166,9 +166,9 @@ default { integer playerCount = (integer)str; llOwnerSay("๐Ÿ“ˆ Calculator: Calculating dice type for " + (string)playerCount + " players"); integer result = getDiceType(playerCount); - // Send result ONLY back to the requesting script to prevent duplicate processing - // Main Controller will forward it to Game Manager as needed - llMessageLinked(sender, MSG_DICE_TYPE_RESULT, (string)result, NULL_KEY); + // Send result to ALL scripts via LINK_SET so Game Manager receives it directly + // Each script will handle duplicate prevention on their end + llMessageLinked(LINK_SET, MSG_DICE_TYPE_RESULT, (string)result, NULL_KEY); } else if (num == MSG_GET_PICKS_REQUIRED) { integer result = getPicksRequiredFromName(str); diff --git a/Game_Manager.lsl b/Game_Manager.lsl index 60a0d88..addbc87 100644 --- a/Game_Manager.lsl +++ b/Game_Manager.lsl @@ -29,6 +29,7 @@ integer diceType = 6; key currentPicker; integer roundStarted = FALSE; integer diceTypeProcessed = FALSE; // Track if we've already processed dice type for this round +integer ignorePicksSync = FALSE; // Temporarily ignore picks data sync during round initialization // Duplicate request prevention string lastHumanPickMessage = ""; @@ -50,6 +51,8 @@ continueCurrentRound() { picksData = []; globalPickedNumbers = []; + llOwnerSay("๐Ÿ”„ [Game Manager] continueCurrentRound: Cleared picks data, starting fresh"); + // Set round as started for the new round roundStarted = TRUE; diceTypeProcessed = FALSE; // Reset dice type processing for new round @@ -65,7 +68,9 @@ continueCurrentRound() { } currentPickerIdx = 0; - // Request dice type for the new round + llOwnerSay("๐ŸŽฏ [Game Manager] Pick queue created: " + llList2CSV(pickQueue) + ", requesting dice type"); + + // Request dice type directly from Calculator for the new round llMessageLinked(LINK_SET, MSG_GET_DICE_TYPE, (string)llGetListLength(names), NULL_KEY); // Note: showNextPickerDialog() will be called when dice type result is received @@ -282,49 +287,59 @@ default { newPicksData = llParseString2List(picksDataStr, ["^"], []); } - // Don't sync peril player from Main Controller - Game Manager is authoritative - // string receivedPeril = llList2String(parts, 2); - // Game Manager manages its own peril player state - names = llCSV2List(llList2String(parts, 3)); + // Game Manager is authoritative for peril player, but check for eliminations + list newNames = llCSV2List(llList2String(parts, 3)); + + // Check if current peril player was eliminated + if (perilPlayer != "" && perilPlayer != "NONE") { + integer perilStillExists = llListFindList(newNames, [perilPlayer]) != -1; + if (!perilStillExists && llListFindList(names, [perilPlayer]) != -1) { + // Peril player was eliminated! Game Manager needs to assign new peril player + llOwnerSay("๐Ÿšจ [Game Manager] Detected elimination of peril player: " + perilPlayer); + + // Find first remaining alive player to be new peril player + string newPerilPlayer = ""; + integer k; + for (k = 0; k < llGetListLength(newNames) && newPerilPlayer == ""; k++) { + string candidateName = llList2String(newNames, k); + integer candidateIdx = llListFindList(newNames, [candidateName]); + if (candidateIdx != -1 && candidateIdx < llGetListLength(newLives)) { + integer candidateLives = llList2Integer(newLives, candidateIdx); + if (candidateLives > 0) { + newPerilPlayer = candidateName; + } + } + } + + if (newPerilPlayer != "") { + llOwnerSay("๐ŸŽฏ [Game Manager] Assigning new peril player: " + newPerilPlayer); + perilPlayer = newPerilPlayer; + } else { + llOwnerSay("โš ๏ธ [Game Manager] No valid peril player candidates found!"); + perilPlayer = "NONE"; + } + } + } + + names = newNames; // New: also receive players list if available (for dialog targeting) if (llGetListLength(parts) >= 5) { players = llCSV2List(llList2String(parts, 4)); } - // AUTOMATIC ROUND CONTINUATION: Detect post-roll state - // Check if lives have changed (indicating a roll occurred) and all picks are now empty + // Always update state normally - don't auto-continue rounds + // The Roll module will handle post-roll logic and Main Controller will manage round flow + lives = newLives; + picksData = newPicksData; + + // Only log if this appears to be a post-roll update for debugging string oldLivesStr = llList2CSV(lives); string newLivesStr = llList2CSV(newLives); integer livesChanged = (oldLivesStr != newLivesStr); - // Check if all picks are empty (indicating post-roll state) - integer allPicksEmpty = TRUE; - integer i; - for (i = 0; i < llGetListLength(newPicksData) && allPicksEmpty; i++) { - string entry = llList2String(newPicksData, i); - list entryParts = llParseString2List(entry, ["|"], []); - if (llGetListLength(entryParts) >= 2 && llList2String(entryParts, 1) != "") { - allPicksEmpty = FALSE; - } - } - - // If lives changed and all picks are empty, this is post-roll - start new round - if (livesChanged && allPicksEmpty && roundStarted && perilPlayer != "" && perilPlayer != "NONE") { - llOwnerSay("๐ŸŽฏ [Game Manager] Post-roll detected - automatically continuing to next round"); - - // Update our state first - lives = newLives; - picksData = newPicksData; - - // Reset for new round and continue - roundStarted = FALSE; - currentPickerIdx = 0; - continueCurrentRound(); - } else { - // Normal state update - lives = newLives; - picksData = newPicksData; + if (livesChanged) { + llOwnerSay("๐Ÿ” [Game Manager] Lives changed - roll completed, waiting for proper round continuation signal"); } } return; diff --git a/Main_Controller.lsl b/Main_Controller.lsl index 9e201b2..adab35d 100644 --- a/Main_Controller.lsl +++ b/Main_Controller.lsl @@ -133,8 +133,6 @@ list floaterChannels = []; // Track actual floater channels for cleanup list pickQueue = []; integer currentPickerIdx = 0; -integer diceType = 6; - integer MSG_SHOW_DIALOG = 101; integer MSG_ROLL_RESULT = 102; integer MSG_UPDATE_FLOAT = 103; @@ -157,10 +155,9 @@ 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 -integer MSG_GET_DICE_TYPE = 1001; +// Dice type handling is now done by Game Manager directly integer MSG_GET_PICKS_REQUIRED = 1002; integer MSG_GET_PICKER_INDEX = 1003; -integer MSG_DICE_TYPE_RESULT = 1005; integer MSG_SERIALIZE_GAME_STATE = 1004; integer MSG_SYNC_PICKQUEUE = 2001; @@ -168,12 +165,13 @@ integer MSG_SYNC_PICKQUEUE = 2001; // mirror the value used in Floater Manager to avoid inconsistencies. integer MAX_PLAYERS = 10; -integer TIMEOUT_SECONDS = 600; +integer TIMEOUT_SECONDS = 120; // Reduced from 600 (10 min) to 120 (2 min) integer timeoutTimer; -integer warning2min = 120; -integer warning5min = 300; -integer warning9min = 540; +// Updated warning times for 2-minute timeout +integer warning30sec = 30; // 30 seconds remaining +integer warning1min = 60; // 1 minute remaining +integer warning90sec = 90; // 1.5 minutes remaining integer lastWarning = 0; key currentPicker; @@ -266,10 +264,6 @@ updateHelpers() { } // Request helper-calculated values -requestDiceType() { - llMessageLinked(LINK_SET, MSG_GET_DICE_TYPE, (string)llGetListLength(names), NULL_KEY); -} - requestPicksRequired(integer idx) { llMessageLinked(LINK_SET, MSG_GET_PICKS_REQUIRED, llList2String(names, idx), NULL_KEY); } @@ -329,11 +323,9 @@ resetGame() { currentPickerIdx = 0; roundStarted = FALSE; // Reset round flag gameStarting = FALSE; // Reset game starting flag - diceTypeProcessed = FALSE; // Reset dice type processing flag currentPicker = NULL_KEY; // Reset current picker timeoutTimer = 0; // Reset timeout timer lastWarning = 0; // Reset warning timer - diceType = 6; // Reset dice type to default // Send reset message to all other scripts to clear their state llMessageLinked(LINK_SET, -99999, "FULL_RESET", NULL_KEY); @@ -370,7 +362,6 @@ resetGame() { integer roundStarted = FALSE; integer gameStarting = FALSE; // Track when game is in startup sequence -integer diceTypeProcessed = FALSE; // Track if we've already processed initial dice type // CONTINUE_ROUND logic now handled entirely by Game Manager @@ -547,24 +538,8 @@ if (toucher == llGetOwner() && roundStarted) { link_message(integer sender, integer num, string str, key id) { // Player list and pick data handling now delegated to Controller_MessageHandler.lsl - // Handle dice type result from helper - forward to Game Manager once - if (num == MSG_DICE_TYPE_RESULT) { - // Prevent duplicate processing of the same dice type result - if (diceTypeProcessed) { - return; - } - - diceType = (integer)str; - diceTypeProcessed = TRUE; - - // Set round started flag and update helpers - roundStarted = TRUE; - updateHelpers(); - - // Forward to Game Manager ONCE - llMessageLinked(LINK_SET, MSG_DICE_TYPE_RESULT, (string)diceType, NULL_KEY); - return; - } + // Main Controller no longer needs to handle dice type results + // Game Manager handles all dice type logic directly if (num == MSG_GET_PICKS_REQUIRED) { integer picksRequired = (integer)str; llOwnerSay(" Picks required: " + str); @@ -704,41 +679,9 @@ sendStatusMessage("Victory"); // Show victory status return; } - // If the eliminated player was the peril player, assign a new one and continue game - if (eliminatedPlayer == perilPlayer) { - // Find first remaining alive player to be new peril player - perilPlayer = ""; - integer k; - for (k = 0; k < llGetListLength(names) && perilPlayer == ""; k++) { - string candidateName = llList2String(names, k); - if (llList2Integer(lives, k) > 0) { - perilPlayer = candidateName; - llOwnerSay("๐Ÿ”„ After elimination, new peril player: " + perilPlayer); - } - } - - // Continue the game with the new peril player (don't reset roundStarted) - if (perilPlayer != "") { - llOwnerSay("๐ŸŽฏ Continuing game with " + perilPlayer + " as new peril player"); - // Reset picks and queue for new round, but keep roundStarted = TRUE - picksData = []; - globalPickedNumbers = []; - pickQueue = [perilPlayer]; - integer i; - for (i = 0; i < llGetListLength(names); i++) { - string playerName = llList2String(names, i); - if (playerName != perilPlayer) { - pickQueue += [playerName]; - } - } - currentPickerIdx = 0; - updateHelpers(); - requestDiceType(); - } - } else { - // Continue with existing peril player - updateHelpers(); - } + // Let Game Manager handle peril player reassignment after elimination + // Just update helpers and let Game Manager detect the elimination via sync + updateHelpers(); } } return; @@ -836,11 +779,18 @@ resetGame(); } // Handle start next round request - delegate to Game Manager if (num == 997) { + // Check sender to prevent infinite loops - don't process our own messages + if (sender == llGetLinkNumber()) { + return; // Ignore messages from ourself + } + // Additional protection against round start spam if (roundStarted && str == "START_NEXT_ROUND") { llOwnerSay("โš ๏ธ [Main Controller] Round already in progress, ignoring START_NEXT_ROUND"); return; } + // Game Manager will handle dice type processing for new round + llOwnerSay("๐Ÿ”„ [Main Controller] Starting new round - Game Manager will handle dice type"); // Forward the request to Game Manager llMessageLinked(LINK_SET, num, str, id); // Don't request dice type here - it's handled in the "Start Game" listener below @@ -1016,6 +966,31 @@ llRegionSay(SCOREBOARD_CHANNEL_1, "RESET_LEADERBOARD"); // Reset leaderboard sco llMessageLinked(LINK_SET, MSG_SHOW_MENU, "owner|" + (string)ownerIsStarter, llGetOwner()); return; } + if (msg == "Force Floaters") { + // Force creation of floaters for all registered players + 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); // Brief delay between rezzing to prevent spam + } + llOwnerSay("โœ… Floater creation requests sent for all players!"); + // Refresh owner menu + integer ownerIdx = llListFindList(players, [llGetOwner()]); + integer ownerIsStarter = TRUE; // Default to TRUE for owner + integer n; + for (n = 0; n < ownerIdx && ownerIsStarter; n++) { + string existingName = llList2String(names, n); + // If there's a human player before the owner, owner is not starter + if (llSubStringIndex(existingName, "TestBot") != 0) { + ownerIsStarter = FALSE; + } + } + llMessageLinked(LINK_SET, MSG_SHOW_MENU, "owner|" + (string)ownerIsStarter, llGetOwner()); + return; + } } // Allow the first registered player (index 0) or the owner to start the game. // When the starter clicks "Start Game", begin the round. @@ -1102,15 +1077,15 @@ llRegionSay(SCOREBOARD_CHANNEL_1, "RESET_LEADERBOARD"); // Reset leaderboard sco llSetTimerEvent(0); } else { integer remaining = TIMEOUT_SECONDS - elapsed; - if (remaining <= warning2min && lastWarning < warning2min) { - llOwnerSay("โš ๏ธ " + (string)(remaining / 60) + " minutes remaining for dialog response"); - lastWarning = warning2min; - } else if (remaining <= warning5min && lastWarning < warning5min) { - llOwnerSay("โš ๏ธ " + (string)(remaining / 60) + " minutes remaining for dialog response"); - lastWarning = warning5min; - } else if (remaining <= warning9min && lastWarning < warning9min) { - llOwnerSay("โš ๏ธ " + (string)(remaining / 60) + " minutes remaining for dialog response"); - lastWarning = warning9min; + if (remaining <= warning30sec && lastWarning < warning30sec) { + llOwnerSay("โš ๏ธ 30 seconds remaining for dialog response"); + lastWarning = warning30sec; + } else if (remaining <= warning1min && lastWarning < warning1min) { + llOwnerSay("โš ๏ธ 1 minute remaining for dialog response"); + lastWarning = warning1min; + } else if (remaining <= warning90sec && lastWarning < warning90sec) { + llOwnerSay("โš ๏ธ 1.5 minutes remaining for dialog response"); + lastWarning = warning90sec; } } } diff --git a/Player_DialogHandler.lsl b/Player_DialogHandler.lsl index b97ad72..617b8bc 100644 --- a/Player_DialogHandler.lsl +++ b/Player_DialogHandler.lsl @@ -93,7 +93,7 @@ list mainOwnerOptions = ["๐ŸŽฎ Game Participation", "๐Ÿ‘ฅ Player Management", " list gameParticipationOptions = ["Join Game", "Leave Game", "โฌ…๏ธ Back to Main"]; list playerManagementOptions = ["Add Test Player", "โฌ…๏ธ Back to Main"]; list resetOptions = ["Reset Game", "Reset Leaderboard", "Reset All", "โฌ…๏ธ Back to Main"]; -list troubleshootingOptions = ["Cleanup Floaters", "Reset Follower Positions", "โฌ…๏ธ Back to Main"]; +list troubleshootingOptions = ["Cleanup Floaters", "Force Floaters", "Reset Follower Positions", "โฌ…๏ธ Back to Main"]; // State tracking for dynamic ready menu with race condition protection key pendingMenuPlayer = NULL_KEY; @@ -102,7 +102,7 @@ integer pendingMenuIsOwner = FALSE; integer pendingMenuRequestID = 0; // Unique ID for each menu request integer currentRequestID = 0; // Counter for generating unique request IDs float pendingMenuTimestamp = 0.0; // Timeout for pending requests -float MENU_REQUEST_TIMEOUT = 10.0; // 10 second timeout for menu requests +float MENU_REQUEST_TIMEOUT = 5.0; // 5 second timeout for menu requests // Display the main categorized owner menu showOwnerMenu(key id) { @@ -372,9 +372,15 @@ default { } else if (msg == "Cleanup Floaters") { // Universal floater cleanup that works even after script resets - llOwnerSay("๐Ÿงน Force cleaning ALL possible floater channels..."); + llOwnerSay("๐Ÿงฉ Force cleaning ALL possible floater channels..."); llMessageLinked(LINK_SET, MSG_CLEANUP_ALL_FLOATERS, "", NULL_KEY); } + else if (msg == "Force Floaters") { + // Force creation of floaters for all registered players - send as dialog response to Main Controller + llOwnerSay("๐Ÿ”ง Requesting floater creation for all registered players..."); + // This will be handled by Main Controller's dialog listener + // No need to forward via link_message - the Main Controller is already listening on DIALOG_CHANNEL + } else if (msg == "Reset Follower Positions") { // Integrated position reset functionality with automation options if (id == llGetOwner()) { diff --git a/README.md b/README.md index ecc459b..edd8772 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # ๐ŸŽฒ Peril Dice โ€” Modular Game System for Second Life +**Created by Rebecca Nod and Noose the Bunny** + ## Overview Peril Dice is a multiplayer elimination game where each player selects numbers before a die is rolled. If the peril player's number is rolled, they lose a life. Players are eliminated when they reach zero lives. diff --git a/README_Notecard.txt b/README_Notecard.txt index 53cbbc8..295bf70 100644 --- a/README_Notecard.txt +++ b/README_Notecard.txt @@ -1,6 +1,8 @@ ๐ŸŽฒ PERIL DICE โ€” MODULAR GAME SYSTEM FOR SECOND LIFE ======================================================== +CREATED BY REBECCA NOD AND NOOSE THE BUNNY + OVERVIEW -------- Peril Dice is a multiplayer elimination game where each player selects numbers before a die is rolled. If the peril player's number is rolled, they lose a life. Players are eliminated when they reach zero lives. diff --git a/Roll_ConfettiModule.lsl b/Roll_ConfettiModule.lsl index b0cd070..cf3c0b7 100644 --- a/Roll_ConfettiModule.lsl +++ b/Roll_ConfettiModule.lsl @@ -59,6 +59,8 @@ list names = []; list lives = []; list picksData = []; string perilPlayer = ""; +integer diceType = 6; // Store the dice type for rolling +integer shouldRoll = FALSE; // Flag to trigger roll after dice type is received list getPicksFor(string nameInput) { integer i; @@ -197,7 +199,9 @@ default { if (llSubStringIndex(str, "TestBot") == 0) { // Auto-roll for bots llOwnerSay("๐Ÿค– " + str + " (bot) is auto-rolling..."); - llMessageLinked(LINK_SET, 996, "GET_DICE_TYPE", NULL_KEY); + shouldRoll = TRUE; // Set flag for bot auto-roll + // Request dice type directly from Calculator + llMessageLinked(LINK_SET, 1001, (string)llGetListLength(names), NULL_KEY); } else { // Show dialog for human players llDialog(id, "๐ŸŽฒ THE MOMENT OF TRUTH! You're in ultimate peril. Will you face your fate?", ["ROLL THE DICE OF FATE"], rollDialogChannel); @@ -205,6 +209,31 @@ default { } } + // Handle dice type requests from listen() events + else if (num == 996) { + // This is a GET_DICE_TYPE request from our own listen handler + // Forward it to the Main Controller to get the current dice type + llMessageLinked(LINK_SET, 1001, (string)llGetListLength(names), NULL_KEY); + return; + } + + // Handle dice type response - store dice type and perform roll if requested + else if (num == 1005) { // MSG_DICE_TYPE_RESULT + diceType = (integer)str; + llOwnerSay("๐ŸŽฒ [Roll Module] Received dice type: d" + (string)diceType); + + // If roll was requested from listen handler, perform it now + if (shouldRoll) { + shouldRoll = FALSE; // Reset flag + llOwnerSay("๐ŸŽฒ [Roll Module] Performing requested roll with d" + (string)diceType); + + // Use MSG_ROLL_RESULT handler to perform the actual roll + llMessageLinked(LINK_THIS, MSG_ROLL_RESULT, (string)diceType, NULL_KEY); + } + return; + } + + // Legacy MSG_ROLL_RESULT handler (if still used somewhere) else if (num == MSG_ROLL_RESULT) { integer diceType = (integer)str; integer result = rollDice(diceType); @@ -344,9 +373,11 @@ default { listen(integer channel, string name, key id, string msg) { if (msg == "Roll" || msg == "ROLL THE DICE OF FATE") { - // Handle dice roll - need to get dice type from main controller + // Handle dice roll - need to get dice type from Calculator llOwnerSay("๐ŸŽฒ Roll button clicked by " + name + " (key: " + (string)id + ")"); - llMessageLinked(LINK_SET, 996, "GET_DICE_TYPE", NULL_KEY); + shouldRoll = TRUE; // Set flag to perform roll when dice type is received + // Request dice type directly from Calculator + llMessageLinked(LINK_SET, 1001, (string)llGetListLength(names), NULL_KEY); } else { llMessageLinked(LINK_THIS, channel, msg, id); }