// ╔══════════════════════════════════════════════════════════════════╗ // ║ IMAGE-NPC-Machine - Unified NPC Creation, sorting & management ║ // ║ One script, root prim. All operations. ║ // ║ OpenSim 0.9.2.1 Yeti / Mono / XEngine / BulletSim / OSSL ║ // ║ Engine: Spax Orion & Dirty Helga xoaox.de:7000 ║ // ║ License: CC BY-NC ║ // ╚══════════════════════════════════════════════════════════════════╝ // ┌──────────────────────────────────────────────────────────────────┐ // │ LINKSET MAP │ // │ Link 1 (root) — Modeling stand / animation pager / brain │ // │ Link 2 — Sorter prim (touch → NPC preview menu) │ // │ Link 3 — Gen prim (touch → snapshot appearance) │ // │ Link 4 — Mgr prim (touch → region NPC kill menu) │ // └──────────────────────────────────────────────────────────────────┘ integer LINK_SORTER = 2; // sorter child prim integer LINK_GEN = 3; // generator child prim integer LINK_MGR = 4; // manager child prim vector NPC_REZ_OFFSET = <0, 0, 2>; // where NPC appears before sitting vector SIT_OFFSET = <0, 0, 1.5>; // sit target on root string CARD_PREFIX = "NPC-Card"; // appearance notecard prefix float MENU_TIMEOUT = 120.0; // seconds before menu listen dies // Menu modes integer MODE_NONE = 0; integer MODE_SORTER = 1; integer MODE_MGR = 2; integer MODE_CONFIRM = 3; // TAKE confirmation key gNpcKey = NULL_KEY; // currently seated NPC list gAnimations = []; // animation names from root inventory integer gAnimIndex = 0; // current animation index string gCurrentAnim = ""; // current animation name list gAppearCards = []; // appearance notecard names integer gAppearIndex = 0; // current appearance index integer gMenuChannel = 0; // dialog channel integer gMenuHandle = 0; // listen handle integer gMenuMode = 0; // which menu is active // MGR variables list gMgrNpcKeys = []; // NPC keys from region scan list gMgrNpcNames = []; // NPC names from region scan integer gMgrPage = 0; // pagination offset buildAnimationList() { gAnimations = []; integer count = llGetInventoryNumber(INVENTORY_ANIMATION); integer i; for (i = 0; i < count; i++) { gAnimations += llGetInventoryName(INVENTORY_ANIMATION, i); } if (llGetListLength(gAnimations) > 0) { gAnimIndex = 0; gCurrentAnim = llList2String(gAnimations, 0); } else { gAnimIndex = 0; gCurrentAnim = ""; } } buildAppearanceList() { gAppearCards = []; integer count = llGetInventoryNumber(INVENTORY_NOTECARD); integer i; for (i = 0; i < count; i++) { gAppearCards += llGetInventoryName(INVENTORY_NOTECARD, i); } if (llGetListLength(gAppearCards) > 0) { if (gAppearIndex >= llGetListLength(gAppearCards)) { gAppearIndex = 0; } } else { gAppearIndex = 0; } } // count existing ..APP_ cards to generate next numbered name string generateCardName() { integer highest = 0; integer count = llGetInventoryNumber(INVENTORY_NOTECARD); integer i; for (i = 0; i < count; i++) { string name = llGetInventoryName(INVENTORY_NOTECARD, i); if (llSubStringIndex(name, CARD_PREFIX) == 0) { string numPart = llGetSubString(name, llStringLength(CARD_PREFIX), -1); integer val = (integer)numPart; if (val > highest) { highest = val; } } } return CARD_PREFIX + (string)(highest + 1); } npcPlayAnimation(string animName) { if (gNpcKey == NULL_KEY) return; if (animName == "") return; // stop current first if (gCurrentAnim != "") { osAvatarStopAnimation(gNpcKey, gCurrentAnim); } osAvatarPlayAnimation(gNpcKey, animName); gCurrentAnim = animName; } npcRez() { if (llGetListLength(gAppearCards) == 0) { llOwnerSay("No cards in sorter, add some."); return; } string card = llList2String(gAppearCards, gAppearIndex); vector rezPos = llGetPos() + NPC_REZ_OFFSET; gNpcKey = osNpcCreate("NPC", "Model", rezPos, card, OS_NPC_OBJECT_GROUP); osNpcSit(gNpcKey, llGetKey(), OS_NPC_SIT_NOW); // animation handled in changed(CHANGED_LINK) when NPC actually sits } npcKill() { if (gNpcKey != NULL_KEY) { if (osIsNpc(gNpcKey)) { osNpcStand(gNpcKey); osNpcRemove(gNpcKey); } gNpcKey = NULL_KEY; gCurrentAnim = ""; } } npcSwapAppearance() { if (gNpcKey == NULL_KEY) return; if (llGetListLength(gAppearCards) == 0) return; string card = llList2String(gAppearCards, gAppearIndex); osNpcLoadAppearance(gNpcKey, card); // replay current animation after appearance swap if (gCurrentAnim != "") { osAvatarPlayAnimation(gNpcKey, gCurrentAnim); } } menuOpen(integer mode) { menuClose(); gMenuMode = mode; gMenuChannel = 0x80000000 | (integer)("0x" + (string)llGetKey()) + mode; gMenuHandle = llListen(gMenuChannel, "", llGetOwner(), ""); llSetTimerEvent(MENU_TIMEOUT); } menuClose() { if (gMenuHandle) { llListenRemove(gMenuHandle); gMenuHandle = 0; } llSetTimerEvent(0.0); gMenuMode = MODE_NONE; } showSorterMenu() { menuOpen(MODE_SORTER); if (llGetListLength(gAppearCards) == 0) { llOwnerSay("No cards in sorter, add some."); menuClose(); return; } string cardName = llList2String(gAppearCards, gAppearIndex); string txt = "\nAppearance: " + cardName + "\n(" + (string)(gAppearIndex + 1) + " of " + (string)llGetListLength(gAppearCards) + ")"; list buttons; if (gNpcKey == NULL_KEY) { buttons = ["DONE", "-", "REZ", "<< PREV", "-", "NEXT >>"]; } else { buttons = ["DONE", "KILL", "TAKE", "<< PREV", "DELETE", "NEXT >>"]; } llDialog(llGetOwner(), txt, buttons, gMenuChannel); } showTakeConfirm() { menuOpen(MODE_CONFIRM); string card = llList2String(gAppearCards, gAppearIndex); string txt = "\nTake appearance and remove NPC?\n\nCard: " + card; llDialog(llGetOwner(), txt, ["No", "-", "Yes"], gMenuChannel); } doTake() { string card = llList2String(gAppearCards, gAppearIndex); if (card == "" || llGetInventoryType(card) != INVENTORY_NOTECARD) { llOwnerSay("ERROR: Card not found. Resetting."); llResetScript(); return; } // give card to owner llGiveInventory(llGetOwner(), card); // kill the NPC npcKill(); // delete the card from inventory llRemoveInventory(card); llOwnerSay("Taken: " + card); // inventory changed will fire and rebuild lists } mgrScan() { gMgrNpcKeys = []; gMgrNpcNames = []; gMgrPage = 0; list inRegion = osGetAvatarList(); integer count = llGetListLength(inRegion); integer i; for (i = 0; i < count; i += 3) { key avKey = llList2Key(inRegion, i); if (osIsNpc(avKey)) { gMgrNpcKeys += avKey; gMgrNpcNames += llList2String(inRegion, i + 2); } } } showMgrMenu() { menuOpen(MODE_MGR); integer total = llGetListLength(gMgrNpcKeys); if (total == 0) { llOwnerSay("No NPCs found in region."); menuClose(); return; } string txt = "\nRegion NPCs: " + (string)total + "\n"; // LSL dialog: 12 buttons max // reserve KILL ALL, DONE, and possibly MORE integer maxShow = 10; integer needMore = FALSE; if ((total - gMgrPage) > maxShow) { maxShow = 9; needMore = TRUE; } list buttons = []; integer i; integer end = gMgrPage + maxShow; if (end > total) end = total; for (i = gMgrPage; i < end; i++) { string label = (string)(i + 1); string npcName = llList2String(gMgrNpcNames, i); txt += "\n" + label + ". " + npcName; buttons += label; } // pad to fill rows of 3 integer btnCount = llGetListLength(buttons); while (btnCount % 3 != 0) { buttons += "-"; btnCount++; } // bottom row list bottomRow = ["DONE", "KILL ALL"]; if (needMore) { bottomRow += "MORE"; } else { bottomRow += "-"; } // LSL dialog: buttons display bottom-to-top, left-to-right // so bottom row goes first in list buttons = bottomRow + buttons; llDialog(llGetOwner(), txt, buttons, gMenuChannel); } mgrKillOne(integer index) { if (index < 0 || index >= llGetListLength(gMgrNpcKeys)) return; key killKey = llList2Key(gMgrNpcKeys, index); string killName = llList2String(gMgrNpcNames, index); // if this is our seated NPC, clear tracking if (killKey == gNpcKey) { gNpcKey = NULL_KEY; gCurrentAnim = ""; } if (llGetAgentSize(killKey) != ZERO_VECTOR) { osNpcRemove(killKey); } llOwnerSay("Removed: " + killName); } mgrKillAll() { integer i; integer count = llGetListLength(gMgrNpcKeys); for (i = 0; i < count; i++) { key killKey = llList2Key(gMgrNpcKeys, i); if (llGetAgentSize(killKey) != ZERO_VECTOR) { osNpcRemove(killKey); } } // clear our own NPC tracking too gNpcKey = NULL_KEY; gCurrentAnim = ""; llOwnerSay("All NPCs obliterated."); menuClose(); } prevAnim() { if (llGetListLength(gAnimations) == 0) return; gAnimIndex--; if (gAnimIndex < 0) { gAnimIndex = llGetListLength(gAnimations) - 1; } npcPlayAnimation(llList2String(gAnimations, gAnimIndex)); } nextAnim() { if (llGetListLength(gAnimations) == 0) return; gAnimIndex++; if (gAnimIndex >= llGetListLength(gAnimations)) { gAnimIndex = 0; } npcPlayAnimation(llList2String(gAnimations, gAnimIndex)); } // ═══════════════════════════════════════════════════════════════════ default { state_entry() { llSetText("", ZERO_VECTOR, 0.0); llSitTarget(SIT_OFFSET, ZERO_ROTATION); buildAnimationList(); buildAppearanceList(); // check if an NPC is already seated (post-reset recovery) key seated = llAvatarOnSitTarget(); if (seated != NULL_KEY) { if (osIsNpc(seated)) { gNpcKey = seated; if (gCurrentAnim != "") { osAvatarPlayAnimation(gNpcKey, gCurrentAnim); } } else { // human on NPC machine — silent contempt llUnSit(seated); } } } changed(integer change) { if (change & CHANGED_LINK) { key seated = llAvatarOnSitTarget(); if (seated != NULL_KEY) { if (osIsNpc(seated)) { // NPC has sat down — begin gNpcKey = seated; // stop the default sit animation osAvatarStopAnimation(gNpcKey, "sit"); // play current animation if (gCurrentAnim != "") { osAvatarPlayAnimation(gNpcKey, gCurrentAnim); } } else { // human tried to sit — silent contempt llUnSit(seated); } } else { // NPC stood or was removed gNpcKey = NULL_KEY; gCurrentAnim = llList2String(gAnimations, gAnimIndex); } } if (change & CHANGED_INVENTORY) { buildAnimationList(); buildAppearanceList(); } if (change & CHANGED_OWNER) { llResetScript(); } } touch_start(integer num) { key who = llDetectedKey(0); if (who != llGetOwner()) return; // owner only, silent integer link = llDetectedLinkNumber(0); // ── LINK 3: GEN — snapshot appearance ── if (link == LINK_GEN) { // disable if NPC is seated if (gNpcKey != NULL_KEY) { llOwnerSay("Remove the NPC first."); return; } string cardName = generateCardName(); // truncate to 63 chars (OpenSim inventory name limit) if (llStringLength(cardName) > 63) { cardName = llGetSubString(cardName, 0, 62); } osAgentSaveAppearance(who, cardName); llOwnerSay("Appearance saved: " + cardName); // reset to pick up new card cleanly llResetScript(); return; } // ── LINK 2: SORTER — NPC preview menu ── if (link == LINK_SORTER) { showSorterMenu(); return; } // ── LINK 4: MGR — region NPC killer ── if (link == LINK_MGR) { mgrScan(); showMgrMenu(); return; } // ── LINK 1 (ROOT): animation paging ── if (link <= 1) { // no NPC seated — silent contempt if (gNpcKey == NULL_KEY) return; integer face = llDetectedTouchFace(0); if (face == 1) { // glow feedback on curved side llSetPrimitiveParams([PRIM_GLOW, 1, 0.3]); vector touchST = llDetectedTouchST(0); if (touchST.x < 0.5) { prevAnim(); } else { nextAnim(); } } // else: touched wrong face — silent contempt } } touch_end(integer num) { // clear glow on root curved face llSetPrimitiveParams([PRIM_GLOW, 1, 0.0]); } listen(integer channel, string name, key who, string message) { if (who != llGetOwner()) return; // ── SORTER MENU ── if (gMenuMode == MODE_SORTER) { if (message == "DONE") { menuClose(); return; } if (message == "-") { showSorterMenu(); return; } if (message == "REZ") { npcRez(); showSorterMenu(); return; } if (message == "KILL") { npcKill(); showSorterMenu(); return; } if (message == "TAKE") { showTakeConfirm(); return; } if (message == "DELETE") { string card = llList2String(gAppearCards, gAppearIndex); if (card == "" || llGetInventoryType(card) != INVENTORY_NOTECARD) { llOwnerSay("ERROR: Card not found. Resetting."); llResetScript(); return; } // if NPC is showing this card, kill it first if (gNpcKey != NULL_KEY) { npcKill(); } llRemoveInventory(card); llOwnerSay("Deleted: " + card); // CHANGED_INVENTORY will rebuild list // adjust index buildAppearanceList(); if (llGetListLength(gAppearCards) == 0) { llOwnerSay("No cards remaining."); menuClose(); return; } if (gAppearIndex >= llGetListLength(gAppearCards)) { gAppearIndex = 0; } showSorterMenu(); return; } if (message == "<< PREV") { if (llGetListLength(gAppearCards) <= 1) { llOwnerSay("Only one appearance card."); showSorterMenu(); return; } gAppearIndex--; if (gAppearIndex < 0) { gAppearIndex = llGetListLength(gAppearCards) - 1; } npcSwapAppearance(); showSorterMenu(); return; } if (message == "NEXT >>") { if (llGetListLength(gAppearCards) <= 1) { llOwnerSay("Only one appearance card."); showSorterMenu(); return; } gAppearIndex++; if (gAppearIndex >= llGetListLength(gAppearCards)) { gAppearIndex = 0; } npcSwapAppearance(); showSorterMenu(); return; } // unknown — re-show showSorterMenu(); return; } // ── TAKE CONFIRMATION ── if (gMenuMode == MODE_CONFIRM) { if (message == "Yes") { doTake(); // after take, if cards remain show sorter if (llGetListLength(gAppearCards) > 0) { showSorterMenu(); } else { menuClose(); } } else { // No or anything else — back to sorter showSorterMenu(); } return; } // ── MGR MENU ── if (gMenuMode == MODE_MGR) { if (message == "DONE") { menuClose(); return; } if (message == "-") { showMgrMenu(); return; } if (message == "KILL ALL") { mgrKillAll(); return; } if (message == "MORE") { gMgrPage += 9; if (gMgrPage >= llGetListLength(gMgrNpcKeys)) { gMgrPage = 0; } showMgrMenu(); return; } // numbered NPC selection integer sel = (integer)message - 1; // convert 1-based label to 0-based index if (sel >= 0 && sel < llGetListLength(gMgrNpcKeys)) { mgrKillOne(sel); // rescan and re-show mgrScan(); if (llGetListLength(gMgrNpcKeys) > 0) { showMgrMenu(); } else { llOwnerSay("No NPCs remaining in region."); menuClose(); } } return; } } timer() { menuClose(); } on_rez(integer param) { llResetScript(); } }