## [2.2.0] - 2025-08-04

This commit is contained in:
RebeccaNod1 2025-08-03 23:47:46 -05:00
parent 494f01c7bf
commit 4069379bf1
9 changed files with 175 additions and 183 deletions

View file

@ -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, [":"], []);

View file

@ -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

View file

@ -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);
}
}

View file

@ -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

135
Main.lsl
View file

@ -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();
}

View file

@ -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");
}

View file

@ -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
---

View file

@ -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);

View file

@ -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") {