# 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:
parent
61ff39cee0
commit
494f01c7bf
12 changed files with 1708 additions and 651 deletions
53
Bot Manager
53
Bot Manager
|
|
@ -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
154
Bot Manager.lsl
Normal 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
99
CHANGELOG.md
Normal 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
|
||||||
25
Debugger.lsl
25
Debugger.lsl
|
|
@ -34,8 +34,12 @@ list validatePicksData(list picksData, list names) {
|
||||||
allPicksFlat += [pick];
|
allPicksFlat += [pick];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only show picks if player has actually picked something
|
||||||
|
if (picks != "") {
|
||||||
llOwnerSay("✅ " + name + "'s picks: " + llList2CSV(pickNums));
|
llOwnerSay("✅ " + name + "'s picks: " + llList2CSV(pickNums));
|
||||||
} else {
|
}
|
||||||
|
} else if (entry != "") {
|
||||||
|
// Only warn about non-empty malformed entries
|
||||||
llOwnerSay("⚠️ Malformed entry in picksData: " + entry);
|
llOwnerSay("⚠️ Malformed entry in picksData: " + entry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -58,6 +62,10 @@ list validatePicksData(list picksData, list names) {
|
||||||
|
|
||||||
|
|
||||||
default {
|
default {
|
||||||
|
state_entry() {
|
||||||
|
llOwnerSay("🔍 Debugger ready!");
|
||||||
|
}
|
||||||
|
|
||||||
link_message(integer sender, integer num, string str, key id) {
|
link_message(integer sender, integer num, string str, key id) {
|
||||||
if (num == MSG_SYNC_GAME_STATE) {
|
if (num == MSG_SYNC_GAME_STATE) {
|
||||||
list parts = llParseString2List(str, ["~"], []);
|
list parts = llParseString2List(str, ["~"], []);
|
||||||
|
|
@ -71,7 +79,22 @@ default {
|
||||||
string perilPlayer = llList2String(parts, 2);
|
string perilPlayer = llList2String(parts, 2);
|
||||||
list names = llCSV2List(llList2String(parts, 3));
|
list names = llCSV2List(llList2String(parts, 3));
|
||||||
|
|
||||||
|
// 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...");
|
llOwnerSay("🔍 Checking picksData...");
|
||||||
|
}
|
||||||
validatePicksData(picksData, names);
|
validatePicksData(picksData, names);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,18 +25,26 @@ list getPicksFor(string nameInput) {
|
||||||
string entry = llList2String(picksData, i);
|
string entry = llList2String(picksData, i);
|
||||||
|
|
||||||
if (llSubStringIndex(entry, "|") == -1) {
|
if (llSubStringIndex(entry, "|") == -1) {
|
||||||
llOwnerSay("⚠️ Malformed picks entry (missing pipe): " + entry);
|
if (entry != "") llOwnerSay("⚠️ Malformed picks entry (missing pipe): " + entry);
|
||||||
} else {
|
} else {
|
||||||
list parts = llParseString2List(entry, ["|"], []);
|
list parts = llParseString2List(entry, ["|"], []);
|
||||||
if (llGetListLength(parts) < 2) {
|
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) {
|
else if (llList2String(parts, 0) == nameInput) {
|
||||||
string pickString = llList2String(parts, 1);
|
string pickString = llList2String(parts, 1);
|
||||||
if (pickString == "") {
|
if (pickString == "") {
|
||||||
llOwnerSay("⚠️ Malformed or empty picks entry: " + entry);
|
// Empty picks are normal during initialization, don't warn
|
||||||
|
return [];
|
||||||
} else {
|
} 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 = [];
|
list filtered = [];
|
||||||
integer j;
|
integer j;
|
||||||
for (j = 0; j < llGetListLength(all); j++) {
|
for (j = 0; j < llGetListLength(all); j++) {
|
||||||
|
|
@ -64,6 +72,10 @@ string getNameFromKey(key id) {
|
||||||
|
|
||||||
// Main event handler
|
// Main event handler
|
||||||
default {
|
default {
|
||||||
|
state_entry() {
|
||||||
|
llOwnerSay("📦 Floater Manager ready!");
|
||||||
|
}
|
||||||
|
|
||||||
link_message(integer sender, integer num, string str, key id) {
|
link_message(integer sender, integer num, string str, key id) {
|
||||||
if (num == MSG_REGISTER_PLAYER) {
|
if (num == MSG_REGISTER_PLAYER) {
|
||||||
// Enforce the maximum number of players
|
// Enforce the maximum number of players
|
||||||
|
|
@ -150,7 +162,8 @@ default {
|
||||||
if (perilPlayer == "" || llSubStringIndex(perilPlayer, ",") != -1) {
|
if (perilPlayer == "" || llSubStringIndex(perilPlayer, ",") != -1) {
|
||||||
perilName = "🧍 Status: Waiting for game to start...";
|
perilName = "🧍 Status: Waiting for game to start...";
|
||||||
} else {
|
} else {
|
||||||
perilName = "🧍 Peril: " + getNameFromKey(perilPlayer);
|
// perilPlayer is already a name string, not a key
|
||||||
|
perilName = "🧍 Peril: " + perilPlayer;
|
||||||
}
|
}
|
||||||
|
|
||||||
string picksDisplay = llList2CSV(picks);
|
string picksDisplay = llList2CSV(picks);
|
||||||
|
|
@ -161,30 +174,47 @@ default {
|
||||||
else if (num == MSG_CLEANUP_FLOAT) {
|
else if (num == MSG_CLEANUP_FLOAT) {
|
||||||
integer ch = (integer)str;
|
integer ch = (integer)str;
|
||||||
integer idx = ch - (-777000);
|
integer idx = ch - (-777000);
|
||||||
if (idx >= 0 && idx < llGetListLength(players)) {
|
|
||||||
|
// Always send cleanup message to the channel (even for orphaned floaters)
|
||||||
llRegionSay(ch, "CLEANUP");
|
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)) {
|
||||||
players = llDeleteSubList(players, idx, idx);
|
players = llDeleteSubList(players, idx, idx);
|
||||||
names = llDeleteSubList(names, idx, idx);
|
names = llDeleteSubList(names, idx, idx);
|
||||||
lives = llDeleteSubList(lives, idx, idx);
|
lives = llDeleteSubList(lives, idx, idx);
|
||||||
picksData = llDeleteSubList(picksData, idx, idx);
|
picksData = llDeleteSubList(picksData, idx, idx);
|
||||||
|
llOwnerSay("🗑️ Cleaned up internal data for player index " + (string)idx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (num == MSG_SYNC_GAME_STATE) {
|
else if (num == MSG_SYNC_GAME_STATE) {
|
||||||
// Synchronize the lists for lives and picksData when receiving a new 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, ["~"], []);
|
list parts = llParseString2List(str, ["~"], []);
|
||||||
lives = llCSV2List(llList2String(parts, 0));
|
lives = llCSV2List(llList2String(parts, 0));
|
||||||
list rawPicks = llCSV2List(llList2String(parts, 1));
|
// Use ^ delimiter for picksData to avoid comma conflicts
|
||||||
|
string picksDataStr = llList2String(parts, 1);
|
||||||
|
llOwnerSay("🔍 DEBUG: Raw picksDataStr: '" + picksDataStr + "'");
|
||||||
|
if (picksDataStr == "" || picksDataStr == "EMPTY") {
|
||||||
picksData = [];
|
picksData = [];
|
||||||
integer i;
|
llOwnerSay("🔍 DEBUG: Parsed picksData: (empty)");
|
||||||
for (i = 0; i < llGetListLength(rawPicks); i++) {
|
|
||||||
string entry = llList2String(rawPicks, i);
|
|
||||||
if (llSubStringIndex(entry, "|") != -1) {
|
|
||||||
picksData += [entry];
|
|
||||||
} else {
|
} else {
|
||||||
llOwnerSay("⚠️ Ignored malformed picksData during sync: " + entry);
|
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
|
// After synchronizing the game state, update all existing floats so
|
||||||
// they reflect the current peril status. Without this, floats
|
// they reflect the current peril status. Without this, floats
|
||||||
|
|
|
||||||
442
Helper.lsl
442
Helper.lsl
|
|
@ -1,358 +1,162 @@
|
||||||
// === Main Peril Dice Controller (Refactored with Game Helpers Integration, with dynamic player join) ===
|
// === 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.
|
||||||
// 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;
|
|
||||||
|
|
||||||
integer MSG_GET_DICE_TYPE = 1001;
|
integer MSG_GET_DICE_TYPE = 1001;
|
||||||
|
integer MSG_DICE_TYPE_RESULT = 1005;
|
||||||
integer MSG_GET_PICKS_REQUIRED = 1002;
|
integer MSG_GET_PICKS_REQUIRED = 1002;
|
||||||
integer MSG_GET_PICKER_INDEX = 1003;
|
integer MSG_GET_PICKER_INDEX = 1003;
|
||||||
integer MSG_DICE_TYPE_RESULT = 1005;
|
|
||||||
integer MSG_SERIALIZE_GAME_STATE = 1004;
|
integer MSG_SERIALIZE_GAME_STATE = 1004;
|
||||||
|
integer MSG_SYNC_GAME_STATE = 107;
|
||||||
integer MSG_SYNC_PICKQUEUE = 2001;
|
integer MSG_SYNC_PICKQUEUE = 2001;
|
||||||
|
|
||||||
// Maximum number of players allowed (including test players). This should
|
list lives;
|
||||||
// mirror the value used in Floater Manager to avoid inconsistencies.
|
list picksData;
|
||||||
integer MAX_PLAYERS = 10;
|
string perilPlayer;
|
||||||
|
list names;
|
||||||
|
list pickQueue;
|
||||||
|
|
||||||
integer TIMEOUT_SECONDS = 600;
|
// List of supported dice sizes; ensure the last entry can accommodate 10 players × 3 picks = 30
|
||||||
integer timeoutTimer;
|
list STANDARD_DICE = [6, 12, 20, 30];
|
||||||
|
|
||||||
integer warning2min = 120;
|
// Determine which die to use based on player count.
|
||||||
integer warning5min = 300;
|
// Each player should have at least 3 numbers available to choose from.
|
||||||
integer warning9min = 540;
|
integer getDiceType(integer playerCount) {
|
||||||
integer lastWarning = 0;
|
integer minimumSides = playerCount * 3;
|
||||||
|
integer i;
|
||||||
key currentPicker;
|
for (i = 0; i < llGetListLength(STANDARD_DICE); i++) {
|
||||||
|
integer sides = llList2Integer(STANDARD_DICE, i);
|
||||||
// Forward game state to helpers when it changes
|
if (sides >= minimumSides) {
|
||||||
updateHelpers() {
|
return sides;
|
||||||
string serialized = llList2CSV(lives) + "~" + llList2CSV(picksData) +
|
}
|
||||||
"~" + perilPlayer + "~" + llList2CSV(names);
|
}
|
||||||
llMessageLinked(LINK_SET, MSG_SYNC_GAME_STATE, serialized, NULL_KEY);
|
// If no predefined die has enough sides, return the largest supported die (d30)
|
||||||
llMessageLinked(LINK_SET, MSG_SYNC_PICKQUEUE, llList2CSV(pickQueue), NULL_KEY);
|
return llList2Integer(STANDARD_DICE, llGetListLength(STANDARD_DICE) - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Request helper-calculated values
|
// Determine how many picks a player should make based on the peril player's lives
|
||||||
requestDiceType() {
|
integer getPicksRequiredFromName(string name) {
|
||||||
llMessageLinked(LINK_SET, MSG_GET_DICE_TYPE, (string)llGetListLength(names), NULL_KEY);
|
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) {
|
// Find the index of a player in the pickQueue by name
|
||||||
llMessageLinked(LINK_SET, MSG_GET_PICKS_REQUIRED, llList2String(names, idx), NULL_KEY);
|
integer getPickerIndex(string name) {
|
||||||
|
return llListFindList(pickQueue, [name]);
|
||||||
}
|
}
|
||||||
|
|
||||||
requestPickerIndex(string name) {
|
// Serialize the current game state for syncing with other scripts
|
||||||
llMessageLinked(LINK_SET, MSG_GET_PICKER_INDEX, name, NULL_KEY);
|
string serializeGameState() {
|
||||||
}
|
|
||||||
|
|
||||||
string generateSerializedState() {
|
|
||||||
return llList2CSV(lives) + "~" + llList2CSV(picksData) + "~" + perilPlayer + "~" + llList2CSV(names);
|
return llList2CSV(lives) + "~" + llList2CSV(picksData) + "~" + perilPlayer + "~" + llList2CSV(names);
|
||||||
}
|
}
|
||||||
|
|
||||||
resetGame() {
|
// Show a pick management dialog for the owner.
|
||||||
// Remove floats in reverse order so indices remain correct as entries are
|
// LSL does not support a void return type, so this returns an integer which is ignored.
|
||||||
// deleted from the Floater Manager's lists. Otherwise, deleting index 0
|
integer showPickManager(string player, key id) {
|
||||||
// first shifts subsequent indices and some floats are left behind.
|
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;
|
integer i;
|
||||||
for (i = llGetListLength(names) - 1; i >= 0; i--) {
|
for (i = 0; i < llGetListLength(names); i++) {
|
||||||
integer ch = -777000 + i;
|
string testName = llList2String(names, i);
|
||||||
llMessageLinked(LINK_SET, MSG_CLEANUP_FLOAT, (string)ch, NULL_KEY);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
players = names = lives = picksData = globalPickedNumbers = [];
|
|
||||||
perilPlayer = "";
|
|
||||||
pickQueue = [];
|
|
||||||
currentPickerIdx = 0;
|
|
||||||
llSay(syncChannel, "RESET");
|
|
||||||
llOwnerSay(" Game reset!");
|
|
||||||
llSleep(0.2);
|
|
||||||
llSetTimerEvent(0);
|
|
||||||
updateHelpers();
|
|
||||||
}
|
|
||||||
|
|
||||||
startNextRound() {
|
integer maxPicks = getPicksRequiredFromName(llList2String(names, idx));
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
showNextPickerDialog() {
|
string currentData = llList2String(picksData, idx);
|
||||||
string firstName = llList2String(pickQueue, currentPickerIdx);
|
list parts = llParseString2List(currentData, ["|"], []);
|
||||||
currentPicker = llList2Key(players, llListFindList(names, [firstName]));
|
list currentPicks = [];
|
||||||
string dialogPayload = firstName + "|" + (string)diceType;
|
if (llGetListLength(parts) > 1) {
|
||||||
llMessageLinked(LINK_SET, MSG_SHOW_DIALOG, dialogPayload, currentPicker);
|
currentPicks = llParseString2List(llList2String(parts, 1), [","], []);
|
||||||
timeoutTimer = llGetUnixTime();
|
}
|
||||||
lastWarning = 0;
|
|
||||||
llSetTimerEvent(60.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
integer roundStarted = FALSE;
|
list buttons = [];
|
||||||
|
integer i;
|
||||||
|
for (i = 1; i <= 6; i++) {
|
||||||
|
string numStr = (string)i;
|
||||||
|
if (llListFindList(currentPicks, [numStr]) != -1) {
|
||||||
|
buttons += ["❌ " + numStr];
|
||||||
|
} else if (llGetListLength(currentPicks) < maxPicks) {
|
||||||
|
buttons += ["➕ " + numStr];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
llOwnerSay("🛠 Showing pick dialog to: " + (string)id);
|
||||||
|
llDialog(id, "🛠 Managing picks for: " + searchName + "\nCurrent: " + llList2CSV(currentPicks), buttons, -88888);
|
||||||
|
|
||||||
|
// Return dummy value since LSL does not have a void return type
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
default {
|
default {
|
||||||
state_entry() {
|
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) {
|
link_message(integer sender, integer num, string str, key id) {
|
||||||
// Player list and pick list handling remain unchanged
|
if (num == MSG_SYNC_GAME_STATE) {
|
||||||
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) {
|
|
||||||
list parts = llParseString2List(str, ["~"], []);
|
list parts = llParseString2List(str, ["~"], []);
|
||||||
string action = llList2String(parts, 0);
|
lives = llCSV2List(llList2String(parts, 0));
|
||||||
list args = llParseString2List(llList2String(parts, 1), ["|"], []);
|
picksData = llCSV2List(llList2String(parts, 1));
|
||||||
string name = llList2String(args, 0);
|
perilPlayer = llList2String(parts, 2);
|
||||||
string pick = llList2String(args, 1);
|
names = llCSV2List(llList2String(parts, 3));
|
||||||
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") {
|
else if (num == MSG_SYNC_PICKQUEUE) {
|
||||||
if (llListFindList(pickList, [pick]) == -1) {
|
pickQueue = llCSV2List(str);
|
||||||
pickList += [pick];
|
|
||||||
llOwnerSay("➕ Added " + pick + " to " + name);
|
|
||||||
}
|
}
|
||||||
} else if (action == "REMOVE_PICK") {
|
else if (num == MSG_GET_DICE_TYPE) {
|
||||||
integer idx = llListFindList(pickList, [pick]);
|
integer playerCount = (integer)str;
|
||||||
if (idx != -1) {
|
llOwnerSay("📊 playerCount received: " + (string)playerCount);
|
||||||
pickList = llDeleteSubList(pickList, idx, idx);
|
integer result = getDiceType(playerCount);
|
||||||
llOwnerSay("➖ Removed " + pick + " from " + name);
|
// 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);
|
||||||
}
|
}
|
||||||
picksData = llListReplaceList(picksData, [name + "|" + llList2CSV(pickList)], i, i);
|
else if (num == MSG_GET_PICKER_INDEX) {
|
||||||
updateHelpers();
|
integer result = getPickerIndex(str);
|
||||||
return;
|
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);
|
||||||
}
|
}
|
||||||
return;
|
else if (num == 206) {
|
||||||
}
|
llOwnerSay("📨 MSG 206 received for: " + str + " from: " + (string)id);
|
||||||
// New: handle dynamic registration of players (owner or players) via MSG_REGISTER_PLAYER
|
showPickManager(str, id);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
771
Main.lsl
771
Main.lsl
|
|
@ -20,6 +20,7 @@ string perilPlayer = "";
|
||||||
list globalPickedNumbers = [];
|
list globalPickedNumbers = [];
|
||||||
list picksData = [];
|
list picksData = [];
|
||||||
list readyPlayers = [];
|
list readyPlayers = [];
|
||||||
|
list floaterChannels = []; // Track actual floater channels for cleanup
|
||||||
|
|
||||||
list pickQueue = [];
|
list pickQueue = [];
|
||||||
integer currentPickerIdx = 0;
|
integer currentPickerIdx = 0;
|
||||||
|
|
@ -33,6 +34,10 @@ integer MSG_REZ_FLOAT = 105;
|
||||||
integer MSG_REGISTER_PLAYER = 106;
|
integer MSG_REGISTER_PLAYER = 106;
|
||||||
integer MSG_SYNC_GAME_STATE = 107;
|
integer MSG_SYNC_GAME_STATE = 107;
|
||||||
integer MSG_SHOW_MENU = 201;
|
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_SHOW_ROLL_DIALOG = 301;
|
||||||
|
|
||||||
integer MSG_GET_DICE_TYPE = 1001;
|
integer MSG_GET_DICE_TYPE = 1001;
|
||||||
|
|
@ -58,8 +63,54 @@ key currentPicker;
|
||||||
|
|
||||||
// Forward game state to helpers when it changes
|
// Forward game state to helpers when it changes
|
||||||
updateHelpers() {
|
updateHelpers() {
|
||||||
string serialized = llList2CSV(lives) + "~" + llList2CSV(picksData) +
|
// Don't send peril player name if game hasn't started yet
|
||||||
"~" + perilPlayer + "~" + llList2CSV(names);
|
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_GAME_STATE, serialized, NULL_KEY);
|
||||||
llMessageLinked(LINK_SET, MSG_SYNC_PICKQUEUE, llList2CSV(pickQueue), NULL_KEY);
|
llMessageLinked(LINK_SET, MSG_SYNC_PICKQUEUE, llList2CSV(pickQueue), NULL_KEY);
|
||||||
}
|
}
|
||||||
|
|
@ -82,18 +133,27 @@ string generateSerializedState() {
|
||||||
}
|
}
|
||||||
|
|
||||||
resetGame() {
|
resetGame() {
|
||||||
// Remove floats in reverse order so indices remain correct as entries are
|
// Clean up all tracked floater channels
|
||||||
// deleted from the Floater Manager's lists. Otherwise, deleting index 0
|
|
||||||
// first shifts subsequent indices and some floats are left behind.
|
|
||||||
integer i;
|
integer i;
|
||||||
for (i = llGetListLength(names) - 1; i >= 0; i--) {
|
for (i = 0; i < llGetListLength(floaterChannels); i++) {
|
||||||
integer ch = -777000 + 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);
|
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 = "";
|
perilPlayer = "";
|
||||||
pickQueue = [];
|
pickQueue = [];
|
||||||
currentPickerIdx = 0;
|
currentPickerIdx = 0;
|
||||||
|
roundStarted = FALSE; // Reset round flag
|
||||||
llSay(syncChannel, "RESET");
|
llSay(syncChannel, "RESET");
|
||||||
llOwnerSay(" Game reset!");
|
llOwnerSay(" Game reset!");
|
||||||
llSleep(0.2);
|
llSleep(0.2);
|
||||||
|
|
@ -101,6 +161,85 @@ resetGame() {
|
||||||
updateHelpers();
|
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() {
|
startNextRound() {
|
||||||
// Do not start a round if there are fewer than 2 players. This guard prevents
|
// 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
|
// accidental starts when only one player is present (e.g. after a reset or
|
||||||
|
|
@ -115,33 +254,98 @@ startNextRound() {
|
||||||
resetGame();
|
resetGame();
|
||||||
return;
|
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 = [];
|
picksData = [];
|
||||||
globalPickedNumbers = [];
|
globalPickedNumbers = [];
|
||||||
pickQueue = names;
|
// Create pick queue with peril player first, then all others
|
||||||
currentPickerIdx = 0;
|
pickQueue = [perilPlayer];
|
||||||
integer j;
|
integer i;
|
||||||
for (j = 0; j < llGetListLength(names); j++) {
|
for (i = 0; i < llGetListLength(names); i++) {
|
||||||
picksData += [llList2String(names, j) + "|"];
|
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();
|
updateHelpers();
|
||||||
// requestDiceType(); // Removed to avoid recursion loop
|
// requestDiceType(); // Removed to avoid recursion loop
|
||||||
}
|
}
|
||||||
|
|
||||||
showNextPickerDialog() {
|
showNextPickerDialog() {
|
||||||
|
if (diceType <= 0) {
|
||||||
|
llOwnerSay("❌ Cannot show picker dialog: diceType not set (" + (string)diceType + ")");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
string firstName = llList2String(pickQueue, currentPickerIdx);
|
string firstName = llList2String(pickQueue, currentPickerIdx);
|
||||||
currentPicker = llList2Key(players, llListFindList(names, [firstName]));
|
currentPicker = llList2Key(players, llListFindList(names, [firstName]));
|
||||||
string dialogPayload = firstName + "|" + (string)diceType;
|
|
||||||
|
// 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);
|
llMessageLinked(LINK_SET, MSG_SHOW_DIALOG, dialogPayload, currentPicker);
|
||||||
timeoutTimer = llGetUnixTime();
|
timeoutTimer = llGetUnixTime();
|
||||||
lastWarning = 0;
|
lastWarning = 0;
|
||||||
llSetTimerEvent(60.0);
|
llSetTimerEvent(60.0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
integer roundStarted = FALSE;
|
integer roundStarted = FALSE;
|
||||||
|
|
||||||
default {
|
default {
|
||||||
state_entry() {
|
state_entry() {
|
||||||
|
llOwnerSay("🎮 Main Controller ready!");
|
||||||
|
llOwnerSay("🎮 Main Controller key: " + (string)llGetKey());
|
||||||
|
llOwnerSay("🎮 Main Controller position: " + (string)llGetPos());
|
||||||
llListen(DIALOG_CHANNEL, "", NULL_KEY, "");
|
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) {
|
touch_start(integer total_number) {
|
||||||
|
|
@ -153,21 +357,49 @@ default {
|
||||||
integer isStarter = FALSE;
|
integer isStarter = FALSE;
|
||||||
if (idx == -1) {
|
if (idx == -1) {
|
||||||
// No index found; the owner is not yet registered.
|
// No index found; the owner is not yet registered.
|
||||||
// If there are no existing players, they will become the starter.
|
// Owner becomes starter if there are no existing human players (excluding bots)
|
||||||
if (llGetListLength(players) == 0) {
|
isStarter = TRUE; // Default to TRUE for owner
|
||||||
isStarter = TRUE;
|
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);
|
string ownerName = llKey2Name(toucher);
|
||||||
// Send a registration request; Main.lsl will handle adding them and rezzing a float
|
// 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);
|
llMessageLinked(LINK_SET, MSG_REGISTER_PLAYER, ownerName + "|" + (string)toucher, NULL_KEY);
|
||||||
} else {
|
} else {
|
||||||
// Already registered; starter if at index 0
|
// Already registered; check if owner is the first human player
|
||||||
isStarter = (idx == 0);
|
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
|
// Show the owner menu with the appropriate starter flag
|
||||||
llMessageLinked(LINK_SET, MSG_SHOW_MENU, "owner|" + (string)isStarter, toucher);
|
llMessageLinked(LINK_SET, MSG_SHOW_MENU, "owner|" + (string)isStarter, toucher);
|
||||||
} else if (idx != -1) {
|
} 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);
|
llMessageLinked(LINK_SET, MSG_SHOW_MENU, "player|" + (string)isStarter, toucher);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -208,6 +440,14 @@ default {
|
||||||
roundStarted = TRUE;
|
roundStarted = TRUE;
|
||||||
diceType = (integer)str;
|
diceType = (integer)str;
|
||||||
llOwnerSay(" Dice type set to: " + 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();
|
showNextPickerDialog();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
|
@ -269,14 +509,36 @@ default {
|
||||||
list parts = llParseString2List(str, ["|"], []);
|
list parts = llParseString2List(str, ["|"], []);
|
||||||
string newName = llList2String(parts, 0);
|
string newName = llList2String(parts, 0);
|
||||||
key newKey = (key)llList2String(parts, 1);
|
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
|
// Do not register if already present
|
||||||
integer existingIdx = llListFindList(players, [newKey]);
|
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) {
|
if (existingIdx == -1) {
|
||||||
// Add to local lists
|
// Add to local lists
|
||||||
players += [newKey];
|
players += [newKey];
|
||||||
names += [newName];
|
names += [newName];
|
||||||
lives += [3];
|
lives += [3];
|
||||||
|
llOwnerSay("🐛 DEBUG: Added player " + newName + " with 3 lives. Lives now: " + llList2CSV(lives));
|
||||||
picksData += [newName + "|"];
|
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
|
// The float will be rezzed by the Floater Manager after it processes
|
||||||
// this registration. Avoid sending MSG_REZ_FLOAT here to prevent
|
// this registration. Avoid sending MSG_REZ_FLOAT here to prevent
|
||||||
// race conditions; instead, request the float from Floater Manager
|
// race conditions; instead, request the float from Floater Manager
|
||||||
|
|
@ -288,6 +550,196 @@ default {
|
||||||
}
|
}
|
||||||
return;
|
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
|
// Handle leave game requests
|
||||||
if (num == 107) {
|
if (num == 107) {
|
||||||
list parts = llParseString2List(str, ["|"], []);
|
list parts = llParseString2List(str, ["|"], []);
|
||||||
|
|
@ -296,20 +748,29 @@ default {
|
||||||
key leavingKey = (key)llList2String(parts, 2);
|
key leavingKey = (key)llList2String(parts, 2);
|
||||||
integer idx = llListFindList(players, [leavingKey]);
|
integer idx = llListFindList(players, [leavingKey]);
|
||||||
if (idx != -1) {
|
if (idx != -1) {
|
||||||
// Remove player's float
|
// Remove player's float using the tracked channel
|
||||||
integer ch = -777000 + idx;
|
integer ch = llList2Integer(floaterChannels, idx);
|
||||||
|
llOwnerSay("🔍 Cleaning up floater channel " + (string)ch + " for " + leavingName);
|
||||||
llMessageLinked(LINK_SET, MSG_CLEANUP_FLOAT, (string)ch, NULL_KEY);
|
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);
|
players = llDeleteSubList(players, idx, idx);
|
||||||
names = llDeleteSubList(names, idx, idx);
|
names = llDeleteSubList(names, idx, idx);
|
||||||
lives = llDeleteSubList(lives, idx, idx);
|
lives = llDeleteSubList(lives, idx, idx);
|
||||||
picksData = llDeleteSubList(picksData, 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
|
// Update helpers
|
||||||
updateHelpers();
|
updateHelpers();
|
||||||
llOwnerSay("👋 " + leavingName + " left the game");
|
llOwnerSay("👋 " + leavingName + " left the game");
|
||||||
// Check if game should end (less than 2 players)
|
// Check if game should end (less than 2 players)
|
||||||
if (llGetListLength(names) == 1) {
|
if (llGetListLength(names) == 1) {
|
||||||
llSay(0, " " + llList2String(names, 0) + " is the last player standing and wins the game!");
|
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();
|
resetGame();
|
||||||
} else if (llGetListLength(names) == 0) {
|
} else if (llGetListLength(names) == 0) {
|
||||||
resetGame();
|
resetGame();
|
||||||
|
|
@ -318,9 +779,217 @@ default {
|
||||||
}
|
}
|
||||||
return;
|
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) {
|
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) {
|
if (channel == DIALOG_CHANNEL) {
|
||||||
// Owner-specific commands: only owner messages in the dialog should be processed here.
|
// Owner-specific commands: only owner messages in the dialog should be processed here.
|
||||||
if (id == llGetOwner()) {
|
if (id == llGetOwner()) {
|
||||||
|
|
@ -350,8 +1019,18 @@ default {
|
||||||
key fake = llGenerateKey();
|
key fake = llGenerateKey();
|
||||||
// Delegate registration to the MSG_REGISTER_PLAYER handler; it will add to lists and rez a float
|
// 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);
|
llMessageLinked(LINK_SET, MSG_REGISTER_PLAYER, testName + "|" + (string)fake, NULL_KEY);
|
||||||
// Show the owner menu again (not flagged as starter)
|
// Show the owner menu again - owner should still be starter after adding bots
|
||||||
llMessageLinked(LINK_SET, MSG_SHOW_MENU, "owner|0", llGetOwner());
|
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;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -363,22 +1042,46 @@ default {
|
||||||
llOwnerSay("⚠️ Need at least 2 players to start the game.");
|
llOwnerSay("⚠️ Need at least 2 players to start the game.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Check if the sender is the owner
|
|
||||||
|
// Check if all non-starter players are ready
|
||||||
|
string starterName = "";
|
||||||
if (id == llGetOwner()) {
|
if (id == llGetOwner()) {
|
||||||
startNextRound();
|
starterName = llKey2Name(llGetOwner());
|
||||||
requestDiceType();
|
} else if (llGetListLength(players) > 0) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Otherwise, check if the sender is the very first player registered
|
|
||||||
if (llGetListLength(players) > 0) {
|
|
||||||
key firstPlayer = llList2Key(players, 0);
|
key firstPlayer = llList2Key(players, 0);
|
||||||
if (id == firstPlayer) {
|
if (id == firstPlayer) {
|
||||||
startNextRound();
|
starterName = llKey2Name(firstPlayer);
|
||||||
requestDiceType();
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (starterName == "") {
|
||||||
|
llOwnerSay("⚠️ Only the game starter can start the game.");
|
||||||
return;
|
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 the message text matches a player name, request their pick list
|
||||||
if (llListFindList(names, [msg]) != -1) {
|
if (llListFindList(names, [msg]) != -1) {
|
||||||
llMessageLinked(LINK_SET, 206, msg, id);
|
llMessageLinked(LINK_SET, 206, msg, id);
|
||||||
|
|
|
||||||
|
|
@ -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 MSG_SHOW_DIALOG = 101;
|
||||||
integer numberPickChannel = -77888;
|
integer numberPickChannel = -77888;
|
||||||
|
|
||||||
list getDialogButtons(integer diceType) {
|
// Pagination and picking state
|
||||||
list buttons = [];
|
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;
|
integer i;
|
||||||
for (i = 1; i <= diceType; 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;
|
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) {
|
if (diceType <= 0) {
|
||||||
llOwnerSay("⚠️ Cannot show dialog: diceType is 0 or invalid.");
|
llOwnerSay("⚠️ Cannot show dialog: diceType is 0 or invalid.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
list options = getDialogButtons(diceType);
|
// Initialize or update picking session
|
||||||
llOwnerSay("🗨️ Showing dialog to: " + name);
|
if (currentPlayer != name) {
|
||||||
llOwnerSay("📋 Page: 0, DiceType: " + (string)diceType);
|
currentPlayer = name;
|
||||||
llOwnerSay("🔢 Options: " + llList2CSV(options));
|
currentPlayerKey = id;
|
||||||
llDialog(id, "Choose a number (1 - " + (string)diceType + "):", options, numberPickChannel);
|
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 {
|
default {
|
||||||
state_entry() {
|
state_entry() {
|
||||||
|
llOwnerSay("🎮 Number Picker Dialog Handler ready!");
|
||||||
llListen(numberPickChannel, "", NULL_KEY, "");
|
llListen(numberPickChannel, "", NULL_KEY, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
link_message(integer sender, integer num, string str, key id) {
|
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) {
|
if (num == MSG_SHOW_DIALOG) {
|
||||||
|
llOwnerSay("✅ Processing MSG_SHOW_DIALOG...");
|
||||||
list parts = llParseString2List(str, ["|"], []);
|
list parts = llParseString2List(str, ["|"], []);
|
||||||
if (llGetListLength(parts) < 2) {
|
if (llGetListLength(parts) < 3) {
|
||||||
llOwnerSay("⚠️ Malformed MSG_SHOW_DIALOG input: " + str);
|
llOwnerSay("⚠️ Malformed MSG_SHOW_DIALOG input: " + str);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
string name = llList2String(parts, 0);
|
string name = llList2String(parts, 0);
|
||||||
integer diceType = (integer)llList2String(parts, 1);
|
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) {
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -11,9 +11,18 @@ integer MSG_PICK_LIST_RESULT = 205;
|
||||||
integer MSG_LIFE_LOOKUP = 207;
|
integer MSG_LIFE_LOOKUP = 207;
|
||||||
integer MSG_REGISTER_PLAYER = 106;
|
integer MSG_REGISTER_PLAYER = 106;
|
||||||
integer MSG_REZ_FLOAT = 105;
|
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
|
// 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
|
// Display the debug/owner menu
|
||||||
showOwnerMenu(key id) {
|
showOwnerMenu(key id) {
|
||||||
|
|
@ -21,23 +30,48 @@ showOwnerMenu(key id) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Display the combined Ready/Leave menu for both players and owners
|
// 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.
|
// Owners receive an extra "Owner" button to access the advanced owner menu.
|
||||||
showReadyLeaveMenu(key id, integer isStarter, integer isOwner) {
|
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;
|
list options;
|
||||||
// If this user is flagged as the starter, they should see the "Start Game"
|
string menuText = "Select an option:";
|
||||||
// button so they can initiate the round (regardless of ownership). Otherwise
|
|
||||||
// they see the regular "Ready" button.
|
|
||||||
if (isStarter) {
|
if (isStarter) {
|
||||||
options = ["Start Game", "Leave Game"];
|
options = ["Start Game", "Leave Game"];
|
||||||
|
menuText = "You are the game starter. You can start the game when all other players are ready.";
|
||||||
|
} else {
|
||||||
|
// 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 {
|
} else {
|
||||||
options = ["Ready", "Leave Game"];
|
options = ["Ready", "Leave Game"];
|
||||||
|
menuText = "You are NOT READY to play. Click 'Ready' when you want to participate.";
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (isOwner) {
|
if (isOwner) {
|
||||||
options += ["Owner"];
|
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
|
// Manage picks UI helpers remain unchanged
|
||||||
|
|
@ -75,6 +109,7 @@ askForNewPick(key id) {
|
||||||
|
|
||||||
default {
|
default {
|
||||||
state_entry() {
|
state_entry() {
|
||||||
|
llOwnerSay("🎭 Owner and Player Dialog Handler ready!");
|
||||||
llListen(DIALOG_CHANNEL, "", NULL_KEY, "");
|
llListen(DIALOG_CHANNEL, "", NULL_KEY, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -94,6 +129,23 @@ default {
|
||||||
showReadyLeaveMenu(id, isStarter, FALSE);
|
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) {
|
else if (num == MSG_PLAYER_LIST_RESULT) {
|
||||||
list playerNames = llParseString2List(str, [","], []);
|
list playerNames = llParseString2List(str, [","], []);
|
||||||
llOwnerSay("📋 Fetching list of players for pick management...");
|
llOwnerSay("📋 Fetching list of players for pick management...");
|
||||||
|
|
@ -130,13 +182,23 @@ default {
|
||||||
// Use the avatar's key as the identifier
|
// Use the avatar's key as the identifier
|
||||||
string pname = llKey2Name(id);
|
string pname = llKey2Name(id);
|
||||||
llMessageLinked(LINK_SET, MSG_REGISTER_PLAYER, pname + "|" + (string)id, NULL_KEY);
|
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") {
|
else if (msg == "Leave Game") {
|
||||||
// Send leave game message to main controller
|
// Send leave game message to main controller
|
||||||
string pname = llKey2Name(id);
|
string pname = llKey2Name(id);
|
||||||
llMessageLinked(LINK_SET, 107, "LEAVE_GAME|" + pname + "|" + (string)id, NULL_KEY);
|
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") {
|
else if (msg == "Manage Picks") {
|
||||||
llMessageLinked(LINK_THIS, 202, "REQUEST_PLAYER_LIST", id);
|
llMessageLinked(LINK_THIS, 202, "REQUEST_PLAYER_LIST", id);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
246
README.md
246
README.md
|
|
@ -1,186 +1,108 @@
|
||||||
🎲 Peril Dice — Modular Game System for Second Life
|
# 🎲 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.
|
|
||||||
|
|
||||||
🔧 Features
|
## Overview
|
||||||
✅ Player System
|
|
||||||
Max players: 10
|
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
|
## Installation
|
||||||
Dice type is automatically chosen to ensure at least 3 picks per player
|
|
||||||
|
|
||||||
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
|
## How to Play
|
||||||
1–2 d6
|
|
||||||
3–4 d12
|
|
||||||
5–6 d20
|
|
||||||
7–10 d30
|
|
||||||
|
|
||||||
🧍 Test Players
|
### For Owner
|
||||||
Owner can add/remove test players (bots)
|
- **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
|
The system uses a modular LSL architecture with inter-script communication:
|
||||||
Players receive a floating HUD showing:
|
|
||||||
|
|
||||||
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
|
## Known Issues
|
||||||
Separated scripts:
|
|
||||||
|
|
||||||
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
|
Game rules were created by **Noose the Bunny** (djmusica28) in Second Life. This automated version builds upon their original manual gameplay concept.
|
||||||
Players touch the controller object to join the game
|
|
||||||
|
|
||||||
Once 2+ players have joined, the first player gets the option to Start Game
|
### Original Manual Rules Summary:
|
||||||
|
- Each player starts with 3 lives
|
||||||
Dice type is selected automatically
|
- Players pick numbers based on peril player's remaining lives:
|
||||||
|
- 3 Lives → Pick 1 number
|
||||||
Each player is prompted (in order) to select 3 numbers
|
- 2 Lives → Pick 2 numbers
|
||||||
|
- 1 Life → Pick 3 numbers
|
||||||
After all picks are made:
|
- Roll dice to determine outcome
|
||||||
|
- Last player with lives wins
|
||||||
The dice is rolled
|
|
||||||
|
---
|
||||||
The result is compared to each player’s picks
|
|
||||||
|
*Peril Dice provides hours of entertainment for Second Life communities with its blend of strategy, luck, and social interaction!*
|
||||||
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
|
|
||||||
|
|
@ -21,9 +21,28 @@ list getPicksFor(string nameInput) {
|
||||||
string entry = llList2String(picksData, i);
|
string entry = llList2String(picksData, i);
|
||||||
list parts = llParseString2List(entry, ["|"], []);
|
list parts = llParseString2List(entry, ["|"], []);
|
||||||
if (llList2String(parts, 0) == nameInput) {
|
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 [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -48,14 +67,62 @@ confetti() {
|
||||||
|
|
||||||
default {
|
default {
|
||||||
state_entry() {
|
state_entry() {
|
||||||
|
llOwnerSay("🎲 Roll Confetti Module ready!");
|
||||||
llListen(rollDialogChannel, "", NULL_KEY, "");
|
llListen(rollDialogChannel, "", NULL_KEY, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
link_message(integer sender, integer num, string str, key id) {
|
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) {
|
if (num == MSG_SHOW_ROLL_DIALOG) {
|
||||||
|
// 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.");
|
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);
|
llDialog(id, "🎲 All picks are in! You're the peril player. Roll the dice?", ["Roll"], rollDialogChannel);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
else if (num == MSG_ROLL_RESULT) {
|
else if (num == MSG_ROLL_RESULT) {
|
||||||
integer diceType = (integer)str;
|
integer diceType = (integer)str;
|
||||||
|
|
@ -65,40 +132,121 @@ default {
|
||||||
|
|
||||||
string newPeril = "";
|
string newPeril = "";
|
||||||
integer matched = FALSE;
|
integer matched = FALSE;
|
||||||
|
list matchedPlayers = [];
|
||||||
integer i;
|
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++) {
|
for (i = 0; i < llGetListLength(names); i++) {
|
||||||
string pname = llList2String(names, i);
|
string pname = llList2String(names, i);
|
||||||
list picks = getPicksFor(pname);
|
list picks = getPicksFor(pname);
|
||||||
|
llOwnerSay("🔍 DEBUG: " + pname + " picks: " + llList2CSV(picks));
|
||||||
if (llListFindList(picks, [resultStr]) != -1) {
|
if (llListFindList(picks, [resultStr]) != -1) {
|
||||||
matched = TRUE;
|
matched = TRUE;
|
||||||
if (pname != perilPlayer && newPeril == "") {
|
matchedPlayers += [pname];
|
||||||
newPeril = 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 != "") {
|
if (matched && newPeril != "") {
|
||||||
llSay(0, "🎯 " + newPeril + " matched the roll and becomes the new peril player!");
|
llSay(0, "🎯 " + newPeril + " matched the roll and becomes the new peril player!");
|
||||||
perilPlayer = newPeril;
|
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 {
|
} else {
|
||||||
integer pidx = llListFindList(names, [perilPlayer]);
|
integer pidx = llListFindList(names, [perilPlayer]);
|
||||||
if (pidx != -1) {
|
if (pidx != -1) {
|
||||||
integer currentLives = llList2Integer(lives, pidx);
|
integer currentLives = llList2Integer(lives, pidx);
|
||||||
lives = llListReplaceList(lives, [currentLives - 1], pidx, pidx);
|
lives = llListReplaceList(lives, [currentLives - 1], pidx, pidx);
|
||||||
llSay(0, "💀 " + perilPlayer + " was hit and lost a life!");
|
llSay(0, "💀 " + perilPlayer + " was hit and lost a life!");
|
||||||
confetti();
|
|
||||||
llMessageLinked(LINK_SET, MSG_REZ_FLOAT, perilPlayer, NULL_KEY);
|
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);
|
llMessageLinked(LINK_SET, MSG_SYNC_GAME_STATE, gameSync, NULL_KEY);
|
||||||
llSleep(0.2);
|
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) {
|
listen(integer channel, string name, key id, string msg) {
|
||||||
|
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);
|
llMessageLinked(LINK_THIS, channel, msg, id);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,11 +14,19 @@ list getPicksFor(string nameInput) {
|
||||||
integer i;
|
integer i;
|
||||||
for (i = 0; i < llGetListLength(picksData); i++) {
|
for (i = 0; i < llGetListLength(picksData); i++) {
|
||||||
string entry = llList2String(picksData, i);
|
string entry = llList2String(picksData, i);
|
||||||
if (llSubStringIndex(entry, nameInput + "|") == 0) {
|
|
||||||
list parts = llParseString2List(entry, ["|"], []);
|
list parts = llParseString2List(entry, ["|"], []);
|
||||||
if (llGetListLength(parts) >= 2) {
|
if (llGetListLength(parts) >= 2 && llList2String(parts, 0) == nameInput) {
|
||||||
return llParseString2List(llList2String(parts, 1), [","], []);
|
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 [];
|
return [];
|
||||||
|
|
@ -57,18 +65,21 @@ default {
|
||||||
list parts = llParseString2List(str, ["~"], []);
|
list parts = llParseString2List(str, ["~"], []);
|
||||||
if (llGetListLength(parts) < 4) return;
|
if (llGetListLength(parts) < 4) return;
|
||||||
lives = llCSV2List(llList2String(parts, 0));
|
lives = llCSV2List(llList2String(parts, 0));
|
||||||
list rawPicks = llCSV2List(llList2String(parts, 1));
|
|
||||||
|
// Use ^ delimiter for picksData to match the main system
|
||||||
|
string picksDataStr = llList2String(parts, 1);
|
||||||
|
if (picksDataStr == "" || picksDataStr == "EMPTY") {
|
||||||
picksData = [];
|
picksData = [];
|
||||||
integer i;
|
|
||||||
for (i = 0; i < llGetListLength(rawPicks); i++) {
|
|
||||||
string entry = llList2String(rawPicks, i);
|
|
||||||
if (llSubStringIndex(entry, "|") != -1) {
|
|
||||||
picksData += [entry];
|
|
||||||
} else {
|
} else {
|
||||||
llOwnerSay("⚠️ Ignored malformed picksData during sync: " + entry);
|
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));
|
names = llCSV2List(llList2String(parts, 3));
|
||||||
|
|
||||||
integer nameIdx = llListFindList(names, [myName]);
|
integer nameIdx = llListFindList(names, [myName]);
|
||||||
|
|
@ -78,10 +89,15 @@ default {
|
||||||
integer lifeCount = llList2Integer(lives, nameIdx);
|
integer lifeCount = llList2Integer(lives, nameIdx);
|
||||||
|
|
||||||
string perilDisplay;
|
string perilDisplay;
|
||||||
if (perilPlayer != "") {
|
// If the perilPlayer string is empty or contains a comma (indicating
|
||||||
perilDisplay = perilPlayer;
|
// 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 {
|
} else {
|
||||||
perilDisplay = "Waiting for peril...";
|
perilDisplay = perilPlayer;
|
||||||
}
|
}
|
||||||
|
|
||||||
string picksDisplay = llList2CSV(picks);
|
string picksDisplay = llList2CSV(picks);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue