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
237 lines
No EOL
11 KiB
Text
237 lines
No EOL
11 KiB
Text
// === FloatManager (Consolidated) ===
|
||
// This version enforces a maximum of 10 players.
|
||
|
||
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_REGISTER_PLAYER = 106;
|
||
integer MSG_SYNC_GAME_STATE = 107;
|
||
|
||
// Maximum number of players allowed in the game
|
||
integer MAX_PLAYERS = 10;
|
||
|
||
list players = [];
|
||
list names = [];
|
||
list lives = [];
|
||
list picksData = [];
|
||
string perilPlayer = "";
|
||
|
||
// Returns a list of picks for the given player name, filtering out invalid values
|
||
list getPicksFor(string nameInput) {
|
||
integer i;
|
||
for (i = 0; i < llGetListLength(picksData); i++) {
|
||
string entry = llList2String(picksData, i);
|
||
|
||
if (llSubStringIndex(entry, "|") == -1) {
|
||
if (entry != "") llOwnerSay("⚠️ Malformed picks entry (missing pipe): " + entry);
|
||
} else {
|
||
list parts = llParseString2List(entry, ["|"], []);
|
||
if (llGetListLength(parts) < 2) {
|
||
if (entry != "") llOwnerSay("⚠️ Malformed picks entry (too few parts): " + entry);
|
||
}
|
||
else if (llList2String(parts, 0) == nameInput) {
|
||
string pickString = llList2String(parts, 1);
|
||
if (pickString == "") {
|
||
// Empty picks are normal during initialization, don't warn
|
||
return [];
|
||
} else {
|
||
// Check for corruption markers (^ symbols that shouldn't be in picks)
|
||
if (llSubStringIndex(pickString, "^") != -1) {
|
||
llOwnerSay("⚠️ Corrupted picks data for " + nameInput + " (contains ^): '" + pickString + "'");
|
||
return [];
|
||
}
|
||
// Convert semicolons back to commas, then parse
|
||
string convertedPicks = llDumpList2String(llParseString2List(pickString, [";"], []), ",");
|
||
list all = llParseString2List(convertedPicks, [","], []);
|
||
list filtered = [];
|
||
integer j;
|
||
for (j = 0; j < llGetListLength(all); j++) {
|
||
string val = llStringTrim(llList2String(all, j), STRING_TRIM);
|
||
if (val != "" && (string)((integer)val) == val) {
|
||
filtered += [val];
|
||
} else {
|
||
llOwnerSay("⚠️ Invalid number in picks for " + nameInput + ": '" + val + "'");
|
||
}
|
||
}
|
||
return filtered;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return [];
|
||
}
|
||
|
||
// Converts a player's key into their name (if registered)
|
||
string getNameFromKey(key id) {
|
||
integer i = llListFindList(players, [id]);
|
||
if (i != -1) return llList2String(names, i);
|
||
return (string)id;
|
||
}
|
||
|
||
// Main event handler
|
||
default {
|
||
state_entry() {
|
||
llOwnerSay("📦 Floater Manager ready!");
|
||
}
|
||
|
||
link_message(integer sender, integer num, string str, key id) {
|
||
if (num == MSG_REGISTER_PLAYER) {
|
||
// Enforce the maximum number of players
|
||
if (llGetListLength(players) >= MAX_PLAYERS) {
|
||
llOwnerSay("⚠️ Cannot register new player; the game is full (max " + (string)MAX_PLAYERS + ").");
|
||
return;
|
||
}
|
||
// Register a new player: store their name and avatar key
|
||
list info = llParseString2List(str, ["|"], []);
|
||
string name = llList2String(info, 0);
|
||
key avKey = llList2Key(info, 1);
|
||
names += [name];
|
||
players += [avKey];
|
||
llOwnerSay("📝 Registered: " + name);
|
||
|
||
// Immediately rez the float after registration. This ensures the
|
||
// float manager has updated its internal lists before attempting
|
||
// to look up the player's index. Without this, other scripts
|
||
// sending MSG_REZ_FLOAT in parallel could cause a race condition
|
||
// where the float manager has not yet seen the new name, so it
|
||
// cannot resolve the index and fails to rez the float. By
|
||
// initiating the rezzing here, we guarantee the float is created
|
||
// only after the registration is complete.
|
||
llMessageLinked(LINK_SET, MSG_REZ_FLOAT, name, avKey);
|
||
}
|
||
else if (num == MSG_REZ_FLOAT) {
|
||
string name = str;
|
||
integer idx = llListFindList(names, [name]);
|
||
if (idx == -1) return;
|
||
key avKey = llList2Key(players, idx);
|
||
// Determine the rez position. For real avatars, use their current
|
||
// location plus an offset. For test players (fake keys) or when
|
||
// OBJECT_POS cannot be obtained, fall back to rezzing relative to
|
||
// the object's own position to ensure a float is created.
|
||
list details = llGetObjectDetails(avKey, [OBJECT_POS]);
|
||
vector basePos;
|
||
if (llGetListLength(details) > 0) {
|
||
basePos = llList2Vector(details, 0);
|
||
} else {
|
||
basePos = llGetPos();
|
||
}
|
||
vector pos;
|
||
if (llGetListLength(details) > 0) {
|
||
// Real avatars: rez next to the avatar with the standard offset
|
||
pos = basePos + <1, 0, 1>;
|
||
} else {
|
||
// Test players: space floats apart along the X axis so they don't overlap.
|
||
// Each test player gets an additional 0.5m offset based on their index.
|
||
// This uses the current prim's position as the base since there's no
|
||
// avatar position available.
|
||
pos = basePos + <1 + (float)idx * 0.5, 0, 1>;
|
||
}
|
||
integer ch = -777000 + idx;
|
||
llOwnerSay("📦 Rezzing float for " + name + " at " + (string)pos);
|
||
llSetObjectDesc(name);
|
||
llRezObject("StatFloat", pos, ZERO_VECTOR, ZERO_ROTATION, ch);
|
||
// After rezzing, immediately update the float so it displays the
|
||
// correct lives, picks and peril status. Use the avatar key as
|
||
// the id to keep the float tied to the player. This ensures that
|
||
// test players also receive an update even if they don’t have a
|
||
// valid avatar key.
|
||
llMessageLinked(LINK_SET, MSG_UPDATE_FLOAT, name, avKey);
|
||
}
|
||
else if (num == MSG_UPDATE_FLOAT) {
|
||
string name = str;
|
||
integer idx = llListFindList(names, [name]);
|
||
if (idx == -1) return;
|
||
key avKey = llList2Key(players, idx);
|
||
integer ch = -777000 + idx;
|
||
integer lifeCount = llList2Integer(lives, idx);
|
||
|
||
llOwnerSay("🔍 Checking picksData: " + name);
|
||
list picks = getPicksFor(name);
|
||
|
||
llOwnerSay("🛠️ Updating float for " + name);
|
||
llOwnerSay("🔎 Found picks: " + llList2CSV(picks));
|
||
|
||
string perilName;
|
||
// If the perilPlayer string is empty or contains a comma (indicating
|
||
// multiple names), treat the game as not yet started. This avoids
|
||
// showing multiple players as the peril player before the first
|
||
// round begins. Once a single peril player is assigned, it will
|
||
// not contain a comma and will be displayed normally.
|
||
if (perilPlayer == "" || llSubStringIndex(perilPlayer, ",") != -1) {
|
||
perilName = "🧍 Status: Waiting for game to start...";
|
||
} else {
|
||
// perilPlayer is already a name string, not a key
|
||
perilName = "🧍 Peril: " + perilPlayer;
|
||
}
|
||
|
||
string picksDisplay = llList2CSV(picks);
|
||
string txt = "🎲 Peril Dice\n👤 " + name + "\n❤️ Lives: " + (string)lifeCount + "\n" + perilName + "\n🔢 Picks: " + picksDisplay;
|
||
llRegionSay(ch, "FLOAT:" + (string)avKey + "|" + txt);
|
||
llOwnerSay("📤 Sent FLOAT message on channel " + (string)ch + ": " + txt);
|
||
}
|
||
else if (num == MSG_CLEANUP_FLOAT) {
|
||
integer ch = (integer)str;
|
||
integer idx = ch - (-777000);
|
||
|
||
// Always send cleanup message to the channel (even for orphaned floaters)
|
||
llRegionSay(ch, "CLEANUP");
|
||
llOwnerSay("🧹 Sent CLEANUP to channel " + (string)ch);
|
||
|
||
// Only clean up internal lists if this corresponds to a valid player
|
||
if (idx >= 0 && idx < llGetListLength(players)) {
|
||
players = llDeleteSubList(players, idx, idx);
|
||
names = llDeleteSubList(names, idx, idx);
|
||
lives = llDeleteSubList(lives, idx, idx);
|
||
picksData = llDeleteSubList(picksData, idx, idx);
|
||
llOwnerSay("🗑️ Cleaned up internal data for player index " + (string)idx);
|
||
}
|
||
}
|
||
else if (num == MSG_SYNC_GAME_STATE) {
|
||
// Synchronize the lists for lives and picksData when receiving a new game state
|
||
llOwnerSay("🔍 DEBUG: Floater Manager received sync: " + str);
|
||
list parts = llParseString2List(str, ["~"], []);
|
||
lives = llCSV2List(llList2String(parts, 0));
|
||
// Use ^ delimiter for picksData to avoid comma conflicts
|
||
string picksDataStr = llList2String(parts, 1);
|
||
llOwnerSay("🔍 DEBUG: Raw picksDataStr: '" + picksDataStr + "'");
|
||
if (picksDataStr == "" || picksDataStr == "EMPTY") {
|
||
picksData = [];
|
||
llOwnerSay("🔍 DEBUG: Parsed picksData: (empty)");
|
||
} else {
|
||
picksData = llParseString2List(picksDataStr, ["^"], []);
|
||
llOwnerSay("🔍 DEBUG: Parsed picksData: " + llDumpList2String(picksData, " | "));
|
||
}
|
||
string receivedPeril = llList2String(parts, 2);
|
||
string oldPeril = perilPlayer;
|
||
if (receivedPeril == "NONE") {
|
||
perilPlayer = ""; // Convert placeholder back to empty
|
||
} else {
|
||
perilPlayer = receivedPeril;
|
||
}
|
||
|
||
// Debug peril player changes
|
||
if (oldPeril != perilPlayer) {
|
||
llOwnerSay("🔄 Floater Manager: Peril player changed from '" + oldPeril + "' to '" + perilPlayer + "'");
|
||
}
|
||
|
||
// After synchronizing the game state, update all existing floats so
|
||
// they reflect the current peril status. Without this, floats
|
||
// created in previous rounds may continue to display an old
|
||
// peril name until another manual update occurs.
|
||
integer idx;
|
||
for (idx = 0; idx < llGetListLength(names); idx++) {
|
||
string n = llList2String(names, idx);
|
||
// Use the player's key if it exists; fallback to NULL_KEY for test bots
|
||
key k;
|
||
if (idx < llGetListLength(players)) {
|
||
k = llList2Key(players, idx);
|
||
} else {
|
||
k = NULL_KEY;
|
||
}
|
||
llMessageLinked(LINK_SET, MSG_UPDATE_FLOAT, n, k);
|
||
}
|
||
}
|
||
}
|
||
} |