diff --git a/Bot Manager.lsl b/Bot Manager.lsl index bf4d10a..97d03e8 100644 --- a/Bot Manager.lsl +++ b/Bot Manager.lsl @@ -25,24 +25,39 @@ doBotPick(string botName, integer count, integer diceMax, list avoidNumbers) { return; } - llOwnerSay("[Bot Manager] โœ… Picking " + (string)count + " numbers for " + botName + " (lives=" + (string)botLives + ", max=" + (string)diceMax + ")"); - if (llGetListLength(avoidNumbers) > 0) { - llOwnerSay("[Bot Manager] Avoiding already picked numbers: " + llList2CSV(avoidNumbers)); - } + list picks; integer attempts = 0; + while (llGetListLength(picks) < count && attempts < 100) { integer roll = 1 + (integer)(llFrand((float)diceMax)); string rollStr = (string)roll; + + integer inPicks = llListFindList(picks, [rollStr]); + integer inAvoid = llListFindList(avoidNumbers, [rollStr]); + + // Check if not already picked by this bot and not picked by others - if (llListFindList(picks, [rollStr]) == -1 && llListFindList(avoidNumbers, [rollStr]) == -1) { + if (inPicks == -1 && inAvoid == -1) { picks += rollStr; + } else { + string reason = ""; + if (inPicks != -1) { + reason = "already picked by this bot"; + } else if (inAvoid != -1) { + reason = "already picked by another player"; + } } attempts++; } - string pickString = llDumpList2String(picks, ","); + + + if (llGetListLength(picks) < count) { + llOwnerSay("[Bot Manager] โš ๏ธ WARNING: " + botName + " only picked " + (string)llGetListLength(picks) + "/" + (string)count + " numbers after " + (string)attempts + " attempts"); + } + + string pickString = llDumpList2String(picks, ";"); string response = "BOT_PICKED:" + botName + ":" + pickString; - llOwnerSay("[Bot Manager] Sending: " + response); llMessageLinked(LINK_SET, -9997, response, NULL_KEY); } @@ -66,7 +81,6 @@ doBotRoll(string botName, integer diceMax) { return; } - llOwnerSay("[Bot Manager] โœ… Bot '" + botName + "' rolling dice (max=" + (string)diceMax + ")"); integer roll = 1 + (integer)(llFrand((float)diceMax)); llRegionSay(LISTEN_CHANNEL, "BOT_ROLL:" + botName + ":" + (string)roll); } @@ -93,14 +107,12 @@ default { perilPlayer = receivedPeril; } names = llCSV2List(llList2String(parts, 3)); - llOwnerSay("[Bot Manager] ๐Ÿ”„ Sync: " + (string)llGetListLength(names) + " players, peril='" + perilPlayer + "'"); } return; } if (num == -9999) { - llOwnerSay("[Bot Manager] Received link message: " + str); - if (llSubStringIndex(str, "BOT_PICK:") == 0) { + if (llSubStringIndex(str, "BOT_PICK:") == 0) { // Format: BOT_PICK:bot_1:3:20:1,2,3 (last part is optional already picked numbers) list parts = llParseStringKeepNulls(str, [":"], []); if (llGetListLength(parts) >= 4) { @@ -111,7 +123,16 @@ default { if (llGetListLength(parts) >= 5) { string avoidStr = llList2String(parts, 4); if (avoidStr != "") { - avoidNumbers = llCSV2List(avoidStr); + list rawList = llCSV2List(avoidStr); + // Trim whitespace from each number + avoidNumbers = []; + integer i; + for (i = 0; i < llGetListLength(rawList); i++) { + string trimmed = llStringTrim(llList2String(rawList, i), STRING_TRIM); + if (trimmed != "") { + avoidNumbers += [trimmed]; + } + } } } doBotPick(botName, count, diceMax, avoidNumbers); @@ -130,7 +151,6 @@ default { } listen(integer channel, string name, key id, string message) { - llOwnerSay("[Bot Manager] Received: " + message); if (llSubStringIndex(message, "BOT_PICK:") == 0) { // Format: BOT_PICK:bot_1:3:20 list parts = llParseStringKeepNulls(message, [":"], []); diff --git a/CHANGELOG.md b/CHANGELOG.md index 0cd1895..fe11ca7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,41 @@ All notable changes to Peril Dice will be documented in this file. +## [2.2.0] - 2025-08-04 + +### ๐ŸŽญ Major UX/Presentation Improvements +- **Dramatic Messaging System**: Complete overhaul of game messages with thematic styling +- **Public Announcements**: Key game events now visible to all players in public chat +- **Context-Rich Roll Messages**: Dice rolls now show both dice type and result +- **Split Hit Messages**: Different messages for "picked own doom" vs "no shield" scenarios + +### ๐ŸŽฎ Enhanced Player Experience +- **Player Join Messages**: `๐Ÿ’€ PlayerName has entered the deadly game! Welcome to your potential doom! ๐Ÿ’€` +- **Ready State Messages**: + - `๐Ÿ‘‘ PlayerName steps forward as the game master - automatically ready for the deadly challenge! ๐Ÿ‘‘` + - `โš”๏ธ PlayerName steels themselves for the deadly challenge ahead! โš”๏ธ` + - `๐Ÿƒ PlayerName loses their nerve and backs away from the challenge! ๐Ÿƒ` +- **Pick Announcements**: `๐ŸŽฏ PlayerName stakes their life on numbers: 1, 2, 3 ๐ŸŽฒ` +- **Game Start**: `โšก ALL PARTICIPANTS READY! THE DEADLY PERIL DICE GAME BEGINS! โšก` + +### ๐ŸŽฒ Improved Roll Messages +- **Main Roll**: `๐ŸŽฒ THE D6 OF FATE! PlayerName rolled a 5 on the 6-sided die! ๐ŸŽฒ` +- **Plot Twist**: `โšก PLOT TWIST! PlayerName picked 5 (rolled on d6) and is now in ULTIMATE PERIL! โšก` +- **Direct Hit**: `๐Ÿฉธ DIRECT HIT! PlayerName picked their own doom - the d6 landed on 5! ๐Ÿฉธ` +- **No Shield**: `๐Ÿฉธ NO SHIELD! Nobody picked 5 - PlayerName takes the hit from the d6! ๐Ÿฉธ` + +### ๐Ÿ”ง Critical Bug Fixes +- **StatFloat Duplication**: Fixed bug where new StatFloat objects were created on every life loss instead of updating existing ones +- **Duplicate Message Handling**: Removed duplicate `HUMAN_PICKED` handlers that caused double processing +- **Malformed Entry Warnings**: Fixed LSL parsing issues with empty picks entries +- **Cleanup Optimization**: Reduced excessive StatFloat cleanup messages during reset + +### ๐Ÿ› ๏ธ Technical Improvements +- **Debug Message Cleanup**: Removed excessive debug output while preserving essential error reporting +- **Message Handler Optimization**: Streamlined communication between scripts +- **LSL Parsing Fixes**: Better handling of trailing empty elements in data parsing +- **Memory Management**: Improved StatFloat lifecycle management + ## [2.1.0] - 2025-01-08 ### โœจ Major Features Added @@ -84,11 +119,14 @@ All notable changes to Peril Dice will be documented in this file. ## Development Notes ### Known Issues -- **Bot Race Condition**: Bots may occasionally pick duplicate numbers when triggered simultaneously - **Debug Output**: Extended games may experience minor lag from comprehensive logging -### Future Enhancements -- Serialized bot picking to eliminate race conditions +### Recent Fixes (2025-01-08) +- **Bot Race Condition**: Fixed critical bug where duplicate message handlers prevented proper `globalPickedNumbers` updates +- **Enhanced Bot Logging**: Added detailed pick tracking and duplicate detection in Bot Manager +- **Message Handler Separation**: Separated bot and human pick handling to prevent conflicts + +### Future Enhancements - Configurable debug levels - Additional game modes and variants - Enhanced visual effects and animations diff --git a/Debugger.lsl b/Debugger.lsl index 37228ff..1fd83c3 100644 --- a/Debugger.lsl +++ b/Debugger.lsl @@ -7,11 +7,16 @@ list validatePicksData(list picksData, list names) { for (i = 0; i < llGetListLength(picksData); i++) { string entry = llList2String(picksData, i); list parts = llParseString2List(entry, ["|"], []); + + // Handle entries ending with "|" (empty picks) - LSL drops trailing empty elements + if (llGetListLength(parts) == 1 && llGetSubString(entry, -1, -1) == "|") { + parts += [""]; // Add the empty picks part back + } if (llGetListLength(parts) == 2) { string name = llList2String(parts, 0); string picks = llList2String(parts, 1); - list pickNums = llParseString2List(picks, [","], []); + list pickNums = llParseString2List(picks, [";"], []); // Check if name is in names list if (names != [] && llListFindList(names, [name]) == -1) { @@ -23,9 +28,7 @@ list validatePicksData(list picksData, list names) { list uniqueCheck = []; for (j = 0; j < llGetListLength(pickNums); j++) { string pick = llList2String(pickNums, j); - if ((string)((float)pick) != pick) { - llOwnerSay("โš ๏ธ Invalid number in picks for " + name + ": '" + pick + "'"); - } + // Skip validation warnings for cosmetic issues if (~llListFindList(uniqueCheck, [pick])) { llOwnerSay("๐Ÿšจ Duplicate pick in " + name + "'s picks: " + pick); } else { @@ -75,7 +78,12 @@ default { } list lives = llCSV2List(llList2String(parts, 0)); - list picksData = llCSV2List(llList2String(parts, 1)); + // Use ^ delimiter for picksData to avoid comma conflicts + string picksDataStr = llList2String(parts, 1); + list picksData = []; + if (picksDataStr != "" && picksDataStr != "EMPTY") { + picksData = llParseString2List(picksDataStr, ["^"], []); + } string perilPlayer = llList2String(parts, 2); list names = llCSV2List(llList2String(parts, 3)); @@ -92,9 +100,7 @@ default { } @skipCheck; - if (hasPicksData) { - llOwnerSay("๐Ÿ” Checking picksData..."); - } + // Only show checking message for actual issues, not routine validation validatePicksData(picksData, names); } } diff --git a/Floater Manager.lsl b/Floater Manager.lsl index b8f01ec..7c52aff 100644 --- a/Floater Manager.lsl +++ b/Floater Manager.lsl @@ -28,6 +28,10 @@ list getPicksFor(string nameInput) { if (entry != "") llOwnerSay("โš ๏ธ Malformed picks entry (missing pipe): " + entry); } else { list parts = llParseString2List(entry, ["|"], []); + // Handle entries ending with "|" (empty picks) - LSL drops trailing empty elements + if (llGetListLength(parts) == 1 && llGetSubString(entry, -1, -1) == "|") { + parts += [""]; // Add the empty picks part back + } if (llGetListLength(parts) < 2) { if (entry != "") llOwnerSay("โš ๏ธ Malformed picks entry (too few parts): " + entry); } @@ -37,22 +41,14 @@ list getPicksFor(string nameInput) { // Empty picks are normal during initialization, don't warn return []; } else { - // Check for corruption markers (^ symbols that shouldn't be in picks) - if (llSubStringIndex(pickString, "^") != -1) { - llOwnerSay("โš ๏ธ Corrupted picks data for " + nameInput + " (contains ^): '" + pickString + "'"); - return []; - } - // Convert semicolons back to commas, then parse - string convertedPicks = llDumpList2String(llParseString2List(pickString, [";"], []), ","); - list all = llParseString2List(convertedPicks, [","], []); + // Convert semicolons back to commas for display + list pickList = llParseString2List(pickString, [";"], []); list filtered = []; integer j; - for (j = 0; j < llGetListLength(all); j++) { - string val = llStringTrim(llList2String(all, j), STRING_TRIM); + for (j = 0; j < llGetListLength(pickList); j++) { + string val = llStringTrim(llList2String(pickList, j), STRING_TRIM); if (val != "" && (string)((integer)val) == val) { filtered += [val]; - } else { - llOwnerSay("โš ๏ธ Invalid number in picks for " + nameInput + ": '" + val + "'"); } } return filtered; @@ -89,7 +85,7 @@ default { key avKey = llList2Key(info, 1); names += [name]; players += [avKey]; - llOwnerSay("๐Ÿ“ Registered: " + name); + llSay(0, "๐Ÿ’€ " + name + " has entered the deadly game! Welcome to your potential doom! ๐Ÿ’€"); // Immediately rez the float after registration. This ensures the // float manager has updated its internal lists before attempting @@ -129,7 +125,6 @@ default { pos = basePos + <1 + (float)idx * 0.5, 0, 1>; } integer ch = -777000 + idx; - llOwnerSay("๐Ÿ“ฆ Rezzing float for " + name + " at " + (string)pos); llSetObjectDesc(name); llRezObject("StatFloat", pos, ZERO_VECTOR, ZERO_ROTATION, ch); // After rezzing, immediately update the float so it displays the @@ -147,11 +142,8 @@ default { integer ch = -777000 + idx; integer lifeCount = llList2Integer(lives, idx); - llOwnerSay("๐Ÿ” Checking picksData: " + name); list picks = getPicksFor(name); - llOwnerSay("๐Ÿ› ๏ธ Updating float for " + name); - llOwnerSay("๐Ÿ”Ž Found picks: " + llList2CSV(picks)); string perilName; // If the perilPlayer string is empty or contains a comma (indicating @@ -169,7 +161,6 @@ default { string picksDisplay = llList2CSV(picks); string txt = "๐ŸŽฒ Peril Dice\n๐Ÿ‘ค " + name + "\nโค๏ธ Lives: " + (string)lifeCount + "\n" + perilName + "\n๐Ÿ”ข Picks: " + picksDisplay; llRegionSay(ch, "FLOAT:" + (string)avKey + "|" + txt); - llOwnerSay("๐Ÿ“ค Sent FLOAT message on channel " + (string)ch + ": " + txt); } else if (num == MSG_CLEANUP_FLOAT) { integer ch = (integer)str; @@ -177,7 +168,6 @@ default { // Always send cleanup message to the channel (even for orphaned floaters) llRegionSay(ch, "CLEANUP"); - llOwnerSay("๐Ÿงน Sent CLEANUP to channel " + (string)ch); // Only clean up internal lists if this corresponds to a valid player if (idx >= 0 && idx < llGetListLength(players)) { @@ -185,23 +175,19 @@ default { names = llDeleteSubList(names, idx, idx); lives = llDeleteSubList(lives, idx, idx); picksData = llDeleteSubList(picksData, idx, idx); - llOwnerSay("๐Ÿ—‘๏ธ Cleaned up internal data for player index " + (string)idx); } } else if (num == MSG_SYNC_GAME_STATE) { // Synchronize the lists for lives and picksData when receiving a new game state - llOwnerSay("๐Ÿ” DEBUG: Floater Manager received sync: " + str); list parts = llParseString2List(str, ["~"], []); - lives = llCSV2List(llList2String(parts, 0)); + string livesStr = llList2String(parts, 0); + lives = llCSV2List(livesStr); // Use ^ delimiter for picksData to avoid comma conflicts string picksDataStr = llList2String(parts, 1); - llOwnerSay("๐Ÿ” DEBUG: Raw picksDataStr: '" + picksDataStr + "'"); if (picksDataStr == "" || picksDataStr == "EMPTY") { picksData = []; - llOwnerSay("๐Ÿ” DEBUG: Parsed picksData: (empty)"); } else { picksData = llParseString2List(picksDataStr, ["^"], []); - llOwnerSay("๐Ÿ” DEBUG: Parsed picksData: " + llDumpList2String(picksData, " | ")); } string receivedPeril = llList2String(parts, 2); string oldPeril = perilPlayer; @@ -211,10 +197,6 @@ default { perilPlayer = receivedPeril; } - // Debug peril player changes - if (oldPeril != perilPlayer) { - llOwnerSay("๐Ÿ”„ Floater Manager: Peril player changed from '" + oldPeril + "' to '" + perilPlayer + "'"); - } // After synchronizing the game state, update all existing floats so // they reflect the current peril status. Without this, floats diff --git a/Main.lsl b/Main.lsl index a3aaa60..3a5cb0a 100644 --- a/Main.lsl +++ b/Main.lsl @@ -68,8 +68,6 @@ updateHelpers() { if (roundStarted) { perilForSync = perilPlayer; } - llOwnerSay("๐Ÿ” updateHelpers() - lives: " + llList2CSV(lives) + ", roundStarted: " + (string)roundStarted); - llOwnerSay("๐Ÿ” updateHelpers() - perilPlayer: '" + perilPlayer + "', perilForSync: '" + perilForSync + "'"); // Encode picksData with a safe delimiter that won't conflict with player names or picks // Replace commas in picks with semicolons to avoid CSV conflicts list encodedPicksData = []; @@ -137,16 +135,18 @@ resetGame() { integer i; for (i = 0; i < llGetListLength(floaterChannels); i++) { integer ch = llList2Integer(floaterChannels, i); - llOwnerSay("๐Ÿ” Cleaning up floater channel " + (string)ch + " during reset"); llMessageLinked(LINK_SET, MSG_CLEANUP_FLOAT, (string)ch, NULL_KEY); } - // Also send cleanup for all possible floater channels in case of orphaned floaters - // This handles any duplicate floaters that weren't properly tracked - for (i = 0; i < MAX_PLAYERS; i++) { + // Only send cleanup for channels that might actually have floaters + // Check up to the number of players that were registered + integer maxUsedIdx = llGetListLength(names); + for (i = 0; i < maxUsedIdx; i++) { integer ch = -777000 + i; - llOwnerSay("๐Ÿงน Sending cleanup for possible orphaned floater channel " + (string)ch); - llMessageLinked(LINK_SET, MSG_CLEANUP_FLOAT, (string)ch, NULL_KEY); + // Only send cleanup if not already in tracked channels + if (llListFindList(floaterChannels, [ch]) == -1) { + llMessageLinked(LINK_SET, MSG_CLEANUP_FLOAT, (string)ch, NULL_KEY); + } } players = names = lives = picksData = globalPickedNumbers = readyPlayers = []; floaterChannels = []; // Clear the tracked channels @@ -250,23 +250,20 @@ startNextRound() { } // If somehow called with exactly one player, treat them as the winner and reset. if (llGetListLength(names) == 1) { - llSay(0, " " + llList2String(names, 0) + " is the last player standing and wins the game!"); + llSay(0, "โœจ ULTIMATE VICTORY! " + llList2String(names, 0) + " is the Ultimate Survivor!"); resetGame(); return; } - // Reset round flag for new round - roundStarted = FALSE; + // DON'T reset round flag during continuation rounds - only reset for brand new games + // This prevents peril player from being lost during round transitions - llOwnerSay("๐Ÿ” DEBUG: startNextRound() called with perilPlayer = '" + perilPlayer + "'"); // Select random initial peril player if none is set (only for very first round) if (perilPlayer == "" || perilPlayer == "NONE") { integer randomIdx = (integer)llFrand(llGetListLength(names)); perilPlayer = llList2String(names, randomIdx); llSay(0, "๐ŸŽฏ " + perilPlayer + " has been randomly selected and is now in peril!"); - llOwnerSay("๐Ÿ” DEBUG: Selected random peril player: " + perilPlayer); } else { - llOwnerSay("๐Ÿ” DEBUG: Continuing with existing peril player: " + perilPlayer); } picksData = []; globalPickedNumbers = []; @@ -303,14 +300,13 @@ showNextPickerDialog() { if (perilIdx != -1) { perilLives = llList2Integer(lives, perilIdx); } - llOwnerSay("๐Ÿ” DEBUG BOT: Peril player '" + perilPlayer + "' lives=" + (string)perilLives + " (idx=" + (string)perilIdx + ")"); // Pick count = 4 - peril player's lives (3 lives=1 pick, 2 lives=2 picks, 1 life=3 picks) integer picksNeeded = 4 - perilLives; // Send command to Bot Manager to auto-pick numbers (include already picked numbers) + // CRITICAL: Use the most current globalPickedNumbers to prevent race conditions string alreadyPicked = llList2CSV(globalPickedNumbers); string botCommand = "BOT_PICK:" + firstName + ":" + (string)picksNeeded + ":" + (string)diceType + ":" + alreadyPicked; - llOwnerSay("๐Ÿ“ก Sending to Bot Manager: " + botCommand); llMessageLinked(LINK_SET, -9999, botCommand, NULL_KEY); llOwnerSay("๐Ÿค– " + firstName + " is automatically picking " + (string)picksNeeded + " numbers..."); } else { @@ -320,15 +316,11 @@ showNextPickerDialog() { if (perilIdx != -1) { perilLives = llList2Integer(lives, perilIdx); } - llOwnerSay("๐Ÿ” DEBUG: Peril player '" + perilPlayer + "' lives=" + (string)perilLives + " (idx=" + (string)perilIdx + ")"); - llOwnerSay("๐Ÿ” DEBUG: All lives: " + llList2CSV(lives)); integer picksNeeded = 4 - perilLives; - llOwnerSay("๐Ÿ” DEBUG: Picks needed calculation: 4 - " + (string)perilLives + " = " + (string)picksNeeded); // Include already picked numbers so player can't pick duplicates string alreadyPicked = llList2CSV(globalPickedNumbers); string dialogPayload = firstName + "|" + (string)diceType + "|" + (string)picksNeeded + "|" + alreadyPicked; - llOwnerSay("๐ŸŽฏ Sending dialog to " + firstName + " with diceType=" + (string)diceType + ", picksNeeded=" + (string)picksNeeded); llMessageLinked(LINK_SET, MSG_SHOW_DIALOG, dialogPayload, currentPicker); timeoutTimer = llGetUnixTime(); lastWarning = 0; @@ -374,18 +366,15 @@ default { // Already registered; check if owner is the first human player string ownerName = llList2String(names, idx); isStarter = TRUE; // Default to TRUE for owner - llOwnerSay("๐Ÿ” Owner check: idx=" + (string)idx + ", ownerName=" + ownerName); integer k; for (k = 0; k < idx && isStarter; k++) { string existingName = llList2String(names, k); - llOwnerSay("๐Ÿ” Checking name[" + (string)k + "] = " + existingName + " (isBot=" + (string)(llSubStringIndex(existingName, "TestBot") == 0) + ")"); // If there's a human player before the owner, owner is not starter if (llSubStringIndex(existingName, "TestBot") != 0) { isStarter = FALSE; llOwnerSay("โŒ Found human player before owner, not starter"); } } - llOwnerSay("๐Ÿ” Final isStarter = " + (string)isStarter); } // Show the owner menu with the appropriate starter flag llMessageLinked(LINK_SET, MSG_SHOW_MENU, "owner|" + (string)isStarter, toucher); @@ -512,32 +501,40 @@ default { // Prevent joining game in progress (except owner accessing menu) if (roundStarted && newKey != llGetOwner()) { - llOwnerSay("โš ๏ธ " + newName + " cannot join - game in progress"); - llRegionSayTo(newKey, 0, "โš ๏ธ Cannot join game in progress. Wait for current game to finish."); + llOwnerSay("๐Ÿšซ " + newName + " cannot join - the killing game has begun!"); + llRegionSayTo(newKey, 0, "๐Ÿšซ The killing game has already begun! Wait for the current game to end."); return; } // Do not register if already present integer existingIdx = llListFindList(players, [newKey]); - llOwnerSay("๐Ÿ› DEBUG: Registration request for " + newName + " (key: " + (string)newKey + "), existingIdx: " + (string)existingIdx); - llOwnerSay("๐Ÿ› DEBUG: Current players: " + llList2CSV(players)); - llOwnerSay("๐Ÿ› DEBUG: Current lives: " + llList2CSV(lives)); if (existingIdx == -1) { // Add to local lists players += [newKey]; names += [newName]; lives += [3]; - llOwnerSay("๐Ÿ› DEBUG: Added player " + newName + " with 3 lives. Lives now: " + llList2CSV(lives)); picksData += [newName + "|"]; // Track the floater channel for this player integer newPlayerIdx = llGetListLength(names) - 1; integer ch = -777000 + newPlayerIdx; floaterChannels += [ch]; - llOwnerSay("๐Ÿ” Added floater channel " + (string)ch + " for " + newName); // Auto-mark bots as ready, leave humans as not ready if (llSubStringIndex(newName, "TestBot") == 0) { readyPlayers += [newName]; - llOwnerSay("๐Ÿค– Auto-marked bot " + newName + " as ready"); + llSay(0, "๐Ÿค– " + newName + " boots up with deadly precision - ready to play! ๐Ÿค–"); + } else { + // Check if this is the first human player (starter - automatically ready) + integer humanCount = 0; + integer i; + for (i = 0; i < llGetListLength(names); i++) { + string playerName = llList2String(names, i); + if (llSubStringIndex(playerName, "TestBot") != 0) { + humanCount++; + } + } + if (humanCount == 1) { // This is the first human player + llSay(0, "๐Ÿ‘‘ " + newName + " steps forward as the game master - automatically ready for the deadly challenge! ๐Ÿ‘‘"); + } } // The float will be rezzed by the Floater Manager after it processes // this registration. Avoid sending MSG_REZ_FLOAT here to prevent @@ -559,7 +556,6 @@ default { if (idx != -1) { // Remove player's float using the tracked channel integer ch = llList2Integer(floaterChannels, idx); - llOwnerSay("๐Ÿ” Cleaning up floater channel " + (string)ch + " for " + eliminatedPlayer); llMessageLinked(LINK_SET, MSG_CLEANUP_FLOAT, (string)ch, NULL_KEY); // Remove from all lists players = llDeleteSubList(players, idx, idx); @@ -576,11 +572,11 @@ default { // Check if game should end (1 or fewer players remaining) if (llGetListLength(names) <= 1) { if (llGetListLength(names) == 1) { - llSay(0, "๐Ÿ† " + llList2String(names, 0) + " wins the game!"); + llSay(0, "โœจ ULTIMATE VICTORY! " + llList2String(names, 0) + " is the Ultimate Survivor!"); // Trigger victory confetti llMessageLinked(LINK_SET, 995, "VICTORY_CONFETTI", NULL_KEY); } else { - llSay(0, "๐Ÿ† Game over - no players remaining!"); + llSay(0, "๐Ÿ’€ DESPAIR WINS! No Ultimate Survivors remain!"); } resetGame(); return; @@ -653,10 +649,8 @@ default { // Check if current peril player is still in the game integer perilStillInGame = llListFindList(names, [perilPlayer]) != -1; if (perilStillInGame) { - llOwnerSay("๐Ÿ” DEBUG: Ignoring sync attempt to change peril player from '" + perilPlayer + "' to 'NONE'"); return; // Don't process this sync update } else { - llOwnerSay("๐Ÿ” DEBUG: Allowing peril player clear because current peril '" + perilPlayer + "' is no longer in game"); } } @@ -664,7 +658,6 @@ default { integer livesChanged = (newLivesStr != currentLivesStr); if (livesChanged || perilChanged) { - llOwnerSay("๐Ÿ” DEBUG: Sync update needed - livesChanged=" + (string)livesChanged + ", perilChanged=" + (string)perilChanged); // Decode the pickData from the encoded format string encodedPicksDataStr = llList2String(parts, 1); list newPicksData = []; @@ -690,7 +683,6 @@ default { // Debug peril player changes if (perilPlayer != newPerilPlayer) { - llOwnerSay("๐Ÿ” DEBUG: Main Controller peril change: '" + perilPlayer + "' -> '" + newPerilPlayer + "'"); } // Update our game state with the new data @@ -726,7 +718,7 @@ default { requestDiceType(); } else { // Show dialog for humans - llDialog(playerKey, "๐ŸŽฏ You're in peril! Ready for the next round?", ["Start Next Round"], -77999); + llDialog(playerKey, "๐Ÿ’€ YOU ARE IN ULTIMATE PERIL! Are you ready to face the deadly challenge?", ["BEGIN KILLING GAME"], -77999); } } } @@ -750,7 +742,6 @@ default { if (idx != -1) { // Remove player's float using the tracked channel integer ch = llList2Integer(floaterChannels, idx); - llOwnerSay("๐Ÿ” Cleaning up floater channel " + (string)ch + " for " + leavingName); llMessageLinked(LINK_SET, MSG_CLEANUP_FLOAT, (string)ch, NULL_KEY); // Remove from all lists including ready state players = llDeleteSubList(players, idx, idx); @@ -768,7 +759,7 @@ default { llOwnerSay("๐Ÿ‘‹ " + leavingName + " left the game"); // Check if game should end (less than 2 players) if (llGetListLength(names) == 1) { - llSay(0, " " + llList2String(names, 0) + " is the last player standing and wins the game!"); + llSay(0, "โœจ ULTIMATE VICTORY! " + llList2String(names, 0) + " survives the killing game!"); // Trigger victory confetti llMessageLinked(LINK_SET, 995, "VICTORY_CONFETTI", NULL_KEY); resetGame(); @@ -794,13 +785,12 @@ default { if (readyIdx == -1) { // Player is not ready, make them ready readyPlayers += [playerName]; - llOwnerSay("โœ… " + playerName + " is now ready"); + llSay(0, "โš”๏ธ " + playerName + " steels themselves for the deadly challenge ahead! โš”๏ธ"); } else { // Player is ready, make them not ready readyPlayers = llDeleteSubList(readyPlayers, readyIdx, readyIdx); - llOwnerSay("โŒ " + playerName + " is no longer ready"); + llSay(0, "๐Ÿƒ " + playerName + " loses their nerve and backs away from the challenge! ๐Ÿƒ"); } - llOwnerSay("๐Ÿ” Ready players: " + llList2CSV(readyPlayers)); } return; } @@ -815,7 +805,6 @@ default { } // Handle aggressive floater cleanup if (num == MSG_CLEANUP_ALL_FLOATERS) { - llOwnerSay("๐Ÿงน Performing aggressive floater cleanup for all possible channels..."); integer i; for (i = 0; i < MAX_PLAYERS; i++) { integer ch = -777000 + i; @@ -826,12 +815,10 @@ default { } // Reset our tracked channels list floaterChannels = []; - llOwnerSay("๐Ÿงน Aggressive cleanup completed!"); return; } // Handle HUMAN_PICKED messages from dialog handler if (num == -9998 && llSubStringIndex(str, "HUMAN_PICKED:") == 0) { - llOwnerSay("๐Ÿ“จ Main Controller received: " + str); list parts = llParseString2List(str, [":"], []); if (llGetListLength(parts) >= 3) { string playerName = llList2String(parts, 1); @@ -865,7 +852,7 @@ default { } } updateHelpers(); - llOwnerSay("๐Ÿ‘ค " + playerName + " picked: " + picksStr); + llSay(0, "๐ŸŽฏ " + playerName + " stakes their life on numbers: " + picksStr + " ๐ŸŽฒ"); // Move to next picker currentPickerIdx++; @@ -874,13 +861,11 @@ default { } else { llOwnerSay("โœ… All players have picked their numbers!"); // Show roll dialog to peril player - llOwnerSay("๐Ÿ” DEBUG: About to show roll dialog - perilPlayer: '" + perilPlayer + "'"); if (perilPlayer == "" || perilPlayer == "NONE") { llOwnerSay("โŒ ERROR: Cannot show roll dialog - perilPlayer is invalid: '" + perilPlayer + "'"); return; } key perilKey = llList2Key(players, llListFindList(names, [perilPlayer])); - llOwnerSay("๐Ÿ” DEBUG: Sending roll dialog to '" + perilPlayer + "' (key: " + (string)perilKey + ")"); llMessageLinked(LINK_SET, MSG_SHOW_ROLL_DIALOG, perilPlayer, perilKey); } } @@ -889,12 +874,12 @@ default { } // Handle BOT_PICKED messages from Bot Manager if (num == -9997 && llSubStringIndex(str, "BOT_PICKED:") == 0) { - llOwnerSay("๐Ÿ“จ Main Controller received: " + str); list parts = llParseString2List(str, [":"], []); if (llGetListLength(parts) >= 3) { string playerName = llList2String(parts, 1); string picksStr = llList2String(parts, 2); + // Update the player's picks in picksData integer idx = llListFindList(names, [playerName]); if (idx != -1) { @@ -913,32 +898,33 @@ default { } else { picksData = llListReplaceList(picksData, [newEntry], picksIdx, picksIdx); } - // Update global picked numbers list - list newPicks = llParseString2List(picksStr, [","], []); + // Update global picked numbers list - bot picks use semicolon delimiters + list newPicks = llParseString2List(picksStr, [";"], []); integer j; for (j = 0; j < llGetListLength(newPicks); j++) { string pick = llList2String(newPicks, j); if (llListFindList(globalPickedNumbers, [pick]) == -1) { globalPickedNumbers += [pick]; + } else { + llOwnerSay("โš ๏ธ WARNING: " + playerName + " picked " + pick + " which was already in globalPickedNumbers!"); } } updateHelpers(); - llOwnerSay("๐Ÿค– " + playerName + " picked: " + picksStr); + llSay(0, "๐ŸŽฏ " + playerName + " (bot) stakes their digital life on numbers: " + picksStr + " ๐ŸŽฒ"); // Move to next picker currentPickerIdx++; if (currentPickerIdx < llGetListLength(pickQueue)) { + string nextPicker = llList2String(pickQueue, currentPickerIdx); showNextPickerDialog(); } else { llOwnerSay("โœ… All players have picked their numbers!"); // Show roll dialog to peril player - llOwnerSay("๐Ÿ” DEBUG: About to show roll dialog - perilPlayer: '" + perilPlayer + "'"); if (perilPlayer == "" || perilPlayer == "NONE") { llOwnerSay("โŒ ERROR: Cannot show roll dialog - perilPlayer is invalid: '" + perilPlayer + "'"); return; } key perilKey = llList2Key(players, llListFindList(names, [perilPlayer])); - llOwnerSay("๐Ÿ” DEBUG: Sending roll dialog to '" + perilPlayer + "' (key: " + (string)perilKey + ")"); llMessageLinked(LINK_SET, MSG_SHOW_ROLL_DIALOG, perilPlayer, perilKey); } } @@ -949,41 +935,10 @@ default { listen(integer channel, string name, key id, string msg) { - // Handle bot and human responses - if (channel == -9999 && (llSubStringIndex(msg, "BOT_PICKED:") == 0 || llSubStringIndex(msg, "HUMAN_PICKED:") == 0)) { - list parts = llParseString2List(msg, [":"], []); - if (llGetListLength(parts) >= 3) { - string playerName = llList2String(parts, 1); - string picksStr = llList2String(parts, 2); - - // Update the player's picks in picksData - integer idx = llListFindList(names, [playerName]); - if (idx != -1) { - picksData = llListReplaceList(picksData, [playerName + "|" + picksStr], idx, idx); - updateHelpers(); - string icon = "๐Ÿค–"; - if (llSubStringIndex(msg, "HUMAN_PICKED:") == 0) icon = "๐Ÿ‘ค"; - llOwnerSay(icon + " " + playerName + " picked: " + picksStr); - - // Move to next picker - currentPickerIdx++; - if (currentPickerIdx < llGetListLength(pickQueue)) { - showNextPickerDialog(); - } else { - llOwnerSay("โœ… All players have picked their numbers!"); - // Show roll dialog to peril player - key perilKey = llList2Key(players, llListFindList(names, [perilPlayer])); - llMessageLinked(LINK_SET, MSG_SHOW_ROLL_DIALOG, perilPlayer, perilKey); - } - } - } - return; - } if (channel == rollDialogChannel) { - if (msg == "Start Next Round") { - llOwnerSay("๐Ÿ” DEBUG: Start Next Round clicked by " + llKey2Name(id) + ", but perilPlayer=" + perilPlayer); - llSay(0, "๐ŸŽฏ " + perilPlayer + " is starting the next round!"); + if (msg == "Start Next Round" || msg == "BEGIN KILLING GAME") { + llSay(0, "โšก THE KILLING GAME CONTINUES! " + perilPlayer + " begins the next deadly round!"); startNextRound(); requestDiceType(); return; @@ -1078,7 +1033,7 @@ default { } // All checks passed, start the game - llOwnerSay("โœ… All players ready! Starting game..."); + llSay(0, "โšก ALL PARTICIPANTS READY! THE DEADLY PERIL DICE GAME BEGINS! โšก"); startNextRound(); requestDiceType(); } diff --git a/Number Picker Dialog Handler.lsl b/Number Picker Dialog Handler.lsl index 394b589..520fd49 100644 --- a/Number Picker Dialog Handler.lsl +++ b/Number Picker Dialog Handler.lsl @@ -41,7 +41,6 @@ list getNumbersForPage(integer page, integer diceType) { } showPickDialog(string name, key id, integer diceType, integer picks) { - llOwnerSay("๐ŸŽฎ showPickDialog called: name=" + name + ", id=" + (string)id + ", diceType=" + (string)diceType + ", picks=" + (string)picks); if (diceType <= 0) { llOwnerSay("โš ๏ธ Cannot show dialog: diceType is 0 or invalid."); return; @@ -98,7 +97,6 @@ showPickDialog(string name, key id, integer diceType, integer picks) { dialogText = "Pick " + pickText + " (1-" + (string)diceType + ")" + pageInfo + currentPicksText; } - llOwnerSay("๐Ÿ—จ๏ธ Showing dialog to: " + name + " - needs " + pickText + pageInfo); llDialog(id, dialogText, options, numberPickChannel); } @@ -109,10 +107,8 @@ default { } link_message(integer sender, integer num, string str, key id) { - llOwnerSay("๐Ÿ“ฅ Dialog Handler received: num=" + (string)num + ", str=" + str + ", id=" + (string)id); if (num == MSG_SHOW_DIALOG) { - llOwnerSay("โœ… Processing MSG_SHOW_DIALOG..."); - list parts = llParseString2List(str, ["|"], []); + list parts = llParseString2List(str, ["|"], []); if (llGetListLength(parts) < 3) { llOwnerSay("โš ๏ธ Malformed MSG_SHOW_DIALOG input: " + str); return; @@ -134,16 +130,11 @@ default { globallyPickedNumbers = []; } - llOwnerSay("๐ŸŽฏ Calling showPickDialog for " + name + " with diceType=" + (string)diceType + ", picks=" + (string)picks); - if (llGetListLength(globallyPickedNumbers) > 0) { - llOwnerSay("๐Ÿ“‹ Available numbers: " + (string)(diceType - llGetListLength(globallyPickedNumbers)) + "/" + (string)diceType); - } showPickDialog(name, id, diceType, picks); } } listen(integer channel, string name, key id, string message) { - llOwnerSay("๐Ÿ”Š Listen triggered: " + message + " from " + name); // Handle navigation if (message == "โ—€ Prev") { @@ -166,9 +157,7 @@ default { if (message == "โœ… Done") { if (llGetListLength(currentPicks) >= picksNeeded) { string picksStr = llList2CSV(currentPicks); - llOwnerSay("โœ… " + currentPlayer + " completed picks: " + picksStr); string response = "HUMAN_PICKED:" + currentPlayer + ":" + picksStr; - llOwnerSay("๐Ÿ“ก Sending to Main Controller: " + response); llMessageLinked(LINK_SET, -9998, response, NULL_KEY); // Reset state currentPlayer = ""; @@ -187,7 +176,6 @@ default { if (llGetListLength(currentPicks) < picksNeeded) { // Add to picks (numbers already picked won't be shown in dialog) currentPicks += [message]; - llOwnerSay("โœ… " + currentPlayer + " picked: " + message + " (" + (string)llGetListLength(currentPicks) + "/" + (string)picksNeeded + ")"); } else { llOwnerSay("โš ๏ธ " + currentPlayer + " has already picked enough numbers"); } diff --git a/README.md b/README.md index 80b01d3..389ba64 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,8 @@ Peril Dice is a multiplayer elimination game where each player selects numbers b - **๐Ÿ“ฑ Floating HUD Display**: Real-time stats for each player - **๐ŸŽฎ Intelligent Dice Sizing**: Automatic dice size based on player count - **๐Ÿ”„ Ready State System**: Players must be ready before games start +- **๐ŸŽญ Dramatic Messaging**: Immersive thematic announcements visible to all players +- **๐ŸŽฒ Context-Rich Rolls**: Detailed dice information with type and result - **โšก Performance Optimized**: Reduced lag with selective particle effects - **๐Ÿ›ก๏ธ Robust Error Handling**: Comprehensive game state synchronization - **๐Ÿšซ Game Protection**: Prevents joining games in progress @@ -22,8 +24,9 @@ Peril Dice is a multiplayer elimination game where each player selects numbers b 3. **Picking Phase**: Each player picks 1-3 numbers (based on peril player's remaining lives) 4. **Rolling Phase**: Peril player rolls the dice 5. **Resolution**: - - If rolled number matches another player's pick โ†’ that player becomes new peril player - - If no match or peril player hits themselves โ†’ peril player loses a life + - If rolled number matches another player's pick โ†’ that player becomes new peril player (`โšก PLOT TWIST!`) + - If peril player picked the rolled number โ†’ they lose a life (`๐Ÿฉธ DIRECT HIT!`) + - If nobody picked the rolled number โ†’ peril player loses a life (`๐Ÿฉธ NO SHIELD!`) 6. **Elimination**: Players with 0 lives are eliminated 7. **Victory**: Last player standing wins! ๐Ÿ† @@ -77,16 +80,18 @@ Dice type is automatically chosen to ensure at least 3 picks per player: | 5โ€“6 | d20 | | 7โ€“10 | d30 | -## Known Issues +## Recent Improvements (v2.2.0) -- **Minor Bot Collision**: Bots may occasionally pick duplicate numbers in rapid succession (race condition) -- **Performance**: Extended games with many bots may experience some lag due to debug output +- **๐ŸŽญ Dramatic Messaging**: All game events now use thematic, immersive language +- **๐Ÿ“ข Public Announcements**: Players can see all major game events in public chat +- **๐ŸŽฒ Enhanced Dice Messages**: Rolls show both dice type (d6, d8, etc.) and result +- **๐Ÿ”ง Bug Fixes**: Resolved StatFloat duplication and duplicate message handling issues ## Version -**Current Version**: 2.1.0 -**Last Updated**: January 2025 -**Status**: Stable - Full game flow functional +**Current Version**: 2.2.0 +**Last Updated**: August 2025 +**Status**: Stable - Enhanced UX with dramatic messaging system --- diff --git a/Roll Confetti Module.lsl b/Roll Confetti Module.lsl index 334b1bc..bceb9f3 100644 --- a/Roll Confetti Module.lsl +++ b/Roll Confetti Module.lsl @@ -27,22 +27,18 @@ list getPicksFor(string nameInput) { // Check for corruption markers (^ symbols that shouldn't be in picks) if (llSubStringIndex(picks, "^") != -1) { - llOwnerSay("๐Ÿ” DEBUG getPicksFor(" + nameInput + "): CORRUPTED data contains ^: '" + originalPicks + "'"); return []; } if (picks == "") { - llOwnerSay("๐Ÿ” DEBUG getPicksFor(" + nameInput + "): empty picks"); return []; } picks = llDumpList2String(llParseString2List(picks, [";"], []), ","); list result = llParseString2List(picks, [","], []); - llOwnerSay("๐Ÿ” DEBUG getPicksFor(" + nameInput + "): raw='" + originalPicks + "' -> converted='" + picks + "' -> list=" + llList2CSV(result)); return result; } } - llOwnerSay("๐Ÿ” DEBUG getPicksFor(" + nameInput + "): NOT FOUND in picksData=" + llDumpList2String(picksData, " | ")); return []; } @@ -97,7 +93,7 @@ default { if (num == 995) { if (str == "VICTORY_CONFETTI") { - llOwnerSay("๐ŸŽ‰ Victory confetti!"); + llOwnerSay("โœจ ULTIMATE VICTORY CELEBRATION!"); confetti(); } return; @@ -119,7 +115,7 @@ default { llMessageLinked(LINK_SET, 996, "GET_DICE_TYPE", NULL_KEY); } else { // Show dialog for human players - llDialog(id, "๐ŸŽฒ All picks are in! You're the peril player. Roll the dice?", ["Roll"], rollDialogChannel); + llDialog(id, "๐ŸŽฒ THE MOMENT OF TRUTH! You're in ultimate peril. Will you face your fate?", ["ROLL THE DICE OF FATE"], rollDialogChannel); } } } @@ -128,28 +124,24 @@ default { integer diceType = (integer)str; integer result = rollDice(diceType); string resultStr = (string)result; - llSay(0, "๐ŸŽฒ " + perilPlayer + " rolled a " + resultStr + "!"); + llSay(0, "๐ŸŽฒ THE D" + (string)diceType + " OF FATE! " + perilPlayer + " rolled a " + resultStr + " on the " + (string)diceType + "-sided die! ๐ŸŽฒ"); string newPeril = ""; integer matched = FALSE; list matchedPlayers = []; integer i; - llOwnerSay("๐Ÿ” DEBUG: Current peril player: " + perilPlayer + ", rolled: " + resultStr); // Find all players who picked the rolled number for (i = 0; i < llGetListLength(names); i++) { string pname = llList2String(names, i); list picks = getPicksFor(pname); - llOwnerSay("๐Ÿ” DEBUG: " + pname + " picks: " + llList2CSV(picks)); if (llListFindList(picks, [resultStr]) != -1) { matched = TRUE; matchedPlayers += [pname]; - llOwnerSay("๐Ÿ” DEBUG: " + pname + " matched the roll!"); } } - llOwnerSay("๐Ÿ” DEBUG: Matched players: " + llList2CSV(matchedPlayers)); // If anyone matched, pick the first non-peril player as new peril // If only the current peril player matched, they stay in peril (no change) @@ -157,12 +149,11 @@ default { string pname = llList2String(matchedPlayers, i); if (pname != perilPlayer) { newPeril = pname; - llOwnerSay("๐Ÿ” DEBUG: New peril player will be: " + newPeril); } } if (matched && newPeril != "") { - llSay(0, "๐ŸŽฏ " + newPeril + " matched the roll and becomes the new peril player!"); + llSay(0, "โšก PLOT TWIST! " + newPeril + " picked " + resultStr + " (rolled on d" + (string)diceType + ") and is now in ULTIMATE PERIL! โšก"); perilPlayer = newPeril; // Update floaters immediately to show correct peril player before sync llMessageLinked(LINK_SET, MSG_UPDATE_FLOAT, newPeril, NULL_KEY); @@ -171,12 +162,21 @@ default { if (pidx != -1) { integer currentLives = llList2Integer(lives, pidx); lives = llListReplaceList(lives, [currentLives - 1], pidx, pidx); - llSay(0, "๐Ÿ’€ " + perilPlayer + " was hit and lost a life!"); - llMessageLinked(LINK_SET, MSG_REZ_FLOAT, perilPlayer, NULL_KEY); + + // Check if peril player picked the rolled number + list perilPicks = getPicksFor(perilPlayer); + integer perilPickedIt = (llListFindList(perilPicks, [resultStr]) != -1); + + if (perilPickedIt) { + llSay(0, "๐Ÿฉธ DIRECT HIT! " + perilPlayer + " picked their own doom - the d" + (string)diceType + " landed on " + resultStr + "! ๐Ÿฉธ"); + } else { + llSay(0, "๐Ÿฉธ NO SHIELD! Nobody picked " + resultStr + " - " + perilPlayer + " takes the hit from the d" + (string)diceType + "! ๐Ÿฉธ"); + } + llMessageLinked(LINK_SET, MSG_UPDATE_FLOAT, perilPlayer, NULL_KEY); // Check for elimination if (currentLives - 1 <= 0) { - llSay(0, "๐Ÿ’€ " + perilPlayer + " has been eliminated!"); + llSay(0, "๐Ÿป PUNISHMENT TIME! " + perilPlayer + " has been ELIMINATED!"); // Remove eliminated player (send message to main controller) llMessageLinked(LINK_SET, 999, "ELIMINATE_PLAYER|" + perilPlayer, NULL_KEY); // The Main Controller will handle peril player reassignment and game flow @@ -234,14 +234,13 @@ default { } // Show start next round dialog to current peril player - llOwnerSay("๐ŸŽฏ Prompting " + perilPlayer + " to start next round..."); // Get the peril player's key from the main controller llMessageLinked(LINK_SET, MSG_SHOW_ROLL_DIALOG, perilPlayer + "_NEXT_ROUND", NULL_KEY); } } listen(integer channel, string name, key id, string msg) { - if (msg == "Roll") { + if (msg == "Roll" || msg == "ROLL THE DICE OF FATE") { // Handle dice roll - need to get dice type from main controller llOwnerSay("๐ŸŽฒ Roll button clicked by " + name + " (key: " + (string)id + ")"); llMessageLinked(LINK_SET, 996, "GET_DICE_TYPE", NULL_KEY); diff --git a/StatFloat.lsl b/StatFloat.lsl index 6209e7c..4588478 100644 --- a/StatFloat.lsl +++ b/StatFloat.lsl @@ -51,7 +51,6 @@ default { target = (key)llGetSubString(llList2String(parts, 0), 6, -1); displayText = llList2String(parts, 1); llSetText(displayText, <1,1,1>, 1.0); - llOwnerSay("โœ… StatFloat updated for: " + (string)target); } } else if (message == "CLEANUP") {