🎲 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)
This commit is contained in:
parent
e7b853600a
commit
ede3f6ec75
6 changed files with 335 additions and 33 deletions
78
Debugger.lsl
78
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 {
|
default {
|
||||||
link_message(integer sender, integer num, string str, key id) {
|
link_message(integer sender, integer num, string str, key id) {
|
||||||
if (num == MSG_SYNC_GAME_STATE) {
|
if (num == MSG_SYNC_GAME_STATE) {
|
||||||
|
|
|
||||||
|
|
@ -78,17 +78,54 @@ default {
|
||||||
names += [name];
|
names += [name];
|
||||||
players += [avKey];
|
players += [avKey];
|
||||||
llOwnerSay("📝 Registered: " + name);
|
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) {
|
else if (num == MSG_REZ_FLOAT) {
|
||||||
string name = str;
|
string name = str;
|
||||||
integer idx = llListFindList(names, [name]);
|
integer idx = llListFindList(names, [name]);
|
||||||
if (idx == -1) return;
|
if (idx == -1) return;
|
||||||
key avKey = llList2Key(players, idx);
|
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;
|
integer ch = -777000 + idx;
|
||||||
llOwnerSay("📦 Rezzing float for " + name + " at " + (string)pos);
|
llOwnerSay("📦 Rezzing float for " + name + " at " + (string)pos);
|
||||||
llSetObjectDesc(name);
|
llSetObjectDesc(name);
|
||||||
llRezObject("StatFloat", pos, ZERO_VECTOR, ZERO_ROTATION, ch);
|
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) {
|
else if (num == MSG_UPDATE_FLOAT) {
|
||||||
string name = str;
|
string name = str;
|
||||||
|
|
@ -105,7 +142,12 @@ default {
|
||||||
llOwnerSay("🔎 Found picks: " + llList2CSV(picks));
|
llOwnerSay("🔎 Found picks: " + llList2CSV(picks));
|
||||||
|
|
||||||
string perilName;
|
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...";
|
perilName = "🧍 Status: Waiting for game to start...";
|
||||||
} else {
|
} else {
|
||||||
perilName = "🧍 Peril: " + getNameFromKey(perilPlayer);
|
perilName = "🧍 Peril: " + getNameFromKey(perilPlayer);
|
||||||
|
|
@ -143,6 +185,23 @@ default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
perilPlayer = llList2String(parts, 2);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -133,7 +133,12 @@ default {
|
||||||
integer playerCount = (integer)str;
|
integer playerCount = (integer)str;
|
||||||
llOwnerSay("📊 playerCount received: " + (string)playerCount);
|
llOwnerSay("📊 playerCount received: " + (string)playerCount);
|
||||||
integer result = getDiceType(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) {
|
else if (num == MSG_GET_PICKS_REQUIRED) {
|
||||||
integer result = getPicksRequiredFromName(str);
|
integer result = getPicksRequiredFromName(str);
|
||||||
|
|
@ -151,4 +156,4 @@ default {
|
||||||
showPickManager(str, id);
|
showPickManager(str, id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
96
Main.lsl
96
Main.lsl
|
|
@ -42,6 +42,10 @@ integer MSG_DICE_TYPE_RESULT = 1005;
|
||||||
integer MSG_SERIALIZE_GAME_STATE = 1004;
|
integer MSG_SERIALIZE_GAME_STATE = 1004;
|
||||||
integer MSG_SYNC_PICKQUEUE = 2001;
|
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 TIMEOUT_SECONDS = 600;
|
||||||
integer timeoutTimer;
|
integer timeoutTimer;
|
||||||
|
|
||||||
|
|
@ -78,8 +82,11 @@ string generateSerializedState() {
|
||||||
}
|
}
|
||||||
|
|
||||||
resetGame() {
|
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;
|
integer i;
|
||||||
for (i = 0; i < llGetListLength(names); i++) {
|
for (i = llGetListLength(names) - 1; i >= 0; i--) {
|
||||||
integer ch = -777000 + i;
|
integer ch = -777000 + i;
|
||||||
llMessageLinked(LINK_SET, MSG_CLEANUP_FLOAT, (string)ch, NULL_KEY);
|
llMessageLinked(LINK_SET, MSG_CLEANUP_FLOAT, (string)ch, NULL_KEY);
|
||||||
}
|
}
|
||||||
|
|
@ -95,6 +102,14 @@ resetGame() {
|
||||||
}
|
}
|
||||||
|
|
||||||
startNextRound() {
|
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) {
|
if (llGetListLength(names) == 1) {
|
||||||
llSay(0, " " + llList2String(names, 0) + " is the last player standing and wins the game!");
|
llSay(0, " " + llList2String(names, 0) + " is the last player standing and wins the game!");
|
||||||
resetGame();
|
resetGame();
|
||||||
|
|
@ -134,9 +149,23 @@ default {
|
||||||
key toucher = llDetectedKey(0);
|
key toucher = llDetectedKey(0);
|
||||||
integer idx = llListFindList(players, [toucher]);
|
integer idx = llListFindList(players, [toucher]);
|
||||||
if (toucher == llGetOwner()) {
|
if (toucher == llGetOwner()) {
|
||||||
// Owner touches the prim: show menu for owner if already registered; else start as not starter
|
// Determine if this will be the first registered player (starter) before registration
|
||||||
// Pass isStarter=0 because owner is never automatically starter until players join
|
integer isStarter = FALSE;
|
||||||
llMessageLinked(LINK_SET, MSG_SHOW_MENU, "owner|0", toucher);
|
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) {
|
} else if (idx != -1) {
|
||||||
integer isStarter = (idx == 0);
|
integer isStarter = (idx == 0);
|
||||||
llMessageLinked(LINK_SET, MSG_SHOW_MENU, "player|" + (string)isStarter, toucher);
|
llMessageLinked(LINK_SET, MSG_SHOW_MENU, "player|" + (string)isStarter, toucher);
|
||||||
|
|
@ -248,9 +277,11 @@ default {
|
||||||
names += [newName];
|
names += [newName];
|
||||||
lives += [3];
|
lives += [3];
|
||||||
picksData += [newName + "|"];
|
picksData += [newName + "|"];
|
||||||
// Rez a float for the new player
|
// The float will be rezzed by the Floater Manager after it processes
|
||||||
llMessageLinked(LINK_SET, MSG_REZ_FLOAT, newName, newKey);
|
// this registration. Avoid sending MSG_REZ_FLOAT here to prevent
|
||||||
llSleep(0.2);
|
// race conditions; instead, request the float from Floater Manager
|
||||||
|
// after it has updated its own lists. Immediately propagate
|
||||||
|
// helper updates now.
|
||||||
updateHelpers();
|
updateHelpers();
|
||||||
// Notify owner of registration
|
// Notify owner of registration
|
||||||
llOwnerSay("🔔 Added player: " + newName);
|
llOwnerSay("🔔 Added player: " + newName);
|
||||||
|
|
@ -267,11 +298,8 @@ default {
|
||||||
resetGame();
|
resetGame();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (msg == "Start Game") {
|
// Note: do not handle "Start Game" here; allow the generic start logic below to apply,
|
||||||
startNextRound();
|
// so that minimum player checks are enforced and non-owner starters can initiate the game.
|
||||||
requestDiceType();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (msg == "Dump Players") {
|
if (msg == "Dump Players") {
|
||||||
llOwnerSay(" Players: " + llList2CSV(names));
|
llOwnerSay(" Players: " + llList2CSV(names));
|
||||||
return;
|
return;
|
||||||
|
|
@ -282,21 +310,45 @@ default {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (msg == "Add Test Player") {
|
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();
|
key fake = llGenerateKey();
|
||||||
// Add the test player to lists and register them with floater manager
|
// Delegate registration to the MSG_REGISTER_PLAYER handler; it will add to lists and rez a float
|
||||||
players += [fake];
|
llMessageLinked(LINK_SET, MSG_REGISTER_PLAYER, testName + "|" + (string)fake, NULL_KEY);
|
||||||
names += [name];
|
// Show the owner menu again (not flagged as starter)
|
||||||
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();
|
|
||||||
llMessageLinked(LINK_SET, MSG_SHOW_MENU, "owner|0", llGetOwner());
|
llMessageLinked(LINK_SET, MSG_SHOW_MENU, "owner|0", llGetOwner());
|
||||||
return;
|
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 the message text matches a player name, request their pick list
|
||||||
if (llListFindList(names, [msg]) != -1) {
|
if (llListFindList(names, [msg]) != -1) {
|
||||||
llMessageLinked(LINK_SET, 206, msg, id);
|
llMessageLinked(LINK_SET, 206, msg, id);
|
||||||
|
|
|
||||||
|
|
@ -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 and players share the same Ready/Leave dialog.
|
||||||
// Owners also get an "Owner" button to access advanced options.
|
// 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 DIALOG_CHANNEL = -88888;
|
||||||
integer MSG_SHOW_MENU = 201;
|
integer MSG_SHOW_MENU = 201;
|
||||||
|
|
@ -8,6 +9,8 @@ integer MSG_PICK_ACTION = 204;
|
||||||
integer MSG_PLAYER_LIST_RESULT = 203;
|
integer MSG_PLAYER_LIST_RESULT = 203;
|
||||||
integer MSG_PICK_LIST_RESULT = 205;
|
integer MSG_PICK_LIST_RESULT = 205;
|
||||||
integer MSG_LIFE_LOOKUP = 207;
|
integer MSG_LIFE_LOOKUP = 207;
|
||||||
|
integer MSG_REGISTER_PLAYER = 106;
|
||||||
|
integer MSG_REZ_FLOAT = 105;
|
||||||
|
|
||||||
// Owner-specific options shown when pressing the "Owner" button
|
// 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"];
|
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.
|
// Owners receive an extra "Owner" button to access the advanced owner menu.
|
||||||
showReadyLeaveMenu(key id, integer isStarter, integer isOwner) {
|
showReadyLeaveMenu(key id, integer isStarter, integer isOwner) {
|
||||||
list options;
|
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) {
|
if (isStarter) {
|
||||||
options = ["Start", "Leave Game"];
|
options = ["Start Game", "Leave Game"];
|
||||||
} else {
|
} else {
|
||||||
options = ["Ready", "Leave Game"];
|
options = ["Ready", "Leave Game"];
|
||||||
}
|
}
|
||||||
|
|
@ -82,8 +88,6 @@ default {
|
||||||
}
|
}
|
||||||
string targetType = llList2String(args, 0);
|
string targetType = llList2String(args, 0);
|
||||||
integer isStarter = (integer)llList2String(args, 1);
|
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") {
|
if (targetType == "owner") {
|
||||||
showReadyLeaveMenu(id, isStarter, TRUE);
|
showReadyLeaveMenu(id, isStarter, TRUE);
|
||||||
} else if (targetType == "player") {
|
} else if (targetType == "player") {
|
||||||
|
|
@ -117,10 +121,17 @@ default {
|
||||||
}
|
}
|
||||||
|
|
||||||
listen(integer channel, string name, key id, string msg) {
|
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") {
|
if (msg == "Owner") {
|
||||||
showOwnerMenu(id);
|
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") {
|
else if (msg == "Manage Picks") {
|
||||||
llMessageLinked(LINK_THIS, 202, "REQUEST_PLAYER_LIST", id);
|
llMessageLinked(LINK_THIS, 202, "REQUEST_PLAYER_LIST", id);
|
||||||
}
|
}
|
||||||
|
|
@ -168,4 +179,4 @@ default {
|
||||||
llMessageLinked(LINK_THIS, 206, currentPickTarget, id);
|
llMessageLinked(LINK_THIS, 206, currentPickTarget, id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
99
README.md
99
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
|
# 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
|
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
|
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
|
One player clicks "Set Yourself In Peril" on the HUD to start
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue