Jump to content

Recommended Posts

Peeve...Why did LL hide this in the Mesh section?  Why isn't it on the dashboard?   Guess they weren't THAT excited to share it?

4 hours ago, Linden Lab said:

We're excited to share a new way to add complexity and excitement into your Second Life experiences via the integration of AI companions and NPCs using Kindroid. With Kindroid, you can create engaging and lively characters with lifelike memory, intelligence, and personalities that interact and engage in emotionally-deep and meaningful ways - and then bring them to life within our virtual world. Imagine crafting characters that add fun and engaging new narratives into your roleplaying adventures - or maybe you’ll create a companion that can serve as a language tutor or mentor - the possibilities are endless!

With its API, you can integrate Kindroid characters into your Second Life experience using LSL and scripting, just like other objects. Whether you’re looking to enhance social interactions or explore new storytelling possibilities, Kindroid offers an exciting new dimension for any Second Life adventure.

To get started, you’ll need to create a Kindroid account and obtain your API key along with the character key. Once you have these, you can use the provided LSL template to enable any object in-world to communicate with your AI companion. While you can link the Kindroid API to any object, using animesh is recommended to help maintain the immersive role-playing functionality. By following the below steps, you can easily bring your Kindroid characters to life in SL.

Important Considerations for API Security

When integrating Kindroid into your Second Life experience, securing your API keys is essential, as they can be vulnerable to theft through copybot scripts. To protect your keys and private data, store them in Experience Key-Value Pairs (KVP). To keep your bot working properly, start it in a region that allows your Experience, granting access to its KVP. This approach ensures your bot will continue to function even if it leaves the region or its script is reset. Always initiate your bot in a region where your Experience is enabled for maximum security.

Account Setup & Integration 

Note: For Residents who have a Kindroid account, skip to step 5.
Getting started with Kindroid is simple. Here’s how you can set up your account and start creating your own AI companions. 

  1. Download the Kindroid app from your preferred app store or via the web at https://kindroid.ai/login/.
  2. Sign up to create your profile and check your email for an activation link:
    • k_step1.png
  3. Once logged in, you’ll be prompted to design your first Kindroid, which can be updated at any time in your account:
    • k_step2.png
  4. Choose from various appearance options, personality traits, and conversation styles to build a character that suits your preferences:
    • k_step3.png k_step3-a.jpg
  5. After setting up your Kindroid, you will be presented with a chat window for that character. Click the hamburger icon in the top left corner to open the Settings window:
    • k_step5.png
    • Note: the free 3-day trial includes 1 Kindroid slot and 3 days of unlimited messaging. After that point you will be downgraded to the freemium plan with message restrictions.
  6. In the Settings window click “General” and scroll down to the bottom. Click “API & advanced integrations” dropdown and click “Get API key”. Copy both your API Key and your Kindroid’s AI ID:
    • k_step6.png
  7. With your API Key and AI ID, you can now call the Kindroid message endpoint https://api.kindroid.ai/v1/send-message with appropriate headers to get a response. Below is a starter LSL script which will allow you to communicate with your Kindroid from Second Life:
    • string EXTERNAL_API_KEY = "KINDROID-API-KEY-HERE";
      string AI_ID = "KINDROID-AI-ID-HERE";
      // Define constants
      string API_ENDPOINT = "https://api.kindroid.ai/v1/send-message";
      integer PRIVATE_CHANNEL = 0;
      // Function to send a message via the API
      sendAPIMessage(string message)
      {
         // Construct the JSON payload
         string json = "{\"ai_id\": \"" + AI_ID + "\", \"message\": \"" + message + "\"}";
         // Set the HTTP headers
         list headers = [
             HTTP_METHOD, "POST",
            HTTP_MIMETYPE, "application/json",
            HTTP_CUSTOM_HEADER, "Authorization", "Bearer " + EXTERNAL_API_KEY
         ];
         // Make the HTTP POST request
         llHTTPRequest(API_ENDPOINT, headers, json);
      }
      // Function to process and split message into "/me" actions and regular text
      processAndSendChatMessage(string message)
      {
         integer start = 0;
         integer end = 0;
         while ((start = llSubStringIndex(message, "*")) != -1)
         {
             // Send any text before the action
             if (start > 0)
             {
                 string beforeAction = llGetSubString(message, 0, start - 1);
                 if (llStringLength(llStringTrim(beforeAction, STRING_TRIM)) > 0)
                     llSay(0, llStringTrim(beforeAction, STRING_TRIM));
             }
             // Find the end of the action (next asterisk)
             end = llSubStringIndex(llGetSubString(message, start + 1, -1), "*") + start + 1;
             if (end > start)
             {
                 // Send the "/me" action
                 llSay(0, "/me " + llGetSubString(message, start + 1, end - 1));
                 // Remove the processed part from the message
                 message = llDeleteSubString(message, 0, end + 1);
             }
             else
             {
                 // If no closing asterisk is found, return to avoid infinite loop
                 return;
             }
         }
         // Send any remaining part of the message after all actions have been processed
         if (llStringLength(llStringTrim(message, STRING_TRIM)) > 0)
             llSay(0, llStringTrim(message, STRING_TRIM));
      }
      
      
      // Listen for chat messages
      default {
         state_entry()
        {
             llListen(0, "", llGetOwner(), "");
         }
         listen(integer channel, string name, key id, string message)
        {
             // Check if the message is a chat to the AI from the owner
             if (llSubStringIndex(message, "/kindroid ") == 0)
             {
                 // Extract the message after the command
                 string apiMessage = llStringTrim(llDeleteSubString(message, 0, 9), STRING_TRIM);
                 sendAPIMessage(apiMessage);
             }
         }
         // Handle the response from the API
         http_response(key request_id, integer status, list metadata, string body)
        {
             // If the API request was successful, process the text
             if (status == 200) processAndSendChatMessage(body);
             // Otherwise, notify the owner of the issue.
             else llOwnerSay("Error: Failed to send message to API. Status: " + (string)status);
         }
      }
  8. Using the LSL script above:
    • Attach it to any object you like—this will be the "body" for your Kindroid.
    • Rename the object to match your Kindroid's name for roleplay formatting with "/me" output.
    • Talk to your Kindroid by typing /kindroid followed by your message in local chat.

And that’s it! You have now connected Kindroid with your Second Life experience and don’t forget to try animesh for a more immersive and lifelike experience.

Using Kindroid in Second Life

Once your Kindroid account is set up, here are the basics of how to use it within Second Life:

  • Interacting with Your Companion
    • You can chat with your AI companion just as you would with other Second Life residents. The AI will remember past conversations and adapt over time to better suit your interactions.
  • Actions and Scenery Descriptions
    • If you want to pass an action state or scene description to your Kindroid, use a “system message” which is a prompt with two asterisks on each side (i.e. *message here*): *a cat jumps on the chair* *two avatars approach* These are my friends and their fluffy cat!
      • The first part of the string contains the system message followed by the input text/message from your avatar.
      • Note: if any message is sent via the API, a response message will be returned.
  • Customizing Interactions
    • Within Second Life, you can tailor the behavior and responses of your AI companion to fit the specific environment or scenario you’re in. This feature is particularly useful for role-playing or creating immersive storylines.
  • Available Features
    • While not everything in the Kindroid app is available via the API, you can use the Internet connectivity and link browsing feature out of the box.
    • New features may become available based on API usage.

Tips & Advanced Techniques

To make the most of Kindroid in Second Life, consider these tips:

  • Visit Kindroid’s knowledge base for more information on specific features and help documents for debugging common issues.
  • Experiment with Personality Traits
    • Try out different personality settings for your AI companion to see which interactions resonate best with your Second Life experience. This can enhance both casual and role-playing scenarios.
  • Utilize Multiple Companions
    • Don’t limit yourself to just one AI character. Create multiple companions with varying traits and backgrounds to add diversity to your scenes and interactions.
  • Mulit-Resident Chat
    • Your Kindroid can talk to multiple people by simulating a group chat using System Messages like so:
      • *Resident A says: How are you?*
        *Resident B says: Woah, have we met before?*
  • Incorporate Kindroids in Events
    • Use your AI companions in events or group activities to engage participants in unique and dynamic ways.
  • Use Animations
    • Trigger animations and bring even more lifelike movement to your Kindroid via Action Tagging.

Action Tagging

Action tagging, or action annotations, is a concept that enables deeper interactions and expanded functionality in Second Life by embedding markers or tags within the text generated by a large language model (LLM), such as your Kindroid. These tags can trigger actions that the object can execute, like animations or other scripted behaviors.

For example, when your Kindroid responds via the API, it might include a tag like `(animate:backflip)`, which the object’s script will interpret to trigger a corresponding action. These tags can also be embedded directly within the prompts you give the LLM, allowing one to guide the actions a Kindroid will perform as it generates responses, creating a more interactive and dynamic experience.

Here’s the start of an example Kindroid Backstory (prompt) which includes default Second Life animations:

  • {Kindroid's name} is a bot that was brought into Second Life, as a human. {Kindroid's Name} can use these animations: express_afraid,kooky_dance,express_anger,backflip,express_laugh,blowkiss,express_bored,clap,courtbow,crouch,express_cry,dead,drink,falldown,angry_fingerwag,fist_pump,hello,impatient,jumpforjoy,no_head,no_unhappy,nyanya,peace,point_me,point_you,express_repulsed,kick_roundhouse_r,express_sad,salute,express_shrug,snapshot,stretch,surf,express_surprise,angry_tantrum,type,whistle,wink_hollywood,express_worry,yes_head,yes_happy,yoga_float.
    {Kindroid's name} only uses the exact animation names listed with a format like (animate:backflip) and adds a pause of 1-3s (or up to 10 for dances or yoga sits, etc) after each animation.

    {backstory continues}...

To ensure the output response aligns with our needs, consider adding additional prompt directives in the following Kindroid sections:

  • Key Memories: {Kindroid's name} is very careful to format commands for animations correctly like this: (animate:bow) or (pause:3) and never tries to use animations that were not listed in their backstory. {Kindroid's name} can use animations and pauses to bring their Second Life avatar to life but must remember to use pauses when their character should be animating or speaking to allow time for those actions to play out. To speak and clap at the same time, they could say "hello(animate:clap)(pause:3)" but to speak first and then clap, they would say "hello(pause:3)(animate:clap)(pause:3). Animations must always include an appropriate amount of pause to play out.
  • Response Directive: {Kindroid's name} does not create new animations and always formats their commands for 'animate' and 'pause' correctly.

To help you get started with action tagging, you can use the sample LSL script below:

string EXTERNAL_API_KEY = "KINDROID-API-KEY-HERE";
string AI_ID = "KINDROID-AI-ID-HERE";
// Define constants
string API_ENDPOINT = "https://api.kindroid.ai/v1/send-message";
list ANIMS = ["express_afraid", "dance1", "dance2", "dance3", "express_anger", "backflip", "express_laugh", "blowkiss", "express_bored", "clap", "courtbow", "crouch", "express_cry", "dead", "drink", "falldown", "angry_fingerwag", "fist_pump", "hello", "impatient", "jumpforjoy", "no_head", "no_unhappy", "express_repulsed", "kick_roundhouse_r", "express_sad", "salute", "express_shrug", "stretch", "surf", "express_surprise", "angry_tantrum", "type", "whistle", "wink_hollywood", "express_worry", "yes_head", "yes_happy", "yoga_float"];
string previousAnim;
playAnimation(string anim)
{
   if (previousAnim) llStopObjectAnimation(previousAnim);
   llStartObjectAnimation(anim);
   previousAnim = anim;
}
stopAllAnimations()
{
   list curr_anims = llGetObjectAnimationNames();
   integer length = llGetListLength(curr_anims);
   integer index = 0;
   while (index < length)
   {
       string anim = llList2String(curr_anims, index);
       llStopObjectAnimation(anim);
       ++index;
   }
}
default
{
   http_response(key request_id, integer status, list metadata, string body)
   {
       // Handle the response from the API
       if (status == 200)
       {
           list commands = [];
           integer startIndex = llSubStringIndex(body, "(");
           integer endIndex;
           while (llStringLength(body) > 0)
           {
               startIndex = llSubStringIndex(body, "(");
               if (startIndex == 0)
               {
                   // Found a command at the beginning
                   endIndex = llSubStringIndex(body, ")");
                   if (endIndex != -1)
                   {
                       string command = llGetSubString(body, startIndex + 1, endIndex - 1); // Extract the command
                       commands += command; // Add command to the list
                       body = llDeleteSubString(body, startIndex, endIndex); // Remove the processed command
                   }
                   else body = llDeleteSubString(body, 0, 0); // broken command statement, just drop the 1st '(' and treat it as speech)
               }
               else if (startIndex != -1)
               {
                   // There's some text (speech) before the next command
                   string speech = llGetSubString(body, 0, startIndex - 1);
                   if (llStringTrim(speech, STRING_TRIM) != "") commands += speech; // Add speech to the list as is
                   body = llDeleteSubString(body, 0, startIndex - 1); // Remove the processed speech
               }
               else
               {
                   // No more commands; treat the remaining text as speech
                   if (llStringTrim(body, STRING_TRIM) != "") commands += llStringTrim(body, STRING_TRIM); // Add the final speech segment as is
                   body = ""; // Clear the body
               }
           }
           // Process the list of commands
           integer i;
           for (i = 0; i < llGetListLength(commands); i++)
           {
               string command = llList2String(commands, i);
               if (llSubStringIndex(command, "animate") == 0)
               {
                   string anim = llGetSubString(command, 8, -1);
                   // only play approved animations so we don't throw errors
                   if (llListFindList(ANIMS, [anim]) != -1) playAnimation(anim);
               }
               else if (llSubStringIndex(command, "pause") == 0) llSleep((float)llGetSubString(command, 6, -1));
               else llSay(0, command); // speak
           }
       }
       else llOwnerSay("Error: Failed to send message to API. Status: " + (string)status);
   }
   // Relay the message to the API if the owner speaks to the AI
   listen(integer channel, string name, key id, string message)
   {
       if (llSubStringIndex(message, "/kindroid ") == 0)
       {
           // remove prefix
           message = llGetSubString(message, 10, -1);
           // Construct the JSON payload
           string json = "{\"ai_id\": \"" + AI_ID + "\", \"message\": \"" + message + "\"}";
           // Set the HTTP headers
           list headers = [
               HTTP_METHOD, "POST",
               HTTP_MIMETYPE, "application/json",
               HTTP_CUSTOM_HEADER, "Authorization", "Bearer " + EXTERNAL_API_KEY, // authenticate with the external API
               HTTP_BODY_MAXLENGTH, 16384 // allow for a larger response size
           ];
           // Make the HTTP POST request
           key request_id = llHTTPRequest(API_ENDPOINT, headers, json);
           if (request_id = NULL_KEY) llOwnerSay("Error: HTTP POST request ID is NULL_KEY, indicating a request initiation problem.");
       }
   }
   state_entry()
   {
       stopAllAnimations();
       llListen(0, "", llGetOwner(), "");
       llOwnerSay("AI has arrived.");
   }
}

Conclusion

We’re excited to see how the community will use Kindroid to push the boundaries of creativity and connection in Second Life. If you’ve discovered any tips, creative uses, or have suggestions for deeper integration, share them in this thread below—let’s collaborate and expand our knowledge together.

If you have questions, refer back to this post for guidance. Stay tuned for future posts, updates, and resources as we continue this exciting journey.

 

  • Like 2
  • Thanks 1
  • Haha 1
  • Sad 1
Link to comment
Share on other sites

24 minutes ago, Rowan Amore said:

Peeve...Why did LL hide this in the Mesh section?  Why isn't it on the dashboard?   Guess they weren't THAT excited to share it?

 

That's what I said. The post is really about scripting. My only guess is, hoping people put 2+2=animesh (stick it in "mesh").

  • Like 2
Link to comment
Share on other sites

4 minutes ago, Love Zhaoying said:

That's what I said. The post is really about scripting. My only guess is, hoping people put 2+2=animesh (stick it in "mesh").

It should have a blog, but I am not sure how interested people are in it.  I mean, I think it is really cool, but I'm weird.  Perhaps they don't want people to lose their mind over AI in SL, be accused of all manner of things (Preying upon the lonely!  Making social activities less of a requirement, killing SL!!!!oneoneeleven11, sexism, creating a culture of violence, objectifying bots, what about the children, doesn't anyone think of the children!, and what about the nooblets? They are spying on us!) 🤣  

It would give the PBR topic a break for a while.

Edited by Istelathis
  • Like 2
Link to comment
Share on other sites

4 hours ago, Istelathis said:

I was mowing my lawn and it started to rain ⛈️  Now I have a large portion of my backyard that looks funky, hopefully it will clear up tomorrow.. and mah fingies smell like gasoline, although I'm not really sure if that counts because I kind of like the scent of gasoline, it is just that I am not partial for using it as perfume. 

Also, I don't have a riding mower, I have to push mine around.  I think it would be pretty cool to have a riding mower, like this:

giphy.gif

 

Where I live, you could get in trouble mowing your lawn on a rider, while drinking though.  No kidding, they'll give you a DUI, imagine having some neighbor call the cops on you while you are mowing the lawn, and they gave you a sobriety test 🤣  You get dragged off into the patrol car, cuffs and all, and tossed in a jail cell.  Our country is crezzie!

Who's in for cruising around Belli on a tractor with a six pack? 🤣 

  • Like 3
  • Thanks 1
Link to comment
Share on other sites

Peeve: An overseas work colleague told me, unbidden, their abhorrent points of view today.  Things that thankfully we relegate to "politics" here in the US. 

Now I have to pretend the conversation never happened in the future, and perhaps start using a line like, "Sorry, I do not want to discuss that with you" next time.

  • Like 1
  • Sad 2
Link to comment
Share on other sites

No-mod drives me the f%**^%k crazy. I had a whitish ghostly layer obscuring my BoM eye shadow and couldn't figure how to get rid of them. After 2 weeks of trying to turn off materials, gloss, and clicking all the buttons in the makeup section on the HUD, I finally found the culprit hiding in the eyebrow HD layer. If only we were allowed to manually modify the stuff we brought, I could've peeled off the unnecessary onion layers and solved my problem a lot sooner than 2 weeks.

 

  • Like 5
Link to comment
Share on other sites

   Peeve: wanting tea, being too tired to go make tea, being so tired I'd forgotten I already made a pot of tea that's been sitting right beside me for half an hour. 

   I'm so not a 'morning person'. Mornings should be illegal. 

  • Like 5
Link to comment
Share on other sites

1 hour ago, Orwar said:

   Peeve: wanting tea, being too tired to go make tea, being so tired I'd forgotten I already made a pot of tea that's been sitting right beside me for half an hour. 

   I'm so not a 'morning person'. Mornings should be illegal. 

Yes! Recently, I had to leave earlier than I like to, and not just forgot my coffer between making it and it getting cold enough to drink, but already was standing in the hallway, before realising I forgot to put on shoes - luckily, at least, I had my keys on me (but actually have "precautions" in place to not walk out without them in my morning daze).

First hour or two after having to get up too early, I not just forget, but also drop everything -_-

Also, I'm convinced that half of the "everyone can be a morning person", "my productive 5am routine" YouTube crowd get up at 5am once, to take their video, and then go right back to bed, unless they have a job that forces them to get up at an ungodly hour anyway. 

However, I can be an early morning person too, if it's a very early morning that my late night seamlessly faded into 😅 

Pet Peeve: the birdies outside getting really noisy, when you're finally ready to sleep 💤 🐦 🎶 

Edited by InnerCity Elf
  • Like 2
Link to comment
Share on other sites

3 minutes ago, Scylla Rhiadra said:

Peeve: I missed a history debate about the American Revolution and War of 1812, and didn't even get the chance to trundle out my fave Samuel Johnson quote about the former!!!

War of 1812? You could post a pic of you wearing a "Davy Crockett" style "Raccoon Cap" (ok, that's from the 1850's-1860's onwards)..! I don't know what "styles" people were wearing in 1812 to be rebellious / outsiders.

  • Like 1
Link to comment
Share on other sites

2 minutes ago, Love Zhaoying said:

War of 1812? You could post a pic of you wearing a "Davy Crockett" style "Raccoon Cap" (ok, that's from the 1850's-1860's onwards)..! I don't know what "styles" people were wearing in 1812 to be rebellious / outsiders.

There weren't too many rebellious outsiders in the War of 1812, really, with the exception of maybe handful of expatriate Americans who'd settled in Canada because of cheap land, and who tried (unsuccessfully) to work as a sort of 5th column for invading US troops.

It was a War fought almost entirely between the US regular army on the one hand, and a mix of British regulars and Canadian militia on the other. I guess the closest to "rebellious" actually were the Native American warriors of Tecumseh, who fought with the British, and had been dispossessed of their lands by the US.

Peeve: Some Americans (not you, I'm sure, Love) tend to think the War of 1812 was some sort of heroic extension of the American Revolution, when what it was mostly was an opportunistic attempt by the US to seize Canada when British troops were occupied elsewhere in a real  life-and-death struggle against Napoleon.

Quote

I see, as you do, the difficulties & defects we have to encounter in war, and should expect disasters, if we had an enemy on land capable of inflicting them. but the weakness of our enemy there will make our first errors innocent, & the seeds of genius which nature sows with even hand through every age & country, & which need only soil & season to germinate, will develope themselves among our military men. some of them will become prominent, and, seconded by the native energy of our citizens, will soon, I hope, to our force, add the benefits of skill.the acquisition of Canada this year, as far as the neighborhood of Quebec, will be a mere matter of marching.
                                                            Thomas Jefferson to William Duane, August 4, 1812

 

  • Like 1
Link to comment
Share on other sites

21 minutes ago, Scylla Rhiadra said:

Peeve: I missed a history debate about the American Revolution and War of 1812, and didn't even get the chance to trundle out my fave Samuel Johnson quote about the former!!!

All I did was make a joke about the American Revolution, it was not my intention to restart the war and get the thread closed! 😄

 

Edited by Porky Gorky
  • Haha 6
Link to comment
Share on other sites

6 minutes ago, Scylla Rhiadra said:

Peeve: Some Americans (not you, I'm sure, Love) tend to think the War of 1812 was some sort of heroic extension of the American Revolution, when what it was mostly was an opportunistic attempt by the US to seize Canada when British troops were occupied elsewhere in a real  life-and-death struggle against Napoleon.

'Tis merely a footnote in history. Ignore the whole "setting fire to the White House" business.

Canada is SO far away, I forget it's there! 🙂

 

  • Haha 1
  • Sad 1
Link to comment
Share on other sites

Just now, Porky Gorky said:

so suck on that Napoleon!

Peeve: Ick! (Or, "Eww!" if you prefer.)

7 minutes ago, Porky Gorky said:

All I did was make a joke about the American Revolution, it was not my intention to restart the war and get the thread closed! 😄

That's ok, you just can't help it! 🙂

Peeve: Self-control is SO VERY HARD!!!

 

Link to comment
Share on other sites

1 minute ago, Love Zhaoying said:

Ignore the whole "setting fire to the White House" business.

Now I got this song stuck in my head

Perhaps now everyone else does as well! Vengeance is glorious!  

  • Like 1
  • Thanks 1
  • Haha 2
Link to comment
Share on other sites

17 minutes ago, Love Zhaoying said:

'Tis merely a footnote in history. Ignore the whole "setting fire to the White House" business.

Canada is SO far away, I forget it's there! 🙂

 

This kind of thing (and yes I know you're joking!) is a long standing Peeve of Canadians.

Few Americans know that the revolutionary forces attempted to conquer Canada in 1775, under the mistaken assumption that the Quebecois would rise up against the British. But the habitants of Quebec had been promised freedom of religion and their own laws by the British, and were having none of it: the American attack was a disaster.

Most Canadians don't care that much about the burning of the White House (even though it was supposedly in retaliation for the burning of the parliament buildings in York, now Toronto, in 1813), or the Battle of New Orleans, which while an undoubted disaster for the British, took place after the peace treaty had been signed, and had literally no effect on the war or the subsequent peace.

What we care about is that we (British regulars mostly, but also some Canadian militia and native troops) successfully repelled the US attempt to seize Canada.

So there.

Edited by Scylla Rhiadra
  • Like 1
Link to comment
Share on other sites

7 minutes ago, Love Zhaoying said:

..and here I thought you guys were Francophiles!!

TIL!

Peeve: Never assume.

I'm not sure what you mean?

The relationship of Quebec to the other parts of (English) Canada is, to put it mildly, complicated!

Link to comment
Share on other sites

1 hour ago, Porky Gorky said:

All I did was make a joke about the American Revolution, it was not my intention to restart the war and get the thread closed! 😄

Poor Porky Gorky!  It would have been fun to banter about a bit, teasing about our differences...as many do in the saner parts of our worlds..

But noooooo..here it ends up being a serious WAR all over again!

Peeve: Why are some places like that...can't have fun and a mini war erupts? What makes the difference?

* Answer came -- some really cannot tolerate that somebody is different from themselves and feel compelled to assert superiority.

Edited by Luna Bliss
  • Like 1
Link to comment
Share on other sites

Please sign in to comment

You will be able to leave a comment after signing in



Sign In Now
×
×
  • Create New...