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:
RebeccaNod1 2025-09-16 13:10:41 -05:00
parent dd03bd8eef
commit 03c5d81bb0
23 changed files with 1484 additions and 585 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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, ["|"], []);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 = [];

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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