diff --git a/Debugger.lsl b/Debugger.lsl index e883ed3..548c825 100644 --- a/Debugger.lsl +++ b/Debugger.lsl @@ -57,6 +57,84 @@ list validatePicksData(list picksData, list names) { +default { + link_message(integer sender, integer num, string str, key id) { + if (num == MSG_SYNC_GAME_STATE) { + list parts = llParseString2List(str, ["~"], []); + if (llGetListLength(parts) < 4) { + llOwnerSay("โš ๏ธ Incomplete game state received."); + return; + } + + list lives = llCSV2List(llList2String(parts, 0)); + list picksData = llCSV2List(llList2String(parts, 1)); + string perilPlayer = llList2String(parts, 2); + list names = llCSV2List(llList2String(parts, 3)); + + llOwnerSay("๐Ÿ” Checking picksData..."); + validatePicksData(picksData, names); + } + } +} +integer MSG_SYNC_GAME_STATE = 107; + +list validatePicksData(list picksData, list names) { + list allPicksFlat = []; + integer i; + + for (i = 0; i < llGetListLength(picksData); i++) { + string entry = llList2String(picksData, i); + list parts = llParseString2List(entry, ["|"], []); + + if (llGetListLength(parts) == 2) { + string name = llList2String(parts, 0); + string picks = llList2String(parts, 1); + list pickNums = llParseString2List(picks, [","], []); + + // Check if name is in names list + if (names != [] && llListFindList(names, [name]) == -1) { + llOwnerSay("โš ๏ธ picksData name not in names list: " + name); + } + + // Check for duplicate picks in this player's list + integer j; + list uniqueCheck = []; + for (j = 0; j < llGetListLength(pickNums); j++) { + string pick = llList2String(pickNums, j); + if ((string)((float)pick) != pick) { + llOwnerSay("โš ๏ธ Invalid number in picks for " + name + ": '" + pick + "'"); + } + if (~llListFindList(uniqueCheck, [pick])) { + llOwnerSay("๐Ÿšจ Duplicate pick in " + name + "'s picks: " + pick); + } else { + uniqueCheck += [pick]; + } + allPicksFlat += [pick]; + } + + llOwnerSay("โœ… " + name + "'s picks: " + llList2CSV(pickNums)); + } else { + llOwnerSay("โš ๏ธ Malformed entry in picksData: " + entry); + } + } + + // Check for duplicate picks across all players + list seen = []; + integer k; + for (k = 0; k < llGetListLength(allPicksFlat); k++) { + string val = llList2String(allPicksFlat, k); + if (~llListFindList(seen, [val])) { + llOwnerSay("โŒ Duplicate pick across players: " + val); + } else { + seen += [val]; + } + } + + return picksData; +} + + + default { link_message(integer sender, integer num, string str, key id) { if (num == MSG_SYNC_GAME_STATE) { diff --git a/Floater Manager.lsl b/Floater Manager.lsl index d547377..578f60b 100644 --- a/Floater Manager.lsl +++ b/Floater Manager.lsl @@ -78,17 +78,54 @@ default { 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); - vector pos = llList2Vector(llGetObjectDetails(avKey, [OBJECT_POS]), 0) + <1,0,1>; + // 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; @@ -105,7 +142,12 @@ default { llOwnerSay("๐Ÿ”Ž Found picks: " + llList2CSV(picks)); string perilName; - if (perilPlayer == "") { + // 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 { perilName = "๐Ÿง Peril: " + getNameFromKey(perilPlayer); @@ -143,6 +185,23 @@ default { } } perilPlayer = llList2String(parts, 2); + + // 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); + } } } } \ No newline at end of file diff --git a/Helper.lsl b/Helper.lsl index 78708f0..2d8ad9e 100644 --- a/Helper.lsl +++ b/Helper.lsl @@ -133,7 +133,12 @@ default { integer playerCount = (integer)str; llOwnerSay("๐Ÿ“Š playerCount received: " + (string)playerCount); integer result = getDiceType(playerCount); - llMessageLinked(LINK_THIS, MSG_DICE_TYPE_RESULT, (string)result, NULL_KEY); + // Broadcast the dice type result across the entire linkset so that + // the main game controller can receive it. Using LINK_THIS + // restricted the message to the helper's prim, preventing the + // controller from seeing MSG_DICE_TYPE_RESULT and causing the + // game not to start. + llMessageLinked(LINK_SET, MSG_DICE_TYPE_RESULT, (string)result, NULL_KEY); } else if (num == MSG_GET_PICKS_REQUIRED) { integer result = getPicksRequiredFromName(str); @@ -151,4 +156,4 @@ default { showPickManager(str, id); } } -} +} \ No newline at end of file diff --git a/Main.lsl b/Main.lsl index 20f75b1..8c4a544 100644 --- a/Main.lsl +++ b/Main.lsl @@ -42,6 +42,10 @@ integer MSG_DICE_TYPE_RESULT = 1005; integer MSG_SERIALIZE_GAME_STATE = 1004; integer MSG_SYNC_PICKQUEUE = 2001; +// Maximum number of players allowed (including test players). This should +// mirror the value used in Floater Manager to avoid inconsistencies. +integer MAX_PLAYERS = 10; + integer TIMEOUT_SECONDS = 600; integer timeoutTimer; @@ -78,8 +82,11 @@ string generateSerializedState() { } resetGame() { + // Remove floats in reverse order so indices remain correct as entries are + // deleted from the Floater Manager's lists. Otherwise, deleting index 0 + // first shifts subsequent indices and some floats are left behind. integer i; - for (i = 0; i < llGetListLength(names); i++) { + for (i = llGetListLength(names) - 1; i >= 0; i--) { integer ch = -777000 + i; llMessageLinked(LINK_SET, MSG_CLEANUP_FLOAT, (string)ch, NULL_KEY); } @@ -95,6 +102,14 @@ resetGame() { } startNextRound() { + // Do not start a round if there are fewer than 2 players. This guard prevents + // accidental starts when only one player is present (e.g. after a reset or + // before any other participants join). + if (llGetListLength(names) < 2) { + llOwnerSay("โš ๏ธ Need at least 2 players to start the game."); + return; + } + // If somehow called with exactly one player, treat them as the winner and reset. if (llGetListLength(names) == 1) { llSay(0, " " + llList2String(names, 0) + " is the last player standing and wins the game!"); resetGame(); @@ -134,9 +149,23 @@ default { key toucher = llDetectedKey(0); integer idx = llListFindList(players, [toucher]); if (toucher == llGetOwner()) { - // Owner touches the prim: show menu for owner if already registered; else start as not starter - // Pass isStarter=0 because owner is never automatically starter until players join - llMessageLinked(LINK_SET, MSG_SHOW_MENU, "owner|0", toucher); + // Determine if this will be the first registered player (starter) before registration + integer isStarter = FALSE; + if (idx == -1) { + // No index found; the owner is not yet registered. + // If there are no existing players, they will become the starter. + if (llGetListLength(players) == 0) { + isStarter = TRUE; + } + string ownerName = llKey2Name(toucher); + // Send a registration request; Main.lsl will handle adding them and rezzing a float + llMessageLinked(LINK_SET, MSG_REGISTER_PLAYER, ownerName + "|" + (string)toucher, NULL_KEY); + } else { + // Already registered; starter if at index 0 + isStarter = (idx == 0); + } + // Show the owner menu with the appropriate starter flag + llMessageLinked(LINK_SET, MSG_SHOW_MENU, "owner|" + (string)isStarter, toucher); } else if (idx != -1) { integer isStarter = (idx == 0); llMessageLinked(LINK_SET, MSG_SHOW_MENU, "player|" + (string)isStarter, toucher); @@ -248,9 +277,11 @@ default { names += [newName]; lives += [3]; picksData += [newName + "|"]; - // Rez a float for the new player - llMessageLinked(LINK_SET, MSG_REZ_FLOAT, newName, newKey); - llSleep(0.2); + // The float will be rezzed by the Floater Manager after it processes + // this registration. Avoid sending MSG_REZ_FLOAT here to prevent + // race conditions; instead, request the float from Floater Manager + // after it has updated its own lists. Immediately propagate + // helper updates now. updateHelpers(); // Notify owner of registration llOwnerSay("๐Ÿ”” Added player: " + newName); @@ -267,11 +298,8 @@ default { resetGame(); return; } - if (msg == "Start Game") { - startNextRound(); - requestDiceType(); - return; - } + // Note: do not handle "Start Game" here; allow the generic start logic below to apply, + // so that minimum player checks are enforced and non-owner starters can initiate the game. if (msg == "Dump Players") { llOwnerSay(" Players: " + llList2CSV(names)); return; @@ -282,21 +310,45 @@ default { return; } if (msg == "Add Test Player") { - string name = "TestBot" + (string)llGetUnixTime(); + // Ensure we do not exceed the maximum allowed players + if (llGetListLength(players) >= MAX_PLAYERS) { + llOwnerSay("โš ๏ธ Cannot add test player; the game is full (max " + (string)MAX_PLAYERS + ")."); + return; + } + // Generate a unique name and key for the test player + string testName = "TestBot" + (string)llGetUnixTime(); key fake = llGenerateKey(); - // Add the test player to lists and register them with floater manager - players += [fake]; - names += [name]; - lives += [3]; - picksData += [name + "|"]; - llMessageLinked(LINK_SET, MSG_REGISTER_PLAYER, name + "|" + (string)fake, NULL_KEY); - llMessageLinked(LINK_SET, MSG_REZ_FLOAT, name, fake); - llSleep(0.2); - updateHelpers(); + // Delegate registration to the MSG_REGISTER_PLAYER handler; it will add to lists and rez a float + llMessageLinked(LINK_SET, MSG_REGISTER_PLAYER, testName + "|" + (string)fake, NULL_KEY); + // Show the owner menu again (not flagged as starter) llMessageLinked(LINK_SET, MSG_SHOW_MENU, "owner|0", 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. + if (msg == "Start Game") { + // Enforce a minimum of 2 players before starting + if (llGetListLength(players) < 2) { + llOwnerSay("โš ๏ธ Need at least 2 players to start the game."); + return; + } + // Check if the sender is the owner + if (id == llGetOwner()) { + startNextRound(); + requestDiceType(); + return; + } + // Otherwise, check if the sender is the very first player registered + if (llGetListLength(players) > 0) { + key firstPlayer = llList2Key(players, 0); + if (id == firstPlayer) { + startNextRound(); + requestDiceType(); + return; + } + } + } // If the message text matches a player name, request their pick list if (llListFindList(names, [msg]) != -1) { llMessageLinked(LINK_SET, 206, msg, id); diff --git a/Owner and Player Dialog Handler.lsl b/Owner and Player Dialog Handler.lsl index 91cd847..3f2f327 100644 --- a/Owner and Player Dialog Handler.lsl +++ b/Owner and Player Dialog Handler.lsl @@ -1,6 +1,7 @@ -// === Dialog Handler (Owner & Player) with unified Ready/Leave menu === +// === Dialog Handler (Owner & Player) with unified Ready/Leave menu and join support === // Owners and players share the same Ready/Leave dialog. // Owners also get an "Owner" button to access advanced options. +// Owners can join the game and receive a floating display. integer DIALOG_CHANNEL = -88888; integer MSG_SHOW_MENU = 201; @@ -8,6 +9,8 @@ integer MSG_PICK_ACTION = 204; integer MSG_PLAYER_LIST_RESULT = 203; integer MSG_PICK_LIST_RESULT = 205; integer MSG_LIFE_LOOKUP = 207; +integer MSG_REGISTER_PLAYER = 106; +integer MSG_REZ_FLOAT = 105; // Owner-specific options shown when pressing the "Owner" button list ownerOptions = ["Join Game", "Leave Game", "Add Test Player", "Manage Picks", "Start Game", "Reset Game", "Dump Players"]; @@ -22,8 +25,11 @@ showOwnerMenu(key id) { // Owners receive an extra "Owner" button to access the advanced owner menu. showReadyLeaveMenu(key id, integer isStarter, integer isOwner) { list options; + // If this user is flagged as the starter, they should see the "Start Game" + // button so they can initiate the round (regardless of ownership). Otherwise + // they see the regular "Ready" button. if (isStarter) { - options = ["Start", "Leave Game"]; + options = ["Start Game", "Leave Game"]; } else { options = ["Ready", "Leave Game"]; } @@ -82,8 +88,6 @@ default { } string targetType = llList2String(args, 0); integer isStarter = (integer)llList2String(args, 1); - // We treat owners and players the same for the Ready/Leave menu. - // If the targetType is "owner" we pass isOwner=TRUE and display the extra Owner button. if (targetType == "owner") { showReadyLeaveMenu(id, isStarter, TRUE); } else if (targetType == "player") { @@ -117,10 +121,17 @@ default { } listen(integer channel, string name, key id, string msg) { - // Handle menus: if the user selects "Owner" on the combined menu, display the owner options. + // Handle "Owner" button to show owner options if (msg == "Owner") { showOwnerMenu(id); } + // Owner joins the game: register and rez a float for them + else if (msg == "Join Game") { + // Use the avatar's key as the identifier + string pname = llKey2Name(id); + llMessageLinked(LINK_SET, MSG_REGISTER_PLAYER, pname + "|" + (string)id, NULL_KEY); + llMessageLinked(LINK_SET, MSG_REZ_FLOAT, pname, id); + } else if (msg == "Manage Picks") { llMessageLinked(LINK_THIS, 202, "REQUEST_PLAYER_LIST", id); } @@ -168,4 +179,4 @@ default { llMessageLinked(LINK_THIS, 206, currentPickTarget, id); } } -} +} \ No newline at end of file diff --git a/README.md b/README.md index d6a1b10..c13099f 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,100 @@ +๐ŸŽฒ Peril Dice โ€” Modular Game System for Second Life +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. + +๐Ÿ”ง Features +โœ… Player System +Max players: 10 + +Each player starts with 3 lives + +Players can Join, Leave, or mark themselves Ready + +The first player to join gets a Start Game button + +The owner can join the game and also has a special Owner menu + +๐ŸŽฒ Dice Scaling +Dice type is automatically chosen to ensure at least 3 picks per player + +Dice types supported: d6, d12, d20, d30 + +Player Count Dice Type +1โ€“2 d6 +3โ€“4 d12 +5โ€“6 d20 +7โ€“10 d30 + +๐Ÿง Test Players +Owner can add/remove test players (bots) + +Test players rez stat floaters at offset positions to prevent overlap + +Floater objects follow players and update in real time + +๐ŸชŸ HUD & Stat Floaters +Players receive a floating HUD showing: + +Player name + +Lives + +Picks + +Peril status or game state + +๐Ÿ“ฆ Modular Architecture +Separated scripts: + +Main Controller: Handles core game logic + +Dialog Handler: Manages pick dialogs + +Dice Selector: Returns dice type based on player count + +Stat Float Manager: Rezzes and updates floaters + +Stat Float Object: Displays floating text per player + +๐Ÿ•น๏ธ Game Flow +Players touch the controller object to join the game + +Once 2+ players have joined, the first player gets the option to Start Game + +Dice type is selected automatically + +Each player is prompted (in order) to select 3 numbers + +After all picks are made: + +The dice is rolled + +The result is compared to each playerโ€™s picks + +If the peril playerโ€™s number is rolled or no one picked it, the peril player loses a life + +The system checks for eliminations and starts the next round + +๐Ÿงฐ Admin Controls +Owner can: + +Join or leave the game + +Open a special Owner menu + +Add/remove test bots + +Reset the game + +Debug current state (optional) + + + +Below what was started, ill have this up just a reminder + + + + # Peril Dice Notice these rules were made by Noose the Bunny (djmusica28) secondlife:///app/agent/5935e159-63ba-415a-b20b-4813b50367d0/about, im baseing off this but im also automating it @@ -11,7 +108,7 @@ Each player wears a Peril Dice Tracker HUD Everyone starts with 3 lives -Choose a dice type: d6, d12, or d20 +Choose a dice type: d6, d12, or d21 One player clicks "Set Yourself In Peril" on the HUD to start