v3.2.0: The 'Sync Sanctity' Update - Unified life sync, virtualized scoreboard, and documentation finalization

This commit is contained in:
RebeccaNod1 2026-04-05 09:31:37 -06:00
parent a225244241
commit 824824c32d
15 changed files with 419 additions and 149 deletions

View file

@ -4,6 +4,23 @@
All notable changes to Peril Dice will be documented in this file. All notable changes to Peril Dice will be documented in this file.
## [3.2.0] - 2026-04-05
### 🛡️ **Sync Sanctity (Engine Core)**
- **Master Ledger Protocol**: Refactored the core synchronization engine to use the `Main_Controller` as the single source of truth for all player data.
- **Ghost Heal Elimination**: Permanently resolved the life-count desync between the `Roll_ConfettiModule` and the Controller by implementing a unified damage reporting system.
- **Victory Status Lock**: Added a `victoryInProgress` semaphore to protect the "Ultimate Victory" banner from being overwritten by final elimination messages.
- **Furware Compatibility Patch**: Switched to high-reliability ASCII `===` banners for status displays to ensure 100% rendering across all SL viewers.
### 🎰 **Virtualized Scoreboard**
- **3-Column Grid Management**: Re-organized Rank, Name, and Win/Loss records into a professional multi-column leaderboard format.
- **Dynamic Slot Hiding**: Implemented automatic **Alpha 0.0** (Invisibility) for unused player slots (Slots 7-10 in a 6-player session) to maintain a clean aesthetic.
- **Diagnostic Toolset**: Added `/1 testx` (Show All) and `/1 clearx` (Reset Alpha) for linkset layout verification.
### 🏹 **Human-Bot Parity (Shield Patch)**
- **Multi-Delimiter Parser**: Refactored pick-string analysis to support `,`, `;`, `|`, and spaces, ensuring human dialog picks are matched as accurately as automated bot picks.
- **Reconnect Resilience**: Enhanced `MSG_SYNC_GAME_STATE` listeners in all secondary modules to support immediate state recovery for crashed or reconnected players.
## [3.1.0] - 2026-04-04 ## [3.1.0] - 2026-04-04
### 🔍 **Dynamic Link Discovery System** ### 🔍 **Dynamic Link Discovery System**

View file

@ -3,6 +3,24 @@ PERIL DICE CHANGELOG
All notable changes to Peril Dice are documented in this file. All notable changes to Peril Dice are documented in this file.
[3.2.0] - 2026-04-05
====================
🛡️ SYNC SANCTITY (ENGINE CORE)
- Master Ledger Protocol: Refactored the core synchronization engine to use the Main_Controller as the single source of truth for all player data.
- Ghost Heal Elimination: Permanently resolved the life-count desync between modules by implementing a unified damage reporting system (MSG_UPDATE_LIFE).
- Victory Status Lock: Added a victoryInProgress semaphore to protect the "Ultimate Victory" banner from being overwritten.
- Furware Compatibility Patch: Switched to high-reliability ASCII === banners for status displays.
🎰 VIRTUALIZED SCOREBOARD
- 3-Column Grid Management: Re-organized Rank, Name, and Win/Loss records into a professional multi-column leaderboard format.
- Dynamic Slot Hiding: Implemented automatic Alpha 0.0 (Invisibility) for unused player slots.
- Diagnostic Toolset: Added /1 testx (Show All) and /1 clearx (Reset Alpha) for layout verification.
🏹 HUMAN-BOT PARITY
- Multi-Delimiter Parser: Refactored pick-string analysis to support comma, semicolon, pipe, and space.
- Reconnect Resilience: Enhanced MSG_SYNC_GAME_STATE listeners for immediate state recovery.
[3.1.0] - 2026-04-04 [3.1.0] - 2026-04-04
==================== ====================

View file

@ -2,10 +2,21 @@
string leftText = ""; string leftText = "";
// Distribute text to the FURWARE "Leaderboard" box // Distribute text to the FURWARE "Leaderboard" boxes
distributeFullText(string text) { distributeFullText(string text) {
// Send directly to FURWARE. It handles multiple prims and newlines automatically. if (llGetSubString(text, 0, 7) == "COLUMNS|") {
llMessageLinked(LINK_SET, 0, text, (key)(FW_DATA + ":Leaderboard")); // Use llParseStringKeepNulls to ensure empty columns don't break the registration logic
list parts = llParseStringKeepNulls(llGetSubString(text, 8, -1), ["|"], []);
if (llGetListLength(parts) >= 3) {
// Distribute each column to its virtual box
llMessageLinked(LINK_SET, 0, llList2String(parts, 0), (key)(FW_DATA + ":LB_Rank"));
llMessageLinked(LINK_SET, 0, llList2String(parts, 1), (key)(FW_DATA + ":LB_Name"));
llMessageLinked(LINK_SET, 0, llList2String(parts, 2), (key)(FW_DATA + ":LB_WL"));
}
} else {
// Fallback for legacy text or titles (sent to parent box)
llMessageLinked(LINK_SET, 0, text, (key)(FW_DATA + ":Leaderboard"));
}
} }
default { default {
@ -17,29 +28,40 @@ default {
state_entry() { state_entry() {
DISCOVER_CORE_LINKS(); DISCOVER_CORE_LINKS();
REPORT_MEMORY(); REPORT_MEMORY();
dbg("📋 [Leaderboard] Leaderboard Communication Script ready! (FURWARE Version)"); dbg("📋 [Leaderboard] Leaderboard Communication Script ready! (Virtual Multi-Column)");
dbg("📋 [Leaderboard] Managing 'Leaderboard' FURWARE box");
dbg("📋 [Leaderboard] ✅ Discovery complete! Scoreboard: " + (string)LINK_SCOREBOARD); // Wait for engine to be ready, then register virtual columns
llSleep(1.0);
// Registering virtual columns inside the physical "Leaderboard" mesh
// Format: fw_addbox:NewName:ParentName:x,y,w,h
llMessageLinked(LINK_SET, 0, "", (key)("fw_addbox:LB_Rank:Leaderboard:0,1,4,11"));
llMessageLinked(LINK_SET, 0, "", (key)("fw_addbox:LB_Name:Leaderboard:4,1,20,11"));
llMessageLinked(LINK_SET, 0, "", (key)("fw_addbox:LB_WL:Leaderboard:24,1,8,11"));
dbg("📋 [Leaderboard] Virtual column registration requests sent.");
// --- STARTUP HANDSHAKE ---
// Wait a moment for the engine to process ADD_BOX, then ask Scoreboard for a fresh update
llSleep(0.5);
llMessageLinked(LINK_SET, MSG_DISPLAY_LEADERBOARD, "REFRESH_STARTUP", NULL_KEY);
// Start with blank display // Start with blank display
distributeFullText(""); distributeFullText("");
} }
link_message(integer sender, integer num, string str, key id) { link_message(integer sender, integer num, string str, key id) {
// Only listen to messages from the scoreboard // Handle messages from the game logic
if (sender != LINK_SCOREBOARD) { if (num == MSG_DISPLAY_LEADERBOARD) {
return;
}
if (num == MSG_RESET_LEADERBOARD) {
// FURWARE handles the entire leaderboard as a single text block
if (llGetSubString(str, 0, 14) == "FORMATTED_TEXT|") { if (llGetSubString(str, 0, 14) == "FORMATTED_TEXT|") {
distributeFullText(llGetSubString(str, 15, -1)); distributeFullText(llGetSubString(str, 15, -1));
} } else if (llGetSubString(str, 0, 9) == "LEFT_TEXT|") {
// For individual bank updates // For individual bank updates
else if (llGetSubString(str, 0, 9) == "LEFT_TEXT|") {
leftText = llGetSubString(str, 10, -1); leftText = llGetSubString(str, 10, -1);
distributeFullText(leftText); distributeFullText(leftText);
} else {
// Pass directly for COLUMNS| or multi-column data
distributeFullText(str);
} }
} }
else if (llSubStringIndex(str, "LEADERBOARD") == 0) { else if (llSubStringIndex(str, "LEADERBOARD") == 0) {

View file

@ -0,0 +1,44 @@
#include "Peril_Constants.lsl"
// =============================================================================
// FURWARE STATUS BRIDGE - Dynamic Game Info Display
// =============================================================================
// Function to update the Furware "Status" box
updateStatusDisplay(string text) {
// Send to FURWARE Text engine
// We use <!c=darkyellow> to set the color to dark yellow
string formattedText = "<!c=darkyellow>" + text;
// Send directly to the Status box
llMessageLinked(LINK_SET, 0, formattedText, (key)(FW_DATA + ":Status"));
}
default {
on_rez(integer start_param) {
DISCOVER_CORE_LINKS();
llResetScript();
}
state_entry() {
DISCOVER_CORE_LINKS();
REPORT_MEMORY();
dbg("📊 [Status Bridge] Status Display Bridge ready!");
// Wait a tiny bit for Furware to be ready, then set initial text
llSleep(1.0);
updateStatusDisplay("PERIL DICE GAME\nWaiting for players...");
}
link_message(integer sender, integer num, string str, key id) {
// Listen for status text updates
if (num == MSG_STATUS_TEXT) {
updateStatusDisplay(str);
dbg("📊 [Status Bridge] Update: " + str);
}
// Handle full resets
else if (num == MSG_RESET_ALL || num == MSG_CLEAR_GAME) {
updateStatusDisplay("PERIL DICE GAME\nWaiting for players...");
}
}
}

View file

@ -91,6 +91,13 @@ default {
perilPlayer = ""; perilPlayer = "";
dbg("💬 [Floater Manager] 📦 Floater Manager ready!"); dbg("💬 [Floater Manager] 📦 Floater Manager ready!");
// GHOST SWEEP: Immediately attempt to clear any floaters on startup
// This clears orphans if the script was reset while players were active
integer ghostSweep;
for (ghostSweep = 0; ghostSweep < MAX_PLAYERS; ghostSweep++) {
llRegionSay(FLOATER_BASE_CHANNEL + ghostSweep, "CLEANUP");
}
} }
on_rez(integer start_param) { on_rez(integer start_param) {

View file

@ -183,7 +183,7 @@ The game features thematic messaging for maximum immersion:
- **Professional Status Actions** - Large status prim shows current game events - **Professional Status Actions** - Large status prim shows current game events
- **Color-Coded Glow System** - Green (winner), Yellow (peril), Red X (eliminated) - **Color-Coded Glow System** - Green (winner), Yellow (peril), Red X (eliminated)
### **Technical Rules (UPDATED in v3.1.0):** ### **Technical Rules (UPDATED in v3.2.0):**
- **Dynamic Link Discovery** - Scripts find their targets by name; reordering prims won't break the game 🔍 - **Dynamic Link Discovery** - Scripts find their targets by name; reordering prims won't break the game 🔍
- **FURWARE Text Mesh Integration** - Professional mesh-based displays for dice and leaderboard 🎨 - **FURWARE Text Mesh Integration** - Professional mesh-based displays for dice and leaderboard 🎨
- **Centralized Constants** - System-wide settings managed in `Peril_Constants.lsl` 🧩 - **Centralized Constants** - System-wide settings managed in `Peril_Constants.lsl` 🧩
@ -296,7 +296,8 @@ While the core rules remain the same, consider these optional variations:
--- ---
**Version:** 3.1.0 **Version:** 3.2.0
**Last Updated:** April 04, 2026 **Last Updated:** April 05, 2026
**Game System:** Production Ready - Linkset Stability Release **Status:** Production Ready - Sync Sanctity Release
**Credits:** Original concept by Noose the Bunny (djmusica28), Automated system implementation by Rebecca Nod **Credits:** Original concept by Noose the Bunny (djmusica28), Automated system implementation by Rebecca Nod

View file

@ -1,7 +1,16 @@
PERIL DICE GAME - OFFICIAL RULES PERIL DICE GAME - OFFICIAL RULES
========================================================
Created by Rebecca Nod and Noose the Bunny CREATED BY REBECCA NOD AND NOOSE THE BUNNY (djmusica28)
Original concept by Noose the Bunny (djmusica28) in Second Life CURRENT VERSION: 3.2.0 - THE "SYNC SANCTITY" UPDATE
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 V3.2.0: SYNC SANCTITY & SCOREBOARD VIRTUALIZATION! Total refactoring of the synchronization engine. The Main Controller is now the single source of truth for life counts, permanently eliminating "Ghost Heals." The scoreboard is now "Virtualized"—supporting a professional 3-column Rank/Name/Wins/Losses layout and dynamic Alpha 0.0 hiding for unused player slots.
NEW IN V3.1.0: LINKSET STABILITY & DYNAMIC DISCOVERY! Complete architectural decentralization—scripts now find their linked prims by name, making the game board immune to link-order changes or region-side glitches. Full migration from legacy XYText to modern mesh-based FURWARE Text for dice and leaderboards. Centralized system constants in Peril_Constants.lsl for 100% project consistency.
=============================================================================== ===============================================================================
@ -188,10 +197,11 @@ Enhanced Display System (UPDATED in v2.8.6):
- Professional Status Actions - Large status prim shows current events - Professional Status Actions - Large status prim shows current events
- Color-Coded Glow System - Green (winner), Yellow (peril), Red X (eliminated) - Color-Coded Glow System - Green (winner), Yellow (peril), Red X (eliminated)
Technical Rules (UPDATED in v3.1.0): Technical Rules (UPDATED in v3.2.0):
- Dynamic Link Discovery - Scripts find their targets by name; reordering prims won't break the game 🔍 - Sync Sanctity Protocol - 100% unified life counting via the Main Controller 🛡️
- FURWARE Text Mesh Integration - Professional mesh-based displays for dice and leaderboard 🎨 - Scoreboard Virtualization - Dynamic slot hiding and multi-column Rank/Name/WL grid 🎰
- Centralized Constants - System-wide settings managed in Peril_Constants.lsl 🧩 - Dynamic Link Discovery - Unified name-based mapping for Linkset Stability 🔍
- Human-Bot Parity - Delimiter-agnostic picking logic for total shield consistency 🏹
- Game supports up to 10 players maximum - Game supports up to 10 players maximum
- Owner kick functionality for managing disruptive players - Owner kick functionality for managing disruptive players
- Automatic game reset on startup (preserves leaderboard) - Automatic game reset on startup (preserves leaderboard)
@ -302,9 +312,9 @@ Dice! May luck and strategy guide you to Ultimate Victory!
=============================================================================== ===============================================================================
Version: 3.1.0 Version: 3.2.0
Last Updated: April 04, 2026 Last Updated: April 05, 2026
Game System: Production Ready - Linkset Stability Release Status: Production Ready - Sync Sanctity Release
Credits: Original concept by Noose the Bunny (djmusica28), Credits: Original concept by Noose the Bunny (djmusica28),
Automated system implementation by Rebecca Nod Automated system implementation by Rebecca Nod

View file

@ -159,6 +159,18 @@ integer continueCurrentRound() {
// Request dice type directly from Calculator for the new round // Request dice type directly from Calculator for the new round
llMessageLinked(LINK_SET, MSG_GET_DICE_TYPE, (string)llGetListLength(names), NULL_KEY); llMessageLinked(LINK_SET, MSG_GET_DICE_TYPE, (string)llGetListLength(names), NULL_KEY);
// NEW: Update status bar for new round initialization
llMessageLinked(LINK_SET, MSG_STATUS_TEXT, "PREPARING ROUND...\nCalculating dice size...", NULL_KEY);
// Dramatic pause to let the preparation message be read
llSleep(4.0);
// NEW: Update status bar for new round with colored highlight
llMessageLinked(LINK_SET, MSG_STATUS_TEXT, "NEW ROUND STARTING!\nPeril: <!c=red>" + perilPlayer, NULL_KEY);
// Dramatic pause to let the peril player be seen before the dice type is shown
llSleep(4.0);
// Note: showNextPickerDialog() will be called when dice type result is received // Note: showNextPickerDialog() will be called when dice type result is received
return 0; return 0;
} }
@ -184,6 +196,10 @@ integer startNextRound() {
llMessageLinked(LINK_SET, MSG_PLAYER_WON, winner, NULL_KEY); llMessageLinked(LINK_SET, MSG_PLAYER_WON, winner, NULL_KEY);
llMessageLinked(LINK_SET, MSG_EFFECT_CONFETTI, "VICTORY_CONFETTI", NULL_KEY); llMessageLinked(LINK_SET, MSG_EFFECT_CONFETTI, "VICTORY_CONFETTI", NULL_KEY);
// NEW: Update status bar with 2-line victory message
llMessageLinked(LINK_SET, MSG_STATUS_TEXT, "ULTIMATE VICTORY!\n" + winner + " is the survivor!", NULL_KEY);
// Don't use hardcoded channel here - let Main Controller handle scoreboard updates // Don't use hardcoded channel here - let Main Controller handle scoreboard updates
// Main Controller will send the proper GAME_WON message to scoreboard // Main Controller will send the proper GAME_WON message to scoreboard
// Let main controller handle reset // Let main controller handle reset
@ -214,6 +230,12 @@ integer startNextRound() {
string startPlayerName = llList2String(names, startJ); string startPlayerName = llList2String(names, startJ);
llMessageLinked(LINK_SET, MSG_UPDATE_FLOAT, startPlayerName, NULL_KEY); llMessageLinked(LINK_SET, MSG_UPDATE_FLOAT, startPlayerName, NULL_KEY);
} }
// NEW: Update status bar with 2-line round start message and color highlight
llMessageLinked(LINK_SET, MSG_STATUS_TEXT, "ROUND STARTING!\nPeril: <!c=red>" + perilPlayer, NULL_KEY);
// Dramatic pause to let the first peril player be seen before round prep begins
llSleep(4.0);
} }
picksData = []; picksData = [];
@ -234,6 +256,13 @@ integer startNextRound() {
// Request dice type for this round // Request dice type for this round
llMessageLinked(LINK_SET, MSG_GET_DICE_TYPE, (string)llGetListLength(names), NULL_KEY); llMessageLinked(LINK_SET, MSG_GET_DICE_TYPE, (string)llGetListLength(names), NULL_KEY);
// NEW: Update status bar for new round initialization
llMessageLinked(LINK_SET, MSG_STATUS_TEXT, "PREPARING ROUND...\nCalculating dice size...", NULL_KEY);
// Dramatic pause to let the preparation message be read
llSleep(4.0);
dbg("🎯 Game Manager round setup complete, requesting dice type..."); dbg("🎯 Game Manager round setup complete, requesting dice type...");
return 0; return 0;
} }
@ -348,6 +377,9 @@ integer showNextPickerDialog() {
dbg("🎯 [Game Manager] Sending bot command with complete avoid list (" + (string)llGetListLength(showCompleteAvoidList) + " numbers): " + showAvoidListStr); dbg("🎯 [Game Manager] Sending bot command with complete avoid list (" + (string)llGetListLength(showCompleteAvoidList) + " numbers): " + showAvoidListStr);
llMessageLinked(LINK_SET, MSG_BOT_COMMAND, botCommand, NULL_KEY); llMessageLinked(LINK_SET, MSG_BOT_COMMAND, botCommand, NULL_KEY);
dbg("🤖 " + firstName + " is automatically picking " + (string)showPicksNeeded + " numbers..."); dbg("🤖 " + firstName + " is automatically picking " + (string)showPicksNeeded + " numbers...");
// NEW: Update status bar for bot picking
llMessageLinked(LINK_SET, MSG_STATUS_TEXT, "BOTS CALCULATING...\n" + firstName + " is deciding...", NULL_KEY);
} else { } else {
// Bot already has picks, advance to next player immediately // Bot already has picks, advance to next player immediately
currentPickerIdx++; currentPickerIdx++;
@ -379,6 +411,9 @@ integer showNextPickerDialog() {
// Send dialog request through Player_RegistrationManager (it has the correct player keys) // Send dialog request through Player_RegistrationManager (it has the correct player keys)
string dialogRequest = "SHOW_DIALOG|" + firstName + "|" + dialogPayload; string dialogRequest = "SHOW_DIALOG|" + firstName + "|" + dialogPayload;
llMessageLinked(LINK_SET, MSG_DIALOG_FORWARD_REQUEST, dialogRequest, NULL_KEY); llMessageLinked(LINK_SET, MSG_DIALOG_FORWARD_REQUEST, dialogRequest, NULL_KEY);
// NEW: Update status bar for human picking
llMessageLinked(LINK_SET, MSG_STATUS_TEXT, "PICKING NUMBERS...\n" + firstName + " is choosing...", NULL_KEY);
} }
return 0; return 0;
} }
@ -637,7 +672,12 @@ default {
diceType = (integer)str; diceType = (integer)str;
diceTypeProcessed = TRUE; diceTypeProcessed = TRUE;
dbg("🎲 Game Manager received dice type: d" + str + " from Calculator"); dbg("🎲 Game Manager received dice type: d" + str + " from Calculator");
llSleep(0.3); // Brief delay
// NEW: Update status bar when dice is ready
llMessageLinked(LINK_SET, MSG_STATUS_TEXT, "DICE READY!\nRolling a d" + str + " this round", NULL_KEY);
// Dramatic pause to let players see the dice type before picking starts
llSleep(4.0);
// Start dialog if round has been started and we have a queue // Start dialog if round has been started and we have a queue
if (roundStarted && currentPickerIdx < llGetListLength(pickQueue) && llGetListLength(pickQueue) > 0 && diceType > 0) { if (roundStarted && currentPickerIdx < llGetListLength(pickQueue) && llGetListLength(pickQueue) > 0 && diceType > 0) {

View file

@ -51,12 +51,15 @@ discoverLinks() {
} }
dbg("Discovery done"); dbg("Discovery done");
if (BACKGROUND_PRIM == -1) dbg("No backboard"); if (BACKGROUND_PRIM == -1) { dbg("No backboard"); }
// Safety Fallback: Use Root if Scoreboard prim is missing
if (LINK_SCOREBOARD == -1) LINK_SCOREBOARD = 1;
} }
// --- Leaderboard Configuration --- // --- Leaderboard Configuration ---
#define LEADERBOARD_WIDTH 32 // Standard width for 4x8 character Furware grids (4 columns of 8) #define LEADERBOARD_WIDTH 32 // Standard width for 4x8 character Furware grids (4 columns of 8)
#define MAX_LEADERBOARD_ENTRIES 11 // Fits within 12-row board (+1 for title) #define MAX_LEADERBOARD_ENTRIES 11 // Fits within 11 remaining rows (after 1-row header)
// Heart texture UUIDs - REPLACE WITH YOUR ACTUAL TEXTURE UUIDs // Heart texture UUIDs - REPLACE WITH YOUR ACTUAL TEXTURE UUIDs
#define TEXTURE_0_HEARTS "7d8ae121-e171-12ae-f5b6-7cc3c0395c7b" // 0 hearts (dead) #define TEXTURE_0_HEARTS "7d8ae121-e171-12ae-f5b6-7cc3c0395c7b" // 0 hearts (dead)
@ -281,71 +284,58 @@ integer generateLeaderboardText() {
list sortedData = getSortedLeaderboard(); list sortedData = getSortedLeaderboard();
string SPACES = " "; string SPACES = " ";
string title = "TOP BATTLE RECORDS"; string title = "TOP BATTLE RECORDS";
integer titleMargin = 0; string rankCol = "";
string leaderboardText = ""; string nameCol = "";
integer genI = 0; string statsCol = "";
string playerName = "";
integer genWins = 0;
integer genLosses = 0;
string rank = "";
string leftPart = "";
string rightPart = "";
integer spaceNeeded = 0;
integer numSorted = llGetListLength(sortedData) / 3; integer numSorted = llGetListLength(sortedData) / 3;
integer base = 0; integer genI;
string sWins = ""; integer base;
string sLosses = ""; string playerName;
integer genWins;
integer genLosses;
string rank;
string sWins;
string sLosses;
// 1. Build Title Row (Centered) // 1. Send Title to Parent Box (Row 0)
titleMargin = (LEADERBOARD_WIDTH - llStringLength(title)) / 2; integer titleMargin = (LEADERBOARD_WIDTH - llStringLength(title)) / 2;
if (titleMargin < 0) titleMargin = 0; if (titleMargin < 0) titleMargin = 0;
string titleText = llGetSubString(SPACES, 0, titleMargin - 1) + title;
llMessageLinked(LINK_SET, MSG_DISPLAY_LEADERBOARD, "FORMATTED_TEXT|" + titleText, NULL_KEY);
leaderboardText = llGetSubString(SPACES, 0, titleMargin - 1) + title + "\n"; // Add actual player data column-by-column
// Add actual player data (up to MAX_LEADERBOARD_ENTRIES)
for (genI = 0; genI < numSorted && genI < MAX_LEADERBOARD_ENTRIES; genI++) { for (genI = 0; genI < numSorted && genI < MAX_LEADERBOARD_ENTRIES; genI++) {
base = genI * 3; base = genI * 3;
playerName = llList2String(sortedData, base); playerName = llList2String(sortedData, base);
genWins = llList2Integer(sortedData, base + 1); genWins = llList2Integer(sortedData, base + 1);
genLosses = llList2Integer(sortedData, base + 2); genLosses = llList2Integer(sortedData, base + 2);
if (llStringLength(playerName) > 12) { if (llStringLength(playerName) > 19) playerName = llGetSubString(playerName, 0, 16) + "...";
playerName = llGetSubString(playerName, 0, 9) + "...";
}
// Format rank: "01. " through "10. " rank = (string)(genI + 1) + ".";
rank = (string)(genI + 1) + ". ";
if (genI < 9) rank = "0" + rank; if (genI < 9) rank = "0" + rank;
// Format stats 10-char width: "W: 99/L: 9"
sWins = (string)genWins; sWins = (string)genWins;
if (llStringLength(sWins) < 3) sWins = llGetSubString(" ", 0, 2 - llStringLength(sWins)) + sWins;
sLosses = (string)genLosses; sLosses = (string)genLosses;
if (llStringLength(sLosses) < 2) sLosses = llGetSubString(" ", 0, 1 - llStringLength(sLosses)) + sLosses;
leftPart = rank + playerName; rankCol += rank + "\n";
rightPart = "W:" + sWins + "/L:" + sLosses; nameCol += playerName + "\n";
statsCol += "W:" + sWins + "/L:" + sLosses + "\n";
spaceNeeded = LEADERBOARD_WIDTH - llStringLength(leftPart) - 10;
if (spaceNeeded < 1) spaceNeeded = 1;
leaderboardText += leftPart + llGetSubString(SPACES, 0, spaceNeeded - 1) + rightPart + "\n";
} }
// Fill remaining positions with placeholders // Fill remaining positions with placeholders
for (genI = numSorted; genI < MAX_LEADERBOARD_ENTRIES; genI++) { for (genI = numSorted; genI < MAX_LEADERBOARD_ENTRIES; genI++) {
rank = (string)(genI + 1) + ". "; rank = (string)(genI + 1) + ".";
if (genI < 9) rank = "0" + rank; if (genI < 9) rank = "0" + rank;
leftPart = rank + "----------"; rankCol += rank + "\n";
rightPart = "W: 0/L: 0"; nameCol += "----------\n";
spaceNeeded = LEADERBOARD_WIDTH - llStringLength(leftPart) - 10; statsCol += "W:0/L:0\n";
leaderboardText += leftPart + llGetSubString(SPACES, 0, spaceNeeded - 1) + rightPart + "\n";
} }
// Send formatted text to leaderboard bridge // Send 3-column data to bridge
llMessageLinked(LINK_LEADERBOARD_BRIDGE, MSG_RESET_LEADERBOARD, "FORMATTED_TEXT|" + leaderboardText, NULL_KEY); llMessageLinked(LINK_SET, MSG_DISPLAY_LEADERBOARD, "COLUMNS|" + rankCol + "|" + nameCol + "|" + statsCol, NULL_KEY);
return 0; return 0;
} }
@ -386,20 +376,20 @@ integer clearAllPlayers() {
// SAFETY CHECK - use if block instead of continue for maximum compatibility // SAFETY CHECK - use if block instead of continue for maximum compatibility
if (clearProfilePrimIndex > 0) { if (clearProfilePrimIndex > 0) {
// Reset profile prim to default and clear any glow // Reset profile prim to default and hide it
llSetLinkPrimitiveParamsFast(clearProfilePrimIndex, [ llSetLinkPrimitiveParamsFast(clearProfilePrimIndex, [
PRIM_TEXTURE, ALL_SIDES, TEXTURE_DEFAULT_PROFILE, <1,1,0>, <0,0,0>, 0.0, 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_COLOR, ALL_SIDES, <1.0, 1.0, 1.0>, 0.0, // Hidden by default
PRIM_TEXT, "", <1,1,1>, 0.0, PRIM_TEXT, "", <1,1,1>, 0.0,
PRIM_GLOW, ALL_SIDES, 0.0 // Explicitly clear glow PRIM_GLOW, ALL_SIDES, 0.0
]); ]);
// Reset hearts prim to 3 hearts and clear any glow // Reset hearts prim to 3 hearts and hide it
llSetLinkPrimitiveParamsFast(clearHeartsPrimIndex, [ llSetLinkPrimitiveParamsFast(clearHeartsPrimIndex, [
PRIM_TEXTURE, ALL_SIDES, TEXTURE_3_HEARTS, <1,1,0>, <0,0,0>, 0.0, 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_COLOR, ALL_SIDES, <1.0, 1.0, 1.0>, 0.0, // Hidden by default
PRIM_TEXT, "", <1,1,1>, 0.0, PRIM_TEXT, "", <1,1,1>, 0.0,
PRIM_GLOW, ALL_SIDES, 0.0 // Explicitly clear glow PRIM_GLOW, ALL_SIDES, 0.0
]); ]);
// Reset overlay prim to transparent // Reset overlay prim to transparent
@ -446,16 +436,16 @@ integer refreshPlayerDisplay() {
refreshOverlayPrimIndex = getOverlayPrimLink(refreshI); refreshOverlayPrimIndex = getOverlayPrimLink(refreshI);
if (refreshProfilePrimIndex > 0) { if (refreshProfilePrimIndex > 0) {
// Reset to default only for unused slots // Unused slots: Reset and HIDE
llSetLinkPrimitiveParamsFast(refreshProfilePrimIndex, [ llSetLinkPrimitiveParamsFast(refreshProfilePrimIndex, [
PRIM_TEXTURE, ALL_SIDES, TEXTURE_DEFAULT_PROFILE, <1,1,0>, <0,0,0>, 0.0, 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_COLOR, ALL_SIDES, <1.0, 1.0, 1.0>, 0.0, // Invisible
PRIM_TEXT, "", <1,1,1>, 0.0 PRIM_TEXT, "", <1,1,1>, 0.0
]); ]);
llSetLinkPrimitiveParamsFast(refreshHeartsPrimIndex, [ llSetLinkPrimitiveParamsFast(refreshHeartsPrimIndex, [
PRIM_TEXTURE, ALL_SIDES, TEXTURE_3_HEARTS, <1,1,0>, <0,0,0>, 0.0, 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_COLOR, ALL_SIDES, <1.0, 1.0, 1.0>, 0.0, // Invisible
PRIM_TEXT, "", <1,1,1>, 0.0 PRIM_TEXT, "", <1,1,1>, 0.0
]); ]);
@ -780,8 +770,7 @@ default {
} }
link_message(integer sender, integer num, string str, key id) { link_message(integer sender, integer num, string str, key id) {
// Only listen to messages from the main controller (link 1) // Handle messages from the game logic and bridges
if (sender != LINK_CONTROLLER) return;
if (num == MSG_GAME_STATUS) { if (num == MSG_GAME_STATUS) {
updateActionsPrim(str); updateActionsPrim(str);
@ -852,6 +841,11 @@ default {
// Update glow effects - winner glow overrides peril glow // Update glow effects - winner glow overrides peril glow
updatePlayerGlowEffects(); updatePlayerGlowEffects();
} }
else if (num == MSG_DISPLAY_LEADERBOARD && str == "REFRESH_STARTUP") {
// Startup handshake from leaderboard bridge - re-send text now that boxes are ready
dbg("📋 [Scoreboard] Handshake received! Refreshing virtual columns...");
generateLeaderboardText();
}
} }
http_response(key request_id, integer status, list metadata, string body) { http_response(key request_id, integer status, list metadata, string body) {
@ -981,13 +975,17 @@ default {
listen(integer channel, string name, key id, string message) { listen(integer channel, string name, key id, string message) {
string msg = llToLower(llStringTrim(message, STRING_TRIM)); string msg = llToLower(llStringTrim(message, STRING_TRIM));
if (msg == "testx") { if (msg == "testx") {
llOwnerSay("Testing X overlays on all 10 player slots..."); llOwnerSay("🛠️ [Scoreboard] Testing FULL LAYOUT on all 10 slots (Forced Alpha Reveal)...");
integer i; integer i;
integer overlayIdx;
for (i = 0; i < 10; i++) { for (i = 0; i < 10; i++) {
overlayIdx = getOverlayPrimLink(i); integer pIdx = getProfilePrimLink(i);
if (overlayIdx > 0) { integer hIdx = getHeartsPrimLink(i);
llSetLinkPrimitiveParamsFast(overlayIdx, [ integer oIdx = getOverlayPrimLink(i);
if (pIdx > 0) {
llSetLinkPrimitiveParamsFast(pIdx, [PRIM_COLOR, ALL_SIDES, <1,1,1>, 1.0]);
llSetLinkPrimitiveParamsFast(hIdx, [PRIM_COLOR, ALL_SIDES, <1,1,1>, 1.0]);
llSetLinkPrimitiveParamsFast(oIdx, [
PRIM_TEXTURE, ALL_SIDES, TEXTURE_ELIMINATED_X, <1,1,0>, <0,0,0>, 0.0, PRIM_TEXTURE, ALL_SIDES, TEXTURE_ELIMINATED_X, <1,1,0>, <0,0,0>, 0.0,
PRIM_COLOR, ALL_SIDES, <1.0, 1.0, 1.0>, 1.0 PRIM_COLOR, ALL_SIDES, <1.0, 1.0, 1.0>, 1.0
]); ]);

View file

@ -1,12 +1,16 @@
PERIL DICE - Professional Single Linkset Game System for Second Life 🎲 PERIL DICE — PROFESSIONAL SINGLE LINKSET GAME SYSTEM FOR SECOND LIFE
========================================================
Created by Rebecca Nod and Noose the Bunny CREATED BY REBECCA NOD AND NOOSE THE BUNNY (djmusica28)
CURRENT VERSION: 3.2.0 - THE "SYNC SANCTITY" UPDATE
CURRENT VERSION: 3.1.0 - The "Linkset Stability" Update 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.
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 V3.2.0: SYNC SANCTITY & SCOREBOARD VIRTUALIZATION! Total refactoring of the synchronization engine. The Main Controller is now the single source of truth for life counts, permanently eliminating "Ghost Heals." The scoreboard is now "Virtualized"—supporting a professional 3-column Rank/Name/Wins/Losses layout and dynamic Alpha 0.0 hiding for unused player slots.
NEW IN v3.1.0: **LINKSET STABILITY & DYNAMIC DISCOVERY!** Complete architectural decentralization—scripts now find their linked prims by name, making the game board immune to link-order changes or region restarts. Full migration to modern mesh-based **FURWARE Text** for dice and leaderboards. Centralized system constants in `Peril_Constants.lsl` for 100% project consistency. NEW IN V3.1.0: LINKSET STABILITY & DYNAMIC DISCOVERY! Complete architectural decentralization—scripts now find their linked prims by name, making the game board immune to link-order changes or region-side glitches. Full migration from legacy XYText to modern mesh-based FURWARE Text for dice and leaderboards. Centralized system constants in Peril_Constants.lsl for 100% project consistency.
KEY FEATURES: KEY FEATURES:
- Dynamic Link Discovery: "Bulletproof" linkset architecture that auto-configures by prim name - Dynamic Link Discovery: "Bulletproof" linkset architecture that auto-configures by prim name

View file

@ -182,18 +182,41 @@ checkMemoryUsage(string context) {
// Send status message to scoreboard using link messages // Send status message to scoreboard using link messages
sendStatusMessage(string status) { sendStatusMessage(string status) {
// LOCKOUT: If victory is in progress, only allow "Victory" or "Title" statuses
// This prevents elimination messages from stomping on the victory display
if (victoryInProgress) {
if (status != "Victory" && status != "Title") {
dbg("🏆 [Main Controller] 🚫 Status update BLOCKED during victory: " + status);
return;
}
}
statusTimer = llGetUnixTime(); statusTimer = llGetUnixTime();
lastStatus = status; lastStatus = status;
// CHANGED: Use link message instead of llRegionSay // Send texture update to scoreboard
llMessageLinked(LINK_SCOREBOARD, MSG_GAME_STATUS, status, NULL_KEY); llMessageLinked(LINK_SCOREBOARD, MSG_GAME_STATUS, status, NULL_KEY);
// Removed ownerMsg call to save memory // NEW: Send dynamic text update to FURWARE status bridge
llMessageLinked(LINK_SET, MSG_STATUS_TEXT, status, NULL_KEY);
currentTimerMode = TIMER_STATUS; currentTimerMode = TIMER_STATUS;
llSetTimerEvent(STATUS_DISPLAY_TIME + 1.0); llSetTimerEvent(STATUS_DISPLAY_TIME + 1.0);
} }
// UNIFIED SYSTEM: Single source of truth for global game state sync
syncGameState() {
// Build sync message WITHOUT redundant player keys to save memory
// Format: lives~picks~perilPlayer~names
string syncMsg = llList2CSV(lives) + "~" +
(string)llDumpList2String(picksData, "^") + "~" +
(string)llList2String([perilPlayer, "NONE"], (perilPlayer == "")) + "~" +
llList2CSV(names);
llMessageLinked(LINK_SET, MSG_SYNC_GAME_STATE, syncMsg, NULL_KEY);
dbg("🌍 [Main Controller] 📡 Global State Sync broadcasted.");
}
// Direct scoreboard update for most recently added player // Direct scoreboard update for most recently added player
updateHelpers() { updateHelpers() {
@ -249,11 +272,9 @@ resetGame() {
cleanupListeners(); cleanupListeners();
// Let Floater Manager handle cleanup intelligently // Let Floater Manager handle cleanup intelligently
// Only request cleanup if we actually have game data that might have floaters // Always request cleanup on a reset to ensure no orphans are left behind
if (llGetListLength(names) > 0 || llGetListLength(floaterChannels) > 0) { llMessageLinked(LINK_SET, MSG_CLEANUP_ALL_FLOATERS, "RESET", NULL_KEY);
llMessageLinked(LINK_SET, MSG_CLEANUP_ALL_FLOATERS, "RESET", NULL_KEY); llSleep(0.5);
llSleep(1.0);
}
llSleep(0.5); llSleep(0.5);
@ -275,7 +296,7 @@ resetGame() {
llMessageLinked(LINK_SCOREBOARD, MSG_CLEAR_GAME, "", NULL_KEY); llMessageLinked(LINK_SCOREBOARD, MSG_CLEAR_GAME, "", NULL_KEY);
// Clear winner glow from scoreboard (floaters will clear naturally on reset) // Clear winner glow from scoreboard (floaters will clear naturally on reset)
llMessageLinked(LINK_SCOREBOARD, MSG_UPDATE_WINNER, "", NULL_KEY); // Clear winner glow llMessageLinked(LINK_SCOREBOARD, MSG_UPDATE_WINNER, "", NULL_KEY); // Clear winner glow
llMessageLinked(LINK_DICE_BRIDGE, MSG_CLEAR_DICE, "", NULL_KEY); llMessageLinked(LINK_SET, MSG_CLEAR_DICE, "", NULL_KEY);
dbg("🎮 Game reset! All state cleared (including scoreboard)."); dbg("🎮 Game reset! All state cleared (including scoreboard).");
@ -285,6 +306,9 @@ resetGame() {
llSetTexture(TEXTURE_START, CONTROLLER_FACE); llSetTexture(TEXTURE_START, CONTROLLER_FACE);
llSetText("🎮 PERIL DICE GAME\nTouch to play!", <1.0, 1.0, 0.0>, 1.0); llSetText("🎮 PERIL DICE GAME\nTouch to play!", <1.0, 1.0, 0.0>, 1.0);
// NEW: Update status bar for idle state
sendStatusMessage("PERIL DICE GAME\nTouch to Join!");
llSleep(0.5); llSleep(0.5);
llSetTimerEvent(0); llSetTimerEvent(0);
@ -471,7 +495,25 @@ default {
} }
link_message(integer sender, integer num, string str, key id) { link_message(integer sender, integer num, string str, key id) {
// Dialog forwarding now handled by Player_RegistrationManager // Handle MSG_UPDATE_LIFE - Reported by Roll Module when someone takes a hit
if (num == MSG_UPDATE_LIFE) {
list parts = llParseString2List(str, ["|"], []);
if (llGetListLength(parts) >= 2) {
string hitPlayer = llList2String(parts, 0);
integer newLifeCount = (integer)llList2String(parts, 1);
integer pidx = llListFindList(names, [hitPlayer]);
if (pidx != -1) {
lives = llListReplaceList(lives, [newLifeCount], pidx, pidx);
dbg("🛡️ [Main Controller] 💗 Damage Report Confirmed: " + hitPlayer + " -> " + (string)newLifeCount + " hearts.");
// Broadcast the OFFICIAL ledger to the entire Linkset
syncGameState();
}
}
return;
}
// Handle messages from the game logic and bridges
// Handle registration updates from Player_RegistrationManager // Handle registration updates from Player_RegistrationManager
if (num == MSG_UPDATE_MAIN_LISTS) { // MSG_UPDATE_MAIN_LISTS if (num == MSG_UPDATE_MAIN_LISTS) { // MSG_UPDATE_MAIN_LISTS
@ -497,6 +539,9 @@ default {
lives += [3]; lives += [3];
floaterChannels += [ch]; floaterChannels += [ch];
// NEW: Update status bar with registration count
sendStatusMessage("JOIN THE PERIL!\n" + (string)llGetListLength(names) + "/" + (string)MAX_PLAYERS + " Players registered...");
// Clean up pending registrations // Clean up pending registrations
integer pendingIdx = llListFindList(pendingRegistrations, [id]); integer pendingIdx = llListFindList(pendingRegistrations, [id]);
if (pendingIdx != -1) { if (pendingIdx != -1) {
@ -517,6 +562,11 @@ default {
string eliminatedPlayer = llList2String(parts, 1); string eliminatedPlayer = llList2String(parts, 1);
integer idx = llListFindList(names, [eliminatedPlayer]); integer idx = llListFindList(names, [eliminatedPlayer]);
if (idx != -1) { if (idx != -1) {
// NEW: Only announce elimination if it's not the end of the game (winner)
if (llGetListLength(names) > 1) {
sendStatusMessage("PLAYER STRUCK!\n" + eliminatedPlayer + " has been eliminated!");
}
// FIRST: Set the player's lives to 0 and show 0 hearts // FIRST: Set the player's lives to 0 and show 0 hearts
lives = llListReplaceList(lives, [0], idx, idx); lives = llListReplaceList(lives, [0], idx, idx);
@ -577,13 +627,8 @@ default {
} }
} }
// Build sync message WITHOUT redundant player keys (5th part) to save memory // UNIFIED SYNC: Replaces inline sync calls to prevent language rift
// Combined into single message to avoid multiple temporary string allocations syncGameState();
llMessageLinked(LINK_SET, MSG_SYNC_GAME_STATE,
llList2CSV(lives) + "~" +
(string)llDumpList2String(picksData, "^") + "~" +
(string)llList2String([perilPlayer, "NONE"], (perilPlayer == "")) + "~" +
llList2CSV(names), NULL_KEY);
// Direct floater update to ensure eliminated status is displayed // Direct floater update to ensure eliminated status is displayed
llMessageLinked(LINK_SET, MSG_UPDATE_FLOAT, eliminatedPlayer, llList2Key(players, idx)); llMessageLinked(LINK_SET, MSG_UPDATE_FLOAT, eliminatedPlayer, llList2Key(players, idx));
@ -622,6 +667,9 @@ default {
lives = llDeleteSubList(lives, idx, idx); lives = llDeleteSubList(lives, idx, idx);
floaterChannels = llDeleteSubList(floaterChannels, idx, idx); floaterChannels = llDeleteSubList(floaterChannels, idx, idx);
// NEW: FINAL SYNC - Tell everyone the list has actually SHRUNK now
// This prevents "Zombie Respawns" and "Ghost Heals"
syncGameState();
// Peril player assignment already handled before sync message was sent // Peril player assignment already handled before sync message was sent
@ -658,6 +706,10 @@ default {
llSay(0, "✨ ULTIMATE VICTORY! " + winner + " is the Ultimate Survivor!"); llSay(0, "✨ ULTIMATE VICTORY! " + winner + " is the Ultimate Survivor!");
// NEW: Send victory status to board and display winner name!
sendStatusMessage("Victory");
llMessageLinked(LINK_SET, MSG_STATUS_TEXT, "=== ULTIMATE VICTORY ===\n<!c=white>" + winner + " wins!", NULL_KEY);
llMessageLinked(LINK_SET, MSG_PLAYER_WON, winner, NULL_KEY); llMessageLinked(LINK_SET, MSG_PLAYER_WON, winner, NULL_KEY);
llMessageLinked(LINK_SET, MSG_EFFECT_CONFETTI, "VICTORY_CONFETTI", NULL_KEY); llMessageLinked(LINK_SET, MSG_EFFECT_CONFETTI, "VICTORY_CONFETTI", NULL_KEY);
@ -733,8 +785,8 @@ default {
list parts = llParseString2List(str, ["|"], []); list parts = llParseString2List(str, ["|"], []);
if (llList2String(parts, 0) == "GAME_WON") { if (llList2String(parts, 0) == "GAME_WON") {
string winner = llList2String(parts, 1); string winner = llList2String(parts, 1);
// CHANGED: Use link message instead of llRegionSay // Send dice roll winner message using LINK_SET
llMessageLinked(LINK_DICE_BRIDGE, MSG_DICE_ROLL, winner + "|WON", NULL_KEY); llMessageLinked(LINK_SET, MSG_DICE_ROLL, winner + "|WON", NULL_KEY);
llSleep(2.0); llSleep(2.0);
resetGame(); resetGame();
} }
@ -867,6 +919,11 @@ default {
// CHANGED: Remove player from scoreboard BEFORE updating lists // CHANGED: Remove player from scoreboard BEFORE updating lists
llMessageLinked(LINK_SCOREBOARD, MSG_REMOVE_PLAYER, leavingName, NULL_KEY); llMessageLinked(LINK_SCOREBOARD, MSG_REMOVE_PLAYER, leavingName, NULL_KEY);
// Only announce if it's not the end of the game
if (llGetListLength(names) > 1) {
sendStatusMessage("PLAYER FALLEN!\n" + leavingName + " has been eliminated!");
}
// Clean up the leaving player's floater using their current channel // Clean up the leaving player's floater using their current channel
integer ch = FLOATER_BASE_CHANNEL + idx; integer ch = FLOATER_BASE_CHANNEL + idx;
llMessageLinked(LINK_SET, MSG_CLEANUP_FLOAT, (string)ch, NULL_KEY); llMessageLinked(LINK_SET, MSG_CLEANUP_FLOAT, (string)ch, NULL_KEY);
@ -1159,7 +1216,12 @@ default {
integer actualPlayerCount = llGetListLength(names); integer actualPlayerCount = llGetListLength(names);
llSetText("🎮 GAME IN PROGRESS\n" + (string)actualPlayerCount + " players", <1.0, 0.2, 0.2>, 1.0); llSetText("🎮 GAME IN PROGRESS\n" + (string)actualPlayerCount + " players", <1.0, 0.2, 0.2>, 1.0);
sendStatusMessage("Title"); // FIXED: Use proper announcement instead of internal texture name "Title"
sendStatusMessage("GAME STARTING!\nAll players are ready!");
// Dramatic pause to let the start announcement be read before round prep begins
llSleep(4.0);
// For game start, use empty peril player - Game Manager will select one // For game start, use empty peril player - Game Manager will select one
llMessageLinked(LINK_SET, 998, "", NULL_KEY); llMessageLinked(LINK_SET, 998, "", NULL_KEY);
} }

View file

@ -7,6 +7,7 @@ integer LINK_CONTROLLER = 1; // Usually Root (link 1)
integer LINK_SCOREBOARD = -1; // "Scoreboard:0:0" integer LINK_SCOREBOARD = -1; // "Scoreboard:0:0"
integer LINK_DICE_BRIDGE = -1; // "FURWARE text mesh:Dice:0:0" integer LINK_DICE_BRIDGE = -1; // "FURWARE text mesh:Dice:0:0"
integer LINK_LEADERBOARD_BRIDGE = -1; // "FURWARE text mesh:Leaderboard:0:0" integer LINK_LEADERBOARD_BRIDGE = -1; // "FURWARE text mesh:Leaderboard:0:0"
integer LINK_STATUS_BRIDGE = -1; // "FURWARE text mesh:Status:0:0"
// Macro to discover core links by name // Macro to discover core links by name
#define DISCOVER_CORE_LINKS() { \ #define DISCOVER_CORE_LINKS() { \
@ -17,8 +18,11 @@ integer LINK_LEADERBOARD_BRIDGE = -1; // "FURWARE text mesh:Leaderboard:0:0"
if (_name == "Scoreboard:0:0") LINK_SCOREBOARD = _i; \ if (_name == "Scoreboard:0:0") LINK_SCOREBOARD = _i; \
else if (_name == "FURWARE text mesh:Dice:0:0") LINK_DICE_BRIDGE = _i; \ else if (_name == "FURWARE text mesh:Dice:0:0") LINK_DICE_BRIDGE = _i; \
else if (_name == "FURWARE text mesh:Leaderboard:0:0") LINK_LEADERBOARD_BRIDGE = _i; \ else if (_name == "FURWARE text mesh:Leaderboard:0:0") LINK_LEADERBOARD_BRIDGE = _i; \
else if (LINK_CONTROLLER == 1 && _i == 1) { /* Default to root if not found */ } \ else if (_name == "FURWARE text mesh:Status:0:0") LINK_STATUS_BRIDGE = _i; \
else if (LINK_CONTROLLER == 1 && _i == 1) { ; /* Default to root if not found */ } \
} \ } \
/* Safety Fallback: Use Root if Scoreboard prim is missing */ \
if (LINK_SCOREBOARD == -1) LINK_SCOREBOARD = 1; \
} }
// --- FURWARE Text Commands --- // --- FURWARE Text Commands ---
@ -43,6 +47,7 @@ integer LINK_LEADERBOARD_BRIDGE = -1; // "FURWARE text mesh:Leaderboard:0:0"
// --- Game Flow (3000-3099) --- // --- Game Flow (3000-3099) ---
#define MSG_GAME_STATUS 3001 #define MSG_GAME_STATUS 3001
#define MSG_UPDATE_LIFE 80 // Unified damage reporting
#define MSG_PLAYER_UPDATE 3002 #define MSG_PLAYER_UPDATE 3002
#define MSG_CLEAR_GAME 3003 #define MSG_CLEAR_GAME 3003
#define MSG_REMOVE_PLAYER 3004 #define MSG_REMOVE_PLAYER 3004
@ -51,6 +56,8 @@ integer LINK_LEADERBOARD_BRIDGE = -1; // "FURWARE text mesh:Leaderboard:0:0"
#define MSG_GAME_WON 3010 #define MSG_GAME_WON 3010
#define MSG_GAME_LOST 3011 #define MSG_GAME_LOST 3011
#define MSG_RESET_LEADERBOARD 3012 #define MSG_RESET_LEADERBOARD 3012
#define MSG_DISPLAY_LEADERBOARD 3013
#define MSG_STATUS_TEXT 3007
#define MSG_DICE_ROLL 3020 #define MSG_DICE_ROLL 3020
#define MSG_CLEAR_DICE 3021 #define MSG_CLEAR_DICE 3021
#define MSG_DICE_CLEAR 3021 // Alias for consistency in dice bridge #define MSG_DICE_CLEAR 3021 // Alias for consistency in dice bridge

View file

@ -1,12 +1,14 @@
# 🎲 Peril Dice — Professional Single Linkset Game System for Second Life # 🎲 Peril Dice — Professional Single Linkset Game System for Second Life
**Created by Rebecca Nod and Noose the Bunny** **Created by Rebecca Nod and Noose the Bunny**
**Current Version: 3.1.0 - The "Linkset Stability" Update** **Current Version: 3.2.0 - The "Sync Sanctity" Update**
## Overview ## Overview
Peril Dice is a multiplayer elimination game where each player selects numbers before a die is rolled. If the peril player's number is rolled, they lose a life. Players are eliminated when they reach zero lives. Peril Dice 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 v3.2.0**: **SYNC SANCTITY & SCOREBOARD VIRTUALIZATION** - Total refactoring of the synchronization engine. The Main Controller is now the single source of truth for life counts, permanently eliminating "Ghost Heals." The scoreboard is now "Virtualized"—supporting a 3-column layout (Rank, Name, WL) and dynamic Alpha 0.0 hiding for unused slots.
**🚀 NEW in v3.1.0**: **LINKSET STABILITY & DYNAMIC DISCOVERY** - Complete architectural decentralization. Scripts now discovery their linked prims by name at runtime (`Scoreboard:0:0`, `profile:R:C`), making the game board immune to link-order changes or region-side glitches. Full migration from legacy XYText to modern mesh-based **FURWARE Text** for dice and leaderboards. **🚀 NEW in v3.1.0**: **LINKSET STABILITY & DYNAMIC DISCOVERY** - Complete architectural decentralization. Scripts now discovery their linked prims by name at runtime (`Scoreboard:0:0`, `profile:R:C`), making the game board immune to link-order changes or region-side glitches. Full migration from legacy XYText to modern mesh-based **FURWARE Text** for dice and leaderboards.
**📦 NEW in v3.0.0**: **COMMERCIAL CASPERVEND RELEASE & EXPERIENCE LEADERBOARD** **📦 NEW in v3.0.0**: **COMMERCIAL CASPERVEND RELEASE & EXPERIENCE LEADERBOARD**
@ -27,7 +29,23 @@ 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. **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 v3.1.0 Improvements (Current) 🚀 ## Major v3.2.0 Improvements (Current) 🚀
### 🛡️ **Sync Sanctity Protocol**
- **Unified Master Ledger**: Eliminated parallel heart-count logic. The `Roll_ConfettiModule` now reports hits to the `Main_Controller` via the new `MSG_UPDATE_LIFE` (80) channel.
- **Ghost Heal Exorcism**: The Controller now broadcasts the *only* official game state sync, ensuring all scripts (Roll, Scoreboard, Dialog) are perfectly aligned.
- **Victory Status Lock**: Implemented a `victoryInProgress` lock to protect the "Ultimate Victory" status from being overwritten by late-arriving elimination messages.
- **Emoji-Free Status Banners**: Switched to high-compatibility ASCII headers (`===`) for the Furware status bridge to ensure 100% rendering reliability across all viewer types.
### 🎰 **Virtualized Scoreboard Grid**
- **3-Column Data Layout**: Re-organized Rank, Name, and Win/Loss records into a sleek grid for the leaderboard display.
- **Dynamic Slot Hiding**: Unused player slots now automatically use **Alpha 0.0** (invisible). A 2-player game now looks like a professional 2-player table, with no "ghost" slots for players 3-10 visible.
- **Human-Bot Parity Patch**: Refactored the pick-parsing engine to be delimiter-agnostic. Supports human-dialog commas (`,`), bot semicolons (`;`), pipes (`|`), and even spaces.
### 🌎 **Disconnect Resilience**
- **Sync-Pulse Reception**: Added `MSG_SYNC_GAME_STATE` listeners to all core modules. A player rejoining a game now receives a full state-slam instantly, restoring their local board state to the latest master data.
## Major v3.1.0 Improvements 🚀
### 🔍 **Dynamic Link Discovery System** ### 🔍 **Dynamic Link Discovery System**
- **Zero-Fixed Links**: Eliminated all hardcoded link indices. Each script now performs a "Dynamic Discovery" sweep on startup to find its sibling prims by name. - **Zero-Fixed Links**: Eliminated all hardcoded link indices. Each script now performs a "Dynamic Discovery" sweep on startup to find its sibling prims by name.
@ -620,9 +638,17 @@ Dice type is automatically chosen to ensure at least 3 picks per player:
## Version ## Version
**Current Version**: 3.1.0 **Current Version**: 3.2.0
**Last Updated**: April 04, 2026 **Last Updated**: April 05, 2026
**Status**: Production Ready - Linkset Stability Release **Status**: Production Ready - Sync Sanctity Release
### Key Achievements in v3.2.0:
- ✅ **Sync Sanctity Protocol**: 100% unified life counting via the Main Controller.
- ✅ **Ghost Heal Exorcism**: Permanently eliminated parallel sync pathways.
- ✅ **Virtualized Scoreboard**: 3-column Rank/Name/WL grid for leaderboard.
- ✅ **Dynamic Slot Hiding**: Alpha 0.0 invisibility for unused player slots.
- ✅ **Human-Bot Parity**: Multi-delimiter picking logic (`,`, `;`, `|`, ` `).
- ✅ **Victory Status Lock**: GameOver status protection and ASCII-safe banners.
### Key Achievements in v3.1.0: ### Key Achievements in v3.1.0:
- ✅ **Architectural Decentralization**: Implemented Dynamic Link Discovery (no hardcoded links). - ✅ **Architectural Decentralization**: Implemented Dynamic Link Discovery (no hardcoded links).

View file

@ -1,13 +1,15 @@
🎲 PERIL DICE — PROFESSIONAL SINGLE LINKSET GAME SYSTEM FOR SECOND LIFE PERIL DICE - README
======================================================== ========================================================
CREATED BY REBECCA NOD AND NOOSE THE BUNNY CREATED BY REBECCA NOD AND NOOSE THE BUNNY (djmusica28)
CURRENT VERSION: 3.1.0 - THE "LINKSET STABILITY" UPDATE CURRENT VERSION: 3.2.0 - THE "SYNC SANCTITY" UPDATE
OVERVIEW OVERVIEW
-------- --------
Peril Dice is a multiplayer elimination game where each player selects numbers before a die is rolled. If the peril player's number is rolled, they lose a life. Players are eliminated when they reach zero lives. Peril Dice 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 V3.2.0: SYNC SANCTITY & SCOREBOARD VIRTUALIZATION! Total refactoring of the synchronization engine. The Main Controller is now the single source of truth for life counts, permanently eliminating "Ghost Heals." The scoreboard is now "Virtualized"—supporting a professional 3-column Rank/Name/Wins/Losses layout and dynamic Alpha 0.0 hiding for unused player slots.
NEW IN V3.1.0: LINKSET STABILITY & DYNAMIC DISCOVERY! Complete architectural decentralization—scripts now find their linked prims by name, making the game board immune to link-order changes or region-side glitches. Full migration from legacy XYText to modern mesh-based FURWARE Text for dice and leaderboards. Centralized system constants in Peril_Constants.lsl for 100% project consistency. NEW IN V3.1.0: LINKSET STABILITY & DYNAMIC DISCOVERY! Complete architectural decentralization—scripts now find their linked prims by name, making the game board immune to link-order changes or region-side glitches. Full migration from legacy XYText to modern mesh-based FURWARE Text for dice and leaderboards. Centralized system constants in Peril_Constants.lsl for 100% project consistency.
NEW IN V2.8.7: Enhanced LinkScanner System for complete 84-prim structure analysis and verification tools. (Legacy GitHub update system replaced by CasperVend in v3.0.0). NEW IN V2.8.7: Enhanced LinkScanner System for complete 84-prim structure analysis and verification tools. (Legacy GitHub update system replaced by CasperVend in v3.0.0).
@ -547,9 +549,9 @@ PREVIOUS IMPROVEMENTS (V2.4.0)
VERSION INFORMATION VERSION INFORMATION
=================== ===================
Current Version: 3.1.0 Current Version: 3.2.0
Last Updated: April 04, 2026 Last Updated: April 05, 2026
Status: Production Ready - Linkset Stability Release Status: Production Ready - Sync Sanctity Release
ORIGINAL GAME RULES CREDIT ORIGINAL GAME RULES CREDIT
========================== ==========================

View file

@ -68,28 +68,19 @@ list getPicksFor(string nameInput) {
list parts = llParseString2List(entry, ["|"], []); list parts = llParseString2List(entry, ["|"], []);
if (llList2String(parts, 0) == nameInput) { if (llList2String(parts, 0) == nameInput) {
// Convert semicolons back to commas, then parse // Convert semicolons back to commas, then parse
string picks = llList2String(parts, 1); string picks = llStringTrim(llList2String(parts, 1), STRING_TRIM);
string originalPicks = picks;
// Check for corruption markers (^ symbols that shouldn't be in picks) // Check for corruption or empty
if (llSubStringIndex(picks, "^") != -1) { if (llSubStringIndex(picks, "^") != -1 || picks == "") return [];
return [];
}
if (picks == "") { // NEW: Robust multi-delimiter parsing (handles ", " "; " "|" and " ")
return []; list rawResult = llParseString2List(picks, [",", ";", "|", " "], []);
}
picks = llDumpList2String(llParseString2List(picks, [";"], []), ",");
list rawResult = llParseString2List(picks, [","], []);
// Trim whitespace from each number to handle "5, 6" vs "5,6" formats
list result = []; list result = [];
integer j; integer j;
for (j = 0; j < llGetListLength(rawResult); j++) { for (j = 0; j < llGetListLength(rawResult); j++) {
string num = llStringTrim(llList2String(rawResult, j), STRING_TRIM); string num = llStringTrim(llList2String(rawResult, j), STRING_TRIM);
if (num != "") { if (num != "") result += [num];
result += [num];
}
} }
return result; return result;
} }
@ -260,17 +251,24 @@ default {
// Send continuation directly to Game Manager (more efficient than through Main Controller) // Send continuation directly to Game Manager (more efficient than through Main Controller)
llMessageLinked(LINK_SET, MSG_CONTINUE_GAME, "", NULL_KEY); // Empty peril player - Game Manager will select one llMessageLinked(LINK_SET, MSG_CONTINUE_GAME, "", NULL_KEY); // Empty peril player - Game Manager will select one
} else { } else {
// Clear dice display directly (link 83) // Clear dice display directly using LINK_SET since bridge moved to root
llMessageLinked(LINK_DICE_BRIDGE, MSG_CLEAR_DICE, "", NULL_KEY); llMessageLinked(LINK_SET, MSG_CLEAR_DICE, "", NULL_KEY);
// Update peril player from the roll dialog message // Update peril player from the roll dialog message
perilPlayer = str; perilPlayer = str;
dbg("🎲 [Roll Module] 🎲 Prompting " + str + " to roll the dice. Setting perilPlayer to: " + perilPlayer); dbg("🎲 [Roll Module] 🎲 Prompting " + str + " to roll the dice. Setting perilPlayer to: " + perilPlayer);
// NEW: Update status bar for rolling (for everyone, bots and humans)
llMessageLinked(LINK_SET, MSG_STATUS_TEXT, "DICE IN THE AIR!\n" + str + " is rolling...", NULL_KEY);
// Check if this is a bot (Bot names) // Check if this is a bot (Bot names)
if (llSubStringIndex(str, "Bot") == 0) { if (llSubStringIndex(str, "Bot") == 0) {
// Auto-roll for bots - request dice type first // Auto-roll for bots - request dice type first
dbg("🎲 [Roll Module] 🤖 " + str + " (bot) is requesting dice type for auto-roll..."); dbg("🎲 [Roll Module] 🤖 " + str + " (bot) is requesting dice type for auto-roll...");
// Give a moment for "DICE IN THE AIR" message to be seen for bots
llSleep(2.0);
rollInProgress = TRUE; // Set roll in progress for bot rollInProgress = TRUE; // Set roll in progress for bot
shouldRoll = TRUE; // Set flag to perform roll when dice type is received shouldRoll = TRUE; // Set flag to perform roll when dice type is received
// Request current dice type from Calculator // Request current dice type from Calculator
@ -338,8 +336,14 @@ default {
lastRollTime = llGetUnixTime(); // Record roll time lastRollTime = llGetUnixTime(); // Record roll time
llSay(0, "🎲 THE D" + (string)diceType + " OF FATE! " + perilPlayer + " rolled a " + resultStr + " on the " + (string)diceType + "-sided die! 🎲"); llSay(0, "🎲 THE D" + (string)diceType + " OF FATE! " + perilPlayer + " rolled a " + resultStr + " on the " + (string)diceType + "-sided die! 🎲");
// Send dice roll directly to dice display (link 83) // Send dice roll directly to dice display using LINK_SET
llMessageLinked(LINK_DICE_BRIDGE, MSG_DICE_ROLL, perilPlayer + "|" + resultStr, NULL_KEY); llMessageLinked(LINK_SET, MSG_DICE_ROLL, perilPlayer + "|" + resultStr, NULL_KEY);
// NEW: Update status bar with result
llMessageLinked(LINK_SET, MSG_STATUS_TEXT, "DICE SETTLED!\n" + perilPlayer + " rolled a " + resultStr + "!", NULL_KEY);
// Dramatic pause to let the roll result be seen before the win/loss outcome pops up
llSleep(4.0);
string newPeril = ""; string newPeril = "";
integer matched = FALSE; integer matched = FALSE;
@ -377,13 +381,15 @@ default {
perilPlayer = newPeril; perilPlayer = newPeril;
// Direct scoreboard status update // Direct scoreboard status update
// Direct scoreboard status update (texture and text)
llMessageLinked(LINK_SCOREBOARD, MSG_GAME_STATUS, "Plot Twist", NULL_KEY); llMessageLinked(LINK_SCOREBOARD, MSG_GAME_STATUS, "Plot Twist", NULL_KEY);
llMessageLinked(LINK_SET, MSG_STATUS_TEXT, "PLOT TWIST!\n<!c=green>Shield Up!<!c=white> Target shifted!", NULL_KEY);
// IMPORTANT: Send peril player update to scoreboard for glow effect // IMPORTANT: Send peril player update to scoreboard for glow effect
llMessageLinked(LINK_SCOREBOARD, MSG_UPDATE_PERIL_PLAYER, newPeril, NULL_KEY); // MSG_UPDATE_PERIL_PLAYER llMessageLinked(LINK_SCOREBOARD, MSG_UPDATE_PERIL_PLAYER, newPeril, NULL_KEY); // MSG_UPDATE_PERIL_PLAYER
// Add delay to let status display before next phase // Add delay to let status display before next phase
llSleep(2.0); llSleep(4.0);
// Update floaters immediately to show correct peril player before sync // Update floaters immediately to show correct peril player before sync
llMessageLinked(LINK_SET, MSG_UPDATE_FLOAT, newPeril, NULL_KEY); llMessageLinked(LINK_SET, MSG_UPDATE_FLOAT, newPeril, NULL_KEY);
@ -400,6 +406,10 @@ default {
llMessageLinked(LINK_SCOREBOARD, MSG_PLAYER_UPDATE, perilPlayer + "|" + (string)(currentLives - 1) + "|" + "NULL_KEY", NULL_KEY); llMessageLinked(LINK_SCOREBOARD, MSG_PLAYER_UPDATE, perilPlayer + "|" + (string)(currentLives - 1) + "|" + "NULL_KEY", NULL_KEY);
dbg("🎲 [Roll Module] 💗 Player lives updated: " + perilPlayer + " now has " + (string)(currentLives - 1) + " lives"); dbg("🎲 [Roll Module] 💗 Player lives updated: " + perilPlayer + " now has " + (string)(currentLives - 1) + " lives");
// NEW: REPORT LIFE REDUCTION to Main Controller (Source of Truth)
// This replaces the unreliable self-syncing and allows the Controller to broadcast the official state.
llMessageLinked(LINK_SET, MSG_UPDATE_LIFE, perilPlayer + "|" + (string)(currentLives - 1), NULL_KEY);
// Check if peril player picked the rolled number // Check if peril player picked the rolled number
list perilPicks = getPicksFor(perilPlayer); list perilPicks = getPicksFor(perilPlayer);
integer perilPickedIt = (llListFindList(perilPicks, [resultStr]) != -1); integer perilPickedIt = (llListFindList(perilPicks, [resultStr]) != -1);
@ -411,16 +421,18 @@ default {
if (matched && perilPickedIt) { if (matched && perilPickedIt) {
llSay(0, "🩸 DIRECT HIT! " + perilPlayer + " picked their own doom - the d" + (string)diceType + " landed on " + resultStr + "! 🩸"); llSay(0, "🩸 DIRECT HIT! " + perilPlayer + " picked their own doom - the d" + (string)diceType + " landed on " + resultStr + "! 🩸");
// Direct scoreboard status update // Direct scoreboard status update (texture and text)
llMessageLinked(LINK_SCOREBOARD, MSG_GAME_STATUS, "Direct Hit", NULL_KEY); llMessageLinked(LINK_SCOREBOARD, MSG_GAME_STATUS, "Direct Hit", NULL_KEY);
llMessageLinked(LINK_SET, MSG_STATUS_TEXT, "DIRECT HIT!\n<!c=red>" + perilPlayer + " matched!", NULL_KEY);
// Add delay to let status display before next phase // Add delay to let status display before next phase
llSleep(2.0); llSleep(4.0);
} else if (!matched) { } else if (!matched) {
llSay(0, "🩸 NO SHIELD! Nobody picked " + resultStr + " - " + perilPlayer + " takes the hit from the d" + (string)diceType + "! 🩸"); llSay(0, "🩸 NO SHIELD! Nobody picked " + resultStr + " - " + perilPlayer + " takes the hit from the d" + (string)diceType + "! 🩸");
// Direct scoreboard status update // Direct scoreboard status update (texture and text)
llMessageLinked(LINK_SCOREBOARD, MSG_GAME_STATUS, "No Shield", NULL_KEY); llMessageLinked(LINK_SCOREBOARD, MSG_GAME_STATUS, "No Shield", NULL_KEY);
llMessageLinked(LINK_SET, MSG_STATUS_TEXT, "NO SHIELD!\n<!c=red>" + perilPlayer + " took the hit!", NULL_KEY);
// Add delay to let status display before next phase // Add delay to let status display before next phase
llSleep(2.0); llSleep(4.0);
} else { } else {
// This should never happen in normal game flow since Plot Twist case is handled above // This should never happen in normal game flow since Plot Twist case is handled above
// But if it does, it means someone else picked it - this should have been Plot Twist // But if it does, it means someone else picked it - this should have been Plot Twist