v3.2.0: The 'Sync Sanctity' Update - Unified life sync, virtualized scoreboard, and documentation finalization
This commit is contained in:
parent
a225244241
commit
824824c32d
15 changed files with 419 additions and 149 deletions
17
CHANGELOG.md
17
CHANGELOG.md
|
|
@ -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**
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
====================
|
====================
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
44
FURWARE_Status_Bridge_Linkset.lsl
Normal file
44
FURWARE_Status_Bridge_Linkset.lsl
Normal 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...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
]);
|
]);
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
36
README.md
36
README.md
|
|
@ -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).
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
==========================
|
==========================
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue