## [2.6.0] - 2025-08-09

This commit is contained in:
RebeccaNod1 2025-08-09 21:01:57 -05:00
parent 9e480e6bcc
commit 9f918be0c9
9 changed files with 155 additions and 120 deletions

View file

@ -1,5 +1,7 @@
# Changelog
**Peril Dice Game System - Created by Rebecca Nod and Noose the Bunny**
All notable changes to Peril Dice will be documented in this file.
## [2.6.0] - 2025-08-09

View file

@ -1,5 +1,7 @@
# 🎲 **PERIL DICE GAME - OFFICIAL RULES**
**Created by Rebecca Nod and Noose the Bunny**
*Original concept by **Noose the Bunny** (djmusica28) in Second Life*
---

View file

@ -166,9 +166,9 @@ default {
integer playerCount = (integer)str;
llOwnerSay("📈 Calculator: Calculating dice type for " + (string)playerCount + " players");
integer result = getDiceType(playerCount);
// Send result ONLY back to the requesting script to prevent duplicate processing
// Main Controller will forward it to Game Manager as needed
llMessageLinked(sender, MSG_DICE_TYPE_RESULT, (string)result, NULL_KEY);
// Send result to ALL scripts via LINK_SET so Game Manager receives it directly
// Each script will handle duplicate prevention on their end
llMessageLinked(LINK_SET, MSG_DICE_TYPE_RESULT, (string)result, NULL_KEY);
}
else if (num == MSG_GET_PICKS_REQUIRED) {
integer result = getPicksRequiredFromName(str);

View file

@ -29,6 +29,7 @@ integer diceType = 6;
key currentPicker;
integer roundStarted = FALSE;
integer diceTypeProcessed = FALSE; // Track if we've already processed dice type for this round
integer ignorePicksSync = FALSE; // Temporarily ignore picks data sync during round initialization
// Duplicate request prevention
string lastHumanPickMessage = "";
@ -50,6 +51,8 @@ continueCurrentRound() {
picksData = [];
globalPickedNumbers = [];
llOwnerSay("🔄 [Game Manager] continueCurrentRound: Cleared picks data, starting fresh");
// Set round as started for the new round
roundStarted = TRUE;
diceTypeProcessed = FALSE; // Reset dice type processing for new round
@ -65,7 +68,9 @@ continueCurrentRound() {
}
currentPickerIdx = 0;
// Request dice type for the new round
llOwnerSay("🎯 [Game Manager] Pick queue created: " + llList2CSV(pickQueue) + ", requesting dice type");
// Request dice type directly from Calculator for the new round
llMessageLinked(LINK_SET, MSG_GET_DICE_TYPE, (string)llGetListLength(names), NULL_KEY);
// Note: showNextPickerDialog() will be called when dice type result is received
@ -282,49 +287,59 @@ default {
newPicksData = llParseString2List(picksDataStr, ["^"], []);
}
// Don't sync peril player from Main Controller - Game Manager is authoritative
// string receivedPeril = llList2String(parts, 2);
// Game Manager manages its own peril player state
names = llCSV2List(llList2String(parts, 3));
// Game Manager is authoritative for peril player, but check for eliminations
list newNames = llCSV2List(llList2String(parts, 3));
// Check if current peril player was eliminated
if (perilPlayer != "" && perilPlayer != "NONE") {
integer perilStillExists = llListFindList(newNames, [perilPlayer]) != -1;
if (!perilStillExists && llListFindList(names, [perilPlayer]) != -1) {
// Peril player was eliminated! Game Manager needs to assign new peril player
llOwnerSay("🚨 [Game Manager] Detected elimination of peril player: " + perilPlayer);
// Find first remaining alive player to be new peril player
string newPerilPlayer = "";
integer k;
for (k = 0; k < llGetListLength(newNames) && newPerilPlayer == ""; k++) {
string candidateName = llList2String(newNames, k);
integer candidateIdx = llListFindList(newNames, [candidateName]);
if (candidateIdx != -1 && candidateIdx < llGetListLength(newLives)) {
integer candidateLives = llList2Integer(newLives, candidateIdx);
if (candidateLives > 0) {
newPerilPlayer = candidateName;
}
}
}
if (newPerilPlayer != "") {
llOwnerSay("🎯 [Game Manager] Assigning new peril player: " + newPerilPlayer);
perilPlayer = newPerilPlayer;
} else {
llOwnerSay("⚠️ [Game Manager] No valid peril player candidates found!");
perilPlayer = "NONE";
}
}
}
names = newNames;
// New: also receive players list if available (for dialog targeting)
if (llGetListLength(parts) >= 5) {
players = llCSV2List(llList2String(parts, 4));
}
// AUTOMATIC ROUND CONTINUATION: Detect post-roll state
// Check if lives have changed (indicating a roll occurred) and all picks are now empty
// Always update state normally - don't auto-continue rounds
// The Roll module will handle post-roll logic and Main Controller will manage round flow
lives = newLives;
picksData = newPicksData;
// Only log if this appears to be a post-roll update for debugging
string oldLivesStr = llList2CSV(lives);
string newLivesStr = llList2CSV(newLives);
integer livesChanged = (oldLivesStr != newLivesStr);
// Check if all picks are empty (indicating post-roll state)
integer allPicksEmpty = TRUE;
integer i;
for (i = 0; i < llGetListLength(newPicksData) && allPicksEmpty; i++) {
string entry = llList2String(newPicksData, i);
list entryParts = llParseString2List(entry, ["|"], []);
if (llGetListLength(entryParts) >= 2 && llList2String(entryParts, 1) != "") {
allPicksEmpty = FALSE;
}
}
// If lives changed and all picks are empty, this is post-roll - start new round
if (livesChanged && allPicksEmpty && roundStarted && perilPlayer != "" && perilPlayer != "NONE") {
llOwnerSay("🎯 [Game Manager] Post-roll detected - automatically continuing to next round");
// Update our state first
lives = newLives;
picksData = newPicksData;
// Reset for new round and continue
roundStarted = FALSE;
currentPickerIdx = 0;
continueCurrentRound();
} else {
// Normal state update
lives = newLives;
picksData = newPicksData;
if (livesChanged) {
llOwnerSay("🔍 [Game Manager] Lives changed - roll completed, waiting for proper round continuation signal");
}
}
return;

View file

@ -133,8 +133,6 @@ list floaterChannels = []; // Track actual floater channels for cleanup
list pickQueue = [];
integer currentPickerIdx = 0;
integer diceType = 6;
integer MSG_SHOW_DIALOG = 101;
integer MSG_ROLL_RESULT = 102;
integer MSG_UPDATE_FLOAT = 103;
@ -157,10 +155,9 @@ integer CONTROLLER_FACE = 1; // Face to display start image on (front face)
string TEXTURE_START = "title_start"; // Start image texture
string TEXTURE_GAME_ACTIVE = "game_active"; // Optional: different texture during game
integer MSG_GET_DICE_TYPE = 1001;
// Dice type handling is now done by Game Manager directly
integer MSG_GET_PICKS_REQUIRED = 1002;
integer MSG_GET_PICKER_INDEX = 1003;
integer MSG_DICE_TYPE_RESULT = 1005;
integer MSG_SERIALIZE_GAME_STATE = 1004;
integer MSG_SYNC_PICKQUEUE = 2001;
@ -168,12 +165,13 @@ integer MSG_SYNC_PICKQUEUE = 2001;
// mirror the value used in Floater Manager to avoid inconsistencies.
integer MAX_PLAYERS = 10;
integer TIMEOUT_SECONDS = 600;
integer TIMEOUT_SECONDS = 120; // Reduced from 600 (10 min) to 120 (2 min)
integer timeoutTimer;
integer warning2min = 120;
integer warning5min = 300;
integer warning9min = 540;
// Updated warning times for 2-minute timeout
integer warning30sec = 30; // 30 seconds remaining
integer warning1min = 60; // 1 minute remaining
integer warning90sec = 90; // 1.5 minutes remaining
integer lastWarning = 0;
key currentPicker;
@ -266,10 +264,6 @@ updateHelpers() {
}
// Request helper-calculated values
requestDiceType() {
llMessageLinked(LINK_SET, MSG_GET_DICE_TYPE, (string)llGetListLength(names), NULL_KEY);
}
requestPicksRequired(integer idx) {
llMessageLinked(LINK_SET, MSG_GET_PICKS_REQUIRED, llList2String(names, idx), NULL_KEY);
}
@ -329,11 +323,9 @@ resetGame() {
currentPickerIdx = 0;
roundStarted = FALSE; // Reset round flag
gameStarting = FALSE; // Reset game starting flag
diceTypeProcessed = FALSE; // Reset dice type processing flag
currentPicker = NULL_KEY; // Reset current picker
timeoutTimer = 0; // Reset timeout timer
lastWarning = 0; // Reset warning timer
diceType = 6; // Reset dice type to default
// Send reset message to all other scripts to clear their state
llMessageLinked(LINK_SET, -99999, "FULL_RESET", NULL_KEY);
@ -370,7 +362,6 @@ resetGame() {
integer roundStarted = FALSE;
integer gameStarting = FALSE; // Track when game is in startup sequence
integer diceTypeProcessed = FALSE; // Track if we've already processed initial dice type
// CONTINUE_ROUND logic now handled entirely by Game Manager
@ -547,24 +538,8 @@ if (toucher == llGetOwner() && roundStarted) {
link_message(integer sender, integer num, string str, key id) {
// Player list and pick data handling now delegated to Controller_MessageHandler.lsl
// Handle dice type result from helper - forward to Game Manager once
if (num == MSG_DICE_TYPE_RESULT) {
// Prevent duplicate processing of the same dice type result
if (diceTypeProcessed) {
return;
}
diceType = (integer)str;
diceTypeProcessed = TRUE;
// Set round started flag and update helpers
roundStarted = TRUE;
updateHelpers();
// Forward to Game Manager ONCE
llMessageLinked(LINK_SET, MSG_DICE_TYPE_RESULT, (string)diceType, NULL_KEY);
return;
}
// Main Controller no longer needs to handle dice type results
// Game Manager handles all dice type logic directly
if (num == MSG_GET_PICKS_REQUIRED) {
integer picksRequired = (integer)str;
llOwnerSay(" Picks required: " + str);
@ -704,41 +679,9 @@ sendStatusMessage("Victory"); // Show victory status
return;
}
// If the eliminated player was the peril player, assign a new one and continue game
if (eliminatedPlayer == perilPlayer) {
// Find first remaining alive player to be new peril player
perilPlayer = "";
integer k;
for (k = 0; k < llGetListLength(names) && perilPlayer == ""; k++) {
string candidateName = llList2String(names, k);
if (llList2Integer(lives, k) > 0) {
perilPlayer = candidateName;
llOwnerSay("🔄 After elimination, new peril player: " + perilPlayer);
}
}
// Continue the game with the new peril player (don't reset roundStarted)
if (perilPlayer != "") {
llOwnerSay("🎯 Continuing game with " + perilPlayer + " as new peril player");
// Reset picks and queue for new round, but keep roundStarted = TRUE
picksData = [];
globalPickedNumbers = [];
pickQueue = [perilPlayer];
integer i;
for (i = 0; i < llGetListLength(names); i++) {
string playerName = llList2String(names, i);
if (playerName != perilPlayer) {
pickQueue += [playerName];
}
}
currentPickerIdx = 0;
updateHelpers();
requestDiceType();
}
} else {
// Continue with existing peril player
updateHelpers();
}
// Let Game Manager handle peril player reassignment after elimination
// Just update helpers and let Game Manager detect the elimination via sync
updateHelpers();
}
}
return;
@ -836,11 +779,18 @@ resetGame();
}
// Handle start next round request - delegate to Game Manager
if (num == 997) {
// Check sender to prevent infinite loops - don't process our own messages
if (sender == llGetLinkNumber()) {
return; // Ignore messages from ourself
}
// Additional protection against round start spam
if (roundStarted && str == "START_NEXT_ROUND") {
llOwnerSay("⚠️ [Main Controller] Round already in progress, ignoring START_NEXT_ROUND");
return;
}
// Game Manager will handle dice type processing for new round
llOwnerSay("🔄 [Main Controller] Starting new round - Game Manager will handle dice type");
// Forward the request to Game Manager
llMessageLinked(LINK_SET, num, str, id);
// Don't request dice type here - it's handled in the "Start Game" listener below
@ -1016,6 +966,31 @@ llRegionSay(SCOREBOARD_CHANNEL_1, "RESET_LEADERBOARD"); // Reset leaderboard sco
llMessageLinked(LINK_SET, MSG_SHOW_MENU, "owner|" + (string)ownerIsStarter, llGetOwner());
return;
}
if (msg == "Force Floaters") {
// Force creation of floaters for all registered players
llOwnerSay("🔧 Forcing floater creation for all " + (string)llGetListLength(names) + " players...");
integer i;
for (i = 0; i < llGetListLength(names); i++) {
string playerName = llList2String(names, i);
key playerKey = llList2Key(players, i);
llMessageLinked(LINK_SET, MSG_REZ_FLOAT, playerName, playerKey);
llSleep(0.3); // Brief delay between rezzing to prevent spam
}
llOwnerSay("✅ Floater creation requests sent for all players!");
// Refresh owner menu
integer ownerIdx = llListFindList(players, [llGetOwner()]);
integer ownerIsStarter = TRUE; // Default to TRUE for owner
integer n;
for (n = 0; n < ownerIdx && ownerIsStarter; n++) {
string existingName = llList2String(names, n);
// If there's a human player before the owner, owner is not starter
if (llSubStringIndex(existingName, "TestBot") != 0) {
ownerIsStarter = FALSE;
}
}
llMessageLinked(LINK_SET, MSG_SHOW_MENU, "owner|" + (string)ownerIsStarter, llGetOwner());
return;
}
}
// Allow the first registered player (index 0) or the owner to start the game.
// When the starter clicks "Start Game", begin the round.
@ -1102,15 +1077,15 @@ llRegionSay(SCOREBOARD_CHANNEL_1, "RESET_LEADERBOARD"); // Reset leaderboard sco
llSetTimerEvent(0);
} else {
integer remaining = TIMEOUT_SECONDS - elapsed;
if (remaining <= warning2min && lastWarning < warning2min) {
llOwnerSay("⚠️ " + (string)(remaining / 60) + " minutes remaining for dialog response");
lastWarning = warning2min;
} else if (remaining <= warning5min && lastWarning < warning5min) {
llOwnerSay("⚠️ " + (string)(remaining / 60) + " minutes remaining for dialog response");
lastWarning = warning5min;
} else if (remaining <= warning9min && lastWarning < warning9min) {
llOwnerSay("⚠️ " + (string)(remaining / 60) + " minutes remaining for dialog response");
lastWarning = warning9min;
if (remaining <= warning30sec && lastWarning < warning30sec) {
llOwnerSay("⚠️ 30 seconds remaining for dialog response");
lastWarning = warning30sec;
} else if (remaining <= warning1min && lastWarning < warning1min) {
llOwnerSay("⚠️ 1 minute remaining for dialog response");
lastWarning = warning1min;
} else if (remaining <= warning90sec && lastWarning < warning90sec) {
llOwnerSay("⚠️ 1.5 minutes remaining for dialog response");
lastWarning = warning90sec;
}
}
}

View file

@ -93,7 +93,7 @@ list mainOwnerOptions = ["🎮 Game Participation", "👥 Player Management", "
list gameParticipationOptions = ["Join Game", "Leave Game", "⬅️ Back to Main"];
list playerManagementOptions = ["Add Test Player", "⬅️ Back to Main"];
list resetOptions = ["Reset Game", "Reset Leaderboard", "Reset All", "⬅️ Back to Main"];
list troubleshootingOptions = ["Cleanup Floaters", "Reset Follower Positions", "⬅️ Back to Main"];
list troubleshootingOptions = ["Cleanup Floaters", "Force Floaters", "Reset Follower Positions", "⬅️ Back to Main"];
// State tracking for dynamic ready menu with race condition protection
key pendingMenuPlayer = NULL_KEY;
@ -102,7 +102,7 @@ integer pendingMenuIsOwner = FALSE;
integer pendingMenuRequestID = 0; // Unique ID for each menu request
integer currentRequestID = 0; // Counter for generating unique request IDs
float pendingMenuTimestamp = 0.0; // Timeout for pending requests
float MENU_REQUEST_TIMEOUT = 10.0; // 10 second timeout for menu requests
float MENU_REQUEST_TIMEOUT = 5.0; // 5 second timeout for menu requests
// Display the main categorized owner menu
showOwnerMenu(key id) {
@ -372,9 +372,15 @@ default {
}
else if (msg == "Cleanup Floaters") {
// Universal floater cleanup that works even after script resets
llOwnerSay("🧹 Force cleaning ALL possible floater channels...");
llOwnerSay("🧩 Force cleaning ALL possible floater channels...");
llMessageLinked(LINK_SET, MSG_CLEANUP_ALL_FLOATERS, "", NULL_KEY);
}
else if (msg == "Force Floaters") {
// Force creation of floaters for all registered players - send as dialog response to Main Controller
llOwnerSay("🔧 Requesting floater creation for all registered players...");
// This will be handled by Main Controller's dialog listener
// No need to forward via link_message - the Main Controller is already listening on DIALOG_CHANNEL
}
else if (msg == "Reset Follower Positions") {
// Integrated position reset functionality with automation options
if (id == llGetOwner()) {

View file

@ -1,5 +1,7 @@
# 🎲 Peril Dice — Modular Game System for Second Life
**Created by Rebecca Nod and Noose the Bunny**
## 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.

View file

@ -1,6 +1,8 @@
🎲 PERIL DICE — MODULAR GAME SYSTEM FOR SECOND LIFE
========================================================
CREATED BY REBECCA NOD AND NOOSE THE BUNNY
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.

View file

@ -59,6 +59,8 @@ list names = [];
list lives = [];
list picksData = [];
string perilPlayer = "";
integer diceType = 6; // Store the dice type for rolling
integer shouldRoll = FALSE; // Flag to trigger roll after dice type is received
list getPicksFor(string nameInput) {
integer i;
@ -197,7 +199,9 @@ default {
if (llSubStringIndex(str, "TestBot") == 0) {
// Auto-roll for bots
llOwnerSay("🤖 " + str + " (bot) is auto-rolling...");
llMessageLinked(LINK_SET, 996, "GET_DICE_TYPE", NULL_KEY);
shouldRoll = TRUE; // Set flag for bot auto-roll
// Request dice type directly from Calculator
llMessageLinked(LINK_SET, 1001, (string)llGetListLength(names), NULL_KEY);
} else {
// Show dialog for human players
llDialog(id, "🎲 THE MOMENT OF TRUTH! You're in ultimate peril. Will you face your fate?", ["ROLL THE DICE OF FATE"], rollDialogChannel);
@ -205,6 +209,31 @@ default {
}
}
// Handle dice type requests from listen() events
else if (num == 996) {
// This is a GET_DICE_TYPE request from our own listen handler
// Forward it to the Main Controller to get the current dice type
llMessageLinked(LINK_SET, 1001, (string)llGetListLength(names), NULL_KEY);
return;
}
// Handle dice type response - store dice type and perform roll if requested
else if (num == 1005) { // MSG_DICE_TYPE_RESULT
diceType = (integer)str;
llOwnerSay("🎲 [Roll Module] Received dice type: d" + (string)diceType);
// If roll was requested from listen handler, perform it now
if (shouldRoll) {
shouldRoll = FALSE; // Reset flag
llOwnerSay("🎲 [Roll Module] Performing requested roll with d" + (string)diceType);
// Use MSG_ROLL_RESULT handler to perform the actual roll
llMessageLinked(LINK_THIS, MSG_ROLL_RESULT, (string)diceType, NULL_KEY);
}
return;
}
// Legacy MSG_ROLL_RESULT handler (if still used somewhere)
else if (num == MSG_ROLL_RESULT) {
integer diceType = (integer)str;
integer result = rollDice(diceType);
@ -344,9 +373,11 @@ default {
listen(integer channel, string name, key id, string msg) {
if (msg == "Roll" || msg == "ROLL THE DICE OF FATE") {
// Handle dice roll - need to get dice type from main controller
// Handle dice roll - need to get dice type from Calculator
llOwnerSay("🎲 Roll button clicked by " + name + " (key: " + (string)id + ")");
llMessageLinked(LINK_SET, 996, "GET_DICE_TYPE", NULL_KEY);
shouldRoll = TRUE; // Set flag to perform roll when dice type is received
// Request dice type directly from Calculator
llMessageLinked(LINK_SET, 1001, (string)llGetListLength(names), NULL_KEY);
} else {
llMessageLinked(LINK_THIS, channel, msg, id);
}