# 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
This commit is contained in:
RebeccaNod1 2025-08-03 04:38:55 -05:00
parent 61ff39cee0
commit 494f01c7bf
12 changed files with 1708 additions and 651 deletions

View file

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

154
Bot Manager.lsl Normal file
View file

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

99
CHANGELOG.md Normal file
View file

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

View file

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

View file

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

View file

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

781
Main.lsl
View file

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

View file

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

View file

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

246
README.md
View file

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

View file

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

View file

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