diff --git a/Bot Manager b/Bot Manager deleted file mode 100644 index 9c23073..0000000 --- a/Bot Manager +++ /dev/null @@ -1,53 +0,0 @@ -// Bot Manager for Peril Dice -// Handles logic for test players (bots): picking numbers and rolling dice - -// Configuration -integer LISTEN_CHANNEL = -9999; // Change if needed to match main controller - -// Helper to parse and respond to pick commands -doBotPick(string botName, integer count, integer diceMax) { - list picks; - while (llGetListLength(picks) < count) { - integer roll = 1 + (integer)(llFrand((float)diceMax)); - if (llListFindList(picks, [roll]) == -1) { - picks += roll; - } - } - string pickString = llDumpList2String(picks, ","); - llRegionSay(LISTEN_CHANNEL, "BOT_PICKED:" + botName + ":" + pickString); -} - -// Helper to respond to peril roll commands -doBotRoll(string botName, integer diceMax) { - integer roll = 1 + (integer)(llFrand((float)diceMax)); - llRegionSay(LISTEN_CHANNEL, "BOT_ROLL:" + botName + ":" + (string)roll); -} - -default { - state_entry() { - llListen(LISTEN_CHANNEL, "", NULL_KEY, ""); - llOwnerSay("[Bot Manager] Ready and listening on channel " + (string)LISTEN_CHANNEL); - } - - listen(integer channel, string name, key id, string message) { - if (llSubStringIndex(message, "BOT_PICK:") == 0) { - // Format: BOT_PICK:bot_1:3:20 - list parts = llParseStringKeepNulls(message, [":"], []); - if (llGetListLength(parts) == 4) { - string botName = llList2String(parts, 1); - integer count = (integer)llList2String(parts, 2); - integer diceMax = (integer)llList2String(parts, 3); - doBotPick(botName, count, diceMax); - } - } - else if (llSubStringIndex(message, "BOT_PERIL_ROLL:") == 0) { - // Format: BOT_PERIL_ROLL:bot_1:20 - list parts = llParseStringKeepNulls(message, [":"], []); - if (llGetListLength(parts) == 3) { - string botName = llList2String(parts, 1); - integer diceMax = (integer)llList2String(parts, 2); - doBotRoll(botName, diceMax); - } - } - } -} diff --git a/Bot Manager.lsl b/Bot Manager.lsl new file mode 100644 index 0000000..bf4d10a --- /dev/null +++ b/Bot Manager.lsl @@ -0,0 +1,154 @@ +// Bot Manager for Peril Dice +// Handles logic for test players (bots): picking numbers and rolling dice + +// Configuration +integer LISTEN_CHANNEL = -9999; // Change if needed to match main controller +integer MSG_SYNC_GAME_STATE = 107; + +// Track game state to validate bot commands +list names = []; +list lives = []; +string perilPlayer = ""; + +// Helper to parse and respond to pick commands +doBotPick(string botName, integer count, integer diceMax, list avoidNumbers) { + // Validate bot exists and is still alive + integer botIdx = llListFindList(names, [botName]); + if (botIdx == -1) { + llOwnerSay("[Bot Manager] ❌ Bot '" + botName + "' not found in game - ignoring command"); + return; + } + + integer botLives = llList2Integer(lives, botIdx); + if (botLives <= 0) { + llOwnerSay("[Bot Manager] ❌ Bot '" + botName + "' is eliminated (lives=" + (string)botLives + ") - ignoring command"); + 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; + // Check if not already picked by this bot and not picked by others + if (llListFindList(picks, [rollStr]) == -1 && llListFindList(avoidNumbers, [rollStr]) == -1) { + picks += rollStr; + } + attempts++; + } + string pickString = llDumpList2String(picks, ","); + string response = "BOT_PICKED:" + botName + ":" + pickString; + llOwnerSay("[Bot Manager] Sending: " + response); + llMessageLinked(LINK_SET, -9997, response, NULL_KEY); +} + +// Helper to respond to peril roll commands +doBotRoll(string botName, integer diceMax) { + // Validate bot exists and is still alive + integer botIdx = llListFindList(names, [botName]); + if (botIdx == -1) { + llOwnerSay("[Bot Manager] ❌ Bot '" + botName + "' not found in game - ignoring roll command"); + return; + } + + integer botLives = llList2Integer(lives, botIdx); + if (botLives <= 0) { + llOwnerSay("[Bot Manager] ❌ Bot '" + botName + "' is eliminated (lives=" + (string)botLives + ") - ignoring roll command"); + return; + } + + if (botName != perilPlayer) { + llOwnerSay("[Bot Manager] ❌ Bot '" + botName + "' is not the peril player (current peril: '" + perilPlayer + "') - ignoring roll command"); + 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); +} + +default { + state_entry() { + llListen(LISTEN_CHANNEL, "", NULL_KEY, ""); + llOwnerSay("[Bot Manager] Ready and listening on channel " + (string)LISTEN_CHANNEL); + llOwnerSay("[Bot Manager] My key: " + (string)llGetKey()); + llOwnerSay("[Bot Manager] My position: " + (string)llGetPos()); + } + + link_message(integer sender, integer num, string str, key id) { + // Handle game state sync to track player eliminations + if (num == MSG_SYNC_GAME_STATE) { + list parts = llParseString2List(str, ["~"], []); + if (llGetListLength(parts) >= 4) { + lives = llCSV2List(llList2String(parts, 0)); + // Don't need picks data for validation, skip parts[1] + string receivedPeril = llList2String(parts, 2); + if (receivedPeril == "NONE") { + perilPlayer = ""; + } else { + 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) { + // 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) { + string botName = llList2String(parts, 1); + integer count = (integer)llList2String(parts, 2); + integer diceMax = (integer)llList2String(parts, 3); + list avoidNumbers = []; + if (llGetListLength(parts) >= 5) { + string avoidStr = llList2String(parts, 4); + if (avoidStr != "") { + avoidNumbers = llCSV2List(avoidStr); + } + } + doBotPick(botName, count, diceMax, avoidNumbers); + } + } + else if (llSubStringIndex(str, "BOT_PERIL_ROLL:") == 0) { + // Format: BOT_PERIL_ROLL:bot_1:20 + list parts = llParseStringKeepNulls(str, [":"], []); + if (llGetListLength(parts) == 3) { + string botName = llList2String(parts, 1); + integer diceMax = (integer)llList2String(parts, 2); + doBotRoll(botName, diceMax); + } + } + } + } + + 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, [":"], []); + if (llGetListLength(parts) == 4) { + string botName = llList2String(parts, 1); + integer count = (integer)llList2String(parts, 2); + integer diceMax = (integer)llList2String(parts, 3); + doBotPick(botName, count, diceMax, []); + } + } + else if (llSubStringIndex(message, "BOT_PERIL_ROLL:") == 0) { + // Format: BOT_PERIL_ROLL:bot_1:20 + list parts = llParseStringKeepNulls(message, [":"], []); + if (llGetListLength(parts) == 3) { + string botName = llList2String(parts, 1); + integer diceMax = (integer)llList2String(parts, 2); + doBotRoll(botName, diceMax); + } + } + } +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..0cd1895 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,99 @@ +# Changelog + +All notable changes to Peril Dice will be documented in this file. + +## [2.1.0] - 2025-01-08 + +### ✨ Major Features Added +- **Ready State System**: Players must mark themselves ready before games can start +- **Game Protection**: Prevents players from joining games in progress +- **Performance Optimization**: Reduced lag by limiting confetti to wins only +- **Enhanced Bot Intelligence**: Improved bot behavior and duplicate number avoidance + +### 🔧 Core Fixes +- **Complete Game Flow**: Fixed end-to-end gameplay from start to finish +- **Elimination Logic**: Proper handling of player elimination and game continuation +- **Win Condition**: Fixed victory detection and game ending +- **Data Synchronization**: Resolved corruption in pick data between scripts +- **Peril Player Assignment**: Fixed race conditions in peril player transitions + +### 🎮 Gameplay Improvements +- **Dynamic Picking**: Number of picks now properly based on peril player's lives +- **Duplicate Prevention**: Enhanced system to prevent duplicate number selection +- **Dialog Pagination**: Improved number selection interface for d30 dice +- **Floater Updates**: Real-time HUD updates throughout gameplay +- **Bot Continuation**: Bots now properly continue games when humans are eliminated + +### 🛠️ Technical Enhancements +- **Modular Communication**: Improved inter-script messaging system +- **Error Handling**: Comprehensive validation and error recovery +- **Memory Management**: Better floater cleanup and channel tracking +- **Data Encoding**: Fixed CSV conflicts with semicolon encoding system +- **State Management**: Robust game state synchronization across modules + +### 🐛 Bug Fixes +- Fixed missing pick dialogs after game events +- Resolved duplicate message issues between scripts +- Fixed lives reset problems between rounds +- Corrected floater display inconsistencies +- Fixed bot confusion during player elimination +- Resolved pick data corruption issues +- Fixed peril player variable sync problems + +### ⚡ Performance +- Removed confetti from life loss (keeping only victory confetti) +- Optimized debug output to reduce lag +- Improved floater management efficiency +- Enhanced bot processing speed + +## [2.0.0] - 2024-12-XX + +### 🎉 Initial Modular Release +- **Modular Architecture**: Separated functionality into specialized scripts +- **Bot System**: Added AI players for testing and gameplay variety +- **Floating HUD**: Real-time player statistics display +- **Dynamic Dice**: Automatic dice sizing based on player count +- **Player Management**: Runtime player joining and leaving +- **Owner Controls**: Administrative functions for game management + +### 🎲 Core Game Features +- **Pick System**: Number selection with validation +- **Rolling Mechanism**: Automated dice rolling with visual effects +- **Life Management**: 3-life system with elimination +- **Peril Tracking**: Dynamic peril player assignment +- **Victory Conditions**: Last player standing wins + +### 🏗️ Architecture +- **Main Controller**: Central game logic and state management +- **Dialog Handlers**: User interface and input processing +- **Roll Module**: Dice mechanics and particle effects +- **Bot Manager**: AI player behavior and automation +- **Floater Manager**: HUD display coordination +- **Game Helpers**: Utility functions and calculations + +## [1.0.0] - 2024-XX-XX + +### 🌟 Original Release +- Basic Peril Dice game implementation +- Manual gameplay mechanics +- Simple player tracking +- Basic dice rolling functionality + +--- + +## 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 +- Configurable debug levels +- Additional game modes and variants +- Enhanced visual effects and animations + +### Credits +- **Original Game Design**: Noose the Bunny (djmusica28) - Second Life +- **Automation & Development**: Enhanced for automated gameplay +- **Architecture**: Modular LSL system for scalability and maintainability \ No newline at end of file diff --git a/Debugger.lsl b/Debugger.lsl index e883ed3..37228ff 100644 --- a/Debugger.lsl +++ b/Debugger.lsl @@ -34,8 +34,12 @@ list validatePicksData(list picksData, list names) { allPicksFlat += [pick]; } - llOwnerSay("✅ " + name + "'s picks: " + llList2CSV(pickNums)); - } else { + // Only show picks if player has actually picked something + if (picks != "") { + llOwnerSay("✅ " + name + "'s picks: " + llList2CSV(pickNums)); + } + } else if (entry != "") { + // Only warn about non-empty malformed entries llOwnerSay("⚠️ Malformed entry in picksData: " + entry); } } @@ -58,6 +62,10 @@ list validatePicksData(list picksData, list names) { default { + state_entry() { + llOwnerSay("🔍 Debugger ready!"); + } + link_message(integer sender, integer num, string str, key id) { if (num == MSG_SYNC_GAME_STATE) { list parts = llParseString2List(str, ["~"], []); @@ -71,7 +79,22 @@ default { string perilPlayer = llList2String(parts, 2); list names = llCSV2List(llList2String(parts, 3)); - llOwnerSay("🔍 Checking picksData..."); + // Only show checking message if there's actual picks data to validate + integer hasPicksData = FALSE; + integer i; + for (i = 0; i < llGetListLength(picksData); i++) { + string entry = llList2String(picksData, i); + list parts = llParseString2List(entry, ["|"], []); + if (llGetListLength(parts) == 2 && llList2String(parts, 1) != "") { + hasPicksData = TRUE; + jump skipCheck; + } + } + @skipCheck; + + if (hasPicksData) { + llOwnerSay("🔍 Checking picksData..."); + } validatePicksData(picksData, names); } } diff --git a/Floater Manager.lsl b/Floater Manager.lsl index 578f60b..b8f01ec 100644 --- a/Floater Manager.lsl +++ b/Floater Manager.lsl @@ -25,18 +25,26 @@ list getPicksFor(string nameInput) { string entry = llList2String(picksData, i); if (llSubStringIndex(entry, "|") == -1) { - llOwnerSay("⚠️ Malformed picks entry (missing pipe): " + entry); + if (entry != "") llOwnerSay("⚠️ Malformed picks entry (missing pipe): " + entry); } else { list parts = llParseString2List(entry, ["|"], []); if (llGetListLength(parts) < 2) { - llOwnerSay("⚠️ Malformed picks entry (too few parts): " + entry); + if (entry != "") llOwnerSay("⚠️ Malformed picks entry (too few parts): " + entry); } else if (llList2String(parts, 0) == nameInput) { string pickString = llList2String(parts, 1); if (pickString == "") { - llOwnerSay("⚠️ Malformed or empty picks entry: " + entry); + // Empty picks are normal during initialization, don't warn + return []; } else { - list all = llParseString2List(pickString, [","], []); + // 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, [","], []); list filtered = []; integer j; for (j = 0; j < llGetListLength(all); j++) { @@ -64,6 +72,10 @@ string getNameFromKey(key id) { // Main event handler default { + state_entry() { + llOwnerSay("📦 Floater Manager ready!"); + } + link_message(integer sender, integer num, string str, key id) { if (num == MSG_REGISTER_PLAYER) { // Enforce the maximum number of players @@ -150,7 +162,8 @@ default { if (perilPlayer == "" || llSubStringIndex(perilPlayer, ",") != -1) { perilName = "🧍 Status: Waiting for game to start..."; } else { - perilName = "🧍 Peril: " + getNameFromKey(perilPlayer); + // perilPlayer is already a name string, not a key + perilName = "🧍 Peril: " + perilPlayer; } string picksDisplay = llList2CSV(picks); @@ -161,30 +174,47 @@ default { else if (num == MSG_CLEANUP_FLOAT) { integer ch = (integer)str; integer idx = ch - (-777000); + + // 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)) { - llRegionSay(ch, "CLEANUP"); players = llDeleteSubList(players, idx, idx); 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)); - list rawPicks = llCSV2List(llList2String(parts, 1)); - picksData = []; - integer i; - for (i = 0; i < llGetListLength(rawPicks); i++) { - string entry = llList2String(rawPicks, i); - if (llSubStringIndex(entry, "|") != -1) { - picksData += [entry]; - } else { - llOwnerSay("⚠️ Ignored malformed picksData during sync: " + entry); - } + // 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; + if (receivedPeril == "NONE") { + perilPlayer = ""; // Convert placeholder back to empty + } else { + perilPlayer = receivedPeril; + } + + // Debug peril player changes + if (oldPeril != perilPlayer) { + llOwnerSay("🔄 Floater Manager: Peril player changed from '" + oldPeril + "' to '" + perilPlayer + "'"); } - perilPlayer = llList2String(parts, 2); // After synchronizing the game state, update all existing floats so // they reflect the current peril status. Without this, floats diff --git a/Helper.lsl b/Helper.lsl index 8c4a544..9b4b9b6 100644 --- a/Helper.lsl +++ b/Helper.lsl @@ -1,358 +1,162 @@ -// === Main Peril Dice Controller (Refactored with Game Helpers Integration, with dynamic player join) === - -// -// This version of the main game controller includes support for players (including -// the owner) joining the game at runtime via a MSG_REGISTER_PLAYER message. When -// a new player registers, they are added to the internal lists (players, names, -// lives and picksData), a floating display is rezzed for them, and helpers are -// updated. This mirrors the behaviour used when adding a test player, but now -// applies to any avatar joining the game via the dialog handler. - -integer syncChannel = -77777; -integer numberPickChannel = -77888; -integer rollDialogChannel = -77999; -integer DIALOG_CHANNEL = -88888; - -list players = []; -list names = []; -list lives = []; -string perilPlayer = ""; -list globalPickedNumbers = []; -list picksData = []; -list readyPlayers = []; - -list pickQueue = []; -integer currentPickerIdx = 0; -integer diceType = 6; - -integer MSG_SHOW_DIALOG = 101; -integer MSG_ROLL_RESULT = 102; -integer MSG_UPDATE_FLOAT = 103; -integer MSG_CLEANUP_FLOAT = 104; -integer MSG_REZ_FLOAT = 105; -integer MSG_REGISTER_PLAYER = 106; -integer MSG_SYNC_GAME_STATE = 107; -integer MSG_SHOW_MENU = 201; -integer MSG_SHOW_ROLL_DIALOG = 301; +// === Game Helper Functions Module (Max 10 players) === +// This version chooses a dice size based on the number of players, +// ensuring at least three numbers are available per player. integer MSG_GET_DICE_TYPE = 1001; +integer MSG_DICE_TYPE_RESULT = 1005; 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_GAME_STATE = 107; integer MSG_SYNC_PICKQUEUE = 2001; -// Maximum number of players allowed (including test players). This should -// mirror the value used in Floater Manager to avoid inconsistencies. -integer MAX_PLAYERS = 10; +list lives; +list picksData; +string perilPlayer; +list names; +list pickQueue; -integer TIMEOUT_SECONDS = 600; -integer timeoutTimer; +// List of supported dice sizes; ensure the last entry can accommodate 10 players × 3 picks = 30 +list STANDARD_DICE = [6, 12, 20, 30]; -integer warning2min = 120; -integer warning5min = 300; -integer warning9min = 540; -integer lastWarning = 0; - -key currentPicker; - -// Forward game state to helpers when it changes -updateHelpers() { - string serialized = llList2CSV(lives) + "~" + llList2CSV(picksData) + - "~" + perilPlayer + "~" + llList2CSV(names); - llMessageLinked(LINK_SET, MSG_SYNC_GAME_STATE, serialized, NULL_KEY); - llMessageLinked(LINK_SET, MSG_SYNC_PICKQUEUE, llList2CSV(pickQueue), NULL_KEY); +// Determine which die to use based on player count. +// Each player should have at least 3 numbers available to choose from. +integer getDiceType(integer playerCount) { + integer minimumSides = playerCount * 3; + integer i; + for (i = 0; i < llGetListLength(STANDARD_DICE); i++) { + integer sides = llList2Integer(STANDARD_DICE, i); + if (sides >= minimumSides) { + return sides; + } + } + // If no predefined die has enough sides, return the largest supported die (d30) + return llList2Integer(STANDARD_DICE, llGetListLength(STANDARD_DICE) - 1); } -// Request helper-calculated values -requestDiceType() { - llMessageLinked(LINK_SET, MSG_GET_DICE_TYPE, (string)llGetListLength(names), NULL_KEY); +// Determine how many picks a player should make based on the peril player's lives +integer getPicksRequiredFromName(string name) { + integer idx = llListFindList(names, [name]); + if (idx == -1) { + integer i; + for (i = 0; i < llGetListLength(names); i++) { + string testName = llList2String(names, i); + if (llSubStringIndex(testName, name) != -1) { + idx = i; + i = llGetListLength(names); + } + } + } + if (idx == -1) return 0; + integer lifeCount = (integer)llList2String(lives, idx); + // Pick count = 4 - peril player's lives (3 lives=1 pick, 2 lives=2 picks, 1 life=3 picks) + return 4 - lifeCount; } -requestPicksRequired(integer idx) { - llMessageLinked(LINK_SET, MSG_GET_PICKS_REQUIRED, llList2String(names, idx), NULL_KEY); +// Find the index of a player in the pickQueue by name +integer getPickerIndex(string name) { + return llListFindList(pickQueue, [name]); } -requestPickerIndex(string name) { - llMessageLinked(LINK_SET, MSG_GET_PICKER_INDEX, name, NULL_KEY); -} - -string generateSerializedState() { +// Serialize the current game state for syncing with other scripts +string serializeGameState() { return llList2CSV(lives) + "~" + llList2CSV(picksData) + "~" + perilPlayer + "~" + llList2CSV(names); } -resetGame() { - // Remove floats in reverse order so indices remain correct as entries are - // deleted from the Floater Manager's lists. Otherwise, deleting index 0 - // first shifts subsequent indices and some floats are left behind. +// Show a pick management dialog for the owner. +// LSL does not support a void return type, so this returns an integer which is ignored. +integer showPickManager(string player, key id) { + string searchName = player; + if (llSubStringIndex(searchName, "~") != -1) { + list parts = llParseString2List(searchName, ["~"], []); + searchName = llList2String(parts, 1); + } + + integer idx = llListFindList(names, [searchName]); + if (idx == -1) { + integer i; + for (i = 0; i < llGetListLength(names); i++) { + string testName = llList2String(names, i); + if (llSubStringIndex(testName, searchName) != -1) { + idx = i; + llOwnerSay("✅ Matched pick list for: " + testName); + i = llGetListLength(names); + } + } + if (idx == -1) { + llOwnerSay("⚠️ Could not find player: " + player); + return 0; + } + } + + integer maxPicks = getPicksRequiredFromName(llList2String(names, idx)); + + string currentData = llList2String(picksData, idx); + list parts = llParseString2List(currentData, ["|"], []); + list currentPicks = []; + if (llGetListLength(parts) > 1) { + currentPicks = llParseString2List(llList2String(parts, 1), [","], []); + } + + list buttons = []; integer i; - for (i = llGetListLength(names) - 1; i >= 0; i--) { - integer ch = -777000 + i; - llMessageLinked(LINK_SET, MSG_CLEANUP_FLOAT, (string)ch, NULL_KEY); + for (i = 1; i <= 6; i++) { + string numStr = (string)i; + if (llListFindList(currentPicks, [numStr]) != -1) { + buttons += ["❌ " + numStr]; + } else if (llGetListLength(currentPicks) < maxPicks) { + buttons += ["➕ " + numStr]; + } } - players = names = lives = picksData = globalPickedNumbers = []; - perilPlayer = ""; - pickQueue = []; - currentPickerIdx = 0; - llSay(syncChannel, "RESET"); - llOwnerSay(" Game reset!"); - llSleep(0.2); - llSetTimerEvent(0); - updateHelpers(); -} -startNextRound() { - // Do not start a round if there are fewer than 2 players. This guard prevents - // accidental starts when only one player is present (e.g. after a reset or - // before any other participants join). - if (llGetListLength(names) < 2) { - llOwnerSay("⚠️ Need at least 2 players to start the game."); - return; - } - // 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!"); - resetGame(); - return; - } - picksData = []; - globalPickedNumbers = []; - pickQueue = names; - currentPickerIdx = 0; - integer j; - for (j = 0; j < llGetListLength(names); j++) { - picksData += [llList2String(names, j) + "|"]; - } - updateHelpers(); - // requestDiceType(); // Removed to avoid recursion loop -} + llOwnerSay("🛠 Showing pick dialog to: " + (string)id); + llDialog(id, "🛠 Managing picks for: " + searchName + "\nCurrent: " + llList2CSV(currentPicks), buttons, -88888); -showNextPickerDialog() { - string firstName = llList2String(pickQueue, currentPickerIdx); - currentPicker = llList2Key(players, llListFindList(names, [firstName])); - string dialogPayload = firstName + "|" + (string)diceType; - llMessageLinked(LINK_SET, MSG_SHOW_DIALOG, dialogPayload, currentPicker); - timeoutTimer = llGetUnixTime(); - lastWarning = 0; - llSetTimerEvent(60.0); + // Return dummy value since LSL does not have a void return type + return 0; } -integer roundStarted = FALSE; - default { state_entry() { - llListen(DIALOG_CHANNEL, "", NULL_KEY, ""); + llOwnerSay("🧮 Helper Module ready!"); } - - touch_start(integer total_number) { - llOwnerSay("Touched by: " + (string)llDetectedKey(0)); - key toucher = llDetectedKey(0); - integer idx = llListFindList(players, [toucher]); - if (toucher == llGetOwner()) { - // Determine if this will be the first registered player (starter) before registration - integer isStarter = FALSE; - if (idx == -1) { - // No index found; the owner is not yet registered. - // If there are no existing players, they will become the starter. - if (llGetListLength(players) == 0) { - isStarter = TRUE; - } - string ownerName = llKey2Name(toucher); - // Send a registration request; Main.lsl will handle adding them and rezzing a float - llMessageLinked(LINK_SET, MSG_REGISTER_PLAYER, ownerName + "|" + (string)toucher, NULL_KEY); - } else { - // Already registered; starter if at index 0 - isStarter = (idx == 0); - } - // Show the owner menu with the appropriate starter flag - llMessageLinked(LINK_SET, MSG_SHOW_MENU, "owner|" + (string)isStarter, toucher); - } else if (idx != -1) { - integer isStarter = (idx == 0); - llMessageLinked(LINK_SET, MSG_SHOW_MENU, "player|" + (string)isStarter, toucher); - } - } - + link_message(integer sender, integer num, string str, key id) { - // Player list and pick list handling remain unchanged - if (num == 202 && str == "REQUEST_PLAYER_LIST") { - string namesCSV = llList2CSV(names); - llMessageLinked(LINK_SET, 203, namesCSV, id); - return; - } - if (num == 206) { - string targetName = str; - integer i; - for (i = 0; i < llGetListLength(picksData); i++) { - string rawEntry = llList2String(picksData, i); - list parts = llParseString2List(rawEntry, ["|"], []); - if (llList2String(parts, 0) == targetName) { - llMessageLinked(LINK_SET, 205, targetName + "|" + llList2String(parts, 1), id); - return; - } - } - llMessageLinked(LINK_SET, 205, targetName + "|", id); - return; - } - if (num == 208) { - string playerName = str; - integer idx = llListFindList(names, [playerName]); - if (idx != -1) { - string lifeVal = llList2String(lives, idx); - llMessageLinked(LINK_SET, 208, playerName + "|" + lifeVal, id); - } - return; - } - // Handle dice type result from helper - if (num == MSG_DICE_TYPE_RESULT) { - if (!roundStarted) { - roundStarted = TRUE; - diceType = (integer)str; - llOwnerSay(" Dice type set to: " + str); - showNextPickerDialog(); - } - return; - } - if (num == MSG_GET_PICKS_REQUIRED) { - integer picksRequired = (integer)str; - llOwnerSay(" Picks required: " + str); - return; - } - if (num == MSG_GET_PICKER_INDEX) { - integer pickIndex = (integer)str; - llOwnerSay(" Picker index: " + str); - return; - } - if (num == MSG_SERIALIZE_GAME_STATE) { - string serialized = str; - llOwnerSay(" Serialized game state: " + serialized); - return; - } - // Handle pick actions - if (num == 204) { + if (num == MSG_SYNC_GAME_STATE) { list parts = llParseString2List(str, ["~"], []); - string action = llList2String(parts, 0); - list args = llParseString2List(llList2String(parts, 1), ["|"], []); - string name = llList2String(args, 0); - string pick = llList2String(args, 1); - integer i; - for (i = 0; i < llGetListLength(picksData); i++) { - string entry = llList2String(picksData, i); - list pdParts = llParseString2List(entry, ["|"], []); - if (llList2String(pdParts, 0) == name) { - list pickList = []; - string rawPicks = llList2String(pdParts, 1); - if (rawPicks != "") { - pickList = llParseString2List(rawPicks, [","], []); - } - if (action == "ADD_PICK") { - if (llListFindList(pickList, [pick]) == -1) { - pickList += [pick]; - llOwnerSay("➕ Added " + pick + " to " + name); - } - } else if (action == "REMOVE_PICK") { - integer idx = llListFindList(pickList, [pick]); - if (idx != -1) { - pickList = llDeleteSubList(pickList, idx, idx); - llOwnerSay("➖ Removed " + pick + " from " + name); - } - } - picksData = llListReplaceList(picksData, [name + "|" + llList2CSV(pickList)], i, i); - updateHelpers(); - return; - } - } - return; + lives = llCSV2List(llList2String(parts, 0)); + picksData = llCSV2List(llList2String(parts, 1)); + perilPlayer = llList2String(parts, 2); + names = llCSV2List(llList2String(parts, 3)); } - // New: handle dynamic registration of players (owner or players) via MSG_REGISTER_PLAYER - if (num == MSG_REGISTER_PLAYER) { - // str is in the format "Name|" - list parts = llParseString2List(str, ["|"], []); - string newName = llList2String(parts, 0); - key newKey = (key)llList2String(parts, 1); - // Do not register if already present - integer existingIdx = llListFindList(players, [newKey]); - if (existingIdx == -1) { - // Add to local lists - players += [newKey]; - names += [newName]; - lives += [3]; - picksData += [newName + "|"]; - // The float will be rezzed by the Floater Manager after it processes - // this registration. Avoid sending MSG_REZ_FLOAT here to prevent - // race conditions; instead, request the float from Floater Manager - // after it has updated its own lists. Immediately propagate - // helper updates now. - updateHelpers(); - // Notify owner of registration - llOwnerSay("🔔 Added player: " + newName); - } - return; + else if (num == MSG_SYNC_PICKQUEUE) { + pickQueue = llCSV2List(str); + } + else if (num == MSG_GET_DICE_TYPE) { + integer playerCount = (integer)str; + llOwnerSay("📊 playerCount received: " + (string)playerCount); + integer result = getDiceType(playerCount); + // Broadcast the dice type result across the entire linkset so that + // the main game controller can receive it. Using LINK_THIS + // restricted the message to the helper's prim, preventing the + // controller from seeing MSG_DICE_TYPE_RESULT and causing the + // game not to start. + llMessageLinked(LINK_SET, MSG_DICE_TYPE_RESULT, (string)result, NULL_KEY); + } + else if (num == MSG_GET_PICKS_REQUIRED) { + integer result = getPicksRequiredFromName(str); + llMessageLinked(LINK_THIS, MSG_GET_PICKS_REQUIRED, (string)result, NULL_KEY); + } + else if (num == MSG_GET_PICKER_INDEX) { + integer result = getPickerIndex(str); + llMessageLinked(LINK_THIS, MSG_GET_PICKER_INDEX, (string)result, NULL_KEY); + } + else if (num == MSG_SERIALIZE_GAME_STATE) { + llMessageLinked(LINK_THIS, MSG_SERIALIZE_GAME_STATE, serializeGameState(), NULL_KEY); + } + else if (num == 206) { + llOwnerSay("📨 MSG 206 received for: " + str + " from: " + (string)id); + showPickManager(str, id); } } - - listen(integer channel, string name, key id, string msg) { - if (channel == DIALOG_CHANNEL) { - // Owner-specific commands: only owner messages in the dialog should be processed here. - if (id == llGetOwner()) { - if (msg == "Reset Game") { - resetGame(); - return; - } - // Note: do not handle "Start Game" here; allow the generic start logic below to apply, - // so that minimum player checks are enforced and non-owner starters can initiate the game. - if (msg == "Dump Players") { - llOwnerSay(" Players: " + llList2CSV(names)); - return; - } - if (msg == "Manage Picks") { - llOwnerSay(" Fetching list of players for pick management..."); - llMessageLinked(LINK_SET, 202, "REQUEST_PLAYER_LIST", llGetOwner()); - return; - } - if (msg == "Add Test Player") { - // Ensure we do not exceed the maximum allowed players - if (llGetListLength(players) >= MAX_PLAYERS) { - llOwnerSay("⚠️ Cannot add test player; the game is full (max " + (string)MAX_PLAYERS + ")."); - return; - } - // Generate a unique name and key for the test player - string testName = "TestBot" + (string)llGetUnixTime(); - key fake = llGenerateKey(); - // Delegate registration to the MSG_REGISTER_PLAYER handler; it will add to lists and rez a float - llMessageLinked(LINK_SET, MSG_REGISTER_PLAYER, testName + "|" + (string)fake, NULL_KEY); - // Show the owner menu again (not flagged as starter) - llMessageLinked(LINK_SET, MSG_SHOW_MENU, "owner|0", 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. - if (msg == "Start Game") { - // Enforce a minimum of 2 players before starting - if (llGetListLength(players) < 2) { - llOwnerSay("⚠️ Need at least 2 players to start the game."); - return; - } - // Check if the sender is the owner - if (id == llGetOwner()) { - startNextRound(); - requestDiceType(); - return; - } - // Otherwise, check if the sender is the very first player registered - if (llGetListLength(players) > 0) { - key firstPlayer = llList2Key(players, 0); - if (id == firstPlayer) { - startNextRound(); - requestDiceType(); - return; - } - } - } - // If the message text matches a player name, request their pick list - if (llListFindList(names, [msg]) != -1) { - llMessageLinked(LINK_SET, 206, msg, id); - } - } - } -} \ No newline at end of file +} diff --git a/Main.lsl b/Main.lsl index 4592520..a3aaa60 100644 --- a/Main.lsl +++ b/Main.lsl @@ -20,6 +20,7 @@ string perilPlayer = ""; list globalPickedNumbers = []; list picksData = []; list readyPlayers = []; +list floaterChannels = []; // Track actual floater channels for cleanup list pickQueue = []; integer currentPickerIdx = 0; @@ -33,6 +34,10 @@ integer MSG_REZ_FLOAT = 105; integer MSG_REGISTER_PLAYER = 106; integer MSG_SYNC_GAME_STATE = 107; integer MSG_SHOW_MENU = 201; +integer MSG_TOGGLE_READY = 202; +integer MSG_QUERY_READY_STATE = 210; +integer MSG_READY_STATE_RESULT = 211; +integer MSG_CLEANUP_ALL_FLOATERS = 212; integer MSG_SHOW_ROLL_DIALOG = 301; integer MSG_GET_DICE_TYPE = 1001; @@ -58,8 +63,54 @@ key currentPicker; // Forward game state to helpers when it changes updateHelpers() { - string serialized = llList2CSV(lives) + "~" + llList2CSV(picksData) + - "~" + perilPlayer + "~" + llList2CSV(names); + // Don't send peril player name if game hasn't started yet + string perilForSync = "NONE"; // Use placeholder instead of empty string + 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 = []; + integer i; + for (i = 0; i < llGetListLength(picksData); i++) { + string entry = llList2String(picksData, i); + // Replace commas with semicolons in the picks portion only + list parts = llParseString2List(entry, ["|"], []); + if (llGetListLength(parts) >= 2) { + string playerName = llList2String(parts, 0); + string picks = llList2String(parts, 1); + // Only process picks if they're not empty + if (picks != "") { + // Parse picks, trim whitespace, then join with semicolons + list pickList = llParseString2List(picks, [","], []); + list cleanPickList = []; + integer j; + for (j = 0; j < llGetListLength(pickList); j++) { + string pick = llStringTrim(llList2String(pickList, j), STRING_TRIM); + if (pick != "") { + cleanPickList += [pick]; + } + } + picks = llDumpList2String(cleanPickList, ";"); + } + encodedPicksData += [playerName + "|" + picks]; + } else if (llSubStringIndex(entry, "|") != -1) { + // Handle entries with empty picks (e.g., "PlayerName|") + encodedPicksData += [entry]; + } else { + // Skip malformed entries without pipe separator + llOwnerSay("⚠️ Skipping malformed picksData entry: " + entry); + } + } + string picksDataStr = llDumpList2String(encodedPicksData, "^"); + // Ensure we always send valid format even with empty picks + if (picksDataStr == "") { + picksDataStr = "EMPTY"; // Use a clear placeholder for empty picks data + } + string serialized = llList2CSV(lives) + "~" + picksDataStr + + "~" + perilForSync + "~" + llList2CSV(names); llMessageLinked(LINK_SET, MSG_SYNC_GAME_STATE, serialized, NULL_KEY); llMessageLinked(LINK_SET, MSG_SYNC_PICKQUEUE, llList2CSV(pickQueue), NULL_KEY); } @@ -82,18 +133,27 @@ string generateSerializedState() { } resetGame() { - // Remove floats in reverse order so indices remain correct as entries are - // deleted from the Floater Manager's lists. Otherwise, deleting index 0 - // first shifts subsequent indices and some floats are left behind. + // Clean up all tracked floater channels integer i; - for (i = llGetListLength(names) - 1; i >= 0; i--) { - integer ch = -777000 + 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); } - players = names = lives = picksData = globalPickedNumbers = []; + + // 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++) { + integer ch = -777000 + i; + llOwnerSay("🧹 Sending cleanup for possible orphaned floater channel " + (string)ch); + llMessageLinked(LINK_SET, MSG_CLEANUP_FLOAT, (string)ch, NULL_KEY); + } + players = names = lives = picksData = globalPickedNumbers = readyPlayers = []; + floaterChannels = []; // Clear the tracked channels perilPlayer = ""; pickQueue = []; currentPickerIdx = 0; + roundStarted = FALSE; // Reset round flag llSay(syncChannel, "RESET"); llOwnerSay(" Game reset!"); llSleep(0.2); @@ -101,6 +161,85 @@ resetGame() { updateHelpers(); } +continueCurrentRound() { + // Continue the current round after elimination with existing picks intact + // This is different from startNextRound() which resets picks for a new round + llOwnerSay("🔄 Continuing current round with new peril player: " + perilPlayer); + + // Remove eliminated player's pick data but keep others + integer i; + list newPicksData = []; + for (i = 0; i < llGetListLength(picksData); i++) { + string entry = llList2String(picksData, i); + list parts = llParseString2List(entry, ["|"], []); + if (llGetListLength(parts) >= 1) { + string entryPlayerName = llList2String(parts, 0); + // Only keep picks from players still in the game + if (llListFindList(names, [entryPlayerName]) != -1) { + newPicksData += [entry]; + } + } + } + picksData = newPicksData; + + // Update global picked numbers to match remaining players' picks + globalPickedNumbers = []; + for (i = 0; i < llGetListLength(picksData); i++) { + string entry = llList2String(picksData, i); + list parts = llParseString2List(entry, ["|"], []); + if (llGetListLength(parts) >= 2) { + string picks = llList2String(parts, 1); + if (picks != "") { + // Handle semicolon encoding for picks + string decodedPicks = llDumpList2String(llParseString2List(picks, [";"], []), ","); + list playerPicks = llParseString2List(decodedPicks, [","], []); + globalPickedNumbers += playerPicks; + } + } + } + + // Check if all remaining players have picked + integer allPicked = TRUE; + for (i = 0; i < llGetListLength(names); i++) { + string playerName = llList2String(names, i); + // Check if this player has picks + integer hasPicksEntry = FALSE; + integer j; + for (j = 0; j < llGetListLength(picksData); j++) { + string entry = llList2String(picksData, j); + list parts = llParseString2List(entry, ["|"], []); + if (llGetListLength(parts) >= 1 && llList2String(parts, 0) == playerName) { + string picks = ""; + if (llGetListLength(parts) >= 2) { + picks = llList2String(parts, 1); + } + if (picks != "") { + hasPicksEntry = TRUE; + } + } + } + if (!hasPicksEntry) { + allPicked = FALSE; + } + } + + if (allPicked) { + // All players have picked, prompt peril player to roll + llOwnerSay("✅ All remaining players have picks, prompting " + perilPlayer + " to roll"); + integer perilIdx = llListFindList(names, [perilPlayer]); + if (perilIdx != -1) { + key perilKey = llList2Key(players, perilIdx); + llMessageLinked(LINK_SET, MSG_SHOW_ROLL_DIALOG, perilPlayer, perilKey); + } + } else { + // Some players still need to pick, continue with pick phase + llOwnerSay("⏳ Some players still need to pick, continuing pick phase"); + updateHelpers(); + // Check if peril player needs to pick + showNextPickerDialog(); + } +} + startNextRound() { // Do not start a round if there are fewer than 2 players. This guard prevents // accidental starts when only one player is present (e.g. after a reset or @@ -115,33 +254,98 @@ startNextRound() { resetGame(); return; } + // Reset round flag for new round + roundStarted = FALSE; + + 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 = []; - pickQueue = names; - currentPickerIdx = 0; - integer j; - for (j = 0; j < llGetListLength(names); j++) { - picksData += [llList2String(names, j) + "|"]; + // Create pick queue with peril player first, then all others + pickQueue = [perilPlayer]; + integer i; + for (i = 0; i < llGetListLength(names); i++) { + string playerName = llList2String(names, i); + if (playerName != perilPlayer) { + pickQueue += [playerName]; + } } + currentPickerIdx = 0; + // Don't pre-populate picksData with empty entries - let them be added as picks are made + // This prevents encoding corruption with empty "|" entries updateHelpers(); // requestDiceType(); // Removed to avoid recursion loop } showNextPickerDialog() { + if (diceType <= 0) { + llOwnerSay("❌ Cannot show picker dialog: diceType not set (" + (string)diceType + ")"); + return; + } + string firstName = llList2String(pickQueue, currentPickerIdx); currentPicker = llList2Key(players, llListFindList(names, [firstName])); - string dialogPayload = firstName + "|" + (string)diceType; - llMessageLinked(LINK_SET, MSG_SHOW_DIALOG, dialogPayload, currentPicker); - timeoutTimer = llGetUnixTime(); - lastWarning = 0; - llSetTimerEvent(60.0); + + // Check if this is a bot (TestBot names) + if (llSubStringIndex(firstName, "TestBot") == 0) { + // Get number of picks needed based on peril player's lives + integer perilIdx = llListFindList(names, [perilPlayer]); + integer perilLives = 3; // default + 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) + 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 { + // Show dialog for human players - calculate picks needed + integer perilIdx = llListFindList(names, [perilPlayer]); + integer perilLives = 3; // default + 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; + llSetTimerEvent(60.0); + } } integer roundStarted = FALSE; default { state_entry() { + llOwnerSay("🎮 Main Controller ready!"); + llOwnerSay("🎮 Main Controller key: " + (string)llGetKey()); + llOwnerSay("🎮 Main Controller position: " + (string)llGetPos()); llListen(DIALOG_CHANNEL, "", NULL_KEY, ""); + llListen(-9999, "", NULL_KEY, ""); // Listen for bot responses + llListen(rollDialogChannel, "", NULL_KEY, ""); // Listen for roll dialog responses } touch_start(integer total_number) { @@ -153,21 +357,49 @@ default { integer isStarter = FALSE; if (idx == -1) { // No index found; the owner is not yet registered. - // If there are no existing players, they will become the starter. - if (llGetListLength(players) == 0) { - isStarter = TRUE; + // Owner becomes starter if there are no existing human players (excluding bots) + isStarter = TRUE; // Default to TRUE for owner + integer j; + for (j = 0; j < llGetListLength(names) && isStarter; j++) { + string existingName = llList2String(names, j); + // If there's already a human player (not a bot), owner is not starter + if (llSubStringIndex(existingName, "TestBot") != 0) { + isStarter = FALSE; + } } string ownerName = llKey2Name(toucher); // Send a registration request; Main.lsl will handle adding them and rezzing a float llMessageLinked(LINK_SET, MSG_REGISTER_PLAYER, ownerName + "|" + (string)toucher, NULL_KEY); } else { - // Already registered; starter if at index 0 - isStarter = (idx == 0); + // 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); } else if (idx != -1) { - integer isStarter = (idx == 0); + // For non-owner players, check if they're the first human player + integer isStarter = TRUE; // Default to TRUE + integer m; + for (m = 0; m < idx && isStarter; m++) { + string existingName = llList2String(names, m); + // If there's a human player before this player, they're not starter + if (llSubStringIndex(existingName, "TestBot") != 0) { + isStarter = FALSE; + } + } llMessageLinked(LINK_SET, MSG_SHOW_MENU, "player|" + (string)isStarter, toucher); } } @@ -208,6 +440,14 @@ default { roundStarted = TRUE; diceType = (integer)str; llOwnerSay(" Dice type set to: " + str); + // Update helpers now that game has started - this will sync peril player + updateHelpers(); + showNextPickerDialog(); + } else { + // Game already started, but dice type changed (e.g., after elimination) + diceType = (integer)str; + llOwnerSay(" Dice type set to: " + str); + updateHelpers(); showNextPickerDialog(); } return; @@ -269,14 +509,36 @@ default { list parts = llParseString2List(str, ["|"], []); string newName = llList2String(parts, 0); key newKey = (key)llList2String(parts, 1); + + // 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."); + 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"); + } // The float will be rezzed by the Floater Manager after it processes // this registration. Avoid sending MSG_REZ_FLOAT here to prevent // race conditions; instead, request the float from Floater Manager @@ -288,6 +550,196 @@ default { } return; } + // Handle elimination requests + if (num == 999) { + list parts = llParseString2List(str, ["|"], []); + if (llList2String(parts, 0) == "ELIMINATE_PLAYER") { + string eliminatedPlayer = llList2String(parts, 1); + integer idx = llListFindList(names, [eliminatedPlayer]); + if (idx != -1) { + // 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); + names = llDeleteSubList(names, idx, idx); + lives = llDeleteSubList(lives, idx, idx); + picksData = llDeleteSubList(picksData, idx, idx); + floaterChannels = llDeleteSubList(floaterChannels, idx, idx); + + llOwnerSay("🗑️ Eliminated " + eliminatedPlayer + ". Remaining players: " + (string)llGetListLength(names)); + + // Update all scripts with the new player lists after elimination + updateHelpers(); + + // 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!"); + // Trigger victory confetti + llMessageLinked(LINK_SET, 995, "VICTORY_CONFETTI", NULL_KEY); + } else { + llSay(0, "🏆 Game over - no players remaining!"); + } + resetGame(); + 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(); + } + } + } + return; + } + // Handle game won + if (num == 998) { + list parts = llParseString2List(str, ["|"], []); + if (llList2String(parts, 0) == "GAME_WON") { + string winner = llList2String(parts, 1); + llSleep(2.0); // Let celebration message display + resetGame(); + } + return; + } + // Handle incoming sync updates from Roll Confetti Module (only after rolls) + if (num == MSG_SYNC_GAME_STATE) { + // Only accept sync if it contains different data than what we have + list parts = llParseString2List(str, ["~"], []); + if (llGetListLength(parts) >= 4) { + list newLives = llCSV2List(llList2String(parts, 0)); + string currentLivesStr = llList2CSV(lives); + string newLivesStr = llList2CSV(newLives); + + // Check if we need to update (lives changed OR peril player changed) + string newPerilPlayerCheck = llList2String(parts, 2); + + // Don't accept "NONE" as a valid peril player if we already have a real one, + // UNLESS the eliminated player is trying to be set as peril player + if (newPerilPlayerCheck == "NONE" && perilPlayer != "" && perilPlayer != "NONE") { + // 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"); + } + } + + integer perilChanged = (perilPlayer != newPerilPlayerCheck); + 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 = []; + if (encodedPicksDataStr != "") { + list encodedEntries = llParseString2List(encodedPicksDataStr, ["^"], []); + integer i; + for (i = 0; i < llGetListLength(encodedEntries); i++) { + string entry = llList2String(encodedEntries, i); + list entryParts = llParseString2List(entry, ["|"], []); + if (llGetListLength(entryParts) >= 2) { + string playerName = llList2String(entryParts, 0); + string picks = llList2String(entryParts, 1); + // Convert semicolons back to commas + picks = llDumpList2String(llParseString2List(picks, [";"], []), ","); + newPicksData += [playerName + "|" + picks]; + } else { + newPicksData += [entry]; + } + } + } + string newPerilPlayer = llList2String(parts, 2); + list newNames = llCSV2List(llList2String(parts, 3)); + + // Debug peril player changes + if (perilPlayer != newPerilPlayer) { + llOwnerSay("🔍 DEBUG: Main Controller peril change: '" + perilPlayer + "' -> '" + newPerilPlayer + "'"); + } + + // Update our game state with the new data + lives = newLives; + picksData = newPicksData; + perilPlayer = newPerilPlayer; + names = newNames; + + + // Don't call updateHelpers() here to avoid loop - other modules already have the data + } + } + return; + } + // Handle start next round request + if (num == 997) { + if (str == "START_NEXT_ROUND") { + startNextRound(); + requestDiceType(); + } + else if (llSubStringIndex(str, "START_NEXT_ROUND_DIALOG|") == 0) { + string playerName = llGetSubString(str, 24, -1); // Remove "START_NEXT_ROUND_DIALOG|" prefix + integer idx = llListFindList(names, [playerName]); + if (idx != -1) { + key playerKey = llList2Key(players, idx); + llOwnerSay("🎯 Showing next round dialog to " + playerName); + // Check if it's a bot + if (llSubStringIndex(playerName, "TestBot") == 0) { + // Auto-start for bots with delay to allow sync to complete + llOwnerSay("🤖 " + playerName + " (bot) auto-starting next round..."); + llSleep(0.5); // Give sync time to complete + startNextRound(); + requestDiceType(); + } else { + // Show dialog for humans + llDialog(playerKey, "🎯 You're in peril! Ready for the next round?", ["Start Next Round"], -77999); + } + } + } + return; + } + // Handle dice type request from roll module + if (num == 996) { + if (str == "GET_DICE_TYPE") { + llOwnerSay("🎲 Main Controller sending dice type: " + (string)diceType); + llMessageLinked(LINK_SET, MSG_ROLL_RESULT, (string)diceType, NULL_KEY); + } + return; + } // Handle leave game requests if (num == 107) { list parts = llParseString2List(str, ["|"], []); @@ -296,20 +748,29 @@ default { key leavingKey = (key)llList2String(parts, 2); integer idx = llListFindList(players, [leavingKey]); if (idx != -1) { - // Remove player's float - integer ch = -777000 + idx; + // 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 + // Remove from all lists including ready state players = llDeleteSubList(players, idx, idx); names = llDeleteSubList(names, idx, idx); lives = llDeleteSubList(lives, idx, idx); picksData = llDeleteSubList(picksData, idx, idx); + floaterChannels = llDeleteSubList(floaterChannels, idx, idx); + // Remove from ready list if present + integer readyIdx = llListFindList(readyPlayers, [leavingName]); + if (readyIdx != -1) { + readyPlayers = llDeleteSubList(readyPlayers, readyIdx, readyIdx); + } // Update helpers updateHelpers(); 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!"); + // Trigger victory confetti + llMessageLinked(LINK_SET, 995, "VICTORY_CONFETTI", NULL_KEY); resetGame(); } else if (llGetListLength(names) == 0) { resetGame(); @@ -318,9 +779,217 @@ default { } return; } + // Handle ready state toggle + if (num == MSG_TOGGLE_READY) { + string playerName = str; + integer idx = llListFindList(names, [playerName]); + if (idx != -1) { + // Don't allow bots to toggle ready state (they're always ready) + if (llSubStringIndex(playerName, "TestBot") == 0) { + llOwnerSay("🤖 Bots are always ready and cannot change state"); + return; + } + + integer readyIdx = llListFindList(readyPlayers, [playerName]); + if (readyIdx == -1) { + // Player is not ready, make them ready + readyPlayers += [playerName]; + llOwnerSay("✅ " + playerName + " is now ready"); + } else { + // Player is ready, make them not ready + readyPlayers = llDeleteSubList(readyPlayers, readyIdx, readyIdx); + llOwnerSay("❌ " + playerName + " is no longer ready"); + } + llOwnerSay("🔍 Ready players: " + llList2CSV(readyPlayers)); + } + return; + } + // Handle ready state queries + if (num == MSG_QUERY_READY_STATE) { + string playerName = str; + integer isReady = llListFindList(readyPlayers, [playerName]) != -1; + integer isBot = llSubStringIndex(playerName, "TestBot") == 0; + string result = playerName + "|" + (string)isReady + "|" + (string)isBot; + llMessageLinked(LINK_SET, MSG_READY_STATE_RESULT, result, id); + return; + } + // 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; + llMessageLinked(LINK_SET, MSG_CLEANUP_FLOAT, (string)ch, NULL_KEY); + // Also try some potential duplicate channels + llMessageLinked(LINK_SET, MSG_CLEANUP_FLOAT, (string)(ch + 100), NULL_KEY); + llMessageLinked(LINK_SET, MSG_CLEANUP_FLOAT, (string)(ch + 1000), NULL_KEY); + } + // 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); + string picksStr = llList2String(parts, 2); + + // Update the player's picks in picksData + integer idx = llListFindList(names, [playerName]); + if (idx != -1) { + // Find existing picks entry or add new one + integer picksIdx = -1; + integer k; + for (k = 0; k < llGetListLength(picksData); k++) { + string entry = llList2String(picksData, k); + if (llSubStringIndex(entry, playerName + "|") == 0) { + picksIdx = k; + } + } + string newEntry = playerName + "|" + picksStr; + if (picksIdx == -1) { + picksData += [newEntry]; + } else { + picksData = llListReplaceList(picksData, [newEntry], picksIdx, picksIdx); + } + // Update global picked numbers list + list newPicks = llParseString2List(picksStr, [","], []); + integer i; + for (i = 0; i < llGetListLength(newPicks); i++) { + string pick = llList2String(newPicks, i); + if (llListFindList(globalPickedNumbers, [pick]) == -1) { + globalPickedNumbers += [pick]; + } + } + updateHelpers(); + llOwnerSay("👤 " + 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 + 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); + } + } + } + return; + } + // 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) { + // Find existing picks entry or add new one + integer picksIdx = -1; + integer k; + for (k = 0; k < llGetListLength(picksData); k++) { + string entry = llList2String(picksData, k); + if (llSubStringIndex(entry, playerName + "|") == 0) { + picksIdx = k; + } + } + string newEntry = playerName + "|" + picksStr; + if (picksIdx == -1) { + picksData += [newEntry]; + } else { + picksData = llListReplaceList(picksData, [newEntry], picksIdx, picksIdx); + } + // Update global picked numbers list + 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]; + } + } + updateHelpers(); + llOwnerSay("🤖 " + 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 + 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); + } + } + } + return; + } } + 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!"); + startNextRound(); + requestDiceType(); + return; + } + } + if (channel == DIALOG_CHANNEL) { // Owner-specific commands: only owner messages in the dialog should be processed here. if (id == llGetOwner()) { @@ -350,8 +1019,18 @@ default { key fake = llGenerateKey(); // Delegate registration to the MSG_REGISTER_PLAYER handler; it will add to lists and rez a float llMessageLinked(LINK_SET, MSG_REGISTER_PLAYER, testName + "|" + (string)fake, NULL_KEY); - // Show the owner menu again (not flagged as starter) - llMessageLinked(LINK_SET, MSG_SHOW_MENU, "owner|0", llGetOwner()); + // Show the owner menu again - owner should still be starter after adding bots + 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; } } @@ -363,21 +1042,45 @@ default { llOwnerSay("⚠️ Need at least 2 players to start the game."); return; } - // Check if the sender is the owner + + // Check if all non-starter players are ready + string starterName = ""; if (id == llGetOwner()) { - startNextRound(); - requestDiceType(); - return; - } - // Otherwise, check if the sender is the very first player registered - if (llGetListLength(players) > 0) { + starterName = llKey2Name(llGetOwner()); + } else if (llGetListLength(players) > 0) { key firstPlayer = llList2Key(players, 0); if (id == firstPlayer) { - startNextRound(); - requestDiceType(); - return; + starterName = llKey2Name(firstPlayer); } } + + if (starterName == "") { + llOwnerSay("⚠️ Only the game starter can start the game."); + return; + } + + // Check if all non-starter human players are ready + list notReadyPlayers = []; + integer i; + for (i = 0; i < llGetListLength(names); i++) { + string playerName = llList2String(names, i); + // Skip the starter and bots (bots are auto-ready) + if (playerName != starterName && llSubStringIndex(playerName, "TestBot") != 0) { + if (llListFindList(readyPlayers, [playerName]) == -1) { + notReadyPlayers += [playerName]; + } + } + } + + if (llGetListLength(notReadyPlayers) > 0) { + llOwnerSay("⚠️ Cannot start game. These players are not ready: " + llList2CSV(notReadyPlayers)); + return; + } + + // All checks passed, start the game + llOwnerSay("✅ All players ready! Starting game..."); + startNextRound(); + requestDiceType(); } // If the message text matches a player name, request their pick list if (llListFindList(names, [msg]) != -1) { diff --git a/Number Picker Dialog Handler.lsl b/Number Picker Dialog Handler.lsl index 967b29f..394b589 100644 --- a/Number Picker Dialog Handler.lsl +++ b/Number Picker Dialog Handler.lsl @@ -1,50 +1,199 @@ -// === Dialog Handler (Updated for Peril Dice) === +// === Dialog Handler (Updated for Peril Dice - Paginated Multiple Picks) === integer MSG_SHOW_DIALOG = 101; integer numberPickChannel = -77888; -list getDialogButtons(integer diceType) { - list buttons = []; +// Pagination and picking state +string currentPlayer = ""; +key currentPlayerKey = NULL_KEY; +integer currentDiceType = 0; +integer picksNeeded = 1; +list currentPicks = []; +list globallyPickedNumbers = []; // Numbers already picked by other players +integer currentPage = 0; +integer NUMBERS_PER_PAGE = 9; // Leave room for navigation buttons + +list getNumbersForPage(integer page, integer diceType) { + // Build list of available numbers (not picked by others OR by current player) + list availableNumbers = []; integer i; for (i = 1; i <= diceType; i++) { - buttons += [(string)i]; + string numStr = (string)i; + integer pickedByOthers = llListFindList(globallyPickedNumbers, [numStr]) != -1; + integer pickedByMe = llListFindList(currentPicks, [numStr]) != -1; + if (!pickedByOthers && !pickedByMe) { + availableNumbers += [numStr]; + } + } + + // Get the subset for this page + integer startIdx = page * NUMBERS_PER_PAGE; + integer endIdx = startIdx + NUMBERS_PER_PAGE - 1; + if (endIdx >= llGetListLength(availableNumbers)) { + endIdx = llGetListLength(availableNumbers) - 1; + } + + list buttons = []; + for (i = startIdx; i <= endIdx && i < llGetListLength(availableNumbers); i++) { + buttons += [llList2String(availableNumbers, i)]; } return buttons; } -showPickDialog(string name, key id, 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; } - list options = getDialogButtons(diceType); - llOwnerSay("🗨️ Showing dialog to: " + name); - llOwnerSay("📋 Page: 0, DiceType: " + (string)diceType); - llOwnerSay("🔢 Options: " + llList2CSV(options)); - llDialog(id, "Choose a number (1 - " + (string)diceType + "):", options, numberPickChannel); + // Initialize or update picking session + if (currentPlayer != name) { + currentPlayer = name; + currentPlayerKey = id; + currentDiceType = diceType; + picksNeeded = picks; + currentPicks = []; + currentPage = 0; + } + + list options; + integer totalPages = 1; // Default value + + // If player has picked enough numbers, only show Done button + if (llGetListLength(currentPicks) >= picksNeeded) { + options = ["✅ Done"]; + } else { + // Show number selection with pagination + options = getNumbersForPage(currentPage, diceType); + + // Calculate total pages based on available numbers, not all numbers + integer availableCount = diceType - llGetListLength(globallyPickedNumbers) - llGetListLength(currentPicks); + totalPages = (availableCount + NUMBERS_PER_PAGE - 1) / NUMBERS_PER_PAGE; + if (totalPages < 1) totalPages = 1; + if (totalPages > 1) { + if (currentPage > 0) options += ["◀ Prev"]; + if (currentPage < totalPages - 1) options += ["Next ▶"]; + } + } + + string pickText = (string)picksNeeded; + if (picksNeeded == 1) pickText += " number"; + else pickText += " numbers"; + + string pageInfo = ""; + if (totalPages > 1) { + pageInfo = " (Page " + (string)(currentPage + 1) + "/" + (string)totalPages + ")"; + } + + string dialogText; + string currentPicksText = ""; + if (llGetListLength(currentPicks) > 0) { + currentPicksText = "\nSelected: " + llList2CSV(currentPicks); + } + + if (llGetListLength(currentPicks) >= picksNeeded) { + dialogText = "✅ You have picked enough numbers!" + currentPicksText + "\n\nClick Done to confirm your selection."; + } else { + dialogText = "Pick " + pickText + " (1-" + (string)diceType + ")" + pageInfo + currentPicksText; + } + + llOwnerSay("🗨️ Showing dialog to: " + name + " - needs " + pickText + pageInfo); + llDialog(id, dialogText, options, numberPickChannel); } default { state_entry() { + llOwnerSay("🎮 Number Picker Dialog Handler ready!"); llListen(numberPickChannel, "", NULL_KEY, ""); } 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, ["|"], []); - if (llGetListLength(parts) < 2) { + if (llGetListLength(parts) < 3) { llOwnerSay("⚠️ Malformed MSG_SHOW_DIALOG input: " + str); return; } string name = llList2String(parts, 0); integer diceType = (integer)llList2String(parts, 1); - showPickDialog(name, id, diceType); + integer picks = (integer)llList2String(parts, 2); + + // Get globally picked numbers if provided + if (llGetListLength(parts) >= 4) { + string globalPicksStr = llList2String(parts, 3); + if (globalPicksStr != "") { + globallyPickedNumbers = llCSV2List(globalPicksStr); + } else { + globallyPickedNumbers = []; + } + } else { + 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 on channel: " + (string)channel + ", msg: " + message + ", from: " + name + " (" + (string)id + ")"); + llOwnerSay("🔊 Listen triggered: " + message + " from " + name); + + // Handle navigation + if (message == "◀ Prev") { + if (currentPage > 0) { + currentPage--; + showPickDialog(currentPlayer, currentPlayerKey, currentDiceType, picksNeeded); + } + return; + } + if (message == "Next ▶") { + integer totalPages = (currentDiceType + NUMBERS_PER_PAGE - 1) / NUMBERS_PER_PAGE; + if (currentPage < totalPages - 1) { + currentPage++; + showPickDialog(currentPlayer, currentPlayerKey, currentDiceType, picksNeeded); + } + return; + } + + // Handle completion + 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 = ""; + currentPlayerKey = NULL_KEY; + currentPicks = []; + } else { + llOwnerSay("⚠️ " + currentPlayer + " needs " + (string)(picksNeeded - llGetListLength(currentPicks)) + " more picks"); + showPickDialog(currentPlayer, currentPlayerKey, currentDiceType, picksNeeded); + } + return; + } + + // Handle number selection + integer pickedNumber = (integer)message; + if (pickedNumber > 0 && pickedNumber <= currentDiceType) { + 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"); + } + + // Show updated dialog + showPickDialog(currentPlayer, currentPlayerKey, currentDiceType, picksNeeded); + } } -} +} \ No newline at end of file diff --git a/Owner and Player Dialog Handler.lsl b/Owner and Player Dialog Handler.lsl index f496478..c0ca1b5 100644 --- a/Owner and Player Dialog Handler.lsl +++ b/Owner and Player Dialog Handler.lsl @@ -11,9 +11,18 @@ integer MSG_PICK_LIST_RESULT = 205; integer MSG_LIFE_LOOKUP = 207; integer MSG_REGISTER_PLAYER = 106; integer MSG_REZ_FLOAT = 105; +integer MSG_TOGGLE_READY = 202; +integer MSG_QUERY_READY_STATE = 210; +integer MSG_READY_STATE_RESULT = 211; +integer MSG_CLEANUP_ALL_FLOATERS = 212; // Owner-specific options shown when pressing the "Owner" button -list ownerOptions = ["Join Game", "Leave Game", "Add Test Player", "Manage Picks", "Start Game", "Reset Game", "Dump Players"]; +list ownerOptions = ["Join Game", "Leave Game", "Add Test Player", "Manage Picks", "Start Game", "Reset Game", "Dump Players", "Cleanup Floaters"]; + +// State tracking for dynamic ready menu +key pendingMenuPlayer = NULL_KEY; +integer pendingMenuIsStarter = FALSE; +integer pendingMenuIsOwner = FALSE; // Display the debug/owner menu showOwnerMenu(key id) { @@ -21,23 +30,48 @@ showOwnerMenu(key id) { } // Display the combined Ready/Leave menu for both players and owners -// If the caller is the starter, the first button is "Start"; otherwise "Ready". +// If the caller is the starter, the first button is "Start"; otherwise shows dynamic ready state. // Owners receive an extra "Owner" button to access the advanced owner menu. showReadyLeaveMenu(key id, integer isStarter, integer isOwner) { + // Store pending menu state and query ready status + pendingMenuPlayer = id; + pendingMenuIsStarter = isStarter; + pendingMenuIsOwner = isOwner; + + string playerName = llKey2Name(id); + llMessageLinked(LINK_SET, MSG_QUERY_READY_STATE, playerName, id); +} + +// Internal function to show the menu once we have ready state +showReadyLeaveMenuWithState(key id, integer isStarter, integer isOwner, integer isReady, integer isBot) { + // Bots don't get dialogs - they're managed by the owner + if (isBot) { + return; + } + list options; - // If this user is flagged as the starter, they should see the "Start Game" - // button so they can initiate the round (regardless of ownership). Otherwise - // they see the regular "Ready" button. + string menuText = "Select an option:"; + if (isStarter) { options = ["Start Game", "Leave Game"]; + menuText = "You are the game starter. You can start the game when all other players are ready."; } else { - options = ["Ready", "Leave Game"]; + // Dynamic ready button based on current state + if (isReady) { + options = ["Not Ready", "Leave Game"]; + menuText = "You are READY to play. Click 'Not Ready' to change your status."; + } else { + options = ["Ready", "Leave Game"]; + menuText = "You are NOT READY to play. Click 'Ready' when you want to participate."; + } } + if (isOwner) { options += ["Owner"]; } - // Display the combined menu - llDialog(id, "Select an option:", options, DIALOG_CHANNEL); + + // Display the menu with contextual information + llDialog(id, menuText, options, DIALOG_CHANNEL); } // Manage picks UI helpers remain unchanged @@ -75,6 +109,7 @@ askForNewPick(key id) { default { state_entry() { + llOwnerSay("🎭 Owner and Player Dialog Handler ready!"); llListen(DIALOG_CHANNEL, "", NULL_KEY, ""); } @@ -94,6 +129,23 @@ default { showReadyLeaveMenu(id, isStarter, FALSE); } } + else if (num == MSG_READY_STATE_RESULT) { + // Parse ready state response and show appropriate menu + list parts = llParseString2List(str, ["|"], []); + if (llGetListLength(parts) >= 3) { + string playerName = llList2String(parts, 0); + integer isReady = (integer)llList2String(parts, 1); + integer isBot = (integer)llList2String(parts, 2); + + // Show menu with the current ready state + if (pendingMenuPlayer == id) { + showReadyLeaveMenuWithState(pendingMenuPlayer, pendingMenuIsStarter, pendingMenuIsOwner, isReady, isBot); + // Reset pending state + pendingMenuPlayer = NULL_KEY; + } + } + return; + } else if (num == MSG_PLAYER_LIST_RESULT) { list playerNames = llParseString2List(str, [","], []); llOwnerSay("📋 Fetching list of players for pick management..."); @@ -130,13 +182,23 @@ default { // Use the avatar's key as the identifier string pname = llKey2Name(id); llMessageLinked(LINK_SET, MSG_REGISTER_PLAYER, pname + "|" + (string)id, NULL_KEY); - llMessageLinked(LINK_SET, MSG_REZ_FLOAT, pname, id); + // Floater Manager will automatically rez the floater during registration } else if (msg == "Leave Game") { // Send leave game message to main controller string pname = llKey2Name(id); llMessageLinked(LINK_SET, 107, "LEAVE_GAME|" + pname + "|" + (string)id, NULL_KEY); } + else if (msg == "Ready" || msg == "Not Ready") { + // Toggle ready state (both buttons do the same thing - toggle) + string pname = llKey2Name(id); + llMessageLinked(LINK_SET, MSG_TOGGLE_READY, pname, NULL_KEY); + } + else if (msg == "Cleanup Floaters") { + // Send aggressive floater cleanup command + llOwnerSay("🧹 Sending aggressive floater cleanup command..."); + llMessageLinked(LINK_SET, MSG_CLEANUP_ALL_FLOATERS, "", NULL_KEY); + } else if (msg == "Manage Picks") { llMessageLinked(LINK_THIS, 202, "REQUEST_PLAYER_LIST", id); } diff --git a/README.md b/README.md index c13099f..80b01d3 100644 --- a/README.md +++ b/README.md @@ -1,186 +1,108 @@ -🎲 Peril Dice — Modular Game System for Second Life -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. +# 🎲 Peril Dice — Modular Game System for Second Life -🔧 Features -✅ Player System -Max players: 10 +## 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. -Each player starts with 3 lives +## Key Features -Players can Join, Leave, or mark themselves Ready +- **🎯 Dynamic Player Management**: Players can join at runtime (owner and other avatars) +- **🤖 Bot Support**: Add AI bots for testing and gameplay variety +- **📱 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 +- **⚡ Performance Optimized**: Reduced lag with selective particle effects +- **🛡️ Robust Error Handling**: Comprehensive game state synchronization +- **🚫 Game Protection**: Prevents joining games in progress -The first player to join gets a Start Game button +## Game Rules -The owner can join the game and also has a special Owner menu +1. **Setup**: 2-10 players join the game +2. **Ready Phase**: All players except starter must mark themselves ready +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 +6. **Elimination**: Players with 0 lives are eliminated +7. **Victory**: Last player standing wins! 🏆 -🎲 Dice Scaling -Dice type is automatically chosen to ensure at least 3 picks per player +## Installation -Dice types supported: d6, d12, d20, d30 +1. Rez the main controller object in your desired location +2. Ensure all scripts are loaded: + - `Main.lsl` (Main Controller) + - `Owner and Player Dialog Handler.lsl` + - `Number Picker Dialog Handler.lsl` + - `Roll Confetti Module.lsl` + - `Bot Manager.lsl` + - `Floater Manager.lsl` + - `StatFloat.lsl` (for the floating HUD objects) +3. Touch the object to register as a player and start playing! -Player Count Dice Type -1–2 d6 -3–4 d12 -5–6 d20 -7–10 d30 +## How to Play -🧍 Test Players -Owner can add/remove test players (bots) +### For Owner +- **Touch object** → Access owner menu +- **Add Bot** → Add AI players for testing +- **Start Game** → Begin when all players are ready +- **Reset Game** → Reset to initial state +- **Manage Picks** → View/modify player selections -Test players rez stat floaters at offset positions to prevent overlap +### For Players +- **Touch object** → Register and join game +- **Ready/Not Ready** → Toggle your ready state +- **Pick Numbers** → Select your numbers when prompted +- **Roll Dice** → Roll when you're the peril player -Floater objects follow players and update in real time +## Architecture -🪟 HUD & Stat Floaters -Players receive a floating HUD showing: +The system uses a modular LSL architecture with inter-script communication: -Player name +- **Main Controller**: Core game logic and state management +- **Dialog Handlers**: User interface and input processing +- **Roll Module**: Dice rolling and confetti effects +- **Bot Manager**: AI player behavior +- **Floater Manager**: Floating HUD display management +- **Game Helpers**: Utility functions and dice type calculation -Lives +## Dice Scaling -Picks +Dice type is automatically chosen to ensure at least 3 picks per player: -Peril status or game state +| Player Count | Dice Type | +|--------------|-----------| +| 1–2 | d6 | +| 3–4 | d12 | +| 5–6 | d20 | +| 7–10 | d30 | -📦 Modular Architecture -Separated scripts: +## Known Issues -Main Controller: Handles core game logic +- **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 -Dialog Handler: Manages pick dialogs +## Version -Dice Selector: Returns dice type based on player count +**Current Version**: 2.1.0 +**Last Updated**: January 2025 +**Status**: Stable - Full game flow functional -Stat Float Manager: Rezzes and updates floaters +--- -Stat Float Object: Displays floating text per player +## Original Game Rules Credit -🕹️ Game Flow -Players touch the controller object to join the game +Game rules were created by **Noose the Bunny** (djmusica28) in Second Life. This automated version builds upon their original manual gameplay concept. -Once 2+ players have joined, the first player gets the option to Start Game - -Dice type is selected automatically - -Each player is prompted (in order) to select 3 numbers - -After all picks are made: - -The dice is rolled - -The result is compared to each player’s picks - -If the peril player’s number is rolled or no one picked it, the peril player loses a life - -The system checks for eliminations and starts the next round - -🧰 Admin Controls -Owner can: - -Join or leave the game - -Open a special Owner menu - -Add/remove test bots - -Reset the game - -Debug current state (optional) - - - -Below what was started, ill have this up just a reminder - - - - -# Peril Dice -Notice these rules were made by Noose the Bunny (djmusica28) secondlife:///app/agent/5935e159-63ba-415a-b20b-4813b50367d0/about, im baseing off this but im also automating it - -🎲 PERIL DICE – GAME RULES -🧍 Objective: -Avoid becoming the “person in peril” and losing all your lives. -The last player with lives remaining wins! - -🛠️ Setup -Each player wears a Peril Dice Tracker HUD - -Everyone starts with 3 lives - -Choose a dice type: d6, d12, or d21 - -One player clicks "Set Yourself In Peril" on the HUD to start - -🔁 Game Rounds -1. Determine Lives of Person in Peril - -3 Lives → Each player picks 1 number - -2 Lives → Each player picks 2 numbers - -1 Life → Each player picks 3 numbers - -2. Players Pick Numbers -Everyone (including the player in peril) picks their numbers from the die range. -Example: On a d12 with 2 lives in peril → pick 2 numbers between 1–12. - -3. Roll the Die -Use a real or scripted dice object in SL to roll (matching your chosen type). - -🎯 Check the Result -If the roll matches another player's pick → -That player becomes the new person in peril - -If the roll matches no one, or the peril player’s own pick → -The peril player loses 1 life and stays in peril - -💀 Elimination -When a player reaches 0 lives, they're eliminated from the game. - -🏆 Winning the Game -The last remaining player with lives wins! - -✅ Game Example -4 players, using a d12 - -Alice is in peril with 2 lives - -Everyone picks 2 numbers: - -makefile -Copy -Edit -Bob: 3, 7 -Carol: 6, 11 -Dave: 2, 9 -Alice: 1, 12 -🎲 Roll = 6 → Carol becomes the new person in peril - -📟 PERIL DICE TRACKER HUD GUIDE -Touch the tracker to access the menu: - -🔘 Set Dice -Pick d6, d12, or d20 — defines the range of possible numbers. - -🔘 Pick Numbers -Lets you pick numbers (based on how many the current person in peril is allowed). -Your picks are shown in the floating text. - -🔘 Set Yourself in Peril -Marks you as the current player in peril. Everyone’s HUD will update with your name and lives. - -🔘 Lose Life / Reset Lives -Used only by the wearer of the HUD to manage their own lives. -If you’re the player in peril, everyone else will see your life change reflected in their menus. - -🧠 Odds of Rolling a Specific Number -Here’s the chance of any one number being rolled: - -Dice Total Sides % Chance per Number -d6 6 16.7% -d12 12 8.3% -d20 20 5.0% - -The more numbers you can pick, the better your odds — but lives are limited! -# Peril Dice +### Original Manual Rules Summary: +- Each player starts with 3 lives +- Players pick numbers based on peril player's remaining lives: + - 3 Lives → Pick 1 number + - 2 Lives → Pick 2 numbers + - 1 Life → Pick 3 numbers +- Roll dice to determine outcome +- Last player with lives wins + +--- + +*Peril Dice provides hours of entertainment for Second Life communities with its blend of strategy, luck, and social interaction!* \ No newline at end of file diff --git a/Roll Confetti Module.lsl b/Roll Confetti Module.lsl index 043a358..334b1bc 100644 --- a/Roll Confetti Module.lsl +++ b/Roll Confetti Module.lsl @@ -21,9 +21,28 @@ list getPicksFor(string nameInput) { string entry = llList2String(picksData, i); list parts = llParseString2List(entry, ["|"], []); if (llList2String(parts, 0) == nameInput) { - return llParseString2List(llList2String(parts, 1), [","], []); + // Convert semicolons back to commas, then parse + string picks = llList2String(parts, 1); + string originalPicks = picks; + + // 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 []; } @@ -48,13 +67,61 @@ confetti() { default { state_entry() { + llOwnerSay("🎲 Roll Confetti Module ready!"); llListen(rollDialogChannel, "", NULL_KEY, ""); } link_message(integer sender, integer num, string str, key id) { + // Handle game state sync from main controller + if (num == MSG_SYNC_GAME_STATE) { + list parts = llParseString2List(str, ["~"], []); + if (llGetListLength(parts) >= 4) { + lives = llCSV2List(llList2String(parts, 0)); + // Use ^ delimiter for picksData to avoid comma conflicts + string picksDataStr = llList2String(parts, 1); + if (picksDataStr == "" || picksDataStr == "EMPTY") { + picksData = []; + } else { + picksData = llParseString2List(picksDataStr, ["^"], []); + } + string receivedPeril = llList2String(parts, 2); + if (receivedPeril == "NONE") { + perilPlayer = ""; + } else { + perilPlayer = receivedPeril; + } + names = llCSV2List(llList2String(parts, 3)); + } + return; + } + + if (num == 995) { + if (str == "VICTORY_CONFETTI") { + llOwnerSay("🎉 Victory confetti!"); + confetti(); + } + return; + } + if (num == MSG_SHOW_ROLL_DIALOG) { - llOwnerSay("🎲 Prompting " + str + " to roll the dice."); - llDialog(id, "🎲 All picks are in! You're the peril player. Roll the dice?", ["Roll"], rollDialogChannel); + // Check if this is a next round prompt + if (llSubStringIndex(str, "_NEXT_ROUND") != -1) { + string playerName = llGetSubString(str, 0, llSubStringIndex(str, "_NEXT_ROUND") - 1); + llOwnerSay("🎯 Prompting " + playerName + " to start next round..."); + // Send message to main controller to get the player's key and show dialog + llMessageLinked(LINK_SET, 997, "START_NEXT_ROUND_DIALOG|" + playerName, NULL_KEY); + } else { + llOwnerSay("🎲 Prompting " + str + " to roll the dice."); + // Check if this is a bot (TestBot names) + if (llSubStringIndex(str, "TestBot") == 0) { + // Auto-roll for bots + llOwnerSay("🤖 " + str + " (bot) is auto-rolling..."); + 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); + } + } } else if (num == MSG_ROLL_RESULT) { @@ -65,40 +132,121 @@ default { 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; - if (pname != perilPlayer && newPeril == "") { - newPeril = pname; - } + 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) + for (i = 0; i < llGetListLength(matchedPlayers) && newPeril == ""; i++) { + 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!"); perilPlayer = newPeril; - llMessageLinked(LINK_SET, MSG_REZ_FLOAT, newPeril, NULL_KEY); + // Update floaters immediately to show correct peril player before sync + llMessageLinked(LINK_SET, MSG_UPDATE_FLOAT, newPeril, NULL_KEY); } else { integer pidx = llListFindList(names, [perilPlayer]); if (pidx != -1) { integer currentLives = llList2Integer(lives, pidx); lives = llListReplaceList(lives, [currentLives - 1], pidx, pidx); llSay(0, "💀 " + perilPlayer + " was hit and lost a life!"); - confetti(); llMessageLinked(LINK_SET, MSG_REZ_FLOAT, perilPlayer, NULL_KEY); + + // Check for elimination + if (currentLives - 1 <= 0) { + llSay(0, "💀 " + 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 + // Don't continue processing this round since the player was eliminated + return; + } } } - string gameSync = llList2CSV(lives) + "~" + llList2CSV(picksData) + "~" + perilPlayer + "~" + llList2CSV(names); + // Note: Win condition checking is handled by Main Controller after elimination + + // Encode picksData with semicolons to avoid comma conflicts + list encodedPicksData = []; + integer k; + for (k = 0; k < llGetListLength(picksData); k++) { + string entry = llList2String(picksData, k); + list parts = llParseString2List(entry, ["|"], []); + if (llGetListLength(parts) >= 2) { + string playerName = llList2String(parts, 0); + string picks = llList2String(parts, 1); + // Only process picks if they're not empty + if (picks != "") { + // Parse picks, trim whitespace, then join with semicolons + list pickList = llParseString2List(picks, [","], []); + list cleanPickList = []; + integer j; + for (j = 0; j < llGetListLength(pickList); j++) { + string pick = llStringTrim(llList2String(pickList, j), STRING_TRIM); + if (pick != "") { + cleanPickList += [pick]; + } + } + picks = llDumpList2String(cleanPickList, ";"); + } + encodedPicksData += [playerName + "|" + picks]; + } else if (llSubStringIndex(entry, "|") != -1) { + // Handle entries with empty picks (e.g., "PlayerName|") + encodedPicksData += [entry]; + } else { + // Skip malformed entries without pipe separator + llOwnerSay("⚠️ Roll Module: Skipping malformed picksData entry: " + entry); + } + } + string gameSync = llList2CSV(lives) + "~" + llDumpList2String(encodedPicksData, "^") + "~" + perilPlayer + "~" + llList2CSV(names); + llOwnerSay("📤 Roll module sending sync with peril player: " + perilPlayer); llMessageLinked(LINK_SET, MSG_SYNC_GAME_STATE, gameSync, NULL_KEY); llSleep(0.2); + + // Additional floater updates to ensure correct peril player display + llOwnerSay("🔄 Updating all floaters with new peril player: " + perilPlayer); + integer f; + for (f = 0; f < llGetListLength(names); f++) { + string fname = llList2String(names, f); + llMessageLinked(LINK_SET, MSG_UPDATE_FLOAT, fname, NULL_KEY); + } + + // 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) { - llMessageLinked(LINK_THIS, channel, msg, id); + if (msg == "Roll") { + // 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); + } else { + llMessageLinked(LINK_THIS, channel, msg, id); + } } } diff --git a/StatFloat.lsl b/StatFloat.lsl index 2bb0015..6209e7c 100644 --- a/StatFloat.lsl +++ b/StatFloat.lsl @@ -14,11 +14,19 @@ list getPicksFor(string nameInput) { integer i; for (i = 0; i < llGetListLength(picksData); i++) { string entry = llList2String(picksData, i); - if (llSubStringIndex(entry, nameInput + "|") == 0) { - list parts = llParseString2List(entry, ["|"], []); - if (llGetListLength(parts) >= 2) { - return llParseString2List(llList2String(parts, 1), [","], []); + list parts = llParseString2List(entry, ["|"], []); + if (llGetListLength(parts) >= 2 && llList2String(parts, 0) == nameInput) { + string picks = llList2String(parts, 1); + if (picks == "") { + return []; } + // Check for corruption markers (^ symbols that shouldn't be in picks) + if (llSubStringIndex(picks, "^") != -1) { + return []; + } + // Convert semicolons back to commas, then parse + picks = llDumpList2String(llParseString2List(picks, [";"], []), ","); + return llParseString2List(picks, [","], []); } } return []; @@ -57,18 +65,21 @@ default { list parts = llParseString2List(str, ["~"], []); if (llGetListLength(parts) < 4) return; lives = llCSV2List(llList2String(parts, 0)); - list rawPicks = llCSV2List(llList2String(parts, 1)); - picksData = []; - integer i; - for (i = 0; i < llGetListLength(rawPicks); i++) { - string entry = llList2String(rawPicks, i); - if (llSubStringIndex(entry, "|") != -1) { - picksData += [entry]; - } else { - llOwnerSay("⚠️ Ignored malformed picksData during sync: " + entry); - } + + // Use ^ delimiter for picksData to match the main system + string picksDataStr = llList2String(parts, 1); + if (picksDataStr == "" || picksDataStr == "EMPTY") { + picksData = []; + } else { + picksData = llParseString2List(picksDataStr, ["^"], []); + } + + string receivedPeril = llList2String(parts, 2); + if (receivedPeril == "NONE") { + perilPlayer = ""; // Convert placeholder back to empty + } else { + perilPlayer = receivedPeril; } - perilPlayer = llList2String(parts, 2); names = llCSV2List(llList2String(parts, 3)); integer nameIdx = llListFindList(names, [myName]); @@ -78,10 +89,15 @@ default { integer lifeCount = llList2Integer(lives, nameIdx); string perilDisplay; - if (perilPlayer != "") { - perilDisplay = perilPlayer; + // If the perilPlayer string is empty or contains a comma (indicating + // multiple names), treat the game as not yet started. This avoids + // showing multiple players as the peril player before the first + // round begins. Once a single peril player is assigned, it will + // not contain a comma and will be displayed normally. + if (perilPlayer == "" || llSubStringIndex(perilPlayer, ",") != -1) { + perilDisplay = "Waiting for game to start..."; } else { - perilDisplay = "Waiting for peril..."; + perilDisplay = perilPlayer; } string picksDisplay = llList2CSV(picks);