All notable changes to Peril Dice will be documented in this file. ## [2.1.0] - 2025-01-08 ### ✨ Major Features Added - **Ready State System**: Players must mark themselves ready before games can start - **Game Protection**: Prevents players from joining games in progress - **Performance Optimization**: Reduced lag by limiting confetti to wins only - **Enhanced Bot Intelligence**: Improved bot behavior and duplicate number avoidance ### 🔧 Core Fixes - **Complete Game Flow**: Fixed end-to-end gameplay from start to finish - **Elimination Logic**: Proper handling of player elimination and game continuation - **Win Condition**: Fixed victory detection and game ending - **Data Synchronization**: Resolved corruption in pick data between scripts - **Peril Player Assignment**: Fixed race conditions in peril player transitions ### 🎮 Gameplay Improvements - **Dynamic Picking**: Number of picks now properly based on peril player's lives - **Duplicate Prevention**: Enhanced system to prevent duplicate number selection - **Dialog Pagination**: Improved number selection interface for d30 dice - **Floater Updates**: Real-time HUD updates throughout gameplay - **Bot Continuation**: Bots now properly continue games when humans are eliminated ### 🛠️ Technical Enhancements - **Modular Communication**: Improved inter-script messaging system - **Error Handling**: Comprehensive validation and error recovery - **Memory Management**: Better floater cleanup and channel tracking - **Data Encoding**: Fixed CSV conflicts with semicolon encoding system - **State Management**: Robust game state synchronization across modules ### 🐛 Bug Fixes - Fixed missing pick dialogs after game events - Resolved duplicate message issues between scripts - Fixed lives reset problems between rounds - Corrected floater display inconsistencies - Fixed bot confusion during player elimination - Resolved pick data corruption issues - Fixed peril player variable sync problems ### ⚡ Performance - Removed confetti from life loss (keeping only victory confetti) - Optimized debug output to reduce lag - Improved floater management efficiency - Enhanced bot processing speed ## [2.0.0] - 2024-12-XX ### 🎉 Initial Modular Release - **Modular Architecture**: Separated functionality into specialized scripts - **Bot System**: Added AI players for testing and gameplay variety - **Floating HUD**: Real-time player statistics display - **Dynamic Dice**: Automatic dice sizing based on player count - **Player Management**: Runtime player joining and leaving - **Owner Controls**: Administrative functions for game management ### 🎲 Core Game Features - **Pick System**: Number selection with validation - **Rolling Mechanism**: Automated dice rolling with visual effects - **Life Management**: 3-life system with elimination - **Peril Tracking**: Dynamic peril player assignment - **Victory Conditions**: Last player standing wins ### 🏗️ Architecture - **Main Controller**: Central game logic and state management - **Dialog Handlers**: User interface and input processing - **Roll Module**: Dice mechanics and particle effects - **Bot Manager**: AI player behavior and automation - **Floater Manager**: HUD display coordination - **Game Helpers**: Utility functions and calculations ## [1.0.0] - 2024-XX-XX ### 🌟 Original Release - Basic Peril Dice game implementation - Manual gameplay mechanics - Simple player tracking - Basic dice rolling functionality --- ## Development Notes ### Known Issues - **Bot Race Condition**: Bots may occasionally pick duplicate numbers when triggered simultaneously - **Debug Output**: Extended games may experience minor lag from comprehensive logging ### Future Enhancements - Serialized bot picking to eliminate race conditions - Configurable debug levels - Additional game modes and variants - Enhanced visual effects and animations ### Credits - **Original Game Design**: Noose the Bunny (djmusica28) - Second Life - **Automation & Development**: Enhanced for automated gameplay - **Architecture**: Modular LSL system for scalability and maintainability
252 lines
12 KiB
Text
252 lines
12 KiB
Text
// === Roll and Confetti Module (with Roll Dialog Handler) ===
|
|
|
|
integer MSG_SHOW_DIALOG = 101;
|
|
integer MSG_ROLL_RESULT = 102;
|
|
integer MSG_UPDATE_FLOAT = 103;
|
|
integer MSG_CLEANUP_FLOAT = 104;
|
|
integer MSG_REZ_FLOAT = 105;
|
|
integer MSG_SYNC_GAME_STATE = 107;
|
|
integer MSG_SHOW_ROLL_DIALOG = 301;
|
|
|
|
integer rollDialogChannel = -77999;
|
|
|
|
list names = [];
|
|
list lives = [];
|
|
list picksData = [];
|
|
string perilPlayer = "";
|
|
|
|
list getPicksFor(string nameInput) {
|
|
integer i;
|
|
for (i = 0; i < llGetListLength(picksData); i++) {
|
|
string entry = llList2String(picksData, i);
|
|
list parts = llParseString2List(entry, ["|"], []);
|
|
if (llList2String(parts, 0) == nameInput) {
|
|
// Convert semicolons back to commas, then parse
|
|
string picks = llList2String(parts, 1);
|
|
string originalPicks = picks;
|
|
|
|
// Check for corruption markers (^ symbols that shouldn't be in picks)
|
|
if (llSubStringIndex(picks, "^") != -1) {
|
|
llOwnerSay("🔍 DEBUG getPicksFor(" + nameInput + "): CORRUPTED data contains ^: '" + originalPicks + "'");
|
|
return [];
|
|
}
|
|
|
|
if (picks == "") {
|
|
llOwnerSay("🔍 DEBUG getPicksFor(" + nameInput + "): empty picks");
|
|
return [];
|
|
}
|
|
|
|
picks = llDumpList2String(llParseString2List(picks, [";"], []), ",");
|
|
list result = llParseString2List(picks, [","], []);
|
|
llOwnerSay("🔍 DEBUG getPicksFor(" + nameInput + "): raw='" + originalPicks + "' -> converted='" + picks + "' -> list=" + llList2CSV(result));
|
|
return result;
|
|
}
|
|
}
|
|
llOwnerSay("🔍 DEBUG getPicksFor(" + nameInput + "): NOT FOUND in picksData=" + llDumpList2String(picksData, " | "));
|
|
return [];
|
|
}
|
|
|
|
integer rollDice(integer diceType) {
|
|
return 1 + (integer)llFrand(diceType);
|
|
}
|
|
|
|
confetti() {
|
|
llParticleSystem([
|
|
PSYS_SRC_PATTERN, PSYS_SRC_PATTERN_EXPLODE,
|
|
PSYS_PART_START_COLOR, <1,1,1>, PSYS_PART_END_COLOR, <1,1,1>,
|
|
PSYS_PART_START_ALPHA, 1.0, PSYS_PART_END_ALPHA, 0.0,
|
|
PSYS_PART_START_SCALE, <0.2,0.2,0>, PSYS_PART_END_SCALE, <0.5,0.5,0>,
|
|
PSYS_PART_MAX_AGE, 2.0, PSYS_SRC_MAX_AGE, 2.0,
|
|
PSYS_SRC_ACCEL, <0,0,-0.4>,
|
|
PSYS_SRC_BURST_RATE, 0.01, PSYS_SRC_BURST_PART_COUNT, 50,
|
|
PSYS_SRC_BURST_RADIUS, 0.2,
|
|
PSYS_SRC_BURST_SPEED_MIN, 1.0, PSYS_SRC_BURST_SPEED_MAX, 2.0,
|
|
PSYS_PART_FLAGS, PSYS_PART_INTERP_COLOR_MASK | PSYS_PART_INTERP_SCALE_MASK | PSYS_PART_EMISSIVE_MASK
|
|
]);
|
|
}
|
|
|
|
default {
|
|
state_entry() {
|
|
llOwnerSay("🎲 Roll Confetti Module ready!");
|
|
llListen(rollDialogChannel, "", NULL_KEY, "");
|
|
}
|
|
|
|
link_message(integer sender, integer num, string str, key id) {
|
|
// Handle game state sync from main controller
|
|
if (num == MSG_SYNC_GAME_STATE) {
|
|
list parts = llParseString2List(str, ["~"], []);
|
|
if (llGetListLength(parts) >= 4) {
|
|
lives = llCSV2List(llList2String(parts, 0));
|
|
// Use ^ delimiter for picksData to avoid comma conflicts
|
|
string picksDataStr = llList2String(parts, 1);
|
|
if (picksDataStr == "" || picksDataStr == "EMPTY") {
|
|
picksData = [];
|
|
} else {
|
|
picksData = llParseString2List(picksDataStr, ["^"], []);
|
|
}
|
|
string receivedPeril = llList2String(parts, 2);
|
|
if (receivedPeril == "NONE") {
|
|
perilPlayer = "";
|
|
} else {
|
|
perilPlayer = receivedPeril;
|
|
}
|
|
names = llCSV2List(llList2String(parts, 3));
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (num == 995) {
|
|
if (str == "VICTORY_CONFETTI") {
|
|
llOwnerSay("🎉 Victory confetti!");
|
|
confetti();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (num == MSG_SHOW_ROLL_DIALOG) {
|
|
// Check if this is a next round prompt
|
|
if (llSubStringIndex(str, "_NEXT_ROUND") != -1) {
|
|
string playerName = llGetSubString(str, 0, llSubStringIndex(str, "_NEXT_ROUND") - 1);
|
|
llOwnerSay("🎯 Prompting " + playerName + " to start next round...");
|
|
// Send message to main controller to get the player's key and show dialog
|
|
llMessageLinked(LINK_SET, 997, "START_NEXT_ROUND_DIALOG|" + playerName, NULL_KEY);
|
|
} else {
|
|
llOwnerSay("🎲 Prompting " + str + " to roll the dice.");
|
|
// Check if this is a bot (TestBot names)
|
|
if (llSubStringIndex(str, "TestBot") == 0) {
|
|
// Auto-roll for bots
|
|
llOwnerSay("🤖 " + str + " (bot) is auto-rolling...");
|
|
llMessageLinked(LINK_SET, 996, "GET_DICE_TYPE", NULL_KEY);
|
|
} else {
|
|
// Show dialog for human players
|
|
llDialog(id, "🎲 All picks are in! You're the peril player. Roll the dice?", ["Roll"], rollDialogChannel);
|
|
}
|
|
}
|
|
}
|
|
|
|
else if (num == MSG_ROLL_RESULT) {
|
|
integer diceType = (integer)str;
|
|
integer result = rollDice(diceType);
|
|
string resultStr = (string)result;
|
|
llSay(0, "🎲 " + perilPlayer + " rolled a " + resultStr + "!");
|
|
|
|
string newPeril = "";
|
|
integer matched = FALSE;
|
|
list matchedPlayers = [];
|
|
integer i;
|
|
|
|
llOwnerSay("🔍 DEBUG: Current peril player: " + perilPlayer + ", rolled: " + resultStr);
|
|
|
|
// Find all players who picked the rolled number
|
|
for (i = 0; i < llGetListLength(names); i++) {
|
|
string pname = llList2String(names, i);
|
|
list picks = getPicksFor(pname);
|
|
llOwnerSay("🔍 DEBUG: " + pname + " picks: " + llList2CSV(picks));
|
|
if (llListFindList(picks, [resultStr]) != -1) {
|
|
matched = TRUE;
|
|
matchedPlayers += [pname];
|
|
llOwnerSay("🔍 DEBUG: " + pname + " matched the roll!");
|
|
}
|
|
}
|
|
|
|
llOwnerSay("🔍 DEBUG: Matched players: " + llList2CSV(matchedPlayers));
|
|
|
|
// If anyone matched, pick the first non-peril player as new peril
|
|
// If only the current peril player matched, they stay in peril (no change)
|
|
for (i = 0; i < llGetListLength(matchedPlayers) && newPeril == ""; i++) {
|
|
string pname = llList2String(matchedPlayers, i);
|
|
if (pname != perilPlayer) {
|
|
newPeril = pname;
|
|
llOwnerSay("🔍 DEBUG: New peril player will be: " + newPeril);
|
|
}
|
|
}
|
|
|
|
if (matched && newPeril != "") {
|
|
llSay(0, "🎯 " + newPeril + " matched the roll and becomes the new peril player!");
|
|
perilPlayer = newPeril;
|
|
// Update floaters immediately to show correct peril player before sync
|
|
llMessageLinked(LINK_SET, MSG_UPDATE_FLOAT, newPeril, NULL_KEY);
|
|
} else {
|
|
integer pidx = llListFindList(names, [perilPlayer]);
|
|
if (pidx != -1) {
|
|
integer currentLives = llList2Integer(lives, pidx);
|
|
lives = llListReplaceList(lives, [currentLives - 1], pidx, pidx);
|
|
llSay(0, "💀 " + perilPlayer + " was hit and lost a life!");
|
|
llMessageLinked(LINK_SET, MSG_REZ_FLOAT, perilPlayer, NULL_KEY);
|
|
|
|
// Check for elimination
|
|
if (currentLives - 1 <= 0) {
|
|
llSay(0, "💀 " + perilPlayer + " has been eliminated!");
|
|
// Remove eliminated player (send message to main controller)
|
|
llMessageLinked(LINK_SET, 999, "ELIMINATE_PLAYER|" + perilPlayer, NULL_KEY);
|
|
// The Main Controller will handle peril player reassignment and game flow
|
|
// Don't continue processing this round since the player was eliminated
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Note: Win condition checking is handled by Main Controller after elimination
|
|
|
|
// Encode picksData with semicolons to avoid comma conflicts
|
|
list encodedPicksData = [];
|
|
integer k;
|
|
for (k = 0; k < llGetListLength(picksData); k++) {
|
|
string entry = llList2String(picksData, k);
|
|
list parts = llParseString2List(entry, ["|"], []);
|
|
if (llGetListLength(parts) >= 2) {
|
|
string playerName = llList2String(parts, 0);
|
|
string picks = llList2String(parts, 1);
|
|
// Only process picks if they're not empty
|
|
if (picks != "") {
|
|
// Parse picks, trim whitespace, then join with semicolons
|
|
list pickList = llParseString2List(picks, [","], []);
|
|
list cleanPickList = [];
|
|
integer j;
|
|
for (j = 0; j < llGetListLength(pickList); j++) {
|
|
string pick = llStringTrim(llList2String(pickList, j), STRING_TRIM);
|
|
if (pick != "") {
|
|
cleanPickList += [pick];
|
|
}
|
|
}
|
|
picks = llDumpList2String(cleanPickList, ";");
|
|
}
|
|
encodedPicksData += [playerName + "|" + picks];
|
|
} else if (llSubStringIndex(entry, "|") != -1) {
|
|
// Handle entries with empty picks (e.g., "PlayerName|")
|
|
encodedPicksData += [entry];
|
|
} else {
|
|
// Skip malformed entries without pipe separator
|
|
llOwnerSay("⚠️ Roll Module: Skipping malformed picksData entry: " + entry);
|
|
}
|
|
}
|
|
string gameSync = llList2CSV(lives) + "~" + llDumpList2String(encodedPicksData, "^") + "~" + perilPlayer + "~" + llList2CSV(names);
|
|
llOwnerSay("📤 Roll module sending sync with peril player: " + perilPlayer);
|
|
llMessageLinked(LINK_SET, MSG_SYNC_GAME_STATE, gameSync, NULL_KEY);
|
|
llSleep(0.2);
|
|
|
|
// Additional floater updates to ensure correct peril player display
|
|
llOwnerSay("🔄 Updating all floaters with new peril player: " + perilPlayer);
|
|
integer f;
|
|
for (f = 0; f < llGetListLength(names); f++) {
|
|
string fname = llList2String(names, f);
|
|
llMessageLinked(LINK_SET, MSG_UPDATE_FLOAT, fname, NULL_KEY);
|
|
}
|
|
|
|
// Show start next round dialog to current peril player
|
|
llOwnerSay("🎯 Prompting " + perilPlayer + " to start next round...");
|
|
// Get the peril player's key from the main controller
|
|
llMessageLinked(LINK_SET, MSG_SHOW_ROLL_DIALOG, perilPlayer + "_NEXT_ROUND", NULL_KEY);
|
|
}
|
|
}
|
|
|
|
listen(integer channel, string name, key id, string msg) {
|
|
if (msg == "Roll") {
|
|
// Handle dice roll - need to get dice type from main controller
|
|
llOwnerSay("🎲 Roll button clicked by " + name + " (key: " + (string)id + ")");
|
|
llMessageLinked(LINK_SET, 996, "GET_DICE_TYPE", NULL_KEY);
|
|
} else {
|
|
llMessageLinked(LINK_THIS, channel, msg, id);
|
|
}
|
|
}
|
|
}
|