diff --git a/CHANGELOG.md b/CHANGELOG.md index 344a680..80f85c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,23 @@ 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 ### 🔍 **Dynamic Link Discovery System** diff --git a/CHANGELOG_Notecard.txt b/CHANGELOG_Notecard.txt index d41dd83..9c74423 100644 --- a/CHANGELOG_Notecard.txt +++ b/CHANGELOG_Notecard.txt @@ -3,6 +3,24 @@ PERIL DICE CHANGELOG 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 ==================== diff --git a/FURWARE_Leaderboard_Communication_Linkset.lsl b/FURWARE_Leaderboard_Communication_Linkset.lsl index 9138ba6..c1ded8f 100644 --- a/FURWARE_Leaderboard_Communication_Linkset.lsl +++ b/FURWARE_Leaderboard_Communication_Linkset.lsl @@ -2,10 +2,21 @@ string leftText = ""; -// Distribute text to the FURWARE "Leaderboard" box +// Distribute text to the FURWARE "Leaderboard" boxes distributeFullText(string text) { - // Send directly to FURWARE. It handles multiple prims and newlines automatically. - llMessageLinked(LINK_SET, 0, text, (key)(FW_DATA + ":Leaderboard")); + if (llGetSubString(text, 0, 7) == "COLUMNS|") { + // 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 { @@ -17,29 +28,40 @@ default { state_entry() { DISCOVER_CORE_LINKS(); REPORT_MEMORY(); - dbg("📋 [Leaderboard] Leaderboard Communication Script ready! (FURWARE Version)"); - dbg("📋 [Leaderboard] Managing 'Leaderboard' FURWARE box"); - dbg("📋 [Leaderboard] ✅ Discovery complete! Scoreboard: " + (string)LINK_SCOREBOARD); + dbg("📋 [Leaderboard] Leaderboard Communication Script ready! (Virtual Multi-Column)"); + + // 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 distributeFullText(""); } link_message(integer sender, integer num, string str, key id) { - // Only listen to messages from the scoreboard - if (sender != LINK_SCOREBOARD) { - return; - } - - if (num == MSG_RESET_LEADERBOARD) { - // FURWARE handles the entire leaderboard as a single text block + // Handle messages from the game logic + if (num == MSG_DISPLAY_LEADERBOARD) { if (llGetSubString(str, 0, 14) == "FORMATTED_TEXT|") { distributeFullText(llGetSubString(str, 15, -1)); - } - // For individual bank updates - else if (llGetSubString(str, 0, 9) == "LEFT_TEXT|") { + } else if (llGetSubString(str, 0, 9) == "LEFT_TEXT|") { + // For individual bank updates leftText = llGetSubString(str, 10, -1); distributeFullText(leftText); + } else { + // Pass directly for COLUMNS| or multi-column data + distributeFullText(str); } } else if (llSubStringIndex(str, "LEADERBOARD") == 0) { diff --git a/FURWARE_Status_Bridge_Linkset.lsl b/FURWARE_Status_Bridge_Linkset.lsl new file mode 100644 index 0000000..8b6a5ea --- /dev/null +++ b/FURWARE_Status_Bridge_Linkset.lsl @@ -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 to set the color to dark yellow + string formattedText = "" + 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..."); + } + } +} diff --git a/Floater_Manager.lsl b/Floater_Manager.lsl index da25e06..3f87fa2 100644 --- a/Floater_Manager.lsl +++ b/Floater_Manager.lsl @@ -91,6 +91,13 @@ default { perilPlayer = ""; 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) { diff --git a/GAME_RULES.md b/GAME_RULES.md index f390b9b..fbbe419 100644 --- a/GAME_RULES.md +++ b/GAME_RULES.md @@ -183,7 +183,7 @@ The game features thematic messaging for maximum immersion: - **Professional Status Actions** - Large status prim shows current game events - **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 🔍 - **FURWARE Text Mesh Integration** - Professional mesh-based displays for dice and leaderboard 🎨 - **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 -**Last Updated:** April 04, 2026 -**Game System:** Production Ready - Linkset Stability Release +**Version:** 3.2.0 +**Last Updated:** April 05, 2026 +**Status:** Production Ready - Sync Sanctity Release + **Credits:** Original concept by Noose the Bunny (djmusica28), Automated system implementation by Rebecca Nod diff --git a/GAME_RULES_NOTECARD.txt b/GAME_RULES_NOTECARD.txt index e25c86c..793673a 100644 --- a/GAME_RULES_NOTECARD.txt +++ b/GAME_RULES_NOTECARD.txt @@ -1,7 +1,16 @@ PERIL DICE GAME - OFFICIAL RULES +======================================================== -Created by Rebecca Nod and Noose the Bunny -Original concept by Noose the Bunny (djmusica28) in Second Life +CREATED BY REBECCA NOD AND NOOSE THE BUNNY (djmusica28) +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 - Color-Coded Glow System - Green (winner), Yellow (peril), Red X (eliminated) -Technical Rules (UPDATED in v3.1.0): -- 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 🎨 -- Centralized Constants - System-wide settings managed in Peril_Constants.lsl 🧩 +Technical Rules (UPDATED in v3.2.0): +- Sync Sanctity Protocol - 100% unified life counting via the Main Controller 🛡️ +- Scoreboard Virtualization - Dynamic slot hiding and multi-column Rank/Name/WL grid 🎰 +- 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 - Owner kick functionality for managing disruptive players - 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 -Last Updated: April 04, 2026 -Game System: Production Ready - Linkset Stability Release +Version: 3.2.0 +Last Updated: April 05, 2026 +Status: Production Ready - Sync Sanctity Release Credits: Original concept by Noose the Bunny (djmusica28), Automated system implementation by Rebecca Nod diff --git a/Game_Manager.lsl b/Game_Manager.lsl index 8fb1618..1ebe2f7 100644 --- a/Game_Manager.lsl +++ b/Game_Manager.lsl @@ -159,6 +159,18 @@ integer continueCurrentRound() { // Request dice type directly from Calculator for the new round 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: " + 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 return 0; } @@ -184,6 +196,10 @@ integer startNextRound() { llMessageLinked(LINK_SET, MSG_PLAYER_WON, winner, 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 // Main Controller will send the proper GAME_WON message to scoreboard // Let main controller handle reset @@ -214,6 +230,12 @@ integer startNextRound() { string startPlayerName = llList2String(names, startJ); 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: " + perilPlayer, NULL_KEY); + + // Dramatic pause to let the first peril player be seen before round prep begins + llSleep(4.0); } picksData = []; @@ -234,6 +256,13 @@ integer startNextRound() { // Request dice type for this round 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..."); return 0; } @@ -348,6 +377,9 @@ integer showNextPickerDialog() { dbg("🎯 [Game Manager] Sending bot command with complete avoid list (" + (string)llGetListLength(showCompleteAvoidList) + " numbers): " + showAvoidListStr); llMessageLinked(LINK_SET, MSG_BOT_COMMAND, botCommand, NULL_KEY); 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 { // Bot already has picks, advance to next player immediately currentPickerIdx++; @@ -379,6 +411,9 @@ integer showNextPickerDialog() { // Send dialog request through Player_RegistrationManager (it has the correct player keys) string dialogRequest = "SHOW_DIALOG|" + firstName + "|" + dialogPayload; 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; } @@ -637,7 +672,12 @@ default { diceType = (integer)str; diceTypeProcessed = TRUE; 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 if (roundStarted && currentPickerIdx < llGetListLength(pickQueue) && llGetListLength(pickQueue) > 0 && diceType > 0) { diff --git a/Game_Scoreboard_Manager_Linkset.lsl b/Game_Scoreboard_Manager_Linkset.lsl index 739e036..c6a2e24 100644 --- a/Game_Scoreboard_Manager_Linkset.lsl +++ b/Game_Scoreboard_Manager_Linkset.lsl @@ -51,12 +51,15 @@ discoverLinks() { } 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 --- #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 #define TEXTURE_0_HEARTS "7d8ae121-e171-12ae-f5b6-7cc3c0395c7b" // 0 hearts (dead) @@ -281,71 +284,58 @@ integer generateLeaderboardText() { list sortedData = getSortedLeaderboard(); string SPACES = " "; string title = "TOP BATTLE RECORDS"; - integer titleMargin = 0; - string leaderboardText = ""; - integer genI = 0; - string playerName = ""; - integer genWins = 0; - integer genLosses = 0; - string rank = ""; - string leftPart = ""; - string rightPart = ""; - integer spaceNeeded = 0; + string rankCol = ""; + string nameCol = ""; + string statsCol = ""; + integer numSorted = llGetListLength(sortedData) / 3; - integer base = 0; - string sWins = ""; - string sLosses = ""; + integer genI; + integer base; + string playerName; + integer genWins; + integer genLosses; + string rank; + string sWins; + string sLosses; - // 1. Build Title Row (Centered) - titleMargin = (LEADERBOARD_WIDTH - llStringLength(title)) / 2; + // 1. Send Title to Parent Box (Row 0) + integer titleMargin = (LEADERBOARD_WIDTH - llStringLength(title)) / 2; 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 (up to MAX_LEADERBOARD_ENTRIES) + // Add actual player data column-by-column for (genI = 0; genI < numSorted && genI < MAX_LEADERBOARD_ENTRIES; genI++) { base = genI * 3; playerName = llList2String(sortedData, base); genWins = llList2Integer(sortedData, base + 1); genLosses = llList2Integer(sortedData, base + 2); - if (llStringLength(playerName) > 12) { - playerName = llGetSubString(playerName, 0, 9) + "..."; - } + if (llStringLength(playerName) > 19) playerName = llGetSubString(playerName, 0, 16) + "..."; - // Format rank: "01. " through "10. " - rank = (string)(genI + 1) + ". "; + rank = (string)(genI + 1) + "."; if (genI < 9) rank = "0" + rank; - // Format stats 10-char width: "W: 99/L: 9" sWins = (string)genWins; - if (llStringLength(sWins) < 3) sWins = llGetSubString(" ", 0, 2 - llStringLength(sWins)) + sWins; sLosses = (string)genLosses; - if (llStringLength(sLosses) < 2) sLosses = llGetSubString(" ", 0, 1 - llStringLength(sLosses)) + sLosses; - leftPart = rank + playerName; - rightPart = "W:" + sWins + "/L:" + sLosses; - - spaceNeeded = LEADERBOARD_WIDTH - llStringLength(leftPart) - 10; - if (spaceNeeded < 1) spaceNeeded = 1; - - leaderboardText += leftPart + llGetSubString(SPACES, 0, spaceNeeded - 1) + rightPart + "\n"; + rankCol += rank + "\n"; + nameCol += playerName + "\n"; + statsCol += "W:" + sWins + "/L:" + sLosses + "\n"; } // Fill remaining positions with placeholders for (genI = numSorted; genI < MAX_LEADERBOARD_ENTRIES; genI++) { - rank = (string)(genI + 1) + ". "; + rank = (string)(genI + 1) + "."; if (genI < 9) rank = "0" + rank; - leftPart = rank + "----------"; - rightPart = "W: 0/L: 0"; - spaceNeeded = LEADERBOARD_WIDTH - llStringLength(leftPart) - 10; - - leaderboardText += leftPart + llGetSubString(SPACES, 0, spaceNeeded - 1) + rightPart + "\n"; + rankCol += rank + "\n"; + nameCol += "----------\n"; + statsCol += "W:0/L:0\n"; } - // Send formatted text to leaderboard bridge - llMessageLinked(LINK_LEADERBOARD_BRIDGE, MSG_RESET_LEADERBOARD, "FORMATTED_TEXT|" + leaderboardText, NULL_KEY); + // Send 3-column data to bridge + llMessageLinked(LINK_SET, MSG_DISPLAY_LEADERBOARD, "COLUMNS|" + rankCol + "|" + nameCol + "|" + statsCol, NULL_KEY); return 0; } @@ -386,20 +376,20 @@ integer clearAllPlayers() { // SAFETY CHECK - use if block instead of continue for maximum compatibility if (clearProfilePrimIndex > 0) { - // Reset profile prim to default and clear any glow + // Reset profile prim to default and hide it llSetLinkPrimitiveParamsFast(clearProfilePrimIndex, [ 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_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, [ 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_GLOW, ALL_SIDES, 0.0 // Explicitly clear glow + PRIM_GLOW, ALL_SIDES, 0.0 ]); // Reset overlay prim to transparent @@ -446,16 +436,16 @@ integer refreshPlayerDisplay() { refreshOverlayPrimIndex = getOverlayPrimLink(refreshI); if (refreshProfilePrimIndex > 0) { - // Reset to default only for unused slots + // Unused slots: Reset and HIDE llSetLinkPrimitiveParamsFast(refreshProfilePrimIndex, [ 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 ]); llSetLinkPrimitiveParamsFast(refreshHeartsPrimIndex, [ 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 ]); @@ -780,8 +770,7 @@ default { } link_message(integer sender, integer num, string str, key id) { - // Only listen to messages from the main controller (link 1) - if (sender != LINK_CONTROLLER) return; + // Handle messages from the game logic and bridges if (num == MSG_GAME_STATUS) { updateActionsPrim(str); @@ -852,6 +841,11 @@ default { // Update glow effects - winner glow overrides peril glow 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) { @@ -981,13 +975,17 @@ default { listen(integer channel, string name, key id, string message) { string msg = llToLower(llStringTrim(message, STRING_TRIM)); 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 overlayIdx; for (i = 0; i < 10; i++) { - overlayIdx = getOverlayPrimLink(i); - if (overlayIdx > 0) { - llSetLinkPrimitiveParamsFast(overlayIdx, [ + integer pIdx = getProfilePrimLink(i); + integer hIdx = getHeartsPrimLink(i); + 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_COLOR, ALL_SIDES, <1.0, 1.0, 1.0>, 1.0 ]); diff --git a/MARKETPLACE_DESCRIPTION.txt b/MARKETPLACE_DESCRIPTION.txt index b65903f..df5e8e1 100644 --- a/MARKETPLACE_DESCRIPTION.txt +++ b/MARKETPLACE_DESCRIPTION.txt @@ -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: - Dynamic Link Discovery: "Bulletproof" linkset architecture that auto-configures by prim name diff --git a/Main_Controller_Linkset.lsl b/Main_Controller_Linkset.lsl index d422e66..1af3acb 100644 --- a/Main_Controller_Linkset.lsl +++ b/Main_Controller_Linkset.lsl @@ -182,18 +182,41 @@ checkMemoryUsage(string context) { // Send status message to scoreboard using link messages 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(); lastStatus = status; - // CHANGED: Use link message instead of llRegionSay + // Send texture update to scoreboard 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; 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 updateHelpers() { @@ -249,11 +272,9 @@ resetGame() { cleanupListeners(); // 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); - } + // Always request cleanup on a reset to ensure no orphans are left behind + llMessageLinked(LINK_SET, MSG_CLEANUP_ALL_FLOATERS, "RESET", NULL_KEY); + llSleep(0.5); llSleep(0.5); @@ -275,7 +296,7 @@ resetGame() { llMessageLinked(LINK_SCOREBOARD, MSG_CLEAR_GAME, "", NULL_KEY); // Clear winner glow from scoreboard (floaters will clear naturally on reset) 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)."); @@ -285,6 +306,9 @@ resetGame() { llSetTexture(TEXTURE_START, CONTROLLER_FACE); 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); llSetTimerEvent(0); @@ -471,7 +495,25 @@ default { } 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 if (num == MSG_UPDATE_MAIN_LISTS) { // MSG_UPDATE_MAIN_LISTS @@ -497,6 +539,9 @@ default { lives += [3]; 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 integer pendingIdx = llListFindList(pendingRegistrations, [id]); if (pendingIdx != -1) { @@ -517,6 +562,11 @@ default { string eliminatedPlayer = llList2String(parts, 1); integer idx = llListFindList(names, [eliminatedPlayer]); 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 lives = llListReplaceList(lives, [0], idx, idx); @@ -577,13 +627,8 @@ default { } } - // Build sync message WITHOUT redundant player keys (5th part) to save memory - // Combined into single message to avoid multiple temporary string allocations - llMessageLinked(LINK_SET, MSG_SYNC_GAME_STATE, - llList2CSV(lives) + "~" + - (string)llDumpList2String(picksData, "^") + "~" + - (string)llList2String([perilPlayer, "NONE"], (perilPlayer == "")) + "~" + - llList2CSV(names), NULL_KEY); + // UNIFIED SYNC: Replaces inline sync calls to prevent language rift + syncGameState(); // Direct floater update to ensure eliminated status is displayed llMessageLinked(LINK_SET, MSG_UPDATE_FLOAT, eliminatedPlayer, llList2Key(players, idx)); @@ -622,6 +667,9 @@ default { lives = llDeleteSubList(lives, 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 @@ -658,6 +706,10 @@ default { 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" + winner + " wins!", NULL_KEY); + llMessageLinked(LINK_SET, MSG_PLAYER_WON, winner, NULL_KEY); llMessageLinked(LINK_SET, MSG_EFFECT_CONFETTI, "VICTORY_CONFETTI", NULL_KEY); @@ -733,8 +785,8 @@ default { list parts = llParseString2List(str, ["|"], []); if (llList2String(parts, 0) == "GAME_WON") { string winner = llList2String(parts, 1); - // CHANGED: Use link message instead of llRegionSay - llMessageLinked(LINK_DICE_BRIDGE, MSG_DICE_ROLL, winner + "|WON", NULL_KEY); + // Send dice roll winner message using LINK_SET + llMessageLinked(LINK_SET, MSG_DICE_ROLL, winner + "|WON", NULL_KEY); llSleep(2.0); resetGame(); } @@ -867,6 +919,11 @@ default { // CHANGED: Remove player from scoreboard BEFORE updating lists 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 integer ch = FLOATER_BASE_CHANNEL + idx; llMessageLinked(LINK_SET, MSG_CLEANUP_FLOAT, (string)ch, NULL_KEY); @@ -1159,7 +1216,12 @@ default { integer actualPlayerCount = llGetListLength(names); 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 llMessageLinked(LINK_SET, 998, "", NULL_KEY); } diff --git a/Peril_Constants.lsl b/Peril_Constants.lsl index fa4ea17..d8a7c43 100644 --- a/Peril_Constants.lsl +++ b/Peril_Constants.lsl @@ -7,6 +7,7 @@ integer LINK_CONTROLLER = 1; // Usually Root (link 1) integer LINK_SCOREBOARD = -1; // "Scoreboard: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_STATUS_BRIDGE = -1; // "FURWARE text mesh:Status:0:0" // Macro to discover core links by name #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; \ 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 (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 --- @@ -43,6 +47,7 @@ integer LINK_LEADERBOARD_BRIDGE = -1; // "FURWARE text mesh:Leaderboard:0:0" // --- Game Flow (3000-3099) --- #define MSG_GAME_STATUS 3001 +#define MSG_UPDATE_LIFE 80 // Unified damage reporting #define MSG_PLAYER_UPDATE 3002 #define MSG_CLEAR_GAME 3003 #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_LOST 3011 #define MSG_RESET_LEADERBOARD 3012 +#define MSG_DISPLAY_LEADERBOARD 3013 +#define MSG_STATUS_TEXT 3007 #define MSG_DICE_ROLL 3020 #define MSG_CLEAR_DICE 3021 #define MSG_DICE_CLEAR 3021 // Alias for consistency in dice bridge diff --git a/README.md b/README.md index cac4b20..01f23a9 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,14 @@ # 🎲 Peril Dice — Professional Single Linkset Game System for Second Life **Created by Rebecca Nod and Noose the Bunny** -**Current Version: 3.1.0 - The "Linkset Stability" Update** +**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 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.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. -## 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** - **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 -**Current Version**: 3.1.0 -**Last Updated**: April 04, 2026 -**Status**: Production Ready - Linkset Stability Release +**Current Version**: 3.2.0 +**Last Updated**: April 05, 2026 +**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: - ✅ **Architectural Decentralization**: Implemented Dynamic Link Discovery (no hardcoded links). diff --git a/README_Notecard.txt b/README_Notecard.txt index 7d13272..49d715c 100644 --- a/README_Notecard.txt +++ b/README_Notecard.txt @@ -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 -CURRENT VERSION: 3.1.0 - THE "LINKSET STABILITY" UPDATE +CREATED BY REBECCA NOD AND NOOSE THE BUNNY (djmusica28) +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. 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 =================== -Current Version: 3.1.0 -Last Updated: April 04, 2026 -Status: Production Ready - Linkset Stability Release +Current Version: 3.2.0 +Last Updated: April 05, 2026 +Status: Production Ready - Sync Sanctity Release ORIGINAL GAME RULES CREDIT ========================== diff --git a/Roll_ConfettiModule.lsl b/Roll_ConfettiModule.lsl index 997a19a..ce64796 100644 --- a/Roll_ConfettiModule.lsl +++ b/Roll_ConfettiModule.lsl @@ -68,28 +68,19 @@ list getPicksFor(string nameInput) { list parts = llParseString2List(entry, ["|"], []); if (llList2String(parts, 0) == nameInput) { // Convert semicolons back to commas, then parse - string picks = llList2String(parts, 1); - string originalPicks = picks; + string picks = llStringTrim(llList2String(parts, 1), STRING_TRIM); - // Check for corruption markers (^ symbols that shouldn't be in picks) - if (llSubStringIndex(picks, "^") != -1) { - return []; - } + // Check for corruption or empty + if (llSubStringIndex(picks, "^") != -1 || picks == "") return []; - if (picks == "") { - return []; - } + // NEW: Robust multi-delimiter parsing (handles ", " "; " "|" and " ") + 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 = []; integer j; for (j = 0; j < llGetListLength(rawResult); j++) { string num = llStringTrim(llList2String(rawResult, j), STRING_TRIM); - if (num != "") { - result += [num]; - } + if (num != "") result += [num]; } return result; } @@ -260,17 +251,24 @@ default { // 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 } else { - // Clear dice display directly (link 83) - llMessageLinked(LINK_DICE_BRIDGE, MSG_CLEAR_DICE, "", NULL_KEY); + // Clear dice display directly using LINK_SET since bridge moved to root + llMessageLinked(LINK_SET, MSG_CLEAR_DICE, "", NULL_KEY); // Update peril player from the roll dialog message perilPlayer = str; 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) if (llSubStringIndex(str, "Bot") == 0) { // Auto-roll for bots - request dice type first 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 shouldRoll = TRUE; // Set flag to perform roll when dice type is received // Request current dice type from Calculator @@ -338,8 +336,14 @@ default { lastRollTime = llGetUnixTime(); // Record roll time 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) - llMessageLinked(LINK_DICE_BRIDGE, MSG_DICE_ROLL, perilPlayer + "|" + resultStr, NULL_KEY); + // Send dice roll directly to dice display using LINK_SET + 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 = ""; integer matched = FALSE; @@ -377,13 +381,15 @@ default { perilPlayer = newPeril; // Direct scoreboard status update + // Direct scoreboard status update (texture and text) llMessageLinked(LINK_SCOREBOARD, MSG_GAME_STATUS, "Plot Twist", NULL_KEY); + llMessageLinked(LINK_SET, MSG_STATUS_TEXT, "PLOT TWIST!\nShield Up! Target shifted!", NULL_KEY); // IMPORTANT: Send peril player update to scoreboard for glow effect llMessageLinked(LINK_SCOREBOARD, MSG_UPDATE_PERIL_PLAYER, newPeril, NULL_KEY); // MSG_UPDATE_PERIL_PLAYER // 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 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); 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 list perilPicks = getPicksFor(perilPlayer); integer perilPickedIt = (llListFindList(perilPicks, [resultStr]) != -1); @@ -411,16 +421,18 @@ default { if (matched && perilPickedIt) { 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_SET, MSG_STATUS_TEXT, "DIRECT HIT!\n" + perilPlayer + " matched!", NULL_KEY); // Add delay to let status display before next phase - llSleep(2.0); + llSleep(4.0); } else if (!matched) { 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_SET, MSG_STATUS_TEXT, "NO SHIELD!\n" + perilPlayer + " took the hit!", NULL_KEY); // Add delay to let status display before next phase - llSleep(2.0); + llSleep(4.0); } else { // 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