v2.8.6: Winner Glow Celebration System
- Winner Glow Effects: Bright green glow on both scoreboard and floaters for winners - Extended Victory Display: 24-second celebration timer before game reset - Enhanced Visual Feedback: Color-coded glow system (green/yellow/red) - Victory Text Display: Winner floaters show 'ULTIMATE VICTORY!' and 'ULTIMATE SURVIVOR' - Timer System Optimization: Dedicated victory timer mode, removed unused timeout system - Memory Efficiency: Cleaned up unused components for better performance - Synchronized Celebration: Both scoreboard and floater systems celebrate winner simultaneously - Visual Priority: Winner glow overrides peril glow during victory celebration Professional winner recognition system makes victory feel rewarding and celebrated!
This commit is contained in:
parent
dd03bd8eef
commit
03c5d81bb0
23 changed files with 1484 additions and 585 deletions
|
|
@ -60,6 +60,19 @@ list sentBotMessages = [];
|
|||
// Verbose logging control - toggled by owner
|
||||
integer VERBOSE_LOGGING = FALSE;
|
||||
|
||||
// Memory reporting function
|
||||
reportMemoryUsage(string scriptName) {
|
||||
integer used = llGetUsedMemory();
|
||||
integer free = llGetFreeMemory();
|
||||
integer total = used + free;
|
||||
float percentUsed = ((float)used / (float)total) * 100.0;
|
||||
|
||||
llOwnerSay("🧠 [" + scriptName + "] Memory: " +
|
||||
(string)used + " used, " +
|
||||
(string)free + " free (" +
|
||||
llGetSubString((string)percentUsed, 0, 4) + "% used)");
|
||||
}
|
||||
|
||||
// Helper function to check and report memory usage
|
||||
checkMemoryUsage(string context) {
|
||||
integer usedMemory = llGetUsedMemory();
|
||||
|
|
@ -196,6 +209,8 @@ doBotRoll(string botName, integer diceMax) {
|
|||
|
||||
default {
|
||||
state_entry() {
|
||||
reportMemoryUsage("🤖 Bot Manager");
|
||||
|
||||
initializeChannels();
|
||||
LISTEN_CHANNEL = BOT_COMMAND_CHANNEL;
|
||||
|
||||
|
|
@ -215,6 +230,7 @@ default {
|
|||
}
|
||||
|
||||
on_rez(integer start_param) {
|
||||
reportMemoryUsage("🤖 Bot Manager");
|
||||
llOwnerSay("🔄 Bot Manager rezzed - reinitializing...");
|
||||
|
||||
// Re-initialize dynamic channels
|
||||
|
|
|
|||
50
CHANGELOG.md
50
CHANGELOG.md
|
|
@ -4,6 +4,56 @@
|
|||
|
||||
All notable changes to Peril Dice will be documented in this file.
|
||||
|
||||
## [2.8.6] - 2025-09-16
|
||||
|
||||
### 🏆 **Winner Glow Celebration System**
|
||||
- **Victory Visual Enhancement**: Added comprehensive winner celebration with green glow effects
|
||||
- **Scoreboard Glow**: Winner's profile picture and hearts display bright green glow with green tint
|
||||
- **Floater Glow**: Winner's floating display shows green glow with victory text "✨ ULTIMATE VICTORY! ✨" and "🏆 ULTIMATE SURVIVOR 🏆"
|
||||
- **Visual Priority**: Winner glow overrides peril glow - green takes priority over yellow during victory
|
||||
- **Clear Recognition**: Eliminated players show red X markers while winner gets celebration spotlight
|
||||
- **Extended Victory Display**: 24-second timer delay before game reset allows players to appreciate the winner
|
||||
- **Before**: Winner declared → Immediate reset → No time to see who won
|
||||
- **After**: Winner declared → Green glow celebration → 24 seconds to admire → Then reset
|
||||
- **User Experience**: Proper victory moment with visual feedback before returning to lobby
|
||||
|
||||
### 🎨 **Enhanced Visual Feedback System**
|
||||
- **Improved Glow Colors**: Enhanced scoreboard glow system with proper color tinting
|
||||
- **Winner Glow**: Bright green glow + green color tint for unmistakable victory recognition
|
||||
- **Peril Glow**: Yellow glow + yellow color tint for clear peril player identification
|
||||
- **Visual Clarity**: Replaced generic white glow with distinct colored glows for better gameplay feedback
|
||||
- **Floater Victory Detection**: Smart winner detection in floating displays
|
||||
- **Automatic Detection**: Floaters automatically detect when only one player remains alive
|
||||
- **Victory Text**: Winner's floater displays celebration text instead of standard game info
|
||||
- **Consistent Experience**: Both scoreboard and floater systems celebrate the winner simultaneously
|
||||
|
||||
### 🔧 **Timer System Optimization**
|
||||
- **Victory Delay Timer**: Added dedicated timer mode for victory celebration delays
|
||||
- **Memory Efficiency**: Removed unused timeout system to free up memory resources
|
||||
- **Clean Architecture**: Streamlined timer system focuses on essential functionality
|
||||
- **Reliable Operation**: Victory display timing works consistently without interference from other systems
|
||||
- **Message Communication Enhancement**: Improved winner glow message handling
|
||||
- **Scoreboard Integration**: Added proper MSG_UPDATE_WINNER message constant for clean communication
|
||||
- **Floater Integration**: Enhanced floater update system to trigger victory displays
|
||||
- **Synchronized Effects**: Both display systems receive winner updates simultaneously
|
||||
|
||||
### 🎮 **User Experience Improvements**
|
||||
- **Victory Celebration**: Clear winner recognition with proper visual feedback
|
||||
- **Extended Appreciation Time**: 24 seconds to see and congratulate the winner before reset
|
||||
- **Visual Hierarchy**: Clean priority system - Winner (green) > Peril (yellow) > Eliminated (red X)
|
||||
- **Consistent Display**: Winner celebration works on both scoreboard and personal floater displays
|
||||
|
||||
### 📊 **System Performance**
|
||||
- **Memory Optimization**: Removed unused timeout system components saving memory
|
||||
- **Efficient Messaging**: Clean message routing for winner glow effects
|
||||
- **Timer Efficiency**: Dedicated victory timer prevents conflicts with other timing systems
|
||||
|
||||
### 🎯 **Impact Summary**
|
||||
- **Before**: Winners were announced but no visual celebration - immediate reset gave no recognition
|
||||
- **After**: Winners get bright green glow celebration on both scoreboard and floaters with 24 seconds of recognition
|
||||
- **Player Satisfaction**: Proper victory moment makes winning feel rewarding and celebrated
|
||||
- **Visual Polish**: Professional-grade visual feedback system enhances the competitive gaming experience
|
||||
|
||||
## [2.8.5] - 2025-09-15
|
||||
|
||||
### 🧠 **Memory Optimization & Dead Code Cleanup**
|
||||
|
|
|
|||
|
|
@ -3,6 +3,57 @@ PERIL DICE CHANGELOG
|
|||
|
||||
All notable changes to Peril Dice are documented in this file.
|
||||
|
||||
[2.8.6] - 2025-09-16
|
||||
====================
|
||||
|
||||
🏆 WINNER GLOW CELEBRATION SYSTEM
|
||||
- Victory Visual Enhancement: Added comprehensive winner celebration with green glow effects
|
||||
* Scoreboard Glow: Winner's profile picture and hearts display bright green glow with green tint
|
||||
* Floater Glow: Winner's floating display shows green glow with victory text "✨ ULTIMATE VICTORY! ✨" and "🏆 ULTIMATE SURVIVOR 🏆"
|
||||
* Visual Priority: Winner glow overrides peril glow - green takes priority over yellow during victory
|
||||
* Clear Recognition: Eliminated players show red X markers while winner gets celebration spotlight
|
||||
- Extended Victory Display: 24-second timer delay before game reset allows players to appreciate the winner
|
||||
* Before: Winner declared → Immediate reset → No time to see who won
|
||||
* After: Winner declared → Green glow celebration → 24 seconds to admire → Then reset
|
||||
* User Experience: Proper victory moment with visual feedback before returning to lobby
|
||||
|
||||
🎨 ENHANCED VISUAL FEEDBACK SYSTEM
|
||||
- Improved Glow Colors: Enhanced scoreboard glow system with proper color tinting
|
||||
* Winner Glow: Bright green glow + green color tint for unmistakable victory recognition
|
||||
* Peril Glow: Yellow glow + yellow color tint for clear peril player identification
|
||||
* Visual Clarity: Replaced generic white glow with distinct colored glows for better gameplay feedback
|
||||
- Floater Victory Detection: Smart winner detection in floating displays
|
||||
* Automatic Detection: Floaters automatically detect when only one player remains alive
|
||||
* Victory Text: Winner's floater displays celebration text instead of standard game info
|
||||
* Consistent Experience: Both scoreboard and floater systems celebrate the winner simultaneously
|
||||
|
||||
🔧 TIMER SYSTEM OPTIMIZATION
|
||||
- Victory Delay Timer: Added dedicated timer mode for victory celebration delays
|
||||
* Memory Efficiency: Removed unused timeout system to free up memory resources
|
||||
* Clean Architecture: Streamlined timer system focuses on essential functionality
|
||||
* Reliable Operation: Victory display timing works consistently without interference from other systems
|
||||
- Message Communication Enhancement: Improved winner glow message handling
|
||||
* Scoreboard Integration: Added proper MSG_UPDATE_WINNER message constant for clean communication
|
||||
* Floater Integration: Enhanced floater update system to trigger victory displays
|
||||
* Synchronized Effects: Both display systems receive winner updates simultaneously
|
||||
|
||||
🎮 USER EXPERIENCE IMPROVEMENTS
|
||||
- Victory Celebration: Clear winner recognition with proper visual feedback
|
||||
- Extended Appreciation Time: 24 seconds to see and congratulate the winner before reset
|
||||
- Visual Hierarchy: Clean priority system - Winner (green) > Peril (yellow) > Eliminated (red X)
|
||||
- Consistent Display: Winner celebration works on both scoreboard and personal floater displays
|
||||
|
||||
📊 SYSTEM PERFORMANCE
|
||||
- Memory Optimization: Removed unused timeout system components saving memory
|
||||
- Efficient Messaging: Clean message routing for winner glow effects
|
||||
- Timer Efficiency: Dedicated victory timer prevents conflicts with other timing systems
|
||||
|
||||
🎯 IMPACT SUMMARY
|
||||
- Before: Winners were announced but no visual celebration - immediate reset gave no recognition
|
||||
- After: Winners get bright green glow celebration on both scoreboard and floaters with 24 seconds of recognition
|
||||
- Player Satisfaction: Proper victory moment makes winning feel rewarding and celebrated
|
||||
- Visual Polish: Professional-grade visual feedback system enhances the competitive gaming experience
|
||||
|
||||
[2.8.5] - 2025-09-15
|
||||
====================
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,19 @@
|
|||
integer VERBOSE_LOGGING = TRUE; // Global flag for verbose debug logs
|
||||
integer MSG_TOGGLE_VERBOSE_LOGS = 9998; // Message to toggle verbose logging
|
||||
|
||||
// Memory reporting function
|
||||
reportMemoryUsage(string scriptName) {
|
||||
integer used = llGetUsedMemory();
|
||||
integer free = llGetFreeMemory();
|
||||
integer total = used + free;
|
||||
float percentUsed = ((float)used / (float)total) * 100.0;
|
||||
|
||||
llOwnerSay("🧠 [" + scriptName + "] Memory: " +
|
||||
(string)used + " used, " +
|
||||
(string)free + " free (" +
|
||||
llGetSubString((string)percentUsed, 0, 4) + "% used)");
|
||||
}
|
||||
|
||||
// Message constants for communication with main controller
|
||||
integer MSG_REQUEST_PLAYER_DATA = 8001;
|
||||
integer MSG_REQUEST_PICK_DATA = 8002;
|
||||
|
|
@ -367,6 +380,8 @@ handleDialogRequest(string dialogData) {
|
|||
|
||||
default {
|
||||
state_entry() {
|
||||
reportMemoryUsage("📨 Message Handler");
|
||||
|
||||
llOwnerSay("📨 [Message Handler] Helper script ready!");
|
||||
initializeChannels();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,106 +0,0 @@
|
|||
# Direct Communication Architecture
|
||||
|
||||
## Overview
|
||||
To reduce Main Controller memory pressure (from 92% usage), we've implemented direct system-to-system communication, bypassing the Main Controller for specific operations.
|
||||
|
||||
## Direct Communication Flows
|
||||
|
||||
### ✅ Implemented Direct Connections
|
||||
|
||||
#### 1. **Roll Module → Scoreboard** (Status & Life Updates)
|
||||
```lsl
|
||||
// Life updates during gameplay
|
||||
llMessageLinked(12, 3002, perilPlayer + "|" + (string)lives + "|NULL_KEY", NULL_KEY);
|
||||
|
||||
// Game status updates
|
||||
llMessageLinked(12, 3001, "Plot Twist", NULL_KEY); // When player picks rolled number
|
||||
llMessageLinked(12, 3001, "Direct Hit", NULL_KEY); // When peril player picks their doom
|
||||
llMessageLinked(12, 3001, "No Shield", NULL_KEY); // When nobody picks rolled number
|
||||
```
|
||||
**Benefit**: Real-time scoreboard updates without Main Controller memory overhead.
|
||||
|
||||
#### 2. **Roll Module → Dice Display** (Roll Results)
|
||||
```lsl
|
||||
// Display roll results directly
|
||||
llMessageLinked(83, 3020, perilPlayer + "|" + rollResult, NULL_KEY);
|
||||
|
||||
// Clear dice display
|
||||
llMessageLinked(83, 3021, "", NULL_KEY);
|
||||
```
|
||||
**Benefit**: Eliminates dice message routing through Main Controller.
|
||||
|
||||
#### 3. **Main Controller → Scoreboard → Leaderboard** (Win/Loss Tracking)
|
||||
```lsl
|
||||
// Elimination tracking (already optimized)
|
||||
llMessageLinked(SCOREBOARD_LINK, MSG_GAME_LOST, eliminatedPlayer, NULL_KEY);
|
||||
|
||||
// Victory tracking (already optimized)
|
||||
llMessageLinked(SCOREBOARD_LINK, MSG_GAME_WON, winner, NULL_KEY);
|
||||
```
|
||||
**Benefit**: Scoreboard handles leaderboard updates internally, reducing Main Controller load.
|
||||
|
||||
## Architecture Benefits
|
||||
|
||||
### Memory Savings
|
||||
- **Main Controller**: Reduced from 92% to estimated ~85% memory usage
|
||||
- **Eliminated routing logic**: No more message forwarding for status/dice/scoreboard updates
|
||||
- **Reduced string operations**: Direct messages avoid CSV building/parsing in Main Controller
|
||||
|
||||
### Performance Improvements
|
||||
- **Real-time updates**: Scoreboard updates immediately when lives change
|
||||
- **Reduced latency**: No message forwarding delays
|
||||
- **Better reliability**: Fewer message hops = fewer failure points
|
||||
|
||||
### System Resilience
|
||||
- **Distributed processing**: Each system handles its own display updates
|
||||
- **Reduced bottlenecks**: Main Controller no longer central routing point
|
||||
- **Memory stability**: Less memory pressure on critical controller script
|
||||
|
||||
## Message Flow Comparison
|
||||
|
||||
### Before (Centralized Routing)
|
||||
```
|
||||
Roll Module → Main Controller → Scoreboard (Status)
|
||||
Roll Module → Main Controller → Dice Display (Results)
|
||||
↳ Main Controller ← High Memory Usage
|
||||
```
|
||||
|
||||
### After (Direct Communication)
|
||||
```
|
||||
Roll Module → Scoreboard (Status & Lives)
|
||||
Roll Module → Dice Display (Results)
|
||||
↳ Main Controller ← Reduced Memory Usage
|
||||
```
|
||||
|
||||
## Link Mapping Reference
|
||||
- **Link 1**: Main Controller (Root)
|
||||
- **Link 12**: Scoreboard Manager
|
||||
- **Link 35**: Leaderboard Communication
|
||||
- **Link 83**: Dice Display Bridge
|
||||
|
||||
## Message Constants
|
||||
```lsl
|
||||
// Scoreboard Messages
|
||||
integer MSG_GAME_STATUS = 3001; // Status updates (Plot Twist, Direct Hit, etc.)
|
||||
integer MSG_PLAYER_UPDATE = 3002; // Life updates during gameplay
|
||||
integer MSG_GAME_WON = 3010; // Victory notifications
|
||||
integer MSG_GAME_LOST = 3011; // Elimination notifications
|
||||
|
||||
// Dice Display Messages
|
||||
integer MSG_DICE_ROLL = 3020; // Roll result display
|
||||
integer MSG_CLEAR_DICE = 3021; // Clear display
|
||||
```
|
||||
|
||||
## Testing Validation
|
||||
- ✅ Roll Module sends status updates directly to scoreboard
|
||||
- ✅ Roll Module sends life updates directly to scoreboard
|
||||
- ✅ Roll Module sends dice results directly to dice display
|
||||
- ✅ Floater cleanup with timing delays for elimination
|
||||
- ✅ Main Controller routing logic removed for direct paths
|
||||
|
||||
## Future Optimization Opportunities
|
||||
1. **Bot Manager → Game Manager**: Direct bot pick notifications
|
||||
2. **Floater Manager → Individual Floaters**: Direct floater commands
|
||||
3. **Dialog Systems**: Direct dialog routing without Main Controller
|
||||
|
||||
This architecture maintains the robust linkset communication while significantly reducing memory pressure on the Main Controller.
|
||||
|
|
@ -8,6 +8,7 @@ integer MSG_CLEANUP_FLOAT = 104;
|
|||
integer MSG_REZ_FLOAT = 105;
|
||||
integer MSG_REGISTER_PLAYER = 106;
|
||||
integer MSG_SYNC_GAME_STATE = 107;
|
||||
integer MSG_CLEANUP_ALL_FLOATERS = 212;
|
||||
|
||||
// Debug control message constants
|
||||
integer MSG_DEBUG_PICKS_ON = 7001;
|
||||
|
|
@ -54,12 +55,77 @@ integer DEBUG_PICKS = FALSE;
|
|||
// Verbose logging control - affects general debug messages, toggled by owner
|
||||
integer VERBOSE_LOGGING = FALSE;
|
||||
|
||||
list players = [];
|
||||
list names = [];
|
||||
list lives = [];
|
||||
list picksData = [];
|
||||
// Memory reporting function
|
||||
reportMemoryUsage(string scriptName) {
|
||||
integer used = llGetUsedMemory();
|
||||
integer free = llGetFreeMemory();
|
||||
integer total = used + free;
|
||||
float percentUsed = ((float)used / (float)total) * 100.0;
|
||||
|
||||
llOwnerSay("🧠 [" + scriptName + "] Memory: " +
|
||||
(string)used + " used, " +
|
||||
(string)free + " free (" +
|
||||
llGetSubString((string)percentUsed, 0, 4) + "% used)");
|
||||
}
|
||||
|
||||
// ALIVE PLAYERS (shrinks during eliminations)
|
||||
list alivePlayers = [];
|
||||
list aliveNames = [];
|
||||
list aliveLives = [];
|
||||
list alivePicksData = [];
|
||||
|
||||
// ELIMINATED PLAYERS (grows during eliminations)
|
||||
list eliminatedPlayers = [];
|
||||
list eliminatedNames = [];
|
||||
list eliminatedChannels = [];
|
||||
|
||||
// ALL PLAYERS (never shrinks - for channel mapping)
|
||||
list allPlayerNames = []; // Names in registration order
|
||||
list allPlayerChannels = []; // Corresponding channels
|
||||
|
||||
// SYNC STATE VARIABLES (received from Main Controller)
|
||||
list names = []; // Current player names from sync
|
||||
list players = []; // Current player keys from sync
|
||||
list lives = []; // Current lives from sync
|
||||
list picksData = []; // Current picks data from sync
|
||||
list playerNames = []; // Legacy variable for cleanup compatibility
|
||||
list playerChannels = []; // Legacy variable for cleanup compatibility
|
||||
|
||||
string perilPlayer = "";
|
||||
|
||||
// Move a player from alive to eliminated lists
|
||||
eliminatePlayer(string playerName) {
|
||||
integer aliveIdx = llListFindList(aliveNames, [playerName]);
|
||||
if (aliveIdx == -1) {
|
||||
llOwnerSay("⚠️ [Floater Manager] Cannot eliminate " + playerName + " - not found in alive list");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get player data before removing from alive lists
|
||||
key playerKey = llList2Key(alivePlayers, aliveIdx);
|
||||
string picks = llList2String(alivePicksData, aliveIdx);
|
||||
|
||||
// Find their original channel from permanent mapping
|
||||
integer channelIdx = llListFindList(allPlayerNames, [playerName]);
|
||||
integer playerChannel = -1;
|
||||
if (channelIdx != -1) {
|
||||
playerChannel = llList2Integer(allPlayerChannels, channelIdx);
|
||||
}
|
||||
|
||||
// Add to eliminated lists
|
||||
eliminatedNames += [playerName];
|
||||
eliminatedPlayers += [playerKey];
|
||||
eliminatedChannels += [playerChannel];
|
||||
|
||||
// Remove from alive lists
|
||||
alivePlayers = llDeleteSubList(alivePlayers, aliveIdx, aliveIdx);
|
||||
aliveNames = llDeleteSubList(aliveNames, aliveIdx, aliveIdx);
|
||||
aliveLives = llDeleteSubList(aliveLives, aliveIdx, aliveIdx);
|
||||
alivePicksData = llDeleteSubList(alivePicksData, aliveIdx, aliveIdx);
|
||||
|
||||
llOwnerSay("🧹 [Floater Manager] Moved " + playerName + " from alive to eliminated lists");
|
||||
}
|
||||
|
||||
// Returns a list of picks for the given player name, filtering out invalid values
|
||||
list getPicksFor(string nameInput) {
|
||||
// ENHANCED DEBUG: Log the request and current data state
|
||||
|
|
@ -127,37 +193,67 @@ list getPicksFor(string nameInput) {
|
|||
|
||||
// Converts a player's key into their name (if registered)
|
||||
string getNameFromKey(key id) {
|
||||
integer i = llListFindList(players, [id]);
|
||||
if (i != -1) return llList2String(names, i);
|
||||
integer i = llListFindList(alivePlayers, [id]);
|
||||
if (i != -1) return llList2String(aliveNames, i);
|
||||
return (string)id;
|
||||
}
|
||||
|
||||
// Main event handler
|
||||
default {
|
||||
state_entry() {
|
||||
reportMemoryUsage("💬 Floater Manager");
|
||||
|
||||
initializeChannels();
|
||||
|
||||
// Initialize/reset all state variables
|
||||
players = [];
|
||||
alivePlayers = [];
|
||||
aliveNames = [];
|
||||
aliveLives = [];
|
||||
alivePicksData = [];
|
||||
eliminatedPlayers = [];
|
||||
eliminatedNames = [];
|
||||
eliminatedChannels = [];
|
||||
allPlayerNames = [];
|
||||
allPlayerChannels = [];
|
||||
|
||||
// Initialize sync state variables
|
||||
names = [];
|
||||
players = [];
|
||||
lives = [];
|
||||
picksData = [];
|
||||
playerNames = [];
|
||||
playerChannels = [];
|
||||
perilPlayer = "";
|
||||
|
||||
llOwnerSay("📦 Floater Manager ready!");
|
||||
}
|
||||
|
||||
on_rez(integer start_param) {
|
||||
reportMemoryUsage("💬 Floater Manager");
|
||||
|
||||
llOwnerSay("🔄 Floater Manager rezzed - reinitializing...");
|
||||
|
||||
// Re-initialize dynamic channels
|
||||
initializeChannels();
|
||||
|
||||
// Reset all state variables on rez
|
||||
players = [];
|
||||
alivePlayers = [];
|
||||
aliveNames = [];
|
||||
aliveLives = [];
|
||||
alivePicksData = [];
|
||||
eliminatedPlayers = [];
|
||||
eliminatedNames = [];
|
||||
eliminatedChannels = [];
|
||||
allPlayerNames = [];
|
||||
allPlayerChannels = [];
|
||||
|
||||
// Initialize sync state variables
|
||||
names = [];
|
||||
players = [];
|
||||
lives = [];
|
||||
picksData = [];
|
||||
playerNames = [];
|
||||
playerChannels = [];
|
||||
perilPlayer = "";
|
||||
|
||||
llOwnerSay("✅ Floater Manager reset complete after rez!");
|
||||
|
|
@ -167,10 +263,23 @@ default {
|
|||
// Handle full reset from main controller
|
||||
if (num == -99999 && str == "FULL_RESET") {
|
||||
// Reset all game state
|
||||
players = [];
|
||||
alivePlayers = [];
|
||||
aliveNames = [];
|
||||
aliveLives = [];
|
||||
alivePicksData = [];
|
||||
eliminatedPlayers = [];
|
||||
eliminatedNames = [];
|
||||
eliminatedChannels = [];
|
||||
allPlayerNames = [];
|
||||
allPlayerChannels = [];
|
||||
|
||||
// Reset sync state variables
|
||||
names = [];
|
||||
players = [];
|
||||
lives = [];
|
||||
picksData = [];
|
||||
playerNames = [];
|
||||
playerChannels = [];
|
||||
perilPlayer = "";
|
||||
llOwnerSay("📦 Floater Manager reset!");
|
||||
return;
|
||||
|
|
@ -192,7 +301,7 @@ default {
|
|||
|
||||
if (num == MSG_REGISTER_PLAYER) {
|
||||
// Enforce the maximum number of players
|
||||
if (llGetListLength(players) >= MAX_PLAYERS) {
|
||||
if (llGetListLength(alivePlayers) >= MAX_PLAYERS) {
|
||||
llOwnerSay("⚠️ Cannot register new player; the game is full (max " + (string)MAX_PLAYERS + ").");
|
||||
return;
|
||||
}
|
||||
|
|
@ -202,15 +311,22 @@ default {
|
|||
key avKey = llList2Key(info, 1);
|
||||
|
||||
// Check if this player is already registered to prevent duplicate floaters
|
||||
if (llListFindList(players, [avKey]) != -1) {
|
||||
if (llListFindList(alivePlayers, [avKey]) != -1) {
|
||||
llOwnerSay("⚠️ Player " + name + " is already registered, ignoring duplicate registration");
|
||||
return;
|
||||
}
|
||||
|
||||
names += [name];
|
||||
players += [avKey];
|
||||
lives += [3]; // Initialize with 3 lives for new players
|
||||
picksData += [name + "|"]; // Initialize with empty picks
|
||||
// Add to ALIVE lists
|
||||
aliveNames += [name];
|
||||
alivePlayers += [avKey];
|
||||
aliveLives += [3]; // Initialize with 3 lives for new players
|
||||
alivePicksData += [name + "|"]; // Initialize with empty picks
|
||||
|
||||
// Add to PERMANENT mapping (never changes after registration)
|
||||
integer playerIdx = llGetListLength(allPlayerNames);
|
||||
integer ch = FLOATER_BASE_CHANNEL + playerIdx;
|
||||
allPlayerNames += [name];
|
||||
allPlayerChannels += [ch];
|
||||
llSay(0, "💀 " + name + " has entered the deadly game! Welcome to your potential doom! 💀");
|
||||
|
||||
// Immediately rez the float after registration. This ensures the
|
||||
|
|
@ -225,9 +341,9 @@ default {
|
|||
}
|
||||
else if (num == MSG_REZ_FLOAT) {
|
||||
string name = str;
|
||||
integer idx = llListFindList(names, [name]);
|
||||
integer idx = llListFindList(aliveNames, [name]);
|
||||
if (idx == -1) return;
|
||||
key avKey = llList2Key(players, idx);
|
||||
key avKey = llList2Key(alivePlayers, idx);
|
||||
// Determine the rez position. For real avatars, use their current
|
||||
// location plus an offset. For test players (fake keys) or when
|
||||
// OBJECT_POS cannot be obtained, fall back to rezzing relative to
|
||||
|
|
@ -249,7 +365,16 @@ default {
|
|||
// This prevents them from spawning directly on the leaderboard.
|
||||
pos = basePos + <-4.0 - (float)idx * 1.0, 2.0 + (float)idx * 0.8, 1>;
|
||||
}
|
||||
integer ch = FLOATER_BASE_CHANNEL + idx;
|
||||
// Use permanent channel mapping instead of alive list index
|
||||
integer channelIdx = llListFindList(allPlayerNames, [name]);
|
||||
integer ch;
|
||||
if (channelIdx != -1) {
|
||||
ch = llList2Integer(allPlayerChannels, channelIdx);
|
||||
} else {
|
||||
// Fallback - this shouldn't happen but use idx as backup
|
||||
llOwnerSay("⚠️ [Floater Manager] WARNING: " + name + " not in permanent mapping during rez, using fallback");
|
||||
ch = FLOATER_BASE_CHANNEL + idx;
|
||||
}
|
||||
llRezObject("StatFloat", pos, ZERO_VECTOR, ZERO_ROTATION, ch);
|
||||
// Wait a moment then set the description
|
||||
llSleep(0.2);
|
||||
|
|
@ -268,14 +393,39 @@ default {
|
|||
}
|
||||
else if (num == MSG_UPDATE_FLOAT) {
|
||||
string name = str;
|
||||
integer idx = llListFindList(names, [name]);
|
||||
if (idx == -1) {
|
||||
llOwnerSay("⚠️ [Floater Manager] MSG_UPDATE_FLOAT: Player '" + name + "' not found in names list");
|
||||
|
||||
// FIXED: Use permanent channel mapping that doesn't change during eliminations
|
||||
integer channelIdx = llListFindList(allPlayerNames, [name]);
|
||||
integer ch;
|
||||
if (channelIdx != -1) {
|
||||
ch = llList2Integer(allPlayerChannels, channelIdx);
|
||||
} else {
|
||||
// Fallback - this shouldn't happen if permanent mapping is working
|
||||
llOwnerSay("⚠️ [Floater Manager] WARNING: Player " + name + " not in permanent mapping");
|
||||
return;
|
||||
}
|
||||
key avKey = llList2Key(players, idx);
|
||||
integer ch = FLOATER_BASE_CHANNEL + idx;
|
||||
integer lifeCount = llList2Integer(lives, idx);
|
||||
|
||||
// Try to find player in current sync data (names/lives from Main Controller)
|
||||
// This works for both alive and eliminated players during the elimination sync
|
||||
integer syncIdx = llListFindList(names, [name]);
|
||||
key avKey = NULL_KEY;
|
||||
integer lifeCount = 0;
|
||||
|
||||
if (syncIdx != -1) {
|
||||
// Player found in current sync data
|
||||
if (syncIdx < llGetListLength(players)) {
|
||||
avKey = llList2Key(players, syncIdx);
|
||||
}
|
||||
if (syncIdx < llGetListLength(lives)) {
|
||||
lifeCount = llList2Integer(lives, syncIdx);
|
||||
}
|
||||
} else {
|
||||
// Player not in sync data - they might be eliminated
|
||||
if (VERBOSE_LOGGING) {
|
||||
llOwnerSay("🔍 [Floater Manager] Player " + name + " not in current sync - might be eliminated");
|
||||
}
|
||||
// Continue with lifeCount = 0 to show eliminated status
|
||||
}
|
||||
|
||||
if (VERBOSE_LOGGING) llOwnerSay("🔄 [Floater Manager] Updating floater for " + name + " (lives: " + (string)lifeCount + ")");
|
||||
list picks = getPicksFor(name);
|
||||
|
|
@ -303,50 +453,95 @@ default {
|
|||
}
|
||||
|
||||
string picksDisplay = llList2CSV(picks);
|
||||
string txt = "🎲 Peril Dice\n👤 " + name + "\nLives: " + (string)lifeCount + "\n" + perilName + "\n🔢 Picks: " + picksDisplay;
|
||||
|
||||
// Check if this player is the winner (only one player with lives > 0)
|
||||
integer livingPlayers = 0;
|
||||
integer isWinner = FALSE;
|
||||
for (i = 0; i < llGetListLength(lives); i++) {
|
||||
if (llList2Integer(lives, i) > 0) {
|
||||
livingPlayers++;
|
||||
}
|
||||
}
|
||||
|
||||
// If only one player has lives > 0 and this player has lives > 0, they're the winner
|
||||
if (livingPlayers == 1 && lifeCount > 0) {
|
||||
isWinner = TRUE;
|
||||
}
|
||||
|
||||
string txt;
|
||||
if (isWinner) {
|
||||
// Winner display with victory text (triggers green glow in PlayerStatus_Float.lsl)
|
||||
txt = "🎲 Peril Dice\n👤 " + name + "\n✨ ULTIMATE VICTORY! ✨\n🏆 ULTIMATE SURVIVOR 🏆\nLives: " + (string)lifeCount + "\n🔢 Final Picks: " + picksDisplay;
|
||||
} else {
|
||||
// Normal display
|
||||
txt = "🎲 Peril Dice\n👤 " + name + "\nLives: " + (string)lifeCount + "\n" + perilName + "\n🔢 Picks: " + picksDisplay;
|
||||
}
|
||||
|
||||
llRegionSay(ch, "FLOAT:" + (string)avKey + "|" + txt);
|
||||
}
|
||||
else if (num == MSG_CLEANUP_FLOAT) {
|
||||
integer ch = (integer)str;
|
||||
integer idx = ch - FLOATER_BASE_CHANNEL;
|
||||
|
||||
// Always send cleanup message to the channel (even for orphaned floaters)
|
||||
llRegionSay(ch, "CLEANUP");
|
||||
|
||||
// IMPROVED: Clean up internal lists more carefully to handle race conditions
|
||||
// Instead of relying on index calculation, find by channel and remove consistently
|
||||
if (idx >= 0 && idx < llGetListLength(players)) {
|
||||
// Store player info before removal for debugging
|
||||
string removedPlayer = "";
|
||||
if (idx < llGetListLength(names)) {
|
||||
removedPlayer = llList2String(names, idx);
|
||||
// FIXED: Find player by searching the permanent channel mapping
|
||||
string removedPlayer = "";
|
||||
integer foundIdx = -1;
|
||||
|
||||
// Search for the channel in our permanent mapping
|
||||
foundIdx = llListFindList(allPlayerChannels, [ch]);
|
||||
if (foundIdx != -1) {
|
||||
removedPlayer = llList2String(allPlayerNames, foundIdx);
|
||||
}
|
||||
|
||||
if (foundIdx != -1 && removedPlayer != "") {
|
||||
// Find the current index of this player in the active names list
|
||||
integer currentIdx = llListFindList(names, [removedPlayer]);
|
||||
llOwnerSay("🧹 [Floater Manager] Cleaning up floater for: " + removedPlayer + " (channel: " + (string)ch + ")");
|
||||
|
||||
// Remove from current game lists using the current index
|
||||
if (currentIdx != -1) {
|
||||
if (currentIdx < llGetListLength(players)) players = llDeleteSubList(players, currentIdx, currentIdx);
|
||||
if (currentIdx < llGetListLength(names)) names = llDeleteSubList(names, currentIdx, currentIdx);
|
||||
if (currentIdx < llGetListLength(lives)) lives = llDeleteSubList(lives, currentIdx, currentIdx);
|
||||
} else {
|
||||
llOwnerSay("⚠️ [Floater Manager] WARNING: " + removedPlayer + " not found in current names list for removal");
|
||||
}
|
||||
|
||||
// Remove from all lists at the same index to maintain synchronization
|
||||
if (idx < llGetListLength(players)) players = llDeleteSubList(players, idx, idx);
|
||||
if (idx < llGetListLength(names)) names = llDeleteSubList(names, idx, idx);
|
||||
if (idx < llGetListLength(lives)) lives = llDeleteSubList(lives, idx, idx);
|
||||
if (idx < llGetListLength(picksData)) picksData = llDeleteSubList(picksData, idx, idx);
|
||||
// IMPORTANT: Keep permanent mapping intact - never remove from playerNames/playerChannels
|
||||
// This preserves the original channel assignments
|
||||
|
||||
if (removedPlayer != "") {
|
||||
llOwnerSay("🧹 [Floater Manager] Cleaned up floater for: " + removedPlayer + " (channel: " + (string)ch + ")");
|
||||
|
||||
// CRITICAL: Update all remaining floaters to refresh peril status after player removal
|
||||
// This prevents stale peril player information from persisting in floaters
|
||||
llSleep(0.2); // Brief delay to ensure cleanup completes
|
||||
integer i;
|
||||
for (i = 0; i < llGetListLength(names); i++) {
|
||||
string remainingPlayerName = llList2String(names, i);
|
||||
key remainingPlayerKey = NULL_KEY;
|
||||
if (i < llGetListLength(players)) {
|
||||
remainingPlayerKey = llList2Key(players, i);
|
||||
}
|
||||
llMessageLinked(LINK_SET, MSG_UPDATE_FLOAT, remainingPlayerName, remainingPlayerKey);
|
||||
// Remove from picksData by searching for the player name
|
||||
integer pickIdx = -1;
|
||||
integer p;
|
||||
for (p = 0; p < llGetListLength(picksData); p++) {
|
||||
string entry = llList2String(picksData, p);
|
||||
if (llSubStringIndex(entry, removedPlayer + "|") == 0) {
|
||||
pickIdx = p;
|
||||
jump found_pick;
|
||||
}
|
||||
}
|
||||
@found_pick;
|
||||
if (pickIdx != -1) {
|
||||
picksData = llDeleteSubList(picksData, pickIdx, pickIdx);
|
||||
}
|
||||
|
||||
// CRITICAL: Update all remaining floaters to refresh peril status after player removal
|
||||
// This prevents stale peril player information from persisting in floaters
|
||||
llSleep(0.2); // Brief delay to ensure cleanup completes
|
||||
integer i;
|
||||
for (i = 0; i < llGetListLength(names); i++) {
|
||||
string remainingPlayerName = llList2String(names, i);
|
||||
key remainingPlayerKey = NULL_KEY;
|
||||
if (i < llGetListLength(players)) {
|
||||
remainingPlayerKey = llList2Key(players, i);
|
||||
}
|
||||
llMessageLinked(LINK_SET, MSG_UPDATE_FLOAT, remainingPlayerName, remainingPlayerKey);
|
||||
}
|
||||
} else {
|
||||
// Orphaned floater cleanup
|
||||
llOwnerSay("🧹 [Floater Manager] Cleaned up orphaned floater (channel: " + (string)ch + ")");
|
||||
// Orphaned floater cleanup - channel not found in our tracking
|
||||
llOwnerSay("🧹 [Floater Manager] Cleaned up orphaned floater (channel: " + (string)ch + ") - not in tracking list");
|
||||
}
|
||||
}
|
||||
else if (num == MSG_SYNC_GAME_STATE) {
|
||||
|
|
@ -393,7 +588,24 @@ default {
|
|||
}
|
||||
// IMPORTANT: Also sync the names list to prevent desynchronization
|
||||
string namesStr = llList2String(parts, 3);
|
||||
names = llCSV2List(namesStr);
|
||||
list newNames = llCSV2List(namesStr);
|
||||
|
||||
// CRITICAL: If names list changed size, rebuild permanent mapping if needed
|
||||
if (llGetListLength(allPlayerNames) == 0 && llGetListLength(newNames) > 0) {
|
||||
// Initial population of permanent mapping from sync
|
||||
allPlayerNames = newNames;
|
||||
allPlayerChannels = [];
|
||||
integer i;
|
||||
for (i = 0; i < llGetListLength(newNames); i++) {
|
||||
integer ch = FLOATER_BASE_CHANNEL + i;
|
||||
allPlayerChannels += [ch];
|
||||
}
|
||||
if (VERBOSE_LOGGING) {
|
||||
llOwnerSay("🔄 [Floater Manager] Built permanent mapping: " + llList2CSV(allPlayerNames) + " -> " + llList2CSV(allPlayerChannels));
|
||||
}
|
||||
}
|
||||
|
||||
names = newNames;
|
||||
|
||||
// Sync players list if available (5th part)
|
||||
if (llGetListLength(parts) >= 5) {
|
||||
|
|
@ -406,18 +618,16 @@ default {
|
|||
llOwnerSay("🎯 Floater Manager: Peril player updated from '" + oldPeril + "' to '" + perilPlayer + "'");
|
||||
}
|
||||
|
||||
// CRITICAL: After synchronizing the game state, ALWAYS update all existing floats
|
||||
// This ensures they reflect the current peril status and prevents desynchronization
|
||||
// Force update even if peril player didn't change - other state may have changed
|
||||
// CRITICAL: After synchronizing the game state, ALWAYS update ALL registered floaters
|
||||
// This includes both alive and eliminated players so eliminated floaters show proper status
|
||||
integer idx;
|
||||
for (idx = 0; idx < llGetListLength(names); idx++) {
|
||||
string n = llList2String(names, idx);
|
||||
// Use the player's key if it exists; fallback to NULL_KEY for test bots
|
||||
key k;
|
||||
if (idx < llGetListLength(players)) {
|
||||
k = llList2Key(players, idx);
|
||||
} else {
|
||||
k = NULL_KEY;
|
||||
for (idx = 0; idx < llGetListLength(allPlayerNames); idx++) {
|
||||
string n = llList2String(allPlayerNames, idx);
|
||||
// Find player key if they're still alive, otherwise use NULL_KEY
|
||||
key k = NULL_KEY;
|
||||
integer aliveIdx = llListFindList(names, [n]);
|
||||
if (aliveIdx != -1 && aliveIdx < llGetListLength(players)) {
|
||||
k = llList2Key(players, aliveIdx);
|
||||
}
|
||||
llMessageLinked(LINK_SET, MSG_UPDATE_FLOAT, n, k);
|
||||
}
|
||||
|
|
@ -427,19 +637,55 @@ default {
|
|||
if (oldPeril != perilPlayer && perilPlayer != "" && perilPlayer != "NONE") {
|
||||
llOwnerSay("🔄 [Floater Manager] Peril player changed - scheduling secondary update for stability");
|
||||
llSleep(0.3); // Brief delay to let initial updates complete
|
||||
for (idx = 0; idx < llGetListLength(names); idx++) {
|
||||
string n = llList2String(names, idx);
|
||||
key k;
|
||||
if (idx < llGetListLength(players)) {
|
||||
k = llList2Key(players, idx);
|
||||
} else {
|
||||
k = NULL_KEY;
|
||||
for (idx = 0; idx < llGetListLength(allPlayerNames); idx++) {
|
||||
string n = llList2String(allPlayerNames, idx);
|
||||
// Find player key if they're still alive, otherwise use NULL_KEY
|
||||
key k = NULL_KEY;
|
||||
integer aliveIdx = llListFindList(names, [n]);
|
||||
if (aliveIdx != -1 && aliveIdx < llGetListLength(players)) {
|
||||
k = llList2Key(players, aliveIdx);
|
||||
}
|
||||
llMessageLinked(LINK_SET, MSG_UPDATE_FLOAT, n, k);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle aggressive floater cleanup requests
|
||||
else if (num == MSG_CLEANUP_ALL_FLOATERS) {
|
||||
string context = str; // "RESET" for conservative, empty for aggressive
|
||||
|
||||
if (context == "RESET") {
|
||||
// Conservative cleanup during reset - only clean known floaters using permanent mapping
|
||||
if (llGetListLength(allPlayerChannels) > 0) {
|
||||
llOwnerSay("🧹 [Floater Manager] Cleaning up floaters for " + (string)llGetListLength(allPlayerChannels) + " registered channels...");
|
||||
integer i;
|
||||
for (i = 0; i < llGetListLength(allPlayerChannels); i++) {
|
||||
integer ch = llList2Integer(allPlayerChannels, i);
|
||||
llRegionSay(ch, "CLEANUP");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Aggressive cleanup for troubleshooting - scan all possible channels
|
||||
llOwnerSay("🧽 [Floater Manager] Performing aggressive floater cleanup...");
|
||||
integer i;
|
||||
for (i = 0; i < MAX_PLAYERS; i++) {
|
||||
integer ch = FLOATER_BASE_CHANNEL + i;
|
||||
llRegionSay(ch, "CLEANUP");
|
||||
llRegionSay(ch + 100, "CLEANUP");
|
||||
llRegionSay(ch + 1000, "CLEANUP");
|
||||
}
|
||||
llOwnerSay("✅ [Floater Manager] Aggressive cleanup complete");
|
||||
}
|
||||
|
||||
// Clear our internal tracking
|
||||
players = [];
|
||||
names = [];
|
||||
lives = [];
|
||||
picksData = [];
|
||||
perilPlayer = "";
|
||||
playerNames = [];
|
||||
playerChannels = [];
|
||||
}
|
||||
// Handle debug control messages
|
||||
else if (num == MSG_DEBUG_PICKS_ON) {
|
||||
DEBUG_PICKS = TRUE;
|
||||
|
|
|
|||
|
|
@ -77,6 +77,9 @@ Three possible outcomes based on the dice roll:
|
|||
|
||||
### **6️⃣ Victory**
|
||||
- **Last player standing wins!** 🏆
|
||||
- **Winner Glow Celebration** (NEW in v2.8.6): Winner gets bright green glow on both scoreboard and floater 💚
|
||||
- **Extended Victory Display**: 24-second celebration timer before game reset ⏱️
|
||||
- **Victory Text**: Winner's floater displays "✨ ULTIMATE VICTORY! ✨" and "🏆 ULTIMATE SURVIVOR 🏆"
|
||||
- Victory celebration with confetti effects! ✨
|
||||
- Winner's name recorded on the leaderboard
|
||||
|
||||
|
|
@ -169,18 +172,23 @@ The game features thematic messaging for maximum immersion:
|
|||
- **Kick Player** functionality allows owners to remove disruptive players
|
||||
- **Leave Game** system properly cleans up all player data
|
||||
|
||||
### **Enhanced Display System (NEW in v2.8.1):**
|
||||
### **Enhanced Display System (UPDATED in v2.8.6):**
|
||||
- **Winner Glow Celebration** - Winners get bright green glow on both scoreboard and floaters 💚
|
||||
- **Visual Priority System** - Winner glow (green) overrides peril glow (yellow) during victory
|
||||
- **24-Second Victory Timer** - Extended celebration before game reset for proper recognition
|
||||
- **Real-Time Status Updates** - Peril status updates immediately on floating displays
|
||||
- **Enhanced Elimination** - Players see 0 hearts ❤️ → 💔💔💔 before removal
|
||||
- **Enhanced Elimination** - Players see 0 hearts ❤️ → 💔💔💔 before removal with red X markers ❌
|
||||
- **Visual Scoreboard** - Shows all players with profile pictures and heart counts
|
||||
- **Floating HUD Displays** - Personal status displays for each player
|
||||
- **Professional Status Actions** - Large status prim shows current game events
|
||||
- **Color-Coded Glow System** - Green (winner), Yellow (peril), Red X (eliminated)
|
||||
|
||||
### **Technical Rules:**
|
||||
- Game supports up to **10 players maximum**
|
||||
- **2-minute timeout** on dialog responses (improved for faster gameplay)
|
||||
- **Owner kick functionality** for managing disruptive players
|
||||
- **Automatic game reset** on startup (preserves leaderboard)
|
||||
- **Single linkset architecture** (74 prims total) for reliable operation
|
||||
- **Professional winner celebration** with 24-second recognition timer
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -77,6 +77,11 @@ NO SHIELD!
|
|||
|
||||
6. Victory
|
||||
- Last player standing wins!
|
||||
- Winner Glow Celebration (NEW in v2.8.6): Winner gets bright green glow
|
||||
on both scoreboard and floater
|
||||
- Extended Victory Display: 24-second celebration timer before game reset
|
||||
- Victory Text: Winner's floater displays "ULTIMATE VICTORY!" and
|
||||
"ULTIMATE SURVIVOR"
|
||||
- Victory celebration with confetti effects!
|
||||
- Winner's name recorded on the leaderboard
|
||||
|
||||
|
|
@ -169,18 +174,26 @@ Game Security (NEW in v2.8.0):
|
|||
- Kick Player functionality allows owners to remove disruptive players
|
||||
- Leave Game system properly cleans up all player data
|
||||
|
||||
Enhanced Display System (NEW in v2.8.1):
|
||||
Enhanced Display System (UPDATED in v2.8.6):
|
||||
- Winner Glow Celebration - Winners get bright green glow on both
|
||||
scoreboard and floaters
|
||||
- Visual Priority System - Winner glow (green) overrides peril glow (yellow)
|
||||
during victory
|
||||
- 24-Second Victory Timer - Extended celebration before game reset for
|
||||
proper recognition
|
||||
- Real-Time Status Updates - Peril status updates immediately on displays
|
||||
- Enhanced Elimination - Players see 0 hearts before removal
|
||||
- Enhanced Elimination - Players see 0 hearts before removal with red X markers
|
||||
- Visual Scoreboard - Shows all players with profile pictures and hearts
|
||||
- Floating HUD Displays - Personal status displays for each player
|
||||
- Professional Status Actions - Large status prim shows current events
|
||||
- Color-Coded Glow System - Green (winner), Yellow (peril), Red X (eliminated)
|
||||
|
||||
Technical Rules:
|
||||
- Game supports up to 10 players maximum
|
||||
- 2-minute timeout on dialog responses (improved for faster gameplay)
|
||||
- Owner kick functionality for managing disruptive players
|
||||
- Automatic game reset on startup (preserves leaderboard)
|
||||
- Single linkset architecture (74 prims total) for reliable operation
|
||||
- Professional winner celebration with 24-second recognition timer
|
||||
|
||||
===============================================================================
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,19 @@
|
|||
integer VERBOSE_LOGGING = TRUE; // Global flag for verbose debug logs
|
||||
integer MSG_TOGGLE_VERBOSE_LOGS = 9998; // Message to toggle verbose logging
|
||||
|
||||
// Memory reporting function
|
||||
reportMemoryUsage(string scriptName) {
|
||||
integer used = llGetUsedMemory();
|
||||
integer free = llGetFreeMemory();
|
||||
integer total = used + free;
|
||||
float percentUsed = ((float)used / (float)total) * 100.0;
|
||||
|
||||
llOwnerSay("🧠 [" + scriptName + "] Memory: " +
|
||||
(string)used + " used, " +
|
||||
(string)free + " free (" +
|
||||
llGetSubString((string)percentUsed, 0, 4) + "% used)");
|
||||
}
|
||||
|
||||
integer MSG_GET_DICE_TYPE = 1001;
|
||||
integer MSG_DICE_TYPE_RESULT = 1005;
|
||||
integer MSG_GET_PICKS_REQUIRED = 1002;
|
||||
|
|
@ -130,6 +143,8 @@ integer showPickManager(string player, key id) {
|
|||
|
||||
default {
|
||||
state_entry() {
|
||||
reportMemoryUsage("🧮 Game Calculator");
|
||||
|
||||
// Initialize/reset all state variables
|
||||
lives = [];
|
||||
picksData = [];
|
||||
|
|
@ -140,6 +155,8 @@ default {
|
|||
}
|
||||
|
||||
on_rez(integer start_param) {
|
||||
reportMemoryUsage("🧮 Game Calculator");
|
||||
|
||||
if (VERBOSE_LOGGING) {
|
||||
llOwnerSay("🔄 Game Calculator rezzed - reinitializing...");
|
||||
}
|
||||
|
|
|
|||
129
Game_Manager.lsl
129
Game_Manager.lsl
|
|
@ -38,6 +38,46 @@ string lastHumanPickMessage = "";
|
|||
string lastBotPickMessage = "";
|
||||
integer lastProcessTime = 0;
|
||||
|
||||
// Build complete avoidance list from all current picks
|
||||
list buildCompleteAvoidanceList() {
|
||||
list allPicks = [];
|
||||
integer i;
|
||||
|
||||
// Add all picks from picksData (current round)
|
||||
for (i = 0; i < llGetListLength(picksData); i++) {
|
||||
string entry = llList2String(picksData, i);
|
||||
list entryParts = llParseString2List(entry, ["|"], []);
|
||||
if (llGetListLength(entryParts) >= 2) {
|
||||
string picks = llList2String(entryParts, 1);
|
||||
if (picks != "") {
|
||||
// Split multiple picks (comma or semicolon separated)
|
||||
list playerPicks = llParseString2List(picks, [","], []);
|
||||
if (llGetListLength(playerPicks) == 1) {
|
||||
// Also try semicolon separator
|
||||
playerPicks = llParseString2List(picks, [";"], []);
|
||||
}
|
||||
integer j;
|
||||
for (j = 0; j < llGetListLength(playerPicks); j++) {
|
||||
string pick = llStringTrim(llList2String(playerPicks, j), STRING_TRIM);
|
||||
if (pick != "" && llListFindList(allPicks, [pick]) == -1) {
|
||||
allPicks += [pick];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also add any picks from globalPickedNumbers as backup
|
||||
for (i = 0; i < llGetListLength(globalPickedNumbers); i++) {
|
||||
string pick = llList2String(globalPickedNumbers, i);
|
||||
if (pick != "" && llListFindList(allPicks, [pick]) == -1) {
|
||||
allPicks += [pick];
|
||||
}
|
||||
}
|
||||
|
||||
return allPicks;
|
||||
}
|
||||
|
||||
// Memory reporting function
|
||||
reportMemoryUsage(string scriptName) {
|
||||
integer used = llGetUsedMemory();
|
||||
|
|
@ -64,6 +104,7 @@ integer MSG_SYNC_PICKQUEUE = 2001;
|
|||
integer MSG_PLAYER_WON = 551;
|
||||
integer MSG_UPDATE_FLOAT = 2010;
|
||||
integer MSG_DIALOG_FORWARD_REQUEST = 9060; // Request dialog forwarding from Player_RegistrationManager
|
||||
integer MSG_CONTINUE_ROUND = 998; // Continue round after roll processing
|
||||
|
||||
continueCurrentRound() {
|
||||
// MEMORY OPTIMIZED: Skip complex validation - trust game state
|
||||
|
|
@ -147,6 +188,10 @@ startNextRound() {
|
|||
if (llGetListLength(names) == 1) {
|
||||
string winner = llList2String(names, 0);
|
||||
llSay(0, "✨ ULTIMATE VICTORY! " + winner + " is the Ultimate Survivor!");
|
||||
|
||||
// Send winner glow update to scoreboard
|
||||
llMessageLinked(12, 3006, winner, NULL_KEY); // MSG_UPDATE_WINNER
|
||||
|
||||
llMessageLinked(LINK_SET, MSG_PLAYER_WON, winner, NULL_KEY);
|
||||
llMessageLinked(LINK_SET, 995, "VICTORY_CONFETTI", NULL_KEY);
|
||||
// Don't use hardcoded channel here - let Main Controller handle scoreboard updates
|
||||
|
|
@ -167,6 +212,9 @@ startNextRound() {
|
|||
perilPlayer = llList2String(names, randomIdx);
|
||||
llSay(0, "🎯 " + perilPlayer + " has been randomly selected and is now in peril!");
|
||||
|
||||
// Send peril player update to scoreboard for glow effect
|
||||
llMessageLinked(12, 3005, perilPlayer, NULL_KEY); // MSG_UPDATE_PERIL_PLAYER
|
||||
|
||||
// Sync state to floater manager first, then update floaters to show new peril player immediately
|
||||
llOwnerSay("🔄 [Game Manager] Syncing state and updating floaters for new peril player: " + perilPlayer);
|
||||
syncStateToMain(); // Sync the peril player to all modules first
|
||||
|
|
@ -303,8 +351,10 @@ showNextPickerDialog() {
|
|||
integer picksNeeded = 4 - perilLives;
|
||||
|
||||
// Send proper avoid list to bots so they don't pick human numbers
|
||||
string globalPicksStr = llList2CSV(globalPickedNumbers);
|
||||
string botCommand = "BOT_PICK:" + firstName + ":" + (string)picksNeeded + ":" + (string)diceType + ":" + globalPicksStr;
|
||||
list completeAvoidList = buildCompleteAvoidanceList();
|
||||
string avoidListStr = llList2CSV(completeAvoidList);
|
||||
string botCommand = "BOT_PICK:" + firstName + ":" + (string)picksNeeded + ":" + (string)diceType + ":" + avoidListStr;
|
||||
llOwnerSay("🎯 [Game Manager] Sending bot command with complete avoid list (" + (string)llGetListLength(completeAvoidList) + " numbers): " + avoidListStr);
|
||||
llMessageLinked(LINK_SET, -9999, botCommand, NULL_KEY);
|
||||
llOwnerSay("🤖 " + firstName + " is automatically picking " + (string)picksNeeded + " numbers...");
|
||||
} else {
|
||||
|
|
@ -327,9 +377,10 @@ showNextPickerDialog() {
|
|||
}
|
||||
integer picksNeeded = 4 - perilLives;
|
||||
|
||||
// Send dialog payload with current globally picked numbers
|
||||
string globalPicksStr = llList2CSV(globalPickedNumbers);
|
||||
string dialogPayload = firstName + "|" + (string)diceType + "|" + (string)picksNeeded + "|" + globalPicksStr;
|
||||
// Send dialog payload with complete avoid list for humans too
|
||||
list completeAvoidList = buildCompleteAvoidanceList();
|
||||
string avoidListStr = llList2CSV(completeAvoidList);
|
||||
string dialogPayload = firstName + "|" + (string)diceType + "|" + (string)picksNeeded + "|" + avoidListStr;
|
||||
|
||||
llOwnerSay("🎯 Showing pick dialog for " + firstName);
|
||||
llSleep(0.5); // Brief delay to prevent spam
|
||||
|
|
@ -617,22 +668,7 @@ default {
|
|||
return;
|
||||
}
|
||||
|
||||
// Handle start round requests
|
||||
if (num == 997) {
|
||||
if (str == "START_NEXT_ROUND") {
|
||||
startNextRound();
|
||||
}
|
||||
else if (llSubStringIndex(str, "CONTINUE_ROUND|") == 0) {
|
||||
string newPeril = llGetSubString(str, 15, -1);
|
||||
perilPlayer = newPeril;
|
||||
llOwnerSay("🎯 Game Manager received CONTINUE_ROUND for " + newPeril);
|
||||
// Reset round state for new round
|
||||
roundStarted = FALSE;
|
||||
currentPickerIdx = 0;
|
||||
continueCurrentRound();
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Legacy 997 message handler removed - now using direct MSG_CONTINUE_ROUND (998) communication
|
||||
|
||||
// Handle human picks
|
||||
if (num == -9998 && llSubStringIndex(str, "HUMAN_PICKED:") == 0) {
|
||||
|
|
@ -661,16 +697,19 @@ default {
|
|||
return; // Ignore duplicate human picks
|
||||
}
|
||||
|
||||
// Validate picks against global picked numbers
|
||||
// Validate picks against complete avoidance list (all current picks from all players)
|
||||
list completeAvoidList = buildCompleteAvoidanceList();
|
||||
list newPicks = llParseString2List(picksStr, [","], []);
|
||||
list validPicks = [];
|
||||
list duplicatePicks = [];
|
||||
integer i;
|
||||
|
||||
llOwnerSay("🔍 [Game Manager] Validating " + playerName + "'s picks against complete avoid list (" + (string)llGetListLength(completeAvoidList) + " numbers): " + llList2CSV(completeAvoidList));
|
||||
|
||||
for (i = 0; i < llGetListLength(newPicks); i++) {
|
||||
string pick = llStringTrim(llList2String(newPicks, i), STRING_TRIM);
|
||||
if (pick != "") {
|
||||
if (llListFindList(globalPickedNumbers, [pick]) == -1) {
|
||||
if (llListFindList(completeAvoidList, [pick]) == -1) {
|
||||
validPicks += [pick];
|
||||
globalPickedNumbers += [pick];
|
||||
} else {
|
||||
|
|
@ -705,7 +744,9 @@ default {
|
|||
}
|
||||
integer picksNeeded = 4 - perilLives;
|
||||
|
||||
string dialogPayload = playerName + "|" + (string)diceType + "|" + (string)picksNeeded + "|" + llList2CSV(globalPickedNumbers);
|
||||
// Use complete avoid list for re-shown dialog too
|
||||
list updatedCompleteAvoidList = buildCompleteAvoidanceList();
|
||||
string dialogPayload = playerName + "|" + (string)diceType + "|" + (string)picksNeeded + "|" + llList2CSV(updatedCompleteAvoidList);
|
||||
string dialogRequest = "SHOW_DIALOG|" + playerName + "|" + dialogPayload;
|
||||
llMessageLinked(LINK_SET, MSG_DIALOG_FORWARD_REQUEST, dialogRequest, NULL_KEY);
|
||||
}
|
||||
|
|
@ -849,8 +890,11 @@ default {
|
|||
}
|
||||
integer picksNeeded = 4 - perilLives;
|
||||
|
||||
// Send new bot command with updated avoid list
|
||||
string botCommand = "BOT_PICK:" + playerName + ":" + (string)picksNeeded + ":" + (string)diceType + ":" + llList2CSV(globalPickedNumbers);
|
||||
// Send new bot command with complete updated avoid list
|
||||
list completeAvoidList = buildCompleteAvoidanceList();
|
||||
string avoidListStr = llList2CSV(completeAvoidList);
|
||||
string botCommand = "BOT_PICK:" + playerName + ":" + (string)picksNeeded + ":" + (string)diceType + ":" + avoidListStr;
|
||||
llOwnerSay("🎯 [Game Manager] Sending retry bot command with complete avoid list (" + (string)llGetListLength(completeAvoidList) + " numbers): " + avoidListStr);
|
||||
llMessageLinked(LINK_SET, -9999, botCommand, NULL_KEY);
|
||||
return; // Don't save this pick
|
||||
}
|
||||
|
|
@ -946,6 +990,39 @@ default {
|
|||
return;
|
||||
}
|
||||
|
||||
// Handle continue round requests from Main Controller
|
||||
if (num == MSG_CONTINUE_ROUND) {
|
||||
llOwnerSay("🎯 [Game Manager] Received continue round request with peril player: '" + str + "'");
|
||||
|
||||
// Update peril player from the continue message if provided
|
||||
if (str != "" && str != "NONE") {
|
||||
perilPlayer = str;
|
||||
llOwnerSay("🎯 [Game Manager] Updated peril player to: " + perilPlayer);
|
||||
|
||||
// Reset round state and continue with existing peril player
|
||||
roundStarted = FALSE;
|
||||
currentPickerIdx = 0;
|
||||
diceTypeProcessed = FALSE;
|
||||
roundContinueInProgress = FALSE;
|
||||
|
||||
// Continue current round with assigned peril player
|
||||
continueCurrentRound();
|
||||
} else {
|
||||
// Empty peril player means this is initial game start - use startNextRound()
|
||||
llOwnerSay("🎯 [Game Manager] Empty peril player - starting initial game round");
|
||||
|
||||
// Reset round state
|
||||
roundStarted = FALSE;
|
||||
currentPickerIdx = 0;
|
||||
diceTypeProcessed = FALSE;
|
||||
roundContinueInProgress = FALSE;
|
||||
|
||||
// Start initial round (will select random peril player)
|
||||
startNextRound();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle verbose logging toggle from Main Controller
|
||||
if (num == 9011 && llSubStringIndex(str, "VERBOSE_LOGGING|") == 0) {
|
||||
list parts = llParseString2List(str, ["|"], []);
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ integer MSG_GAME_STATUS = 3001;
|
|||
integer MSG_PLAYER_UPDATE = 3002;
|
||||
integer MSG_CLEAR_GAME = 3003;
|
||||
integer MSG_REMOVE_PLAYER = 3004;
|
||||
integer MSG_UPDATE_PERIL_PLAYER = 3005;
|
||||
integer MSG_UPDATE_WINNER = 3006;
|
||||
|
||||
// Leaderboard messages (to link 35)
|
||||
integer MSG_GAME_WON = 3010;
|
||||
|
|
@ -63,6 +65,10 @@ list playerNames = []; // Current game players for display
|
|||
list playerLives = []; // Current game player lives
|
||||
list playerProfiles = []; // Maps to profile texture UUIDs
|
||||
|
||||
// Peril player tracking
|
||||
string currentPerilPlayer = ""; // Track current peril player for glow effects
|
||||
string currentWinner = ""; // Track current winner for green glow effects
|
||||
|
||||
// Leaderboard data (persistent - stored in linkset data)
|
||||
list leaderboardNames = []; // Loaded from linkset data at startup
|
||||
list leaderboardWins = []; // Loaded from linkset data at startup
|
||||
|
|
@ -382,18 +388,20 @@ clearAllPlayers() {
|
|||
return; // Don't modify prims outside our range!
|
||||
}
|
||||
|
||||
// Reset profile prim to default
|
||||
// Reset profile prim to default and clear any glow
|
||||
llSetLinkPrimitiveParamsFast(profilePrimIndex, [
|
||||
PRIM_TEXTURE, ALL_SIDES, TEXTURE_DEFAULT_PROFILE, <1,1,0>, <0,0,0>, 0.0,
|
||||
PRIM_COLOR, ALL_SIDES, <1.0, 1.0, 1.0>, 1.0,
|
||||
PRIM_TEXT, "", <1,1,1>, 0.0
|
||||
PRIM_TEXT, "", <1,1,1>, 0.0,
|
||||
PRIM_GLOW, ALL_SIDES, 0.0 // Explicitly clear glow
|
||||
]);
|
||||
|
||||
// Reset hearts prim to 3 hearts
|
||||
// Reset hearts prim to 3 hearts and clear any glow
|
||||
llSetLinkPrimitiveParamsFast(heartsPrimIndex, [
|
||||
PRIM_TEXTURE, ALL_SIDES, TEXTURE_3_HEARTS, <1,1,0>, <0,0,0>, 0.0,
|
||||
PRIM_COLOR, ALL_SIDES, <1.0, 1.0, 1.0>, 1.0,
|
||||
PRIM_TEXT, "", <1,1,1>, 0.0
|
||||
PRIM_TEXT, "", <1,1,1>, 0.0,
|
||||
PRIM_GLOW, ALL_SIDES, 0.0 // Explicitly clear glow
|
||||
]);
|
||||
|
||||
// Reset overlay prim to transparent
|
||||
|
|
@ -521,6 +529,9 @@ refreshPlayerDisplay() {
|
|||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Update player glow effects after refreshing all players
|
||||
updatePlayerGlowEffects();
|
||||
}
|
||||
|
||||
// Check if a player name indicates it's a bot
|
||||
|
|
@ -597,6 +608,84 @@ string getHeartTexture(integer lives) {
|
|||
else return TEXTURE_3_HEARTS; // 3 or more
|
||||
}
|
||||
|
||||
// Update all glow effects (peril and winner)
|
||||
updatePlayerGlowEffects() {
|
||||
// First, remove glow and reset tint from all players
|
||||
integer i;
|
||||
for (i = 0; i < llGetListLength(playerNames); i++) {
|
||||
integer profilePrimIndex = getProfilePrimLink(i);
|
||||
integer heartsPrimIndex = getHeartsPrimLink(i);
|
||||
|
||||
// SAFETY CHECK
|
||||
if (profilePrimIndex >= FIRST_PROFILE_PRIM && profilePrimIndex <= LAST_PROFILE_PRIM) {
|
||||
llSetLinkPrimitiveParamsFast(profilePrimIndex, [
|
||||
PRIM_GLOW, ALL_SIDES, 0.0,
|
||||
PRIM_COLOR, ALL_SIDES, <1.0, 1.0, 1.0>, 1.0
|
||||
]);
|
||||
llSetLinkPrimitiveParamsFast(heartsPrimIndex, [
|
||||
PRIM_GLOW, ALL_SIDES, 0.0,
|
||||
PRIM_COLOR, ALL_SIDES, <1.0, 1.0, 1.0>, 1.0
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Priority 1: Green glow for winner (overrides peril glow)
|
||||
if (currentWinner != "" && currentWinner != "NONE") {
|
||||
integer winnerIndex = llListFindList(playerNames, [currentWinner]);
|
||||
if (winnerIndex != -1) {
|
||||
integer profilePrimIndex = getProfilePrimLink(winnerIndex);
|
||||
integer heartsPrimIndex = getHeartsPrimLink(winnerIndex);
|
||||
|
||||
// SAFETY CHECK
|
||||
if (profilePrimIndex >= FIRST_PROFILE_PRIM && profilePrimIndex <= LAST_PROFILE_PRIM) {
|
||||
// Add green glow + green tint to winner
|
||||
llSetLinkPrimitiveParamsFast(profilePrimIndex, [
|
||||
PRIM_GLOW, ALL_SIDES, 0.3,
|
||||
PRIM_COLOR, ALL_SIDES, <0.0, 1.0, 0.0>, 1.0
|
||||
]);
|
||||
llSetLinkPrimitiveParamsFast(heartsPrimIndex, [
|
||||
PRIM_GLOW, ALL_SIDES, 0.3,
|
||||
PRIM_COLOR, ALL_SIDES, <0.0, 1.0, 0.0>, 1.0
|
||||
]);
|
||||
|
||||
if (VERBOSE_LOGGING) {
|
||||
llOwnerSay("🏆 Added green glow to winner: " + currentWinner);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Priority 2: Yellow glow for peril player (only if not the winner)
|
||||
else if (currentPerilPlayer != "" && currentPerilPlayer != "NONE") {
|
||||
integer perilIndex = llListFindList(playerNames, [currentPerilPlayer]);
|
||||
if (perilIndex != -1) {
|
||||
integer profilePrimIndex = getProfilePrimLink(perilIndex);
|
||||
integer heartsPrimIndex = getHeartsPrimLink(perilIndex);
|
||||
|
||||
// SAFETY CHECK
|
||||
if (profilePrimIndex >= FIRST_PROFILE_PRIM && profilePrimIndex <= LAST_PROFILE_PRIM) {
|
||||
// Add yellow glow + yellow tint to peril player
|
||||
llSetLinkPrimitiveParamsFast(profilePrimIndex, [
|
||||
PRIM_GLOW, ALL_SIDES, 0.2,
|
||||
PRIM_COLOR, ALL_SIDES, <1.0, 1.0, 0.0>, 1.0
|
||||
]);
|
||||
llSetLinkPrimitiveParamsFast(heartsPrimIndex, [
|
||||
PRIM_GLOW, ALL_SIDES, 0.2,
|
||||
PRIM_COLOR, ALL_SIDES, <1.0, 1.0, 0.0>, 1.0
|
||||
]);
|
||||
|
||||
if (VERBOSE_LOGGING) {
|
||||
llOwnerSay("✨ Added yellow glow to peril player: " + currentPerilPlayer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Backward compatibility wrapper
|
||||
updatePerilPlayerGlow() {
|
||||
updatePlayerGlowEffects();
|
||||
}
|
||||
|
||||
// Update player display on the grid
|
||||
updatePlayerDisplay(string playerName, integer lives, string profileUUID) {
|
||||
// Find existing player or add new one
|
||||
|
|
@ -791,6 +880,9 @@ updatePlayerDisplay(string playerName, integer lives, string profileUUID) {
|
|||
PRIM_TEXT, "", <1,1,1>, 0.0
|
||||
]);
|
||||
}
|
||||
|
||||
// Update player glow effects after display changes
|
||||
updatePlayerGlowEffects();
|
||||
}
|
||||
|
||||
default {
|
||||
|
|
@ -817,6 +909,11 @@ default {
|
|||
// Generate initial leaderboard
|
||||
generateLeaderboardText();
|
||||
|
||||
// Ensure no glow effects remain from previous sessions
|
||||
currentPerilPlayer = "";
|
||||
currentWinner = "";
|
||||
updatePlayerGlowEffects();
|
||||
|
||||
if (VERBOSE_LOGGING) {
|
||||
llOwnerSay("✅ Linkset communication active - no discovery needed!");
|
||||
llOwnerSay("📊 Loaded " + (string)llGetListLength(leaderboardNames) + " players from leaderboard data");
|
||||
|
|
@ -863,6 +960,11 @@ default {
|
|||
clearAllPlayers(); // Clear all current players
|
||||
updateActionsPrim("Title"); // Reset to title
|
||||
|
||||
// Clear peril player and winner tracking and remove any remaining glow
|
||||
currentPerilPlayer = "";
|
||||
currentWinner = "";
|
||||
updatePlayerGlowEffects(); // This will remove glow from all players
|
||||
|
||||
generateLeaderboardText(); // Refresh leaderboard display
|
||||
}
|
||||
else if (num == MSG_GAME_WON) {
|
||||
|
|
@ -877,6 +979,36 @@ default {
|
|||
else if (num == MSG_REMOVE_PLAYER) {
|
||||
removePlayer(str);
|
||||
}
|
||||
else if (num == MSG_UPDATE_PERIL_PLAYER) {
|
||||
// Update peril player and refresh glow effects
|
||||
string oldPeril = currentPerilPlayer;
|
||||
currentPerilPlayer = str;
|
||||
if (currentPerilPlayer == "NONE") {
|
||||
currentPerilPlayer = "";
|
||||
}
|
||||
|
||||
if (VERBOSE_LOGGING && oldPeril != currentPerilPlayer) {
|
||||
llOwnerSay("🎯 Peril player changed from '" + oldPeril + "' to '" + currentPerilPlayer + "'");
|
||||
}
|
||||
|
||||
// Update glow effects
|
||||
updatePlayerGlowEffects();
|
||||
}
|
||||
else if (num == MSG_UPDATE_WINNER) {
|
||||
// Update winner and refresh glow effects
|
||||
string oldWinner = currentWinner;
|
||||
currentWinner = str;
|
||||
if (currentWinner == "NONE") {
|
||||
currentWinner = "";
|
||||
}
|
||||
|
||||
if (VERBOSE_LOGGING && oldWinner != currentWinner) {
|
||||
llOwnerSay("🏆 Winner changed from '" + oldWinner + "' to '" + currentWinner + "'");
|
||||
}
|
||||
|
||||
// Update glow effects - winner glow overrides peril glow
|
||||
updatePlayerGlowEffects();
|
||||
}
|
||||
}
|
||||
|
||||
http_response(key request_id, integer status, list metadata, string body) {
|
||||
|
|
|
|||
|
|
@ -1,69 +0,0 @@
|
|||
# EMERGENCY FIX: Infinite Loop Resolution
|
||||
|
||||
## Problem
|
||||
The delegation system created an infinite loop between Main_Controller_Linkset.lsl and Player_RegistrationManager.lsl:
|
||||
|
||||
1. Main Controller's `touch_start` sends `MSG_REGISTER_PLAYER`
|
||||
2. Player_RegistrationManager receives `MSG_REGISTER_PLAYER` and processes it
|
||||
3. BUT Main Controller's `link_message` handler ALSO receives `MSG_REGISTER_PLAYER`
|
||||
4. Main Controller forwards it AGAIN to Player_RegistrationManager
|
||||
5. INFINITE LOOP 💀
|
||||
|
||||
## Root Cause
|
||||
Both scripts were using the same message constant `MSG_REGISTER_PLAYER = 106`, causing message collision.
|
||||
|
||||
## Solution Applied
|
||||
|
||||
### 1. New Dedicated Message Constant
|
||||
```lsl
|
||||
integer MSG_REGISTER_PLAYER_REQUEST = 9050; // Dedicated message to Player_RegistrationManager
|
||||
```
|
||||
|
||||
### 2. Updated Message Flow
|
||||
- **Main Controller `touch_start`**: Sends `MSG_REGISTER_PLAYER_REQUEST` (new)
|
||||
- **Main Controller `link_message`**: Converts old `MSG_REGISTER_PLAYER` to `MSG_REGISTER_PLAYER_REQUEST` for backward compatibility
|
||||
- **Player_RegistrationManager**: Only listens for `MSG_REGISTER_PLAYER_REQUEST`
|
||||
|
||||
### 3. Files Modified
|
||||
|
||||
#### Main_Controller_Linkset.lsl
|
||||
- **Added**: `MSG_REGISTER_PLAYER_REQUEST = 9050`
|
||||
- **Changed**: `touch_start` uses `MSG_REGISTER_PLAYER_REQUEST` instead of `MSG_REGISTER_PLAYER`
|
||||
- **Changed**: `link_message` converts old messages to new format
|
||||
- **Changed**: "Add Test Player" admin function uses new message
|
||||
|
||||
#### Player_RegistrationManager.lsl
|
||||
- **Changed**: `MSG_REGISTER_PLAYER = 106` → `MSG_REGISTER_PLAYER_REQUEST = 9050`
|
||||
- **Changed**: `link_message` handler listens for `MSG_REGISTER_PLAYER_REQUEST`
|
||||
|
||||
## Message Flow Diagram
|
||||
|
||||
### BEFORE (Infinite Loop)
|
||||
```
|
||||
Touch → Main Controller → MSG_REGISTER_PLAYER → Player_RegistrationManager
|
||||
↓
|
||||
link_message handler
|
||||
↓
|
||||
MSG_REGISTER_PLAYER → Player_RegistrationManager (LOOP!)
|
||||
```
|
||||
|
||||
### AFTER (Fixed)
|
||||
```
|
||||
Touch → Main Controller → MSG_REGISTER_PLAYER_REQUEST → Player_RegistrationManager
|
||||
↓
|
||||
link_message handler (converts old messages only)
|
||||
↓
|
||||
MSG_REGISTER_PLAYER → MSG_REGISTER_PLAYER_REQUEST → Player_RegistrationManager
|
||||
```
|
||||
|
||||
## Backward Compatibility
|
||||
The Main Controller still accepts old `MSG_REGISTER_PLAYER` messages from other scripts and converts them to the new format, ensuring no functionality is lost.
|
||||
|
||||
## Testing
|
||||
- ✅ Single player registration should work without loops
|
||||
- ✅ Bot addition via admin menu should work
|
||||
- ✅ Multiple rapid registrations should not cause loops
|
||||
- ✅ Existing functionality preserved
|
||||
|
||||
## Status: EMERGENCY FIX APPLIED ⚡
|
||||
The infinite loop has been eliminated while preserving all functionality and maintaining the memory optimization benefits.
|
||||
|
|
@ -2,18 +2,20 @@ PERIL DICE - Professional Single Linkset Game System for Second Life
|
|||
|
||||
Created by Rebecca Nod and Noose the Bunny
|
||||
|
||||
CURRENT VERSION: 2.8.2 - Scoreboard Spam & Display Fixes
|
||||
CURRENT VERSION: 2.8.6 - Winner Glow Celebration System
|
||||
|
||||
Thrilling multiplayer elimination game for 2-10 players! Each participant selects numbers before a die is rolled. If the peril player's number comes up, they lose a life. Last player standing wins!
|
||||
|
||||
NEW IN v2.8.2: Fixed critical scoreboard spam bug, re-fixed peril status display on floating HUDs, enhanced elimination sequence to show 0 hearts before player removal.
|
||||
NEW IN v2.8.6: Revolutionary winner recognition with bright green glow effects on both scoreboard and floaters, plus 24-second victory celebration timer giving proper recognition to the Ultimate Survivor before game reset.
|
||||
|
||||
KEY FEATURES:
|
||||
- Winner Glow Celebration: Bright green glow effects on scoreboard and floaters with 24-second victory timer
|
||||
- Visual Scoreboard: Real-time player grid with profile pictures and hearts
|
||||
- Enhanced Status Display: Custom textures for Direct Hit, No Shield, Plot Twist, Elimination, Victory
|
||||
- Color-Coded Glow System: Green (winner), Yellow (peril), Red X (eliminated)
|
||||
- Comprehensive Leaderboard: Persistent win tracking with XyzzyText display
|
||||
- Bot Support: AI players for testing and gameplay variety
|
||||
- Floating HUD Display: Real-time stats for each player
|
||||
- Floating HUD Display: Real-time stats for each player with victory text
|
||||
- Intelligent Dice Sizing: d6/d12/d20/d30 based on player count
|
||||
- Security Features: Owner lockout system, kick player functionality
|
||||
- Single Linkset: Complete 74-prim system for easy deployment
|
||||
|
|
@ -52,6 +54,10 @@ EXPERIENCE: None required
|
|||
Perfect for nightclubs, venues, casinos, private parties, group entertainment, community gatherings.
|
||||
|
||||
VERSION HISTORY:
|
||||
v2.8.6: Winner Glow Celebration System with 24-second victory recognition
|
||||
v2.8.5: Memory optimization and bot profile picture fixes
|
||||
v2.8.4: Disconnect recovery system and verbose logging
|
||||
v2.8.3: Shield detection fixes and initialization overhaul
|
||||
v2.8.2: Scoreboard spam fix, display improvements
|
||||
v2.8.1: Peril status and elimination heart fixes
|
||||
v2.8.0: Game lockout security, automatic reset
|
||||
|
|
@ -60,6 +66,6 @@ v2.7.0: Single linkset architecture revolution
|
|||
ORIGINAL CONCEPT: Noose the Bunny (djmusica28)
|
||||
AUTOMATED SYSTEM: Enhanced for professional gameplay
|
||||
|
||||
Transform your space into an exciting gaming destination!
|
||||
Transform your space into an exciting gaming destination with professional winner celebration effects!
|
||||
|
||||
Keywords: dice game, elimination game, multiplayer, party game, club game, casino, entertainment, social game, LSL script, venue game
|
||||
|
|
|
|||
|
|
@ -1,110 +0,0 @@
|
|||
# Memory Optimization Summary - Peril Dice Game v2.8.2
|
||||
|
||||
## Issue Description
|
||||
The Main Controller script was experiencing stack-heap collisions during player registration, specifically when new players joined the game. This was causing script failures and preventing proper player registration.
|
||||
|
||||
## Root Cause Analysis
|
||||
The player registration process in `Main_Controller_Linkset.lsl` was performing memory-intensive operations:
|
||||
|
||||
1. **Heavy string concatenations** for status messages and announcements
|
||||
2. **Large list manipulations** during player addition
|
||||
3. **Complex bot vs human logic processing**
|
||||
4. **Immediate floater channel calculations**
|
||||
5. **Multiple helper update calls** during registration
|
||||
|
||||
All of these operations happened simultaneously in the main controller's memory space, causing the stack and heap to collide.
|
||||
|
||||
## Solution Implemented
|
||||
**Delegation Pattern**: Moved the memory-intensive player registration logic to a dedicated helper script.
|
||||
|
||||
### Changes Made
|
||||
|
||||
#### 1. Created New Script: `Player_RegistrationManager.lsl`
|
||||
- **Purpose**: Handle all player registration operations
|
||||
- **Memory Benefits**: Isolates heavy string operations from main controller
|
||||
- **Responsibilities**:
|
||||
- Validate game state restrictions (game started, max players)
|
||||
- Handle bot vs human player logic
|
||||
- Manage floater channel assignments
|
||||
- Send appropriate announcements
|
||||
- Update main controller with final results
|
||||
|
||||
#### 2. Modified `Main_Controller_Linkset.lsl`
|
||||
- **Removed**: Entire player registration logic block (lines 617-682 in original)
|
||||
- **Added**: Simple delegation call that forwards registration requests
|
||||
- **Added**: Handler for registration updates from the dedicated manager
|
||||
- **Memory Savings**: ~65 lines of complex string manipulation removed
|
||||
|
||||
### Technical Implementation
|
||||
|
||||
#### Communication Protocol
|
||||
```lsl
|
||||
// Main Controller forwards registration requests
|
||||
if (num == MSG_REGISTER_PLAYER) {
|
||||
llMessageLinked(LINK_SET, MSG_REGISTER_PLAYER, str, id);
|
||||
return;
|
||||
}
|
||||
|
||||
// Registration Manager sends updates back
|
||||
if (num == 9040) { // MSG_UPDATE_MAIN_LISTS
|
||||
// Update main controller lists from registration manager
|
||||
list parts = llParseString2List(str, ["~"], []);
|
||||
if (llGetListLength(parts) >= 6) {
|
||||
players = llCSV2List(llList2String(parts, 0));
|
||||
names = llCSV2List(llList2String(parts, 1));
|
||||
lives = llCSV2List(llList2String(parts, 2));
|
||||
readyPlayers = llCSV2List(llList2String(parts, 3));
|
||||
// ... process update ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Data Flow
|
||||
1. Player touches object → Main Controller
|
||||
2. Main Controller forwards `MSG_REGISTER_PLAYER` → Registration Manager
|
||||
3. Registration Manager processes all complex logic in its own memory space
|
||||
4. Registration Manager sends `MSG_UPDATE_MAIN_LISTS` → Main Controller
|
||||
5. Main Controller updates its lists with final results
|
||||
6. Main Controller calls `updateHelpers()` to sync other scripts
|
||||
|
||||
## Expected Benefits
|
||||
|
||||
### Memory Usage
|
||||
- **Main Controller**: Reduced peak memory usage during player join by ~30-40%
|
||||
- **Registration Manager**: Handles heavy operations in isolated memory space
|
||||
- **Overall System**: Better memory distribution across multiple scripts
|
||||
|
||||
### Performance
|
||||
- **Faster Registration**: Less memory pressure = faster processing
|
||||
- **Reduced Failures**: Eliminates stack-heap collision during joins
|
||||
- **Better Stability**: Main controller maintains lower baseline memory usage
|
||||
|
||||
### Maintainability
|
||||
- **Separation of Concerns**: Registration logic isolated from main game logic
|
||||
- **Easier Debugging**: Registration issues can be traced to dedicated script
|
||||
- **Modular Design**: Can optimize registration logic without affecting main controller
|
||||
|
||||
## Testing Recommendations
|
||||
|
||||
1. **Basic Registration**: Test single player joins
|
||||
2. **Rapid Registration**: Test multiple players joining quickly
|
||||
3. **Bot Registration**: Test bot addition via admin menu
|
||||
4. **Memory Monitoring**: Use "Memory Stats" admin option to verify improvements
|
||||
5. **Edge Cases**: Test max players, game started restrictions
|
||||
|
||||
## File Changes Summary
|
||||
|
||||
### Modified Files
|
||||
- `Main_Controller_Linkset.lsl` - Delegated player registration, reduced memory usage
|
||||
|
||||
### New Files
|
||||
- `Player_RegistrationManager.lsl` - Dedicated player registration handler
|
||||
|
||||
### Architecture Impact
|
||||
- **Single Linkset Design**: Maintained - no external communication required
|
||||
- **Link Message Protocol**: Extended with new message type (9040)
|
||||
- **Script Responsibilities**: Better distributed across helper scripts
|
||||
- **Memory Architecture**: More balanced memory usage across scripts
|
||||
|
||||
## Version Notes
|
||||
This optimization maintains full backward compatibility and preserves all existing functionality while significantly reducing memory pressure in the main controller script.
|
||||
|
|
@ -30,6 +30,8 @@ integer MSG_GAME_STATUS = 3001;
|
|||
integer MSG_PLAYER_UPDATE = 3002;
|
||||
integer MSG_CLEAR_GAME = 3003;
|
||||
integer MSG_REMOVE_PLAYER = 3004;
|
||||
integer MSG_UPDATE_PERIL_PLAYER = 3005;
|
||||
integer MSG_UPDATE_WINNER = 3006;
|
||||
|
||||
// Leaderboard messages (to link 35)
|
||||
integer MSG_GAME_WON = 3010;
|
||||
|
|
@ -73,10 +75,10 @@ integer MSG_MEMORY_STATS_REQUEST = 6006;
|
|||
// Unified Timer System - prevents conflicts between multiple timer needs
|
||||
integer TIMER_IDLE = 0;
|
||||
integer TIMER_STATUS = 1;
|
||||
integer TIMER_TIMEOUT = 2;
|
||||
integer TIMER_DISCOVERY = 3;
|
||||
integer TIMER_VICTORY_DELAY = 2; // Timer mode for victory glow display
|
||||
integer currentTimerMode = 0; // Track what the timer is currently doing
|
||||
float timerInterval = 1.0; // How often timer() is called for checks
|
||||
integer victoryDelayTimer = 0; // Track victory delay timing
|
||||
|
||||
// Debug control - set to TRUE for verbose pick debugging, FALSE for normal operation
|
||||
integer DEBUG_PICKS = FALSE;
|
||||
|
|
@ -151,12 +153,7 @@ string TEXTURE_GAME_ACTIVE = "game_active"; // Optional: different texture duri
|
|||
// Maximum number of players allowed (including test players)
|
||||
integer MAX_PLAYERS = 10;
|
||||
|
||||
integer TIMEOUT_SECONDS = 120; // 2 minute timeout
|
||||
integer timeoutTimer;
|
||||
integer warning30sec = 30;
|
||||
integer warning1min = 60;
|
||||
integer warning90sec = 90;
|
||||
integer lastWarning = 0;
|
||||
// Timeout system removed - using owner kick functionality instead
|
||||
|
||||
key currentPicker;
|
||||
|
||||
|
|
@ -317,20 +314,14 @@ updateFloatingText() {
|
|||
resetGame() {
|
||||
cleanupListeners();
|
||||
|
||||
// Only clean up channels that actually have active floaters
|
||||
integer i;
|
||||
for (i = 0; i < llGetListLength(floaterChannels); i++) {
|
||||
integer ch = llList2Integer(floaterChannels, i);
|
||||
llMessageLinked(LINK_SET, MSG_CLEANUP_FLOAT, (string)ch, NULL_KEY);
|
||||
// Let Floater Manager handle cleanup intelligently
|
||||
// Only request cleanup if we actually have game data that might have floaters
|
||||
if (llGetListLength(names) > 0 || llGetListLength(floaterChannels) > 0) {
|
||||
llMessageLinked(LINK_SET, MSG_CLEANUP_ALL_FLOATERS, "RESET", NULL_KEY);
|
||||
llSleep(1.0);
|
||||
}
|
||||
|
||||
// Also send a few cleanup messages to common channel ranges in case of orphaned floaters
|
||||
for (i = 0; i < llGetListLength(names); i++) {
|
||||
integer ch = FLOATER_BASE_CHANNEL + i;
|
||||
llMessageLinked(LINK_SET, MSG_CLEANUP_FLOAT, (string)ch, NULL_KEY);
|
||||
}
|
||||
|
||||
llSleep(2.0);
|
||||
llSleep(0.5);
|
||||
|
||||
players = names = lives = picksData = globalPickedNumbers = readyPlayers = [];
|
||||
floaterChannels = [];
|
||||
|
|
@ -342,14 +333,14 @@ resetGame() {
|
|||
gameStarting = FALSE;
|
||||
victoryInProgress = FALSE;
|
||||
currentPicker = NULL_KEY;
|
||||
timeoutTimer = 0;
|
||||
lastWarning = 0;
|
||||
|
||||
llMessageLinked(LINK_SET, -99999, "FULL_RESET", NULL_KEY);
|
||||
llSay(syncChannel, "RESET");
|
||||
|
||||
// CHANGED: Use link messages instead of llRegionSay for display cleanup
|
||||
llMessageLinked(SCOREBOARD_LINK, MSG_CLEAR_GAME, "", NULL_KEY);
|
||||
// Clear winner glow from scoreboard (floaters will clear naturally on reset)
|
||||
llMessageLinked(SCOREBOARD_LINK, MSG_UPDATE_WINNER, "", NULL_KEY); // Clear winner glow
|
||||
llMessageLinked(DICE_LINK, MSG_CLEAR_DICE, "", NULL_KEY);
|
||||
|
||||
llOwnerSay("🎮 Game reset! All state cleared (including scoreboard).");
|
||||
|
|
@ -484,6 +475,7 @@ default {
|
|||
}
|
||||
|
||||
if (toucher == llGetOwner()) {
|
||||
llOwnerSay("🔍 [DEBUG] Owner touched - idx: " + (string)idx + ", players list: " + llList2CSV(players));
|
||||
if (idx == -1) {
|
||||
// Owner is not in game - show join/admin choice
|
||||
if (llListFindList(pendingRegistrations, [toucher]) != -1) {
|
||||
|
|
@ -491,10 +483,14 @@ default {
|
|||
return;
|
||||
}
|
||||
|
||||
// Show owner choice menu: join game or access admin functions
|
||||
list options = ["Join Game", "Owner Menu"];
|
||||
string menuText = "👑 Owner Options:\n\n🎮 Join Game - Register as a player\n🔧 Owner Menu - Admin functions without joining";
|
||||
llDialog(toucher, menuText, options, DIALOG_CHANNEL);
|
||||
// Owner is not in game - determine menu type based on player count
|
||||
if (llGetListLength(players) >= 2) {
|
||||
// Enough players for game - show starter menu
|
||||
llMessageLinked(LINK_SET, MSG_SHOW_MENU, "unregistered_owner_starter|1", toucher);
|
||||
} else {
|
||||
// Not enough players - show basic menu
|
||||
llMessageLinked(LINK_SET, MSG_SHOW_MENU, "unregistered_owner|0", toucher);
|
||||
}
|
||||
} else {
|
||||
// Owner is already in game - show player menu with appropriate starter status
|
||||
integer isStarter = TRUE;
|
||||
|
|
@ -608,53 +604,112 @@ default {
|
|||
// Send ONLY a direct scoreboard update to show 0 hearts
|
||||
llMessageLinked(SCOREBOARD_LINK, MSG_PLAYER_UPDATE,
|
||||
eliminatedPlayer + "|0|" + (string)llList2Key(players, idx), NULL_KEY);
|
||||
llSleep(1.5); // Give time for 0 hearts display
|
||||
|
||||
// Check if this will cause victory BEFORE removing player
|
||||
// CRITICAL: Check for victory condition BEFORE assigning new peril player
|
||||
integer willCauseVictory = (llGetListLength(names) <= 2); // After removing one player
|
||||
string potentialWinner = "";
|
||||
|
||||
if (willCauseVictory) {
|
||||
// Find the winner (the remaining non-eliminated player)
|
||||
integer w;
|
||||
for (w = 0; w < llGetListLength(names); w++) {
|
||||
string candidateName = llList2String(names, w);
|
||||
if (candidateName != eliminatedPlayer) {
|
||||
potentialWinner = candidateName;
|
||||
jump found_winner;
|
||||
}
|
||||
}
|
||||
@found_winner;
|
||||
|
||||
if (potentialWinner != "") {
|
||||
// SEND WINNER GLOW to scoreboard and trigger floater update
|
||||
llMessageLinked(SCOREBOARD_LINK, MSG_UPDATE_WINNER, potentialWinner, NULL_KEY);
|
||||
llMessageLinked(LINK_SET, MSG_UPDATE_FLOAT, potentialWinner, NULL_KEY); // Update winner's floater
|
||||
llOwnerSay("🏆 [Main Controller] Pre-victory winner glow sent for: " + potentialWinner);
|
||||
}
|
||||
}
|
||||
|
||||
// Only assign new peril player if NOT causing victory
|
||||
if (!willCauseVictory && (perilPlayer == eliminatedPlayer || perilPlayer == "")) {
|
||||
// Find first remaining alive player to be new peril player (before removing eliminated player)
|
||||
string newPerilPlayer = "";
|
||||
integer k;
|
||||
for (k = 0; k < llGetListLength(names) && newPerilPlayer == ""; k++) {
|
||||
string candidateName = llList2String(names, k);
|
||||
if (candidateName != eliminatedPlayer) { // Skip the player being eliminated
|
||||
integer candidateIdx = llListFindList(names, [candidateName]);
|
||||
if (candidateIdx != -1 && candidateIdx < llGetListLength(lives)) {
|
||||
integer candidateLives = llList2Integer(lives, candidateIdx);
|
||||
if (candidateLives > 0) {
|
||||
newPerilPlayer = candidateName;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (newPerilPlayer != "") {
|
||||
perilPlayer = newPerilPlayer;
|
||||
// Send peril player update to scoreboard for glow effect
|
||||
llMessageLinked(SCOREBOARD_LINK, MSG_UPDATE_PERIL_PLAYER, newPerilPlayer, NULL_KEY);
|
||||
} else {
|
||||
perilPlayer = "NONE";
|
||||
// Clear peril player glow on scoreboard
|
||||
llMessageLinked(SCOREBOARD_LINK, MSG_UPDATE_PERIL_PLAYER, "", NULL_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
// CRITICAL: Send sync message with 0 lives so floater shows elimination state
|
||||
// Build sync message BEFORE any list modifications to prevent corruption
|
||||
string livesStr = llList2CSV(lives);
|
||||
string picksStr = llDumpList2String(picksData, "^");
|
||||
if (picksStr == "") picksStr = "EMPTY"; // Prevent empty string
|
||||
string perilStr = perilPlayer;
|
||||
if (perilStr == "") perilStr = "NONE"; // Prevent empty string
|
||||
string namesStr = llList2CSV(names);
|
||||
string playersStr = llList2CSV(players);
|
||||
|
||||
string eliminationSync = livesStr + "~" + picksStr + "~" + perilStr + "~" + namesStr + "~" + playersStr;
|
||||
llMessageLinked(LINK_SET, MSG_SYNC_GAME_STATE, eliminationSync, NULL_KEY);
|
||||
|
||||
// Direct floater update to ensure eliminated status is displayed
|
||||
llMessageLinked(LINK_SET, MSG_UPDATE_FLOAT, eliminatedPlayer, llList2Key(players, idx));
|
||||
llSleep(2.0); // Give floater time to show eliminated status with red display
|
||||
|
||||
if (willCauseVictory) {
|
||||
// If this elimination will cause victory, give extra time for 0 hearts display
|
||||
llSleep(1.5); // Additional time for victory scenarios
|
||||
}
|
||||
|
||||
// THEN: Clean up and remove the player
|
||||
integer ch = llList2Integer(floaterChannels, idx);
|
||||
llMessageLinked(LINK_SET, MSG_CLEANUP_FLOAT, (string)ch, NULL_KEY);
|
||||
llSleep(0.3); // Give floater time to cleanup
|
||||
// CHANGED: Don't cleanup floaters during elimination - let them show eliminated status
|
||||
// The floater will show red glow + "ELIMINATED!" and stay visible until game end
|
||||
// integer ch = llList2Integer(floaterChannels, idx);
|
||||
// llMessageLinked(LINK_SET, MSG_CLEANUP_FLOAT, (string)ch, NULL_KEY);
|
||||
// llSleep(0.3); // Give floater time to cleanup
|
||||
|
||||
// CRITICAL: Remove eliminated player's picks data by searching for their entry
|
||||
// NOT by index, since picksData format is "PlayerName|picks" strings
|
||||
integer pickIdx = -1;
|
||||
integer p;
|
||||
for (p = 0; p < llGetListLength(picksData); p++) {
|
||||
string entry = llList2String(picksData, p);
|
||||
if (llSubStringIndex(entry, eliminatedPlayer + "|") == 0) {
|
||||
pickIdx = p;
|
||||
jump found_pick_entry;
|
||||
}
|
||||
}
|
||||
@found_pick_entry;
|
||||
if (pickIdx != -1) {
|
||||
picksData = llDeleteSubList(picksData, pickIdx, pickIdx);
|
||||
}
|
||||
|
||||
// Now remove from indexed lists (players, names, lives, floaterChannels)
|
||||
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);
|
||||
|
||||
|
||||
// CRITICAL: Update perilPlayer BEFORE any updateHelpers() calls
|
||||
// This prevents sending stale sync messages with eliminated players
|
||||
if (perilPlayer == eliminatedPlayer) {
|
||||
// Find first remaining alive player to be new peril player
|
||||
string newPerilPlayer = "";
|
||||
integer k;
|
||||
for (k = 0; k < llGetListLength(names) && newPerilPlayer == ""; k++) {
|
||||
string candidateName = llList2String(names, k);
|
||||
integer candidateIdx = llListFindList(names, [candidateName]);
|
||||
if (candidateIdx != -1 && candidateIdx < llGetListLength(lives)) {
|
||||
integer candidateLives = llList2Integer(lives, candidateIdx);
|
||||
if (candidateLives > 0) {
|
||||
newPerilPlayer = candidateName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (newPerilPlayer != "") {
|
||||
llOwnerSay("🎯 [Main Controller] Peril player eliminated - assigning new peril player: " + newPerilPlayer);
|
||||
perilPlayer = newPerilPlayer;
|
||||
} else {
|
||||
llOwnerSay("⚠️ [Main Controller] No valid peril player candidates found after elimination!");
|
||||
perilPlayer = "NONE";
|
||||
}
|
||||
}
|
||||
// Peril player assignment already handled before sync message was sent
|
||||
|
||||
// CHANGED: Send to scoreboard, which will handle leaderboard updates
|
||||
llMessageLinked(SCOREBOARD_LINK, MSG_GAME_LOST, eliminatedPlayer, NULL_KEY);
|
||||
|
|
@ -666,6 +721,11 @@ default {
|
|||
if (llGetListLength(names) == 1) {
|
||||
string winner = llList2String(names, 0);
|
||||
|
||||
// ENHANCED: Give eliminated player's floater time to show eliminated status
|
||||
// before declaring victory and cleaning up
|
||||
llOwnerSay("⏳ [Main Controller] Final elimination detected - allowing display time before victory");
|
||||
llSleep(2.0); // Give final eliminated player time to show red eliminated status
|
||||
|
||||
// CRITICAL: Set victory flag immediately to prevent sync processing
|
||||
victoryInProgress = TRUE;
|
||||
|
||||
|
|
@ -687,10 +747,19 @@ default {
|
|||
llMessageLinked(LINK_SET, MSG_PLAYER_WON, winner, NULL_KEY);
|
||||
llMessageLinked(LINK_SET, 995, "VICTORY_CONFETTI", NULL_KEY);
|
||||
|
||||
// IMPORTANT: Send winner update to scoreboard and trigger floater update
|
||||
llMessageLinked(SCOREBOARD_LINK, MSG_UPDATE_WINNER, winner, NULL_KEY);
|
||||
llMessageLinked(LINK_SET, MSG_UPDATE_FLOAT, winner, NULL_KEY); // Update winner's floater
|
||||
|
||||
// CHANGED: Send to scoreboard, which will handle leaderboard updates
|
||||
llMessageLinked(SCOREBOARD_LINK, MSG_GAME_WON, winner, NULL_KEY);
|
||||
|
||||
llSleep(STATUS_DISPLAY_TIME * 2.0); // Combined delay
|
||||
// Use timer instead of llSleep to avoid immediate reset
|
||||
llOwnerSay("🏆 [Main Controller] Winner glow applied - starting victory delay timer");
|
||||
victoryDelayTimer = llGetUnixTime();
|
||||
currentTimerMode = TIMER_VICTORY_DELAY;
|
||||
llSetTimerEvent(1.0); // Check every second
|
||||
return; // Exit here - timer will handle reset
|
||||
} else {
|
||||
// CRITICAL: Set victory flag and clear state for no survivors scenario too
|
||||
victoryInProgress = TRUE;
|
||||
|
|
@ -706,10 +775,25 @@ default {
|
|||
currentPicker = NULL_KEY;
|
||||
|
||||
llSay(0, "💀 DESPAIR WINS! No Ultimate Survivors remain!");
|
||||
llSleep(STATUS_DISPLAY_TIME + 1.0);
|
||||
|
||||
// Use timer for no survivors scenario too
|
||||
victoryDelayTimer = llGetUnixTime();
|
||||
currentTimerMode = TIMER_VICTORY_DELAY;
|
||||
llSetTimerEvent(1.0);
|
||||
return;
|
||||
}
|
||||
resetGame();
|
||||
return;
|
||||
}
|
||||
|
||||
// CRITICAL: After elimination, sync state and continue game with new peril player
|
||||
if (perilPlayer != "" && perilPlayer != "NONE") {
|
||||
// Sync the updated game state to all modules (5-part format)
|
||||
string picksDataStr = "EMPTY"; // Empty picks for new round
|
||||
string gameSync = llList2CSV(lives) + "~" + picksDataStr + "~" + perilPlayer + "~" + llList2CSV(names) + "~" + llList2CSV(players);
|
||||
llMessageLinked(LINK_SET, MSG_SYNC_GAME_STATE, gameSync, NULL_KEY);
|
||||
llSleep(0.5); // Give sync time to propagate
|
||||
|
||||
// Continue the game with the new peril player - delegate to Game Manager
|
||||
llMessageLinked(LINK_SET, 998, perilPlayer, NULL_KEY);
|
||||
}
|
||||
|
||||
// MINIMAL: Only send direct scoreboard update for remaining players
|
||||
|
|
@ -770,21 +854,7 @@ default {
|
|||
return;
|
||||
}
|
||||
|
||||
// Handle start next round request - delegate to Game Manager
|
||||
if (num == 997) {
|
||||
if (sender == llGetLinkNumber()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (roundStarted && str == "START_NEXT_ROUND") {
|
||||
llOwnerSay("⚠️ [Main Controller] Round already in progress, ignoring START_NEXT_ROUND");
|
||||
return;
|
||||
}
|
||||
|
||||
llOwnerSay("🔄 [Main Controller] Starting new round - Game Manager will handle dice type");
|
||||
llMessageLinked(LINK_SET, num, str, id);
|
||||
return;
|
||||
}
|
||||
// Legacy 997 message handler removed - now using direct communication to Game Manager
|
||||
|
||||
// Handle ready state toggle
|
||||
if (num == MSG_TOGGLE_READY) {
|
||||
|
|
@ -867,18 +937,7 @@ default {
|
|||
return;
|
||||
}
|
||||
|
||||
// Handle aggressive floater cleanup
|
||||
if (num == MSG_CLEANUP_ALL_FLOATERS) {
|
||||
integer i;
|
||||
for (i = 0; i < MAX_PLAYERS; i++) {
|
||||
integer ch = FLOATER_BASE_CHANNEL + i;
|
||||
llMessageLinked(LINK_SET, MSG_CLEANUP_FLOAT, (string)ch, NULL_KEY);
|
||||
llMessageLinked(LINK_SET, MSG_CLEANUP_FLOAT, (string)(ch + 100), NULL_KEY);
|
||||
llMessageLinked(LINK_SET, MSG_CLEANUP_FLOAT, (string)(ch + 1000), NULL_KEY);
|
||||
}
|
||||
floaterChannels = [];
|
||||
return;
|
||||
}
|
||||
// MSG_CLEANUP_ALL_FLOATERS is handled by Floater Manager only
|
||||
|
||||
// Handle leave game requests processed by Controller_MessageHandler
|
||||
if (num == 8006) { // MSG_LEAVE_GAME_REQUEST
|
||||
|
|
@ -919,9 +978,13 @@ default {
|
|||
if (newPerilPlayer != "") {
|
||||
llOwnerSay("🎯 [Main Controller] Peril player left - assigning new peril player: " + newPerilPlayer);
|
||||
perilPlayer = newPerilPlayer;
|
||||
// Send peril player update to scoreboard for glow effect
|
||||
llMessageLinked(SCOREBOARD_LINK, 3005, newPerilPlayer, NULL_KEY); // MSG_UPDATE_PERIL_PLAYER
|
||||
} else {
|
||||
llOwnerSay("⚠️ [Main Controller] No valid peril player candidates found after player left!");
|
||||
perilPlayer = "NONE";
|
||||
// Clear peril player glow on scoreboard
|
||||
llMessageLinked(SCOREBOARD_LINK, MSG_UPDATE_PERIL_PLAYER, "", NULL_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1023,7 +1086,7 @@ default {
|
|||
}
|
||||
|
||||
llSay(0, "⚡ THE KILLING GAME CONTINUES! " + perilPlayer + " begins the next deadly round!");
|
||||
llMessageLinked(LINK_SET, 997, "START_NEXT_ROUND", NULL_KEY);
|
||||
llMessageLinked(LINK_SET, 998, perilPlayer, NULL_KEY);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -1087,21 +1150,23 @@ default {
|
|||
}
|
||||
|
||||
// Create shorter bot names to avoid dialog button length issues
|
||||
integer botNum = llGetListLength(players) + 1;
|
||||
// Count existing bots to determine next bot number
|
||||
integer existingBots = 0;
|
||||
integer i;
|
||||
for (i = 0; i < llGetListLength(names); i++) {
|
||||
string playerName = llList2String(names, i);
|
||||
if (llSubStringIndex(playerName, "Bot") == 0) {
|
||||
existingBots++;
|
||||
}
|
||||
}
|
||||
integer botNum = existingBots + 1;
|
||||
string testName = "Bot" + (string)botNum;
|
||||
key fake = llGenerateKey();
|
||||
llMessageLinked(LINK_SET, MSG_REGISTER_PLAYER_REQUEST, testName + "|" + (string)fake, NULL_KEY);
|
||||
|
||||
integer ownerIdx = llListFindList(players, [llGetOwner()]);
|
||||
integer ownerIsStarter = TRUE;
|
||||
integer n;
|
||||
for (n = 0; n < ownerIdx && ownerIsStarter; n++) {
|
||||
string existingName = llList2String(names, n);
|
||||
if (llSubStringIndex(existingName, "Bot") != 0) {
|
||||
ownerIsStarter = FALSE;
|
||||
}
|
||||
}
|
||||
llMessageLinked(LINK_SET, MSG_SHOW_MENU, "owner|" + (string)ownerIsStarter, llGetOwner());
|
||||
// Don't show menu immediately - let the player touch the board again
|
||||
// This avoids timing issues where the menu is shown before registration completes
|
||||
llOwnerSay("✅ " + testName + " registration sent - touch the board again to refresh menu");
|
||||
return;
|
||||
}
|
||||
if (msg == "Force Floaters") {
|
||||
|
|
@ -1115,16 +1180,27 @@ default {
|
|||
}
|
||||
llOwnerSay("✅ Floater creation requests sent for all players!");
|
||||
|
||||
// After forcing floaters, show appropriate menu based on owner registration status
|
||||
integer ownerIdx = llListFindList(players, [llGetOwner()]);
|
||||
integer ownerIsStarter = TRUE;
|
||||
integer n;
|
||||
for (n = 0; n < ownerIdx && ownerIsStarter; n++) {
|
||||
string existingName = llList2String(names, n);
|
||||
if (llSubStringIndex(existingName, "Bot") != 0) {
|
||||
ownerIsStarter = FALSE;
|
||||
if (ownerIdx != -1) {
|
||||
// Owner is registered - show registered owner menu
|
||||
integer ownerIsStarter = TRUE;
|
||||
integer n;
|
||||
for (n = 0; n < ownerIdx && ownerIsStarter; n++) {
|
||||
string existingName = llList2String(names, n);
|
||||
if (llSubStringIndex(existingName, "Bot") != 0) {
|
||||
ownerIsStarter = FALSE;
|
||||
}
|
||||
}
|
||||
llMessageLinked(LINK_SET, MSG_SHOW_MENU, "owner|" + (string)ownerIsStarter, llGetOwner());
|
||||
} else {
|
||||
// Owner is not registered - show unregistered owner menu
|
||||
if (llGetListLength(players) >= 2) {
|
||||
llMessageLinked(LINK_SET, MSG_SHOW_MENU, "unregistered_owner_starter|1", llGetOwner());
|
||||
} else {
|
||||
llMessageLinked(LINK_SET, MSG_SHOW_MENU, "unregistered_owner|0", llGetOwner());
|
||||
}
|
||||
}
|
||||
llMessageLinked(LINK_SET, MSG_SHOW_MENU, "owner|" + (string)ownerIsStarter, llGetOwner());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -1176,7 +1252,8 @@ default {
|
|||
llSetText("🎮 GAME IN PROGRESS\n" + (string)actualPlayerCount + " players", <1.0, 0.2, 0.2>, 1.0);
|
||||
|
||||
sendStatusMessage("Title");
|
||||
llMessageLinked(LINK_SET, 997, "START_NEXT_ROUND", NULL_KEY);
|
||||
// For game start, use empty peril player - Game Manager will select one
|
||||
llMessageLinked(LINK_SET, 998, "", NULL_KEY);
|
||||
}
|
||||
|
||||
if (llListFindList(names, [msg]) != -1) {
|
||||
|
|
@ -1197,26 +1274,15 @@ default {
|
|||
llSetTimerEvent(0);
|
||||
}
|
||||
}
|
||||
else if (currentTimerMode == TIMER_TIMEOUT) {
|
||||
if (timeoutTimer > 0) {
|
||||
integer elapsed = llGetUnixTime() - timeoutTimer;
|
||||
if (elapsed >= TIMEOUT_SECONDS) {
|
||||
llOwnerSay("⏰ Dialog timeout reached!");
|
||||
timeoutTimer = 0;
|
||||
else if (currentTimerMode == TIMER_VICTORY_DELAY) {
|
||||
if (victoryDelayTimer > 0) {
|
||||
integer elapsed = llGetUnixTime() - victoryDelayTimer;
|
||||
if (elapsed >= (integer)(STATUS_DISPLAY_TIME * 3.0)) { // 24 seconds
|
||||
llOwnerSay("🏆 [Main Controller] Victory display time complete - executing game reset");
|
||||
victoryDelayTimer = 0;
|
||||
currentTimerMode = TIMER_IDLE;
|
||||
llSetTimerEvent(0);
|
||||
} else {
|
||||
integer remaining = TIMEOUT_SECONDS - elapsed;
|
||||
if (remaining <= warning30sec && lastWarning < warning30sec) {
|
||||
llOwnerSay("⚠️ 30 seconds remaining for dialog response");
|
||||
lastWarning = warning30sec;
|
||||
} else if (remaining <= warning1min && lastWarning < warning1min) {
|
||||
llOwnerSay("⚠️ 1 minute remaining for dialog response");
|
||||
lastWarning = warning1min;
|
||||
} else if (remaining <= warning90sec && lastWarning < warning90sec) {
|
||||
llOwnerSay("⚠️ 1.5 minutes remaining for dialog response");
|
||||
lastWarning = warning90sec;
|
||||
}
|
||||
resetGame(); // Now reset after the delay
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,6 +60,19 @@ list globallyPickedNumbers = []; // Numbers already picked by other players
|
|||
integer currentPage = 0;
|
||||
integer NUMBERS_PER_PAGE = 9; // Leave room for navigation buttons
|
||||
|
||||
// Memory reporting function
|
||||
reportMemoryUsage(string scriptName) {
|
||||
integer used = llGetUsedMemory();
|
||||
integer free = llGetFreeMemory();
|
||||
integer total = used + free;
|
||||
float percentUsed = ((float)used / (float)total) * 100.0;
|
||||
|
||||
llOwnerSay("🧠 [" + scriptName + "] Memory: " +
|
||||
(string)used + " used, " +
|
||||
(string)free + " free (" +
|
||||
llGetSubString((string)percentUsed, 0, 4) + "% used)");
|
||||
}
|
||||
|
||||
list getNumbersForPage(integer page, integer diceType) {
|
||||
// Build list of available numbers (not picked by others OR by current player)
|
||||
list availableNumbers = [];
|
||||
|
|
@ -176,6 +189,8 @@ showPickDialog(string name, key id, integer diceType, integer picks) {
|
|||
|
||||
default {
|
||||
state_entry() {
|
||||
reportMemoryUsage("🎮 NumberPicker Dialog");
|
||||
|
||||
// Initialize dynamic channels
|
||||
initializeChannels();
|
||||
numberPickChannel = NUMBERPICK_CHANNEL; // Set legacy variable
|
||||
|
|
@ -200,6 +215,8 @@ default {
|
|||
}
|
||||
|
||||
on_rez(integer start_param) {
|
||||
reportMemoryUsage("🎮 NumberPicker Dialog");
|
||||
|
||||
if (VERBOSE_LOGGING) {
|
||||
llOwnerSay("🔄 Number Picker rezzed - reinitializing...");
|
||||
}
|
||||
|
|
@ -290,13 +307,32 @@ default {
|
|||
if (llGetListLength(parts) >= 4) {
|
||||
string globalPicksStr = llList2String(parts, 3);
|
||||
if (globalPicksStr != "") {
|
||||
// Handle both CSV (comma) and semicolon-separated formats
|
||||
if (llSubStringIndex(globalPicksStr, ";") != -1) {
|
||||
// Semicolon-separated format from bots
|
||||
globallyPickedNumbers = llParseString2List(globalPicksStr, [";"], []);
|
||||
} else {
|
||||
// CSV format from humans
|
||||
globallyPickedNumbers = llCSV2List(globalPicksStr);
|
||||
// IMPROVED: Handle mixed formats properly
|
||||
globallyPickedNumbers = [];
|
||||
|
||||
// First split by comma to handle CSV format
|
||||
list csvParts = llParseString2List(globalPicksStr, [","], []);
|
||||
integer i;
|
||||
for (i = 0; i < llGetListLength(csvParts); i++) {
|
||||
string part = llStringTrim(llList2String(csvParts, i), STRING_TRIM);
|
||||
|
||||
// Check if this part contains semicolons (bot format)
|
||||
if (llSubStringIndex(part, ";") != -1) {
|
||||
// Split this part by semicolon and add each number
|
||||
list semiParts = llParseString2List(part, [";"], []);
|
||||
integer j;
|
||||
for (j = 0; j < llGetListLength(semiParts); j++) {
|
||||
string num = llStringTrim(llList2String(semiParts, j), STRING_TRIM);
|
||||
if (num != "" && llListFindList(globallyPickedNumbers, [num]) == -1) {
|
||||
globallyPickedNumbers += [num];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Single number, add it if not already present
|
||||
if (part != "" && llListFindList(globallyPickedNumbers, [part]) == -1) {
|
||||
globallyPickedNumbers += [part];
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
globallyPickedNumbers = [];
|
||||
|
|
|
|||
|
|
@ -23,6 +23,19 @@ list picksData;
|
|||
string perilPlayer;
|
||||
list names;
|
||||
|
||||
// Memory reporting function
|
||||
reportMemoryUsage(string scriptName) {
|
||||
integer used = llGetUsedMemory();
|
||||
integer free = llGetFreeMemory();
|
||||
integer total = used + free;
|
||||
float percentUsed = ((float)used / (float)total) * 100.0;
|
||||
|
||||
llOwnerSay("🧠 [" + scriptName + "] Memory: " +
|
||||
(string)used + " used, " +
|
||||
(string)free + " free (" +
|
||||
llGetSubString((string)percentUsed, 0, 4) + "% used)");
|
||||
}
|
||||
|
||||
list getPicksFor(string nameInput) {
|
||||
integer i;
|
||||
for (i = 0; i < llGetListLength(picksData); i++) {
|
||||
|
|
@ -47,6 +60,13 @@ list getPicksFor(string nameInput) {
|
|||
|
||||
default {
|
||||
state_entry() {
|
||||
// Get player name from object description if available
|
||||
string playerName = llGetObjectDesc();
|
||||
if (playerName == "" || playerName == "(No Description)") {
|
||||
playerName = "Unknown";
|
||||
}
|
||||
reportMemoryUsage("📱 Player Float (" + playerName + ")");
|
||||
|
||||
llSetText("⏳ Waiting...", <1,1,1>, 1.0);
|
||||
llSetTimerEvent(1.0);
|
||||
}
|
||||
|
|
@ -59,12 +79,11 @@ default {
|
|||
|
||||
// Set up managed listener
|
||||
listenHandle = llListen(start_param, "", NULL_KEY, "");
|
||||
// Wait a moment for the description to be set
|
||||
llSleep(0.1);
|
||||
myName = llGetObjectDesc();
|
||||
if (myName == "" || myName == "(No Description)") {
|
||||
myName = "UnknownPlayer" + (string)start_param;
|
||||
}
|
||||
|
||||
// Initialize myName to empty - will be set when SET_NAME message is received
|
||||
myName = "";
|
||||
|
||||
// Don't report memory immediately - wait for proper name to be set
|
||||
// PlayerStatus_Float ready and listening
|
||||
}
|
||||
|
||||
|
|
@ -87,13 +106,53 @@ default {
|
|||
llSetTexture(heartTexture, 2); // Face 2 (back)
|
||||
llSetTexture(heartTexture, 3); // Face 3 (left side)
|
||||
llSetTexture(heartTexture, 4); // Face 4 (front)
|
||||
// Set white background for all faces to eliminate gaps without tinting
|
||||
llSetColor(<1.0, 1.0, 1.0>, 1); // White background for right side
|
||||
llSetColor(<1.0, 1.0, 1.0>, 2); // White background for back
|
||||
llSetColor(<1.0, 1.0, 1.0>, 3); // White background for left side
|
||||
llSetColor(<1.0, 1.0, 1.0>, 4); // White background for front
|
||||
llSetColor(<0.2, 0.2, 0.2>, 0); // Dark gray for top
|
||||
llSetColor(<0.2, 0.2, 0.2>, 5); // Dark gray for bottom
|
||||
|
||||
// IMPROVED: Special visual treatment for elimination (0 hearts)
|
||||
if (lifeCount == 0) {
|
||||
// Make 0 hearts more prominent with red glow
|
||||
llSetLinkPrimitiveParamsFast(LINK_THIS, [
|
||||
PRIM_GLOW, ALL_SIDES, 0.3,
|
||||
PRIM_COLOR, ALL_SIDES, <1.0, 0.0, 0.0>, 1.0
|
||||
]);
|
||||
|
||||
// Modify display text to show elimination status
|
||||
displayText = llDumpList2String(llParseString2List(displayText, ["\n"], []), "\n") + "\n💀 ELIMINATED! 💀";
|
||||
} else {
|
||||
// Check for winner status first (highest priority)
|
||||
integer isWinner = FALSE;
|
||||
if (llSubStringIndex(displayText, "✨ ULTIMATE VICTORY!") != -1 ||
|
||||
llSubStringIndex(displayText, "ULTIMATE SURVIVOR") != -1) {
|
||||
isWinner = TRUE;
|
||||
}
|
||||
|
||||
// Check if this player is in peril for yellow glow
|
||||
integer isInPeril = FALSE;
|
||||
if (llSubStringIndex(displayText, "⚡ YOU ARE IN PERIL! ⚡") != -1) {
|
||||
isInPeril = TRUE;
|
||||
}
|
||||
|
||||
if (isWinner) {
|
||||
// Green glow for winner (overrides peril)
|
||||
llSetLinkPrimitiveParamsFast(LINK_THIS, [
|
||||
PRIM_GLOW, ALL_SIDES, 0.3,
|
||||
PRIM_COLOR, ALL_SIDES, <0.0, 1.0, 0.0>, 1.0 // Green tint
|
||||
]);
|
||||
} else if (isInPeril) {
|
||||
// Yellow glow for peril player
|
||||
llSetLinkPrimitiveParamsFast(LINK_THIS, [
|
||||
PRIM_GLOW, ALL_SIDES, 0.2,
|
||||
PRIM_COLOR, ALL_SIDES, <1.0, 1.0, 0.0>, 1.0 // Yellow tint
|
||||
]);
|
||||
} else {
|
||||
// Normal colors for living players - remove glow
|
||||
llSetLinkPrimitiveParamsFast(LINK_THIS, [
|
||||
PRIM_GLOW, ALL_SIDES, 0.0,
|
||||
PRIM_COLOR, ALL_SIDES, <1.0, 1.0, 1.0>, 1.0
|
||||
]);
|
||||
llSetColor(<0.2, 0.2, 0.2>, 0); // Dark gray for top
|
||||
llSetColor(<0.2, 0.2, 0.2>, 5); // Dark gray for bottom
|
||||
}
|
||||
}
|
||||
|
||||
// Remove "Lives: X" from display text since hearts show it
|
||||
list lines = llParseString2List(displayText, ["\n"], []);
|
||||
|
|
@ -120,6 +179,9 @@ default {
|
|||
// Name successfully received and set
|
||||
// Update the object description as well for consistency
|
||||
llSetObjectDesc(myName);
|
||||
|
||||
// Now report memory with the proper player name
|
||||
reportMemoryUsage("📱 " + myName);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -165,10 +227,57 @@ default {
|
|||
|
||||
string picksDisplay = llList2CSV(picks);
|
||||
|
||||
// Note: Heart texture is set via listen() path to avoid conflicts
|
||||
// IMPROVED: Set heart texture and handle elimination display
|
||||
string heartTexture = (string)lifeCount + "_hearts";
|
||||
llSetTexture(heartTexture, 1); // Face 1 (right side)
|
||||
llSetTexture(heartTexture, 2); // Face 2 (back)
|
||||
llSetTexture(heartTexture, 3); // Face 3 (left side)
|
||||
llSetTexture(heartTexture, 4); // Face 4 (front)
|
||||
|
||||
string txt;
|
||||
if (lifeCount == 0) {
|
||||
// Special treatment for elimination - red glow and elimination message
|
||||
llSetLinkPrimitiveParamsFast(LINK_THIS, [
|
||||
PRIM_GLOW, ALL_SIDES, 0.3,
|
||||
PRIM_COLOR, ALL_SIDES, <1.0, 0.0, 0.0>, 1.0
|
||||
]);
|
||||
|
||||
txt = "🎲 Peril Dice\n👤 " + myName + "\n💀 ELIMINATED! 💀\n🔢 Final Picks: " + picksDisplay;
|
||||
// Set text to red color as well for extra visibility
|
||||
llSetText(txt, <1.0, 0.2, 0.2>, 1.0);
|
||||
return; // Skip normal text setting below
|
||||
} else {
|
||||
// Check if this is the winner (highest priority) - happens when only 1 player left
|
||||
integer isWinner = (llGetListLength(names) <= 1 && myName != "" && llListFindList(names, [myName]) != -1);
|
||||
|
||||
// Check if this player is the peril player for yellow glow
|
||||
integer isInPeril = (myName == perilPlayer && perilPlayer != "" && llSubStringIndex(perilPlayer, ",") == -1);
|
||||
|
||||
if (isWinner) {
|
||||
// Green glow for winner (overrides peril)
|
||||
llSetLinkPrimitiveParamsFast(LINK_THIS, [
|
||||
PRIM_GLOW, ALL_SIDES, 0.3,
|
||||
PRIM_COLOR, ALL_SIDES, <0.0, 1.0, 0.0>, 1.0 // Green tint
|
||||
]);
|
||||
} else if (isInPeril) {
|
||||
// Yellow glow for peril player
|
||||
llSetLinkPrimitiveParamsFast(LINK_THIS, [
|
||||
PRIM_GLOW, ALL_SIDES, 0.2,
|
||||
PRIM_COLOR, ALL_SIDES, <1.0, 1.0, 0.0>, 1.0 // Yellow tint
|
||||
]);
|
||||
} else {
|
||||
// Normal colors for living players - remove glow
|
||||
llSetLinkPrimitiveParamsFast(LINK_THIS, [
|
||||
PRIM_GLOW, ALL_SIDES, 0.0,
|
||||
PRIM_COLOR, ALL_SIDES, <1.0, 1.0, 1.0>, 1.0
|
||||
]);
|
||||
llSetColor(<0.2, 0.2, 0.2>, 0); // Dark gray for top
|
||||
llSetColor(<0.2, 0.2, 0.2>, 5); // Dark gray for bottom
|
||||
}
|
||||
|
||||
txt = "🎲 Peril Dice\n👤 " + myName + "\n Peril: " + perilDisplay + "\n🔢 Picks: " + picksDisplay;
|
||||
}
|
||||
|
||||
// Update text display (lives now shown by heart textures)
|
||||
string txt = "🎲 Peril Dice\n👤 " + myName + "\n Peril: " + perilDisplay + "\n🔢 Picks: " + picksDisplay;
|
||||
llSetText(txt, <1,1,1>, 1.0);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -111,6 +111,19 @@ float MENU_REQUEST_TIMEOUT = 5.0; // 5 second timeout for menu requests
|
|||
integer isLocked = FALSE;
|
||||
key gameOwner;
|
||||
|
||||
// Memory reporting function
|
||||
reportMemoryUsage(string scriptName) {
|
||||
integer used = llGetUsedMemory();
|
||||
integer free = llGetFreeMemory();
|
||||
integer total = used + free;
|
||||
float percentUsed = ((float)used / (float)total) * 100.0;
|
||||
|
||||
llOwnerSay("🧠 [" + scriptName + "] Memory: " +
|
||||
(string)used + " used, " +
|
||||
(string)free + " free (" +
|
||||
llGetSubString((string)percentUsed, 0, 4) + "% used)");
|
||||
}
|
||||
|
||||
// Display the main categorized owner menu with lock/unlock controls
|
||||
showOwnerMenu(key id) {
|
||||
// Only show full menu to owner
|
||||
|
|
@ -288,8 +301,32 @@ showKickPlayerMenu(key id, list playerNames) {
|
|||
llDialog(id, "👢 Select player to kick from game:", options, DIALOG_CHANNEL);
|
||||
}
|
||||
|
||||
// Show menu for unregistered owner
|
||||
showUnregisteredOwnerMenu(key id, integer canStartGame) {
|
||||
list options = [];
|
||||
string menuText = "👑 Owner Options:\n\n";
|
||||
|
||||
// Always allow owner to join
|
||||
options += ["Join Game"];
|
||||
menuText += "🎮 Join Game - Register as a player\n";
|
||||
|
||||
// Allow starting game if there are enough players (even if all bots)
|
||||
if (canStartGame) {
|
||||
options += ["Start Game"];
|
||||
menuText += "⚡ Start Game - Begin with current players\n";
|
||||
}
|
||||
|
||||
// Always allow owner menu access
|
||||
options += ["Owner Menu"];
|
||||
menuText += "🔧 Owner Menu - Admin functions";
|
||||
|
||||
llDialog(id, menuText, options, DIALOG_CHANNEL);
|
||||
}
|
||||
|
||||
default {
|
||||
state_entry() {
|
||||
reportMemoryUsage("🎭 Player Dialog");
|
||||
|
||||
// Initialize dynamic channels
|
||||
initializeChannels();
|
||||
DIALOG_CHANNEL = MAIN_DIALOG_CHANNEL; // Set legacy variable
|
||||
|
|
@ -331,6 +368,8 @@ default {
|
|||
}
|
||||
|
||||
on_rez(integer start_param) {
|
||||
reportMemoryUsage("🎭 Player Dialog");
|
||||
|
||||
llOwnerSay("🔄 Player Dialog Handler rezzed - reinitializing...");
|
||||
|
||||
// Re-initialize dynamic channels
|
||||
|
|
@ -408,6 +447,12 @@ default {
|
|||
} else if (targetType == "admin") {
|
||||
// Show admin category menu directly
|
||||
showOwnerMenu(id);
|
||||
} else if (targetType == "unregistered_owner") {
|
||||
// Unregistered owner - basic menu (join, owner menu)
|
||||
showUnregisteredOwnerMenu(id, FALSE);
|
||||
} else if (targetType == "unregistered_owner_starter") {
|
||||
// Unregistered owner with enough players to start game
|
||||
showUnregisteredOwnerMenu(id, TRUE);
|
||||
}
|
||||
}
|
||||
else if (num == MSG_READY_STATE_RESULT) {
|
||||
|
|
|
|||
|
|
@ -153,6 +153,9 @@ handlePlayerRegistration(string regData, key requesterId) {
|
|||
// Send pre-built sync message
|
||||
llMessageLinked(LINK_SET, MSG_SYNC_GAME_STATE, syncMessage, NULL_KEY);
|
||||
|
||||
// CRITICAL FIX: Send floater rez message to Floater Manager
|
||||
llMessageLinked(LINK_SET, 106, newName + "|" + (string)newKey, newKey);
|
||||
|
||||
// Send minimal update to Main Controller - just the essential data it needs
|
||||
string updateData = (string)ch + "~" + newName;
|
||||
llMessageLinked(LINK_SET, MSG_UPDATE_MAIN_LISTS, updateData, newKey);
|
||||
|
|
|
|||
39
README.md
39
README.md
|
|
@ -1,12 +1,14 @@
|
|||
# 🎲 Peril Dice — Professional Single Linkset Game System for Second Life
|
||||
|
||||
**Created by Rebecca Nod and Noose the Bunny**
|
||||
**Current Version: 2.8.5 - Memory Optimization & Architecture Cleanup**
|
||||
**Current Version: 2.8.6 - Winner Glow Celebration System**
|
||||
|
||||
## 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.
|
||||
|
||||
**🏆 NEW in v2.8.6**: **WINNER GLOW CELEBRATION SYSTEM** - Revolutionary winner recognition with bright green glow effects on both scoreboard and floaters, plus 24-second victory celebration timer giving proper recognition to the Ultimate Survivor before game reset.
|
||||
|
||||
**🚨 NEW in v2.8.5**: **MEMORY OPTIMIZATION & ARCHITECTURE CLEANUP** - Major system cleanup eliminating dead code, fixing bot profile picture flickering, and optimizing memory usage across all scripts with measurable performance improvements.
|
||||
|
||||
**NEW in v2.8.4**: **DISCONNECT RECOVERY & DEBUG SYSTEM** - Revolutionary disconnect/reconnect recovery system eliminates need to kick players who disconnect during their turn, plus comprehensive system-wide verbose logging toggle.
|
||||
|
|
@ -19,6 +21,41 @@ Peril Dice is a multiplayer elimination game where each player selects numbers b
|
|||
|
||||
**NEW in v2.7.0**: Complete architectural overhaul featuring consolidated single linkset design (74 prims total) with bulletproof link message communication, eliminating all channel conflicts and deployment complexity.
|
||||
|
||||
## Major v2.8.6 Improvements 🏆
|
||||
|
||||
### 🏆 **Winner Glow Celebration System**
|
||||
- **Revolutionary Victory Recognition**: Comprehensive winner celebration with bright green glow effects
|
||||
- **Scoreboard Glory**: Winner's profile picture and hearts glow bright green with green tint
|
||||
- **Floater Celebration**: Winner's floating display shows green glow with victory text "✨ ULTIMATE VICTORY! ✨"
|
||||
- **Visual Priority System**: Winner glow (green) overrides peril glow (yellow) during victory celebration
|
||||
- **Professional Polish**: Eliminated players show red X while winner gets the spotlight
|
||||
- **Extended Victory Moment**: 24-second celebration timer before game reset
|
||||
- **Before**: Winner announced → Instant reset → No recognition
|
||||
- **After**: Winner announced → Green glow celebration → 24 seconds of glory → Reset
|
||||
- **Player Experience**: Proper victory moment makes winning feel rewarding and celebrated
|
||||
|
||||
### 🎨 **Enhanced Visual Feedback System**
|
||||
- **Improved Glow Colors**: Professional-grade color tinting system
|
||||
- **Winner Recognition**: Bright green glow + green tint for unmistakable victory celebration
|
||||
- **Peril Identification**: Yellow glow + yellow tint for clear peril player status
|
||||
- **Visual Clarity**: Replaced generic white glow with distinct colored feedback
|
||||
- **Smart Winner Detection**: Floaters automatically detect victory conditions and display celebration text
|
||||
- **Synchronized Effects**: Both scoreboard and floater systems celebrate winner simultaneously
|
||||
|
||||
### 🔧 **System Optimization**
|
||||
- **Victory Timer System**: Dedicated timer mode for celebration delays without system interference
|
||||
- **Memory Efficiency**: Removed unused timeout components to free up memory resources
|
||||
- **Enhanced Communication**: Improved message routing for winner glow effects
|
||||
- **Clean Architecture**: Streamlined systems focus on essential functionality
|
||||
|
||||
### 🎯 **Before vs After v2.8.6**
|
||||
|| **Before** | **After** |
|
||||
||------------|----------|
|
||||
|| ❌ Winner announced but no visual celebration | ✅ Bright green glow celebration on scoreboard and floaters |
|
||||
|| ❌ Immediate reset after victory - no recognition | ✅ 24-second victory celebration timer for proper recognition |
|
||||
|| ❌ Generic white glow effects | ✅ Professional colored glow system (green/yellow/red) |
|
||||
|| ❌ Winners felt unrewarded | ✅ Ultimate Survivors get proper celebration and recognition |
|
||||
|
||||
## Major v2.8.5 Improvements 🚨
|
||||
|
||||
### 🧠 **Memory Optimization & Dead Code Cleanup**
|
||||
|
|
|
|||
|
|
@ -2,12 +2,14 @@
|
|||
========================================================
|
||||
|
||||
CREATED BY REBECCA NOD AND NOOSE THE BUNNY
|
||||
CURRENT VERSION: 2.8.5 - MEMORY OPTIMIZATION & ARCHITECTURE CLEANUP
|
||||
CURRENT VERSION: 2.8.6 - WINNER GLOW CELEBRATION SYSTEM
|
||||
|
||||
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.
|
||||
|
||||
NEW IN V2.8.6: Revolutionary winner recognition with bright green glow effects on both scoreboard and floaters, plus 24-second victory celebration timer giving proper recognition to the Ultimate Survivor before game reset.
|
||||
|
||||
NEW IN V2.8.5: Major system cleanup eliminating dead code, fixing bot profile picture flickering, and optimizing memory usage across all scripts with measurable performance improvements.
|
||||
|
||||
NEW IN V2.8.4: Revolutionary disconnect/reconnect recovery system eliminates need to kick players who disconnect during their turn, plus comprehensive system-wide verbose logging toggle for production vs development modes.
|
||||
|
|
@ -96,6 +98,40 @@ Player Count | Dice Type
|
|||
5–6 | d20
|
||||
7–10 | d30
|
||||
|
||||
RECENT IMPROVEMENTS (V2.8.6)
|
||||
============================
|
||||
|
||||
🏆 WINNER GLOW CELEBRATION SYSTEM
|
||||
- Revolutionary Victory Recognition: Comprehensive winner celebration with bright green glow effects
|
||||
* Scoreboard Glory: Winner's profile picture and hearts glow bright green with green tint
|
||||
* Floater Celebration: Winner's floating display shows green glow with victory text "ULTIMATE VICTORY!"
|
||||
* Visual Priority System: Winner glow (green) overrides peril glow (yellow) during victory celebration
|
||||
* Professional Polish: Eliminated players show red X while winner gets the spotlight
|
||||
- Extended Victory Moment: 24-second celebration timer before game reset
|
||||
* Before: Winner announced → Instant reset → No recognition
|
||||
* After: Winner announced → Green glow celebration → 24 seconds of glory → Reset
|
||||
* Player Experience: Proper victory moment makes winning feel rewarding and celebrated
|
||||
|
||||
🎨 ENHANCED VISUAL FEEDBACK SYSTEM
|
||||
- Improved Glow Colors: Professional-grade color tinting system
|
||||
* Winner Recognition: Bright green glow + green tint for unmistakable victory celebration
|
||||
* Peril Identification: Yellow glow + yellow tint for clear peril player status
|
||||
* Visual Clarity: Replaced generic white glow with distinct colored feedback
|
||||
- Smart Winner Detection: Floaters automatically detect victory conditions and display celebration text
|
||||
- Synchronized Effects: Both scoreboard and floater systems celebrate winner simultaneously
|
||||
|
||||
🔧 SYSTEM OPTIMIZATION
|
||||
- Victory Timer System: Dedicated timer mode for celebration delays without system interference
|
||||
- Memory Efficiency: Removed unused timeout components to free up memory resources
|
||||
- Enhanced Communication: Improved message routing for winner glow effects
|
||||
- Clean Architecture: Streamlined systems focus on essential functionality
|
||||
|
||||
🎯 BEFORE VS AFTER V2.8.6
|
||||
- Before: Winner announced but no visual celebration | After: Bright green glow celebration on scoreboard and floaters
|
||||
- Before: Immediate reset after victory - no recognition | After: 24-second victory celebration timer for proper recognition
|
||||
- Before: Generic white glow effects | After: Professional colored glow system (green/yellow/red)
|
||||
- Before: Winners felt unrewarded | After: Ultimate Survivors get proper celebration and recognition
|
||||
|
||||
RECENT IMPROVEMENTS (V2.8.5)
|
||||
============================
|
||||
|
||||
|
|
|
|||
|
|
@ -72,6 +72,19 @@ integer diceTypeProcessed = FALSE; // Prevent duplicate dice type processing
|
|||
// Verbose logging control - toggled by owner
|
||||
integer VERBOSE_LOGGING = FALSE;
|
||||
|
||||
// Memory reporting function
|
||||
reportMemoryUsage(string scriptName) {
|
||||
integer used = llGetUsedMemory();
|
||||
integer free = llGetFreeMemory();
|
||||
integer total = used + free;
|
||||
float percentUsed = ((float)used / (float)total) * 100.0;
|
||||
|
||||
llOwnerSay("🧠 [" + scriptName + "] Memory: " +
|
||||
(string)used + " used, " +
|
||||
(string)free + " free (" +
|
||||
llGetSubString((string)percentUsed, 0, 4) + "% used)");
|
||||
}
|
||||
|
||||
list getPicksFor(string nameInput) {
|
||||
integer i;
|
||||
for (i = 0; i < llGetListLength(picksData); i++) {
|
||||
|
|
@ -129,6 +142,8 @@ confetti() {
|
|||
|
||||
default {
|
||||
state_entry() {
|
||||
reportMemoryUsage("🎲 Roll Confetti Module");
|
||||
|
||||
// Initialize dynamic channels
|
||||
initializeChannels();
|
||||
rollDialogChannel = ROLLDIALOG_CHANNEL; // Set legacy variable
|
||||
|
|
@ -155,6 +170,7 @@ default {
|
|||
}
|
||||
|
||||
on_rez(integer start_param) {
|
||||
reportMemoryUsage("🎲 Roll Confetti Module");
|
||||
llOwnerSay("🔄 Roll Confetti Module rezzed - reinitializing...");
|
||||
|
||||
// Re-initialize dynamic channels
|
||||
|
|
@ -273,8 +289,8 @@ default {
|
|||
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);
|
||||
// Send continuation directly to Game Manager (more efficient than through Main Controller)
|
||||
llMessageLinked(LINK_SET, 998, "", NULL_KEY); // Empty peril player - Game Manager will select one
|
||||
} else {
|
||||
// Clear dice display directly (link 83)
|
||||
llMessageLinked(83, 3021, "", NULL_KEY);
|
||||
|
|
@ -394,6 +410,10 @@ default {
|
|||
|
||||
// Direct scoreboard status update
|
||||
llMessageLinked(12, 3001, "Plot Twist", NULL_KEY);
|
||||
|
||||
// IMPORTANT: Send peril player update to scoreboard for glow effect
|
||||
llMessageLinked(12, 3005, newPeril, NULL_KEY); // MSG_UPDATE_PERIL_PLAYER
|
||||
|
||||
// Add delay to let status display before next phase
|
||||
llSleep(2.0);
|
||||
|
||||
|
|
@ -450,36 +470,23 @@ default {
|
|||
|
||||
llSay(0, "🐻 PUNISHMENT TIME! " + perilPlayer + " has been ELIMINATED!");
|
||||
|
||||
// IMPORTANT: Update our local state to reflect the elimination
|
||||
// Remove the eliminated player from our names and lives lists to prevent stale data
|
||||
// CRITICAL FIX: DO NOT update our local state to remove the eliminated player
|
||||
// This would corrupt the sync message. Instead, just mark the player as having 0 lives
|
||||
// but keep them in the lists. Main Controller will handle the actual removal.
|
||||
integer elimIdx = llListFindList(names, [perilPlayer]);
|
||||
if (elimIdx != -1) {
|
||||
names = llDeleteSubList(names, elimIdx, elimIdx);
|
||||
lives = llDeleteSubList(lives, elimIdx, elimIdx);
|
||||
// Also clean up their picks data
|
||||
integer pickIdx = -1;
|
||||
integer p;
|
||||
// LSL doesn't support break, so use a flag instead
|
||||
integer foundEntry = FALSE;
|
||||
for (p = 0; p < llGetListLength(picksData) && !foundEntry; p++) {
|
||||
string entry = llList2String(picksData, p);
|
||||
if (llSubStringIndex(entry, perilPlayer + "|") == 0) {
|
||||
pickIdx = p;
|
||||
foundEntry = TRUE;
|
||||
}
|
||||
}
|
||||
if (pickIdx != -1) {
|
||||
picksData = llDeleteSubList(picksData, pickIdx, pickIdx);
|
||||
}
|
||||
llOwnerSay("🔄 [Roll Module] Updated local state - removed " + perilPlayer + " from names/lives/picks");
|
||||
// Set player's lives to 0 in our local state, but keep them in the lists
|
||||
lives = llListReplaceList(lives, [0], elimIdx, elimIdx);
|
||||
llOwnerSay("🔄 [Roll Module] Updated local state - set " + perilPlayer + "'s lives to 0 but kept in lists");
|
||||
llOwnerSay("🔄 [Roll Module] This ensures proper sync message format before Main Controller handles elimination");
|
||||
}
|
||||
|
||||
// Send message to main controller to handle elimination
|
||||
llMessageLinked(LINK_SET, 999, "ELIMINATE_PLAYER|" + perilPlayer, NULL_KEY);
|
||||
|
||||
// CRITICAL: Don't send any sync messages after elimination
|
||||
// Let Main Controller handle all state sync and peril player reassignment
|
||||
llOwnerSay("🎯 [Roll Module] Elimination complete - letting Main Controller handle state sync");
|
||||
// Let Main Controller handle all state sync, peril player reassignment, AND game continuation
|
||||
llOwnerSay("🎯 [Roll Module] Elimination complete - letting Main Controller handle state sync and continuation");
|
||||
|
||||
// Clear roll protection after processing is complete
|
||||
rollInProgress = FALSE;
|
||||
|
|
@ -490,14 +497,14 @@ default {
|
|||
|
||||
// Note: Win condition checking is handled by Main Controller after elimination
|
||||
|
||||
// IMPORTANT: After processing a roll, clear picks data for the next round
|
||||
// The round is complete, so we should send empty picks to trigger next round logic
|
||||
// IMPORTANT: After processing a roll, preserve the actual picks data that was used for validation
|
||||
// These picks will be used by Game Manager for building complete avoidance lists
|
||||
list encodedPicksData = [];
|
||||
integer k;
|
||||
for (k = 0; k < llGetListLength(names); k++) {
|
||||
string playerName = llList2String(names, k);
|
||||
// Send empty picks for all players since round is complete
|
||||
encodedPicksData += [playerName + "|"];
|
||||
for (k = 0; k < llGetListLength(picksData); k++) {
|
||||
string entry = llList2String(picksData, k);
|
||||
// Preserve the actual picks data that was used for validation
|
||||
encodedPicksData += [entry];
|
||||
}
|
||||
// Validate perilPlayer is not empty before syncing
|
||||
string perilForSync = perilPlayer;
|
||||
|
|
@ -521,12 +528,15 @@ default {
|
|||
llMessageLinked(LINK_SET, MSG_UPDATE_FLOAT, fname, NULL_KEY);
|
||||
}
|
||||
|
||||
// After roll is processed and sync is sent, start next round automatically
|
||||
llOwnerSay("🎯 Round complete, starting next round with peril player: " + perilPlayer);
|
||||
// After roll is processed and sync is sent, send continuation directly to Game Manager
|
||||
llOwnerSay("🎯 Round complete - sending continuation directly to Game Manager");
|
||||
|
||||
// Brief delay to ensure sync propagates, then start next round
|
||||
llSleep(1.0);
|
||||
llMessageLinked(LINK_SET, 997, "CONTINUE_ROUND|" + perilPlayer, NULL_KEY);
|
||||
// Brief delay to ensure sync propagates
|
||||
llSleep(0.5);
|
||||
|
||||
// Send continuation directly to Game Manager (more efficient than through Main Controller)
|
||||
// For non-elimination cases, continue with current peril player
|
||||
llMessageLinked(LINK_SET, 998, perilForSync, NULL_KEY);
|
||||
|
||||
// Clear roll protection after processing is complete
|
||||
rollInProgress = FALSE;
|
||||
|
|
|
|||
211
lsl_validator.py
211
lsl_validator.py
|
|
@ -63,6 +63,22 @@ class LSLValidator:
|
|||
'PI', 'PI_BY_TWO', 'TWO_PI', 'DEG_TO_RAD', 'RAD_TO_DEG',
|
||||
'LINK_ROOT', 'LINK_SET', 'LINK_ALL_OTHERS', 'LINK_ALL_CHILDREN', 'LINK_THIS',
|
||||
}
|
||||
|
||||
# Keywords that are definitely forbidden in LSL (conservative list)
|
||||
self.forbidden_keywords = {
|
||||
'break', 'continue', 'switch', 'case', 'goto',
|
||||
'class', 'struct', 'enum', 'union', 'namespace'
|
||||
}
|
||||
|
||||
# LSL data types
|
||||
self.lsl_types = {
|
||||
'integer', 'float', 'string', 'key', 'vector', 'rotation', 'list'
|
||||
}
|
||||
|
||||
# Scope tracking for variables
|
||||
self.global_vars = set() # Global variables
|
||||
self.local_vars = [] # Stack of local scopes
|
||||
self.current_function = None
|
||||
|
||||
def validate_file(self, filepath):
|
||||
"""Validate a single LSL file"""
|
||||
|
|
@ -77,6 +93,7 @@ class LSLValidator:
|
|||
|
||||
self._check_syntax(lines, filepath)
|
||||
self._check_style(lines, filepath)
|
||||
self._check_scope(lines, filepath)
|
||||
self._check_performance(content, filepath)
|
||||
self._check_memory_usage(content, filepath)
|
||||
|
||||
|
|
@ -135,9 +152,203 @@ class LSLValidator:
|
|||
and '}' not in line and not line.startswith('state') and not line.startswith('default')):
|
||||
self.warnings.append(f"Line {line_num}: Possible missing semicolon")
|
||||
|
||||
# Check for forbidden LSL keywords
|
||||
self._check_forbidden_keywords(line, line_num)
|
||||
|
||||
# Final brace balance check
|
||||
if brace_count != 0:
|
||||
self.errors.append(f"Unmatched braces: {brace_count} excess opening braces" if brace_count > 0 else f"{-brace_count} missing opening braces")
|
||||
|
||||
def _check_forbidden_keywords(self, line, line_num):
|
||||
"""Check for keywords that are forbidden in LSL"""
|
||||
# Skip comments
|
||||
if '//' in line:
|
||||
line = line[:line.index('//')]
|
||||
|
||||
# Remove strings to avoid false positives
|
||||
# Simple approach: remove content between quotes
|
||||
import re
|
||||
line_no_strings = re.sub(r'"[^"]*"', '""', line)
|
||||
|
||||
# Check each forbidden keyword
|
||||
for keyword in self.forbidden_keywords:
|
||||
# Use word boundaries to avoid matching substrings
|
||||
pattern = r'\b' + re.escape(keyword) + r'\b'
|
||||
if re.search(pattern, line_no_strings):
|
||||
self.errors.append(f"Line {line_num}: '{keyword}' is not supported in LSL")
|
||||
|
||||
def _check_scope(self, lines, filepath):
|
||||
"""Check for variable scope issues"""
|
||||
# Reset scope tracking
|
||||
self.global_vars = set()
|
||||
self.local_vars = []
|
||||
self.current_function = None
|
||||
|
||||
in_function = False
|
||||
brace_depth = 0
|
||||
|
||||
for line_num, line in enumerate(lines, 1):
|
||||
original_line = line
|
||||
line = line.strip()
|
||||
|
||||
# Skip empty lines and comments
|
||||
if not line or line.startswith('//') or line.startswith('/*'):
|
||||
continue
|
||||
|
||||
# Remove comments from line
|
||||
if '//' in line:
|
||||
line = line[:line.index('//')]
|
||||
line = line.strip()
|
||||
|
||||
# Track brace depth for scope management
|
||||
open_braces = line.count('{')
|
||||
close_braces = line.count('}')
|
||||
|
||||
# Check for function definitions
|
||||
if not in_function and ('(' in line and ')' in line and '{' in line):
|
||||
# This might be a function definition
|
||||
if any(event in line for event in ['state_entry', 'touch_start', 'listen', 'timer', 'on_rez', 'link_message', 'http_response']):
|
||||
in_function = True
|
||||
self.local_vars.append(set()) # New local scope
|
||||
|
||||
# Handle scope changes
|
||||
if open_braces > 0:
|
||||
for _ in range(open_braces):
|
||||
brace_depth += 1
|
||||
if in_function and brace_depth > 1: # Nested scope within function
|
||||
self.local_vars.append(set())
|
||||
|
||||
# Check for variable declarations
|
||||
self._check_variable_declaration(line, line_num, in_function)
|
||||
|
||||
# Check for variable usage
|
||||
self._check_variable_usage(line, line_num)
|
||||
|
||||
# Handle closing braces
|
||||
if close_braces > 0:
|
||||
for _ in range(close_braces):
|
||||
brace_depth -= 1
|
||||
if in_function and len(self.local_vars) > 1:
|
||||
self.local_vars.pop() # Exit nested scope
|
||||
elif brace_depth == 0 and in_function:
|
||||
# Exiting function
|
||||
in_function = False
|
||||
self.local_vars.clear()
|
||||
|
||||
def _check_variable_declaration(self, line, line_num, in_function):
|
||||
"""Check for variable declarations"""
|
||||
import re
|
||||
|
||||
# Remove string literals to avoid false matches
|
||||
line_no_strings = re.sub(r'"[^"]*"', '""', line)
|
||||
|
||||
# Pattern to match LSL variable declarations: type varname;
|
||||
for lsl_type in self.lsl_types:
|
||||
# Handle special case of for loop declarations: for(integer i = 0; ...)
|
||||
for_loop_pattern = rf'for\s*\(\s*{lsl_type}\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*='
|
||||
for_matches = re.findall(for_loop_pattern, line_no_strings)
|
||||
|
||||
for var_name in for_matches:
|
||||
if var_name.startswith('ll') or var_name in self.lsl_functions:
|
||||
continue
|
||||
# For loop variables are always local and shouldn't conflict with globals
|
||||
if len(self.local_vars) > 0:
|
||||
self.local_vars[-1].add(var_name)
|
||||
continue
|
||||
|
||||
# Regular variable declarations: type identifier; or type identifier =
|
||||
# Skip for loop variable declarations by checking the line content
|
||||
if 'for(' not in line_no_strings.replace(' ', '') and 'for (' not in line_no_strings:
|
||||
pattern = rf'\b{lsl_type}\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*[;=]'
|
||||
matches = re.findall(pattern, line_no_strings)
|
||||
else:
|
||||
matches = []
|
||||
|
||||
for var_name in matches:
|
||||
# Skip LSL built-in functions that might match the pattern
|
||||
if var_name.startswith('ll') or var_name in self.lsl_functions:
|
||||
continue
|
||||
|
||||
# Skip common parameter names that aren't real declarations
|
||||
if var_name in {'total_number', 'sender', 'num', 'str', 'id', 'start_param',
|
||||
'channel', 'name', 'msg', 'message', 'request_id', 'status', 'metadata', 'body'}:
|
||||
continue
|
||||
|
||||
if in_function:
|
||||
# Check if already declared in current local scope
|
||||
if len(self.local_vars) > 0 and var_name in self.local_vars[-1]:
|
||||
self.errors.append(f"Line {line_num}: Variable '{var_name}' already declared in this scope")
|
||||
else:
|
||||
# Add to current local scope
|
||||
if len(self.local_vars) > 0:
|
||||
self.local_vars[-1].add(var_name)
|
||||
else:
|
||||
# Global variable - only flag if it's a real redeclaration
|
||||
if var_name in self.global_vars:
|
||||
# Only error if this looks like a real declaration line
|
||||
if ';' in line or '=' in line:
|
||||
self.errors.append(f"Line {line_num}: Global variable '{var_name}' already declared")
|
||||
else:
|
||||
self.global_vars.add(var_name)
|
||||
|
||||
def _check_variable_usage(self, line, line_num):
|
||||
"""Check for usage of undefined variables (conservative approach)"""
|
||||
import re
|
||||
|
||||
# Skip variable declarations and function definitions
|
||||
if any(lsl_type in line for lsl_type in self.lsl_types):
|
||||
return
|
||||
|
||||
# Skip comments and string literals
|
||||
if line.strip().startswith('//') or line.strip().startswith('/*') or '"' in line:
|
||||
return
|
||||
|
||||
# Skip lines that are clearly not variable usage
|
||||
if (line.startswith('state') or line.startswith('#') or line.strip() == 'default' or
|
||||
'llOwnerSay' in line or 'llSay' in line or 'llMessageLinked' in line or
|
||||
('{' in line and '}' in line and line.count('{') == line.count('}'))):
|
||||
return
|
||||
|
||||
# Only check very specific patterns where we're confident it's a variable usage
|
||||
# Pattern 1: variable assignment: varname = something
|
||||
assignment_vars = re.findall(r'([a-zA-Z_][a-zA-Z0-9_]*)\s*=', line)
|
||||
|
||||
# Pattern 2: variable in arithmetic: something + varname or varname + something
|
||||
arithmetic_vars = re.findall(r'(?:^|[+\-*/=\s])([a-zA-Z_][a-zA-Z0-9_]*)(?:\s*[+\-*/]|\s*;|$)', line)
|
||||
|
||||
# Combine patterns
|
||||
potential_vars = set(assignment_vars + arithmetic_vars)
|
||||
|
||||
for var_name in potential_vars:
|
||||
# Skip LSL functions, constants, keywords, and common false positives
|
||||
if (var_name in self.lsl_functions or var_name in self.lsl_constants or
|
||||
var_name in self.lsl_types or var_name.startswith('ll') or
|
||||
var_name in {'if', 'else', 'for', 'while', 'do', 'return', 'jump', 'state', 'default',
|
||||
'total_number', 'sender', 'num', 'str', 'id', 'start_param', 'channel',
|
||||
'name', 'msg', 'message', 'request_id', 'status', 'metadata', 'body',
|
||||
# Common English words that show up in strings/comments
|
||||
'the', 'and', 'or', 'not', 'has', 'been', 'with', 'from', 'this', 'that',
|
||||
'will', 'can', 'may', 'all', 'any', 'some', 'new', 'old', 'get', 'set'} or
|
||||
var_name.isdigit() or len(var_name) <= 2):
|
||||
continue
|
||||
|
||||
# Check if variable is defined
|
||||
is_defined = var_name in self.global_vars
|
||||
|
||||
# Check local scopes (from innermost to outermost)
|
||||
if not is_defined:
|
||||
for scope in reversed(self.local_vars):
|
||||
if var_name in scope:
|
||||
is_defined = True
|
||||
break
|
||||
|
||||
# Only warn for very likely variable usage patterns
|
||||
if not is_defined:
|
||||
# Must be lowercase and used in assignment or arithmetic context
|
||||
if (var_name.islower() and
|
||||
(var_name in assignment_vars or
|
||||
(var_name in arithmetic_vars and any(op in line for op in ['+', '-', '*', '/'])))):
|
||||
self.warnings.append(f"Line {line_num}: Variable '{var_name}' may not be defined")
|
||||
|
||||
def _check_style(self, lines, filepath):
|
||||
"""Check for style and best practice issues"""
|
||||
|
|
|
|||
Loading…
Reference in a new issue