diff --git a/Bot Manager b/Bot Manager new file mode 100644 index 0000000..9c23073 --- /dev/null +++ b/Bot Manager @@ -0,0 +1,53 @@ +// Bot Manager for Peril Dice +// Handles logic for test players (bots): picking numbers and rolling dice + +// Configuration +integer LISTEN_CHANNEL = -9999; // Change if needed to match main controller + +// Helper to parse and respond to pick commands +doBotPick(string botName, integer count, integer diceMax) { + list picks; + while (llGetListLength(picks) < count) { + integer roll = 1 + (integer)(llFrand((float)diceMax)); + if (llListFindList(picks, [roll]) == -1) { + picks += roll; + } + } + string pickString = llDumpList2String(picks, ","); + llRegionSay(LISTEN_CHANNEL, "BOT_PICKED:" + botName + ":" + pickString); +} + +// Helper to respond to peril roll commands +doBotRoll(string botName, integer diceMax) { + integer roll = 1 + (integer)(llFrand((float)diceMax)); + llRegionSay(LISTEN_CHANNEL, "BOT_ROLL:" + botName + ":" + (string)roll); +} + +default { + state_entry() { + llListen(LISTEN_CHANNEL, "", NULL_KEY, ""); + llOwnerSay("[Bot Manager] Ready and listening on channel " + (string)LISTEN_CHANNEL); + } + + listen(integer channel, string name, key id, string message) { + if (llSubStringIndex(message, "BOT_PICK:") == 0) { + // Format: BOT_PICK:bot_1:3:20 + list parts = llParseStringKeepNulls(message, [":"], []); + if (llGetListLength(parts) == 4) { + string botName = llList2String(parts, 1); + integer count = (integer)llList2String(parts, 2); + integer diceMax = (integer)llList2String(parts, 3); + doBotPick(botName, count, diceMax); + } + } + else if (llSubStringIndex(message, "BOT_PERIL_ROLL:") == 0) { + // Format: BOT_PERIL_ROLL:bot_1:20 + list parts = llParseStringKeepNulls(message, [":"], []); + if (llGetListLength(parts) == 3) { + string botName = llList2String(parts, 1); + integer diceMax = (integer)llList2String(parts, 2); + doBotRoll(botName, diceMax); + } + } + } +} diff --git a/Helper.lsl b/Helper.lsl index 2d8ad9e..8c4a544 100644 --- a/Helper.lsl +++ b/Helper.lsl @@ -1,159 +1,358 @@ -// === Game Helper Functions Module (Max 10 players) === -// This version chooses a dice size based on the number of players, -// ensuring at least three numbers are available per player. +// === Main Peril Dice Controller (Refactored with Game Helpers Integration, with dynamic player join) === + +// +// This version of the main game controller includes support for players (including +// the owner) joining the game at runtime via a MSG_REGISTER_PLAYER message. When +// a new player registers, they are added to the internal lists (players, names, +// lives and picksData), a floating display is rezzed for them, and helpers are +// updated. This mirrors the behaviour used when adding a test player, but now +// applies to any avatar joining the game via the dialog handler. + +integer syncChannel = -77777; +integer numberPickChannel = -77888; +integer rollDialogChannel = -77999; +integer DIALOG_CHANNEL = -88888; + +list players = []; +list names = []; +list lives = []; +string perilPlayer = ""; +list globalPickedNumbers = []; +list picksData = []; +list readyPlayers = []; + +list pickQueue = []; +integer currentPickerIdx = 0; +integer diceType = 6; + +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; +integer MSG_SHOW_MENU = 201; +integer MSG_SHOW_ROLL_DIALOG = 301; integer MSG_GET_DICE_TYPE = 1001; -integer MSG_DICE_TYPE_RESULT = 1005; 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_GAME_STATE = 107; integer MSG_SYNC_PICKQUEUE = 2001; -list lives; -list picksData; -string perilPlayer; -list names; -list pickQueue; +// Maximum number of players allowed (including test players). This should +// mirror the value used in Floater Manager to avoid inconsistencies. +integer MAX_PLAYERS = 10; -// List of supported dice sizes; ensure the last entry can accommodate 10 players × 3 picks = 30 -list STANDARD_DICE = [6, 12, 20, 30]; +integer TIMEOUT_SECONDS = 600; +integer timeoutTimer; -// Determine which die to use based on player count. -// Each player should have at least 3 numbers available to choose from. -integer getDiceType(integer playerCount) { - integer minimumSides = playerCount * 3; - integer i; - for (i = 0; i < llGetListLength(STANDARD_DICE); i++) { - integer sides = llList2Integer(STANDARD_DICE, i); - if (sides >= minimumSides) { - return sides; - } - } - // If no predefined die has enough sides, return the largest supported die (d30) - return llList2Integer(STANDARD_DICE, llGetListLength(STANDARD_DICE) - 1); +integer warning2min = 120; +integer warning5min = 300; +integer warning9min = 540; +integer lastWarning = 0; + +key currentPicker; + +// Forward game state to helpers when it changes +updateHelpers() { + string serialized = llList2CSV(lives) + "~" + llList2CSV(picksData) + + "~" + perilPlayer + "~" + llList2CSV(names); + llMessageLinked(LINK_SET, MSG_SYNC_GAME_STATE, serialized, NULL_KEY); + llMessageLinked(LINK_SET, MSG_SYNC_PICKQUEUE, llList2CSV(pickQueue), NULL_KEY); } -// Determine how many picks a player should make based on the peril player's lives -integer getPicksRequiredFromName(string name) { - integer idx = llListFindList(names, [name]); - if (idx == -1) { - integer i; - for (i = 0; i < llGetListLength(names); i++) { - string testName = llList2String(names, i); - if (llSubStringIndex(testName, name) != -1) { - idx = i; - i = llGetListLength(names); - } - } - } - if (idx == -1) return 0; - integer lifeCount = (integer)llList2String(lives, idx); - if (lifeCount >= 3) return 1; - if (lifeCount == 2) return 2; - return 3; +// Request helper-calculated values +requestDiceType() { + llMessageLinked(LINK_SET, MSG_GET_DICE_TYPE, (string)llGetListLength(names), NULL_KEY); } -// Find the index of a player in the pickQueue by name -integer getPickerIndex(string name) { - return llListFindList(pickQueue, [name]); +requestPicksRequired(integer idx) { + llMessageLinked(LINK_SET, MSG_GET_PICKS_REQUIRED, llList2String(names, idx), NULL_KEY); } -// Serialize the current game state for syncing with other scripts -string serializeGameState() { +requestPickerIndex(string name) { + llMessageLinked(LINK_SET, MSG_GET_PICKER_INDEX, name, NULL_KEY); +} + +string generateSerializedState() { return llList2CSV(lives) + "~" + llList2CSV(picksData) + "~" + perilPlayer + "~" + llList2CSV(names); } -// Show a pick management dialog for the owner. -// LSL does not support a void return type, so this returns an integer which is ignored. -integer showPickManager(string player, key id) { - string searchName = player; - if (llSubStringIndex(searchName, "~") != -1) { - list parts = llParseString2List(searchName, ["~"], []); - searchName = llList2String(parts, 1); - } - - integer idx = llListFindList(names, [searchName]); - if (idx == -1) { - integer i; - for (i = 0; i < llGetListLength(names); i++) { - string testName = llList2String(names, i); - if (llSubStringIndex(testName, searchName) != -1) { - idx = i; - llOwnerSay("✅ Matched pick list for: " + testName); - i = llGetListLength(names); - } - } - if (idx == -1) { - llOwnerSay("⚠️ Could not find player: " + player); - return 0; - } - } - - integer maxPicks = getPicksRequiredFromName(llList2String(names, idx)); - - string currentData = llList2String(picksData, idx); - list parts = llParseString2List(currentData, ["|"], []); - list currentPicks = []; - if (llGetListLength(parts) > 1) { - currentPicks = llParseString2List(llList2String(parts, 1), [","], []); - } - - list buttons = []; +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 = 1; i <= 6; i++) { - string numStr = (string)i; - if (llListFindList(currentPicks, [numStr]) != -1) { - buttons += ["❌ " + numStr]; - } else if (llGetListLength(currentPicks) < maxPicks) { - buttons += ["➕ " + numStr]; - } + for (i = llGetListLength(names) - 1; i >= 0; i--) { + integer ch = -777000 + i; + llMessageLinked(LINK_SET, MSG_CLEANUP_FLOAT, (string)ch, NULL_KEY); } - - llOwnerSay("🛠 Showing pick dialog to: " + (string)id); - llDialog(id, "🛠 Managing picks for: " + searchName + "\nCurrent: " + llList2CSV(currentPicks), buttons, -88888); - - // Return dummy value since LSL does not have a void return type - return 0; + players = names = lives = picksData = globalPickedNumbers = []; + perilPlayer = ""; + pickQueue = []; + currentPickerIdx = 0; + llSay(syncChannel, "RESET"); + llOwnerSay(" Game reset!"); + llSleep(0.2); + llSetTimerEvent(0); + updateHelpers(); } +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(); + return; + } + picksData = []; + globalPickedNumbers = []; + pickQueue = names; + currentPickerIdx = 0; + integer j; + for (j = 0; j < llGetListLength(names); j++) { + picksData += [llList2String(names, j) + "|"]; + } + updateHelpers(); + // requestDiceType(); // Removed to avoid recursion loop +} + +showNextPickerDialog() { + string firstName = llList2String(pickQueue, currentPickerIdx); + currentPicker = llList2Key(players, llListFindList(names, [firstName])); + string dialogPayload = firstName + "|" + (string)diceType; + llMessageLinked(LINK_SET, MSG_SHOW_DIALOG, dialogPayload, currentPicker); + timeoutTimer = llGetUnixTime(); + lastWarning = 0; + llSetTimerEvent(60.0); +} + +integer roundStarted = FALSE; + default { + state_entry() { + llListen(DIALOG_CHANNEL, "", NULL_KEY, ""); + } + + touch_start(integer total_number) { + llOwnerSay("Touched by: " + (string)llDetectedKey(0)); + key toucher = llDetectedKey(0); + integer idx = llListFindList(players, [toucher]); + if (toucher == llGetOwner()) { + // 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); + } + } + link_message(integer sender, integer num, string str, key id) { - if (num == MSG_SYNC_GAME_STATE) { + // Player list and pick list handling remain unchanged + if (num == 202 && str == "REQUEST_PLAYER_LIST") { + string namesCSV = llList2CSV(names); + llMessageLinked(LINK_SET, 203, namesCSV, id); + return; + } + if (num == 206) { + string targetName = str; + integer i; + for (i = 0; i < llGetListLength(picksData); i++) { + string rawEntry = llList2String(picksData, i); + list parts = llParseString2List(rawEntry, ["|"], []); + if (llList2String(parts, 0) == targetName) { + llMessageLinked(LINK_SET, 205, targetName + "|" + llList2String(parts, 1), id); + return; + } + } + llMessageLinked(LINK_SET, 205, targetName + "|", id); + return; + } + if (num == 208) { + string playerName = str; + integer idx = llListFindList(names, [playerName]); + if (idx != -1) { + string lifeVal = llList2String(lives, idx); + llMessageLinked(LINK_SET, 208, playerName + "|" + lifeVal, id); + } + return; + } + // Handle dice type result from helper + if (num == MSG_DICE_TYPE_RESULT) { + if (!roundStarted) { + roundStarted = TRUE; + diceType = (integer)str; + llOwnerSay(" Dice type set to: " + str); + showNextPickerDialog(); + } + return; + } + if (num == MSG_GET_PICKS_REQUIRED) { + integer picksRequired = (integer)str; + llOwnerSay(" Picks required: " + str); + return; + } + if (num == MSG_GET_PICKER_INDEX) { + integer pickIndex = (integer)str; + llOwnerSay(" Picker index: " + str); + return; + } + if (num == MSG_SERIALIZE_GAME_STATE) { + string serialized = str; + llOwnerSay(" Serialized game state: " + serialized); + return; + } + // Handle pick actions + if (num == 204) { list parts = llParseString2List(str, ["~"], []); - lives = llCSV2List(llList2String(parts, 0)); - picksData = llCSV2List(llList2String(parts, 1)); - perilPlayer = llList2String(parts, 2); - names = llCSV2List(llList2String(parts, 3)); + string action = llList2String(parts, 0); + list args = llParseString2List(llList2String(parts, 1), ["|"], []); + string name = llList2String(args, 0); + string pick = llList2String(args, 1); + integer i; + for (i = 0; i < llGetListLength(picksData); i++) { + string entry = llList2String(picksData, i); + list pdParts = llParseString2List(entry, ["|"], []); + if (llList2String(pdParts, 0) == name) { + list pickList = []; + string rawPicks = llList2String(pdParts, 1); + if (rawPicks != "") { + pickList = llParseString2List(rawPicks, [","], []); + } + if (action == "ADD_PICK") { + if (llListFindList(pickList, [pick]) == -1) { + pickList += [pick]; + llOwnerSay("➕ Added " + pick + " to " + name); + } + } else if (action == "REMOVE_PICK") { + integer idx = llListFindList(pickList, [pick]); + if (idx != -1) { + pickList = llDeleteSubList(pickList, idx, idx); + llOwnerSay("➖ Removed " + pick + " from " + name); + } + } + picksData = llListReplaceList(picksData, [name + "|" + llList2CSV(pickList)], i, i); + updateHelpers(); + return; + } + } + return; } - else if (num == MSG_SYNC_PICKQUEUE) { - pickQueue = llCSV2List(str); + // New: handle dynamic registration of players (owner or players) via MSG_REGISTER_PLAYER + if (num == MSG_REGISTER_PLAYER) { + // str is in the format "Name|" + list parts = llParseString2List(str, ["|"], []); + string newName = llList2String(parts, 0); + key newKey = (key)llList2String(parts, 1); + // Do not register if already present + integer existingIdx = llListFindList(players, [newKey]); + if (existingIdx == -1) { + // Add to local lists + players += [newKey]; + names += [newName]; + lives += [3]; + picksData += [newName + "|"]; + // 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); + } + return; } - else if (num == MSG_GET_DICE_TYPE) { - integer playerCount = (integer)str; - llOwnerSay("📊 playerCount received: " + (string)playerCount); - integer result = getDiceType(playerCount); - // 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); - llMessageLinked(LINK_THIS, MSG_GET_PICKS_REQUIRED, (string)result, NULL_KEY); - } - else if (num == MSG_GET_PICKER_INDEX) { - integer result = getPickerIndex(str); - llMessageLinked(LINK_THIS, MSG_GET_PICKER_INDEX, (string)result, NULL_KEY); - } - else if (num == MSG_SERIALIZE_GAME_STATE) { - llMessageLinked(LINK_THIS, MSG_SERIALIZE_GAME_STATE, serializeGameState(), NULL_KEY); - } - else if (num == 206) { - llOwnerSay("📨 MSG 206 received for: " + str + " from: " + (string)id); - showPickManager(str, id); + } + + listen(integer channel, string name, key id, string msg) { + if (channel == DIALOG_CHANNEL) { + // Owner-specific commands: only owner messages in the dialog should be processed here. + if (id == llGetOwner()) { + if (msg == "Reset Game") { + resetGame(); + 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; + } + if (msg == "Manage Picks") { + llOwnerSay(" Fetching list of players for pick management..."); + llMessageLinked(LINK_SET, 202, "REQUEST_PLAYER_LIST", llGetOwner()); + return; + } + if (msg == "Add Test Player") { + // 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(); + // 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); + } } } } \ No newline at end of file