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