✅ New Features Added
Sequential Player Picking Logic Players now pick in order starting with the peril player. Number of picks is determined by the peril player’s current lives. Picks are tracked globally to prevent duplicates. Dynamic Dice Type Adjustment Dice size (diceType) now adjusts based on player count: 1–2 players = 6 3–4 players = 12 5–6 players = 18, etc. Prompt for Dice Roll After all players have made their picks, the peril player is prompted to roll via a dialog. Dice Roll Resolution Logic If another player matches the roll, they become the new peril player. If no one matches or the peril player matches, the peril player loses a life. 🔄 Fixes & Improvements Fixed syntax and scoping errors (resultStr, newPeril, and timer logic). Prevented duplicate rezzing of StatFloat objects. Restored and ensured 🔢 Picks: are shown on the floating stat displays. Updated floaters dynamically after each pick using llSetTimerEvent.
This commit is contained in:
parent
af80271691
commit
8e69688bff
2 changed files with 271 additions and 141 deletions
|
|
@ -1,4 +1,4 @@
|
|||
// === Fully Unified Peril Dice Controller (Testing Enabled + Click-to-Join) ===
|
||||
// === Fully Unified Peril Dice Controller (Sequential Picks + Dynamic Dice Pool) ===
|
||||
|
||||
// Communication channels
|
||||
integer syncChannel = -77777;
|
||||
|
|
@ -12,19 +12,27 @@ list picksData = [];
|
|||
string perilPlayer = "";
|
||||
list globalPickedNumbers = [];
|
||||
list floatSpawnMap = []; // stores avatar key and rezzed object key as pairs
|
||||
string pendingUpdateName = ""; // for delayed float updates
|
||||
|
||||
// Sequential pick management
|
||||
list pickQueue = [];
|
||||
integer currentPickerIdx = -1;
|
||||
|
||||
// Dice and UI settings
|
||||
integer diceType = 6;
|
||||
integer page = 0;
|
||||
integer maxPerPage = 12;
|
||||
|
||||
integer getDiceType(integer playerCount) {
|
||||
return ((playerCount + 1) / 2) * 6;
|
||||
}
|
||||
|
||||
integer getPicksRequired(integer lifeCount) {
|
||||
if (lifeCount == 3) return 1;
|
||||
if (lifeCount == 2) return 2;
|
||||
return 3;
|
||||
}
|
||||
|
||||
|
||||
integer getPicksIndex(string nameInput) {
|
||||
integer i;
|
||||
for (i = 0; i < llGetListLength(picksData); i++) {
|
||||
|
|
@ -41,50 +49,38 @@ list getPicksFor(string nameInput) {
|
|||
return llParseString2List(llList2String(llParseString2List(data, ["|"], []), 1), [","], []);
|
||||
}
|
||||
|
||||
addPick(string nameInput, string number) {
|
||||
integer addPick(string nameInput, string number) {
|
||||
integer idx = getPicksIndex(nameInput);
|
||||
integer playerIdx = llListFindList(names, [nameInput]);
|
||||
integer perilIdx = llListFindList(names, [perilPlayer]);
|
||||
list picks;
|
||||
integer lifeCount = llList2Integer(lives, playerIdx);
|
||||
integer limit = getPicksRequired(lifeCount);
|
||||
list picks;
|
||||
if (perilIdx != -1) {
|
||||
limit = getPicksRequired(llList2Integer(lives, perilIdx));
|
||||
}
|
||||
|
||||
if (idx == -1) {
|
||||
picks = [number];
|
||||
picksData += [nameInput + "|" + number];
|
||||
} else {
|
||||
picks = getPicksFor(nameInput);
|
||||
if (llListFindList(picks, [number]) != -1 || llGetListLength(picks) >= limit) return;
|
||||
if (llListFindList(picks, [number]) != -1 || llGetListLength(picks) >= limit) {
|
||||
return 0;
|
||||
}
|
||||
picks += [number];
|
||||
picksData = llListReplaceList(picksData, [nameInput + "|" + llDumpList2String(picks, ",")], idx, idx);
|
||||
}
|
||||
globalPickedNumbers += [number];
|
||||
integer nameIdx = llListFindList(names, [nameInput]);
|
||||
pendingUpdateName = nameInput;
|
||||
llSetTimerEvent(0.3);
|
||||
updateFloater(nameInput);
|
||||
return 0;
|
||||
}
|
||||
|
||||
showStatus(string name) {
|
||||
integer idx = llListFindList(names, [name]);
|
||||
if (idx == -1) return;
|
||||
integer lifeCount = llList2Integer(lives, idx);
|
||||
list picks = getPicksFor(name);
|
||||
string txt = "🎲 Peril Dice
|
||||
👤 " + name +
|
||||
"
|
||||
❤️ Lives: " + (string)lifeCount +
|
||||
"
|
||||
🧍 Peril: " + perilPlayer +
|
||||
"
|
||||
🔢 Picks: " + llList2CSV(picks);
|
||||
|
||||
key avKey = llList2Key(players, idx);
|
||||
if (llKey2Name(avKey) != "") {
|
||||
vector pos = llList2Vector(llGetObjectDetails(avKey, [OBJECT_POS]), 0);
|
||||
vector rezPos = pos + <1,0,1>; // offset in front and above the player
|
||||
integer channel = -777000 + idx;
|
||||
|
||||
llOwnerSay("📦 Attempting to rez statFloat at: " + (string)rezPos);
|
||||
llRezObject("StatFloat", rezPos, ZERO_VECTOR, ZERO_ROTATION, channel);
|
||||
floatSpawnMap += [avKey, (string)channel];
|
||||
}
|
||||
}
|
||||
showPickDialog(key id, string name) {
|
||||
list picks = getPicksFor(name);
|
||||
integer picksRequired = getPicksRequired(llList2Integer(lives, llListFindList(names, [name])));
|
||||
|
|
@ -112,33 +108,56 @@ showPickDialog(key id, string name) {
|
|||
llDialog(id, "Pick " + (string)picksRequired + " unique number(s):", buttons, numberPickChannel);
|
||||
}
|
||||
|
||||
confetti() {
|
||||
llParticleSystem([
|
||||
PSYS_SRC_PATTERN, PSYS_SRC_PATTERN_EXPLODE,
|
||||
PSYS_PART_START_COLOR, <1,1,1>, PSYS_PART_END_COLOR, <1,1,1>,
|
||||
PSYS_PART_START_ALPHA, 1.0, PSYS_PART_END_ALPHA, 0.0,
|
||||
PSYS_PART_START_SCALE, <0.2,0.2,0>, PSYS_PART_END_SCALE, <0.5,0.5,0>,
|
||||
PSYS_PART_MAX_AGE, 2.0, PSYS_SRC_MAX_AGE, 2.0,
|
||||
PSYS_SRC_ACCEL, <0,0,-0.4>,
|
||||
PSYS_SRC_BURST_RATE, 0.01, PSYS_SRC_BURST_PART_COUNT, 50,
|
||||
PSYS_SRC_BURST_RADIUS, 0.2,
|
||||
PSYS_SRC_BURST_SPEED_MIN, 1.0, PSYS_SRC_BURST_SPEED_MAX, 2.0,
|
||||
PSYS_PART_FLAGS, PSYS_PART_INTERP_COLOR_MASK | PSYS_PART_INTERP_SCALE_MASK | PSYS_PART_EMISSIVE_MASK
|
||||
]);
|
||||
integer startNextPick() {
|
||||
if (currentPickerIdx >= llGetListLength(pickQueue)) {
|
||||
llOwnerSay("✅ All players have finished picking.");
|
||||
|
||||
// Prompt peril player to roll
|
||||
integer pidx = llListFindList(names, [perilPlayer]);
|
||||
if (pidx != -1) {
|
||||
key pkey = llList2Key(players, pidx);
|
||||
llDialog(pkey, "🎲 All picks are in! Ready to roll the dice?", ["Roll"], syncChannel);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
string currentName = llList2String(pickQueue, currentPickerIdx);
|
||||
integer perilIdx = llListFindList(names, [perilPlayer]);
|
||||
if (perilIdx == -1) return 0;
|
||||
integer playerIdx = llListFindList(names, [currentName]);
|
||||
if (playerIdx == -1) {
|
||||
llOwnerSay("⚠️ Could not find player key for " + currentName);
|
||||
return 0;
|
||||
}
|
||||
key avKey = llList2Key(players, playerIdx);
|
||||
showPickDialog(avKey, currentName);
|
||||
return 0;
|
||||
}
|
||||
|
||||
resetGame() {
|
||||
// Tell all active statFloat objects to clean up
|
||||
integer i;
|
||||
for (i = 0; i < llGetListLength(names); i++) {
|
||||
integer ch = -777000 + i;
|
||||
llRegionSay(ch, "CLEANUP");
|
||||
}
|
||||
players = names = lives = picksData = globalPickedNumbers = [];
|
||||
perilPlayer = "";
|
||||
page = 0;
|
||||
llSay(syncChannel, "RESET");
|
||||
llOwnerSay("🔄 Game reset!");
|
||||
showStatus(string name) {
|
||||
integer idx = llListFindList(names, [name]);
|
||||
if (idx == -1) return;
|
||||
key avKey = llList2Key(players, idx);
|
||||
|
||||
// Check if already in floatSpawnMap
|
||||
if (llListFindList(floatSpawnMap, [avKey]) != -1) return;
|
||||
|
||||
integer lifeCount = llList2Integer(lives, idx);
|
||||
list picks = getPicksFor(name);
|
||||
string txt = "🎲 Peril Dice
|
||||
👤 " + name +
|
||||
"
|
||||
❤️ Lives: " + (string)lifeCount +
|
||||
"
|
||||
🧍 Peril: " + perilPlayer +
|
||||
"
|
||||
🔢 Picks: " + llList2CSV(picks);
|
||||
|
||||
vector pos = llList2Vector(llGetObjectDetails(avKey, [OBJECT_POS]), 0);
|
||||
vector rezPos = pos + <1,0,1>;
|
||||
integer channel = -777000 + idx;
|
||||
llOwnerSay("📦 Attempting to rez statFloat at: " + (string)rezPos);
|
||||
llRezObject("StatFloat", rezPos, ZERO_VECTOR, ZERO_ROTATION, channel);
|
||||
floatSpawnMap += [avKey, (string)channel];
|
||||
}
|
||||
|
||||
checkForWinner() {
|
||||
|
|
@ -157,94 +176,205 @@ checkForWinner() {
|
|||
}
|
||||
}
|
||||
|
||||
updateFloater(string name) {
|
||||
integer idx = llListFindList(names, [name]);
|
||||
if (idx == -1) return;
|
||||
key avKey = llList2Key(players, idx);
|
||||
integer ch = -777000 + idx;
|
||||
integer lifeCount = llList2Integer(lives, idx);
|
||||
list picks = getPicksFor(name);
|
||||
string txt = "🎲 Peril Dice
|
||||
👤 " + name +
|
||||
"
|
||||
❤️ Lives: " + (string)lifeCount +
|
||||
"
|
||||
🧍 Peril: " + perilPlayer +
|
||||
"
|
||||
🔢 Picks: " + llList2CSV(picks);
|
||||
llRegionSay(ch, "FLOAT:" + (string)avKey + "|" + txt);
|
||||
}
|
||||
|
||||
resetGame() {
|
||||
integer i;
|
||||
for (i = 0; i < llGetListLength(names); i++) {
|
||||
integer ch = -777000 + i;
|
||||
llRegionSay(ch, "CLEANUP");
|
||||
}
|
||||
players = names = lives = picksData = globalPickedNumbers = [];
|
||||
perilPlayer = "";
|
||||
page = 0;
|
||||
llSay(syncChannel, "RESET");
|
||||
llOwnerSay("🔄 Game reset!");
|
||||
}
|
||||
|
||||
integer rollDice() {
|
||||
return 1 + (integer)llFrand(diceType);
|
||||
}
|
||||
|
||||
default {
|
||||
touch_start(integer total_number) {
|
||||
key toucher = llDetectedKey(0);
|
||||
string name = llDetectedName(0);
|
||||
integer idx = llListFindList(players, [toucher]);
|
||||
if (~idx) {
|
||||
llDialog(toucher, "❓ You are already in the game. Leave the game?", ["Yes", "No"], -88888);
|
||||
return;
|
||||
}
|
||||
|
||||
players += [toucher];
|
||||
names += [name];
|
||||
lives += [3];
|
||||
llOwnerSay("✅ " + name + " has joined the game with 3 lives.");
|
||||
showStatus(name);
|
||||
timer() {
|
||||
if (pendingUpdateName != "") {
|
||||
updateFloater(pendingUpdateName);
|
||||
pendingUpdateName = "";
|
||||
}
|
||||
}
|
||||
|
||||
state_entry() {
|
||||
llVolumeDetect(TRUE);
|
||||
llSetStatus(STATUS_PHYSICS, FALSE);
|
||||
llSetClickAction(CLICK_ACTION_TOUCH);
|
||||
llListen(-88888, "", NULL_KEY, "");
|
||||
llListen(0, "", NULL_KEY, "");
|
||||
llListen(syncChannel, "", NULL_KEY, "");
|
||||
llListen(numberPickChannel, "", NULL_KEY, "");
|
||||
llOwnerSay("🎲 Unified Peril Controller Ready");
|
||||
}
|
||||
|
||||
on_rez(integer p) {
|
||||
llResetScript();
|
||||
}
|
||||
listen(integer chan, string speakerName, key id, string msg) {
|
||||
if (chan == -88888 && msg == "Yes") {
|
||||
integer i = llListFindList(players, [id]);
|
||||
if (i != -1) {
|
||||
integer ch = -777000 + i;
|
||||
llRegionSay(ch, "CLEANUP");
|
||||
players = llDeleteSubList(players, i, i);
|
||||
names = llDeleteSubList(names, i, i);
|
||||
lives = llDeleteSubList(lives, i, i);
|
||||
llOwnerSay("👋 Player left the game: " + llKey2Name(id));
|
||||
}
|
||||
}
|
||||
else if (chan == 0 && llToLower(msg) == "/reset" && id == llGetOwner()) {
|
||||
resetGame();
|
||||
}
|
||||
else if (chan == 0 && llToLower(msg) == "/start" && id == llGetOwner()) {
|
||||
if (llGetListLength(names) == 0) {
|
||||
llOwnerSay("⚠️ No players to choose from.");
|
||||
return;
|
||||
}
|
||||
integer r = (integer)llFrand(llGetListLength(names));
|
||||
string chosen = llList2String(names, r);
|
||||
perilPlayer = chosen;
|
||||
picksData = globalPickedNumbers = [];
|
||||
page = 0;
|
||||
llSay(syncChannel, "PERIL:" + perilPlayer);
|
||||
llOwnerSay("🔥 Peril player is: " + perilPlayer);
|
||||
// Refresh all float displays
|
||||
integer j;
|
||||
for (j = 0; j < llGetListLength(names); j++) {
|
||||
showStatus(llList2String(names, j));
|
||||
}
|
||||
}
|
||||
}
|
||||
object_rez(key id) {
|
||||
integer i;
|
||||
for (i = 0; i < llGetListLength(floatSpawnMap); i += 2) {
|
||||
key avKey = llList2Key(floatSpawnMap, i);
|
||||
integer ch = (integer)llList2String(floatSpawnMap, i + 1);
|
||||
integer idx = llListFindList(players, [avKey]);
|
||||
if (idx != -1 && ch == -777000 + idx) {
|
||||
string name = llList2String(names, idx);
|
||||
integer lifeCount = llList2Integer(lives, idx);
|
||||
list picks = getPicksFor(name);
|
||||
string txt = "🎲 Peril Dice
|
||||
object_rez(key id) {
|
||||
integer i;
|
||||
for (i = 0; i < llGetListLength(floatSpawnMap); i += 2) {
|
||||
key avKey = llList2Key(floatSpawnMap, i);
|
||||
integer ch = (integer)llList2String(floatSpawnMap, i + 1);
|
||||
integer idx = llListFindList(players, [avKey]);
|
||||
if (idx != -1 && ch == -777000 + idx) {
|
||||
string name = llList2String(names, idx);
|
||||
integer lifeCount = llList2Integer(lives, idx);
|
||||
list picks = getPicksFor(name);
|
||||
string txt = "🎲 Peril Dice
|
||||
👤 " + name +
|
||||
"
|
||||
"
|
||||
❤️ Lives: " + (string)lifeCount +
|
||||
"
|
||||
"
|
||||
🧍 Peril: " + perilPlayer +
|
||||
"
|
||||
"
|
||||
🔢 Picks: " + llList2CSV(picks);
|
||||
llRegionSay(ch, "FLOAT:" + (string)avKey + "|" + txt);
|
||||
floatSpawnMap = llDeleteSubList(floatSpawnMap, i, i + 1);
|
||||
return;
|
||||
}
|
||||
llRegionSay(ch, "FLOAT:" + (string)avKey + "|" + txt);
|
||||
floatSpawnMap = llDeleteSubList(floatSpawnMap, i, i + 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
state_entry() {
|
||||
llVolumeDetect(TRUE);
|
||||
llSetStatus(STATUS_PHYSICS, FALSE);
|
||||
llSetClickAction(CLICK_ACTION_TOUCH);
|
||||
llListen(-88888, "", NULL_KEY, "");
|
||||
llListen(0, "", NULL_KEY, "");
|
||||
llListen(syncChannel, "", NULL_KEY, "");
|
||||
llListen(numberPickChannel, "", NULL_KEY, "");
|
||||
llOwnerSay("🎲 Unified Peril Controller Ready");
|
||||
}
|
||||
|
||||
touch_start(integer total_number) {
|
||||
key toucher = llDetectedKey(0);
|
||||
string name = llDetectedName(0);
|
||||
integer idx = llListFindList(players, [toucher]);
|
||||
if (~idx) {
|
||||
llDialog(toucher, "❓ You are already in the game. Leave the game?", ["Yes", "No"], -88888);
|
||||
return;
|
||||
}
|
||||
|
||||
players += [toucher];
|
||||
names += [name];
|
||||
lives += [3];
|
||||
llOwnerSay("✅ " + name + " has joined the game with 3 lives.");
|
||||
showStatus(name);
|
||||
}
|
||||
|
||||
listen(integer chan, string speakerName, key id, string msg) {
|
||||
if (chan == syncChannel && msg == "Roll" && llKey2Name(id) != "") {
|
||||
if (llList2String(names, llListFindList(players, [id])) != perilPlayer) return;
|
||||
string newPeril = "";
|
||||
integer result = rollDice();
|
||||
string resultStr = (string)result;
|
||||
llSay(0, "🎲 " + perilPlayer + " rolled a " + resultStr + "!");
|
||||
|
||||
integer matched = FALSE;
|
||||
integer i;
|
||||
for (i = 0; i < llGetListLength(names); i++) {
|
||||
string pname = llList2String(names, i);
|
||||
list picks = getPicksFor(pname);
|
||||
if (llListFindList(picks, [resultStr]) != -1) {
|
||||
matched = TRUE;
|
||||
if (pname != perilPlayer) {
|
||||
newPeril = pname;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (matched && newPeril != "") {
|
||||
llSay(0, "🎯 " + newPeril + " matched the roll and becomes the new peril player!");
|
||||
perilPlayer = newPeril;
|
||||
} else {
|
||||
integer pidx = llListFindList(names, [perilPlayer]);
|
||||
if (pidx != -1) {
|
||||
integer currentLives = llList2Integer(lives, pidx);
|
||||
lives = llListReplaceList(lives, [currentLives - 1], pidx, pidx);
|
||||
llSay(0, "💀 " + perilPlayer + " was hit and lost a life!");
|
||||
showStatus(perilPlayer);
|
||||
}
|
||||
}
|
||||
|
||||
checkForWinner();
|
||||
}
|
||||
|
||||
else if (chan == numberPickChannel) {
|
||||
string currentName = llList2String(pickQueue, currentPickerIdx);
|
||||
integer playerIdx = llListFindList(names, [currentName]);
|
||||
if (playerIdx == -1) return;
|
||||
key playerKey = llList2Key(players, playerIdx);
|
||||
|
||||
if (id != playerKey) return; // Not their turn
|
||||
|
||||
if (msg == "Next »") {
|
||||
page++;
|
||||
showPickDialog(id, currentName);
|
||||
} else if (msg == "« Prev") {
|
||||
if (page > 0) page--;
|
||||
showPickDialog(id, currentName);
|
||||
} else {
|
||||
addPick(currentName, msg);
|
||||
list picks = getPicksFor(currentName);
|
||||
integer perilIdx = llListFindList(names, [perilPlayer]);
|
||||
if (perilIdx == -1) return;
|
||||
integer required = getPicksRequired(llList2Integer(lives, perilIdx));
|
||||
if (llGetListLength(picks) >= required) {
|
||||
currentPickerIdx++;
|
||||
}
|
||||
page = 0;
|
||||
startNextPick();
|
||||
}
|
||||
}
|
||||
else if (chan == -88888 && msg == "Yes") {
|
||||
integer i = llListFindList(players, [id]);
|
||||
if (i != -1) {
|
||||
integer ch = -777000 + i;
|
||||
llRegionSay(ch, "CLEANUP");
|
||||
players = llDeleteSubList(players, i, i);
|
||||
names = llDeleteSubList(names, i, i);
|
||||
lives = llDeleteSubList(lives, i, i);
|
||||
llOwnerSay("👋 Player left the game: " + llKey2Name(id));
|
||||
}
|
||||
}
|
||||
else if (chan == 0 && llToLower(msg) == "/reset" && id == llGetOwner()) {
|
||||
resetGame();
|
||||
}
|
||||
else if (chan == 0 && llToLower(msg) == "/start" && id == llGetOwner()) {
|
||||
if (llGetListLength(names) == 0) {
|
||||
llOwnerSay("⚠️ No players to choose from.");
|
||||
return;
|
||||
}
|
||||
integer r = (integer)llFrand(llGetListLength(names));
|
||||
string chosen = llList2String(names, r);
|
||||
perilPlayer = chosen;
|
||||
picksData = globalPickedNumbers = [];
|
||||
page = 0;
|
||||
diceType = getDiceType(llGetListLength(names));
|
||||
|
||||
pickQueue = names;
|
||||
currentPickerIdx = 0;
|
||||
|
||||
llSay(syncChannel, "PERIL:" + perilPlayer);
|
||||
llOwnerSay("🔥 Peril player is: " + perilPlayer);
|
||||
|
||||
integer j;
|
||||
for (j = 0; j < llGetListLength(names); j++) {
|
||||
showStatus(llList2String(names, j));
|
||||
}
|
||||
|
||||
startNextPick();
|
||||
}
|
||||
}
|
||||
} // end of default state
|
||||
// end of script
|
||||
|
|
|
|||
|
|
@ -2,13 +2,20 @@ key target;
|
|||
string displayText;
|
||||
|
||||
default {
|
||||
on_rez(integer start_param) {
|
||||
llOwnerSay("🧪 on_rez triggered with start_param: " + (string)start_param);
|
||||
llListen(start_param, "", NULL_KEY, "");
|
||||
llOwnerSay("📡 Listening on channel (on_rez): " + (string)start_param);
|
||||
}
|
||||
|
||||
state_entry() {
|
||||
llOwnerSay("🧭 StatFloat active");
|
||||
llSetText("", <1,1,1>, 1.0);
|
||||
llSetTimerEvent(1.0); // track every second
|
||||
llSetText("Waiting...", <1,1,1>, 1.0);
|
||||
llSetTimerEvent(1.0);
|
||||
}
|
||||
|
||||
listen(integer channel, string name, key id, string message) {
|
||||
llOwnerSay("🔉 Heard on channel " + (string)channel + ": " + message);
|
||||
if (llSubStringIndex(message, "FLOAT:") == 0) {
|
||||
list parts = llParseString2List(message, ["|"], []);
|
||||
if (llGetListLength(parts) >= 2) {
|
||||
|
|
@ -23,17 +30,10 @@ default {
|
|||
}
|
||||
}
|
||||
|
||||
on_rez(integer start_param) {
|
||||
llListen(start_param, "", NULL_KEY, "");
|
||||
llOwnerSay("📡 Listening on channel " + (string)start_param);
|
||||
}
|
||||
|
||||
timer() {
|
||||
if (target != NULL_KEY && llKey2Name(target) != "") {
|
||||
vector pos = llList2Vector(llGetObjectDetails(target, [OBJECT_POS]), 0) + <1,0,1>;
|
||||
llSetRegionPos(pos);
|
||||
} else {
|
||||
llOwnerSay("⚠️ Target key invalid or avatar not found.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue