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.
|
||||
|
||||
## [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**
|
||||
|
|
|
|||
|
|
@ -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
|
||||
====================
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
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 = "";
|
||||
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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: <!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
|
||||
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: <!c=red>" + 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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<!c=white>" + 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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
36
README.md
36
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).
|
||||
|
|
|
|||
|
|
@ -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
|
||||
==========================
|
||||
|
|
|
|||
|
|
@ -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!\n<!c=green>Shield Up!<!c=white> 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<!c=red>" + 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<!c=red>" + 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
|
||||
|
|
|
|||
Loading…
Reference in a new issue