🎲 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:
RebeccaNod1 2025-08-02 00:18:45 -05:00
parent e7b853600a
commit ede3f6ec75
6 changed files with 335 additions and 33 deletions

View file

@ -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) {

View file

@ -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);
}
}
}
}

View file

@ -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);
}
}
}
}

View file

@ -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);

View file

@ -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);
}
}
}
}

View file

@ -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