Peril_Dice/Floater Manager.lsl
RebeccaNod1 ede3f6ec75 🎲 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)
2025-08-02 00:18:45 -05:00

207 lines
No EOL
9.3 KiB
Text
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// === 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) {
llOwnerSay("⚠️ Malformed picks entry (missing pipe): " + entry);
} else {
list parts = llParseString2List(entry, ["|"], []);
if (llGetListLength(parts) < 2) {
llOwnerSay("⚠️ Malformed picks entry (too few parts): " + entry);
}
else if (llList2String(parts, 0) == nameInput) {
string pickString = llList2String(parts, 1);
if (pickString == "") {
llOwnerSay("⚠️ Malformed or empty picks entry: " + entry);
} else {
list all = llParseString2List(pickString, [","], []);
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 {
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 {
perilName = "🧍 Peril: " + getNameFromKey(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);
if (idx >= 0 && idx < llGetListLength(players)) {
llRegionSay(ch, "CLEANUP");
players = llDeleteSubList(players, idx, idx);
names = llDeleteSubList(names, idx, idx);
lives = llDeleteSubList(lives, idx, idx);
picksData = llDeleteSubList(picksData, idx, idx);
}
}
else if (num == MSG_SYNC_GAME_STATE) {
// Synchronize the lists for lives and picksData when receiving a new game state
list parts = llParseString2List(str, ["~"], []);
lives = llCSV2List(llList2String(parts, 0));
list rawPicks = llCSV2List(llList2String(parts, 1));
picksData = [];
integer i;
for (i = 0; i < llGetListLength(rawPicks); i++) {
string entry = llList2String(rawPicks, i);
if (llSubStringIndex(entry, "|") != -1) {
picksData += [entry];
} else {
llOwnerSay("⚠️ Ignored malformed picksData during sync: " + entry);
}
}
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);
}
}
}
}