Connecting areas randomly

I have a set of areas I would like to connect randomly so that the order of the “floors” is different for each playthrough (like shuffling a deck of cards). Since I can’t use arrays, I thought I could do this, but I feel like I am overthinking this or making it more complex than it needs to be. Can someone have a look and maybe give me some tips?

Thanks!
Matt

onModuleLoad script:
void main()
{
int nFloors = 3; //number of desired floors in dungeon
int nTotalAreas; //total areas in module
object oPC = GetEnteringObject();

//a string made of numbers from 1 to nTotalAreas
string sAreaList;

//loop through all the areas (except area 0) in the module and create a string.
//Since each area uses the same tage format ("areaXXX", with "XXX" representing
// a number between 001 and 999) we only need the number
object oArea = GetFirstArea();
int i = 0;
while (oArea != OBJECT_INVALID)
    {
    // we can add filters later so that only dungeon areas are considered by
    // changing the tag format and checking substrings
     if (i == 0)
        {
         oArea = GetNextArea();
         i++;
        }
     else
        {
        sAreaList = sAreaList + IntToString(i) + ",";
        SendMessageToPC(oPC, "sAreaList is now " + sAreaList);
        oArea = GetNextArea();
        i++;
        }
    }
// save the string as a module variable
SetLocalString(OBJECT_SELF, "sAreaList", sAreaList);
SendMessageToPC(oPC, "sAreaList is now " + GetLocalString(OBJECT_SELF, "sAreaList"));
}

and then in the doors onTransition event:

#include "x3_inc_string"

void main()
{
object oPC = GetClickingObject();
string sDestination; //this door's destination
string sReturn; //for the return trip from sDestination
object oTarget;
object oReturn;
string sThisFloor = GetTag(GetArea(OBJECT_SELF));
SendMessageToPC(oPC, "sThisFloor is " + sThisFloor);
string sFloorNum = GetStringRight(sThisFloor, 3);
SendMessageToPC(oPC, "sFloorNum is " + sFloorNum);

// make sure this only runs once
if (GetLocalInt(OBJECT_SELF, "nInitDoor") == 1)
    {
    string sMsg = IntToString(GetLocalInt(OBJECT_SELF, "nInitDoor"));
    SendMessageToPC(oPC, "The script has run once already! nInitDoor is " + sMsg);
    oTarget = GetWaypointByTag(GetLocalString(OBJECT_SELF, "sDest"));
    AssignCommand(oPC, JumpToObject(oTarget));
    return;
    }
    else
    {
    //this should only allow valid choices based on the remaining numbers
    string sTempString = GetLocalString(GetModule(), "sAreaList");
    int j = Random(GetStringLength(sTempString)) + 1;
    string sFloor = IntToString(j);
    SendMessageToPC(oPC, "sFloor is " + sFloor);
    sDestination = "wpAreaEntry" + "00" + sFloor;
    SendMessageToPC(oPC, "sDestination is " + sDestination);

    // set up sReturn
    sReturn = "drAreaEntry" + "00" + sFloor;
    SendMessageToPC(oPC, "sReturn is " + sReturn);
    oReturn =  GetObjectByTag(sReturn);


    //remove nRandom from sAreaList

    SendMessageToPC(oPC, "sTempString is " + sTempString);
    int nRight =  GetStringLength(sTempString);
    SendMessageToPC(oPC, "nRight is " + IntToString(nRight));

    int nLoc = FindSubString(sTempString, sFloor);
    SendMessageToPC(oPC, "nLoc is " + IntToString(nLoc));
    string sLeftString = GetStringLeft(sTempString, nLoc);
    SendMessageToPC(oPC, "sLeftString is " + sLeftString);

    string sRightString = GetStringRight(sTempString, nRight - (nLoc + 1));
    SendMessageToPC(oPC, "sRightString is " + sRightString);
    sTempString = sLeftString + sRightString;
    SendMessageToPC(oPC, sTempString);
    SendMessageToPC(oPC, "sTempString is now " + sTempString);
    //update the module variable
    SetLocalString(GetModule(), "sAreaList", sTempString);
    SendMessageToPC(oPC, "sAreaList is now " + GetLocalString(GetModule(), "sAreaList"));
    // set local int so this won't run twice
    SetLocalInt(OBJECT_SELF, "nInitDoor", 1);

    //set up transition
    SetLocalString(OBJECT_SELF, "sDest", sDestination);
    SendMessageToPC(oPC, "sDestination is " + sDestination);
    oTarget = GetWaypointByTag(GetLocalString(OBJECT_SELF, "sDest"));

    //set up return trip
    string sWPName = "wpAreaExit" + sFloorNum;
    SendMessageToPC(oPC, "sWPName is " + sWPName);
    SetLocalString(oReturn, "sDest", sWPName);
    SetLocalInt(oReturn,"nInitDoor", 1);
    //move the player
    AssignCommand(oPC, JumpToObject(oTarget));
    }

}

i had a bit of fun with this …

string parsing …

the other stuff (set destination, set return, set wp-dest …) I tried not to touch,

// 'ubai_mod_load'
/*
    OnModuleLoad script.
*/

void main()
{
    object oPC = GetEnteringObject();

    // a string made of numbers from 1 to nTotalAreas
    // sAreaList is in format: "XXX,YYY,ZZZ,..."
    string sAreaList = "";
    string sArea;

    // loop through all the areas (except area 0) in the module and create a string.
    // Since each area uses the same tag format ("areaXXX", with "XXX" representing
    // a number between 001 and 999) we only need the number

    int i = 0;
    object oArea = GetFirstArea();
    while (oArea != OBJECT_INVALID)
    {
        if (i != 0) // skip the first area (suggest comparing against the area's tag-string instead of relying on "first area")
        {
            sArea = IntToString(i);
            while (GetStringLength(sArea) != 3)
                sArea = "0" + sArea;

            // we can add filters later so that only dungeon areas are considered by
            // changing the tag format and checking substrings
            sAreaList += sArea + ",";
            SendMessageToPC(oPC, "sAreaList is now " + sAreaList);
        }

        ++i;
        oArea = GetNextArea();
    }

    // save the string as a module variable
    SetLocalString(OBJECT_SELF, "sAreaList", sAreaList);
    SendMessageToPC(oPC, "sAreaList is now " + GetLocalString(OBJECT_SELF, "sAreaList"));
}
// 'ubai_door_used'
/*
    transition script of a door.
*/

void main()
{
    object oPC = GetClickingObject();
    object oTarget;

    string sThisFloor = GetTag(GetArea(OBJECT_SELF));
    SendMessageToPC(oPC, "sThisFloor is " + sThisFloor);

    string sFloorNum = GetStringRight(sThisFloor, 3);
    SendMessageToPC(oPC, "sFloorNum is " + sFloorNum);

    // make sure this only runs once
    if (!GetLocalInt(OBJECT_SELF, "nInitDoor"))
    {
        // set local int so this won't run twice
        SetLocalInt(OBJECT_SELF, "nInitDoor", TRUE);

        // this should only allow valid choices based on the remaining numbers

        string sAreaList = GetLocalString(GetModule(), "sAreaList");

        // pick a floor from sAreaList
        int iLen = GetStringLength(sAreaList);
        int iPos = Random(iLen) / 4 * 4;                    // 4 chars in each entry: eg. "XXX," (truncate and multiply to get the start-pos of any entry)
        string sFloor = GetSubString(sAreaList, iPos, 3);   // but only the first 3 chars are valid.
        SendMessageToPC(oPC, "sFloor is " + sFloor);

        string sDestination = "wpAreaEntry" + sFloor;
        SendMessageToPC(oPC, "sDestination is " + sDestination);

        // set up sReturn
        string sReturn = "drAreaEntry" + sFloor; // for the return trip from sDestination
        SendMessageToPC(oPC, "sReturn is " + sReturn);

        object oReturn = GetObjectByTag(sReturn);

        // remove the floor that was just taken from sAreaList
        SendMessageToPC(oPC, "sAreaList is " + sAreaList);

        iPos = FindSubString(sAreaList, sFloor);

        string sLeft = GetStringLeft(sAreaList, iPos);
        SendMessageToPC(oPC, "sLeft is " + sLeft);
        string sRight = GetStringRight(sAreaList, iLen - iPos - 4);
        SendMessageToPC(oPC, "sRight is " + sRight);

        sAreaList = sLeft + sRight;

        // update the module variable
        SetLocalString(GetModule(), "sAreaList", sAreaList);
        SendMessageToPC(oPC, "sAreaList is now " + GetLocalString(GetModule(), "sAreaList"));

        // set up transition
        SetLocalString(OBJECT_SELF, "sDest", sDestination);
        SendMessageToPC(oPC, "sDestination is " + sDestination);

        oTarget = GetWaypointByTag(GetLocalString(OBJECT_SELF, "sDest"));

        // set up return trip
        string sWPName = "wpAreaExit" + sFloorNum;
        SendMessageToPC(oPC, "sWPName is " + sWPName);

        SetLocalString(oReturn, "sDest", sWPName);
        SetLocalInt(oReturn, "nInitDoor", TRUE); // prevent the return door from trying to set up a different transition.
    }
    else
    {
        SendMessageToPC(oPC, "The script has run once already! nInitDoor is "
                + IntToString(GetLocalInt(OBJECT_SELF, "nInitDoor")));

        oTarget = GetWaypointByTag(GetLocalString(OBJECT_SELF, "sDest"));
    }

    // move the player
    AssignCommand(oPC, JumpToObject(oTarget));
}

* just passing through *

while (GetStringLength(sArea) != 3)
   sArea = "0" + sArea;

Shouldn’t it be replaced with something like this:

switch(GetStringLength(sArea))
{
	case 1:
		sArea = "00" + sArea;
		break;
	case 2:
		sArea = "0" + sArea;
		break;
	default:
		break;
}

?

hypothetically ->

string sArea = "";

if (GetStringLength(sArea) != 3) // true
    sArea = "0";

if (GetStringLength("0") != 3) // true
    sArea = "00";

if (GetStringLength("00") != 3) // true
    sArea = "000";

if (GetStringLength("000") != 3) // false
{
    // break
}

if (sArea == "000")
    // true

The loop is just a series of if-statements that pad the left side with “0” to a total of 3 characters, given an input of 2 or less characters. There’s a bunch of ways to do it …

but it will explode if the module has 1000+ areas

1 Like

Ah, I see, thanks for the explanation. A beatiful trick :slight_smile:

1 Like

i’m not arguing … but it’s “legit” code. This is closer to a “trick” ->

int iPos = Random(iLen) / 4 * 4;

To me, a “trick” is something that relies on some fault or limitation of the underlying system in order to work. I’d say a decent programmer doesn’t take kindly to “clever tricks” … but i understand, Aq

tks

1 Like

Thanks very much! It’s OK if it blows up after 1000 areas, I wasn’t planning on going that high. :wink:

Stuff like this makes me wish I was better at math.

Thanks again,
Matt

it’s a really cool idea … at first I thought it was trans to a random floor every time (which would be easier to script tbh, so kudos) But i guess that’s a different story …

I’m glad you like the idea! I actually came up with a different way of doing this while mowing the lawn, I think it may be more flexible in the long run.

First, I place a chest in an unreachable area and use that as my “array”. Each time a door is used for the first time it loops through the chest’s inventory, and grab a random item named after an area, then sets up its transitions. After that, it destroys the item so that only unused areas remain.

I have most of it figured out, but for some reason I can’t get it to set the return door properly. Knowing me I’m probably overlooking something simple. Here is the code I have so far in case anyone is curious:

/*
this goes in your starting area's on enter event.
Use a chest (or chests) and some items to shuffle the order of the floors in a
module. You could have one chest for dungeons, one for forests, etc, depending
on how you name the chests and the areas.
*/

void main()
{
object oPC = GetEnteringObject();
//this should only run once
if (GetLocalInt(OBJECT_SELF, "nInitAreas") == 1)
    {
    SendMessageToPC(oPC, "are list ran already!" );
    return;
    }
else
    {
    //these two variables are for areas excluded from the shuffle, like the start or
    //floor shuffler area
    string sStart = "Area000";
    string sShuffler = "FloorPicker";
    string sChest = "plcFloorShuffler";

    //loop through all the areas in the module and create an item for each area in
    //the chest. The item tag is itAreaItem
    object oArea = GetFirstArea();

    while (oArea != OBJECT_INVALID)
        {
        // we can add filters later so that only dungeon areas are considered by
        // changing the tag format and checking substrings
         if (oArea == GetObjectByTag(sStart) || oArea == GetObjectByTag(sShuffler))
            {
            oArea = GetNextArea();
            SendMessageToPC(oPC, "Hopefully you see this twice." );
            }
        else
            {
            string sTag = GetTag(oArea);
            SendMessageToPC(oPC, "sTag is now " + sTag );
            object oChest = GetObjectByTag(sChest);
            object oItem = CreateItemOnObject("areaitem", oChest);
            SetName(oItem, sTag);
            //see if item is created
            string sNameCheck = GetName(oItem);
            SendMessageToPC(oPC, "sNameCheck is now " + sNameCheck );
            oArea = GetNextArea();
            }
        }
               //make sure this only runs once
        SetLocalInt(OBJECT_SELF, "nInitAreas", 1);
    }
}

This goes on the door’s onUsed event:

//function declarations
int ChestCount(string sChest, object oPC);
void SetFloor(string sChest, int nCount, object oPC, object oDoor);
void SetReturn(string sFloor, object oDoor, object oPC );

void main()
{
object oPC = GetClickingObject();
string sChest = "plcFloorShuffler";

// make sure this only runs once
if (GetLocalInt(OBJECT_SELF, "nInitDoor") == 1)
    {
    string sMsg = IntToString(GetLocalInt(OBJECT_SELF, "nInitDoor"));
    SendMessageToPC(oPC, "The script has run once already! nInitDoor is " + sMsg);
    object oTarget = GetWaypointByTag(GetLocalString(OBJECT_SELF, "sDest"));
    AssignCommand(oPC, JumpToObject(oTarget));
    return;
    }
else
    {
    int nCount = ChestCount(sChest, oPC);
    SendMessageToPC(oPC, "nCount is " + IntToString(nCount));
    object oDoor = OBJECT_SELF;
    SetFloor(sChest, nCount, oPC, oDoor);
    }
}

int ChestCount(string sChest, object oPC)
{
//choose a random item from the inventory of the shuffling chest. Set
//the destination of the door to the name of the item
object oChest = GetObjectByTag(sChest);

//get an up to date count of the items in the chest
object oItem = GetFirstItemInInventory(oChest);
int nCount = 0;
while (oItem != OBJECT_INVALID)
    {
    nCount++;
    oItem = GetNextItemInInventory(oChest);
    }
return nCount;
}

void SetFloor(string sChest, int nCount, object oPC, object oDoor)
{
object oChest = GetObjectByTag(sChest);
object oItem = GetFirstItemInInventory(oChest);
int nChoice = Random(nCount);
    int i;
    for ( i = 1; i <= nCount; i++)
        {
        if (i != nCount)
            {
            GetNextItemInInventory(oChest);
            SendMessageToPC(oPC, "i is " + IntToString(i));
            }
        else
            {
            //set sDest on the target door
            string sDestination = GetName(oItem);
            SendMessageToPC(oPC, "sDestination (oItem) is " + sDestination);
            //grab the last three characters
            string sTemp = GetStringRight(sDestination, 3);
            SendMessageToPC(oPC, "sTemp is " + sTemp);
            //get the door for the return trip
            string sFloor = GetTag(GetArea(oPC));
            //we need the destination door to set up the return trip
            string sNewDoor = "drAreaEntry" + sTemp;
            object oNewDoor = GetObjectByTag(sNewDoor);
            SetReturn(sFloor, oNewDoor, oPC);
            sDestination = "wpAreaEntry" + sTemp;
            SendMessageToPC(oPC, "sDestination is " + sDestination);
            SetLocalString(oDoor, "sDest", sDestination);
            SendMessageToPC(oPC, "i is " + IntToString(i));
            // set local int so this won't run twice
            SetLocalInt(oDoor, "nInitDoor", 1);
            //delete the object
            DestroyObject(oItem, 0.5f);
            object oTarget = GetWaypointByTag(GetLocalString(oDoor, "sDest"));
            AssignCommand(oPC, JumpToObject(oTarget));
            }
        }
}

void SetReturn(string sFloor, object oDoor, object oPC )
{
SendMessageToPC(oPC, "sFloor is " + sFloor);
string sFloorNum = GetStringRight(sFloor, 3);
SendMessageToPC(oPC, "sFloorNum is " + sFloorNum);
string sReturn = "drAreaExit" + sFloorNum;
SendMessageToPC(oPC, "sReturn is " + sReturn);
string sWPName = "wpAreaExit" + sFloorNum;
SendMessageToPC(oPC, "sWPName is " + sWPName);
object oReturn = GetObjectByTag(sReturn);
SetLocalString(oReturn, "sDest", sWPName);
SetLocalInt(oReturn,"nInitDoor", 1);
}

why is there 2 different tag prefixes for doors (“drAreaExit”/“drAreaEntry”) and destination-wps (“wpAreaExit”/“wpAreaEntry”)

It seems to me, in a basic setup, 1 door connects to 1 wp and the other door connects to the other wp. And that the system relies on the prefixes being the same …

I kind of figured that the exit of each floor would connect to the entry of the next, just to keep it clear in my head. I can’t say it make things much clearer, but I haven’t thought of anything better yet. :stuck_out_tongue:

well that could be why there are troubles getting the return-door, your script could be assuming tag-prefix A but the door has tag-prefix B.

I didn’t look (my eyes!), i just rewrote it:

// 'ubai_door_used'
/*
    transition script of a door.
*/

const string sDESTVAR = "sDest";
const string sDOORTAG = "drAreaExit";
const string sDESTTAG = "wpAreaEntry";
const string VAR_DONE = "nInitDoor";

object PickChestItem(string sChest);


// OBJECT_SELF is this door.
void main()
{
    object oDoorSrc = OBJECT_SELF;
    object oTarget;

    if (!GetLocalInt(oDoorSrc, VAR_DONE))
    {
        // set the dest-wp of this door to a random door ->
        SetLocalInt(oDoorSrc, VAR_DONE, TRUE);

        object oItem = PickChestItem("plcFloorShuffler");
        string sFloorId = GetStringRight(GetName(oItem), 3);

        string sDest = sDESTTAG + sFloorId;
        SetLocalString(oDoorSrc, sDESTVAR, sDest);
        oTarget = GetWaypointByTag(sDest);

        DestroyObject(oItem); // can't pick that again.


        // set the dest-wp of the other door to this door ->
        sFloorId = GetStringRight(GetTag(GetArea(oDoorSrc)), 3);

        object oDoorDst = GetObjectByTag(sDOORTAG + sFloorId);
        SetLocalString(oDoorDst, sDESTVAR, sDESTTAG + sFloorId);

        SetLocalInt(oDoorDst, VAR_DONE, TRUE); // the other door is init'd also
    }
    else
        oTarget = GetWaypointByTag(GetLocalString(oDoorSrc, sDESTVAR));

    AssignCommand(GetClickingObject(), JumpToObject(oTarget));
}

// Picks an item from the inventory of an object w/ Tag 'sChest'.
// The items are weighted equally. Returns OBJECT_INVALID if 'sChest' is not a
// valid container or its inventory is empty.
object PickChestItem(string sChest)
{
    object oPick = OBJECT_INVALID;

    object oChest = GetObjectByTag(sChest);
    if (GetIsObjectValid(oChest) && GetHasInventory(oChest))
    {
        int iCount = 1;

        object oItem = GetFirstItemInInventory(oChest);
        while (GetIsObjectValid(oItem))
        {
            // https://stackoverflow.com/questions/2395350/select-a-random-item-without-knowing-the-total-number-of-items
            if (!Random(iCount++))
                oPick = oItem;

            oItem = GetNextItemInInventory(oChest);
        }
    }
    return oPick;
}

yes it requires a consistent tagging scheme: doors are exits, waypoints are entries

Ps. no safeties.

Thanks! I’m sorry if my code is sloppy, I am trying to make it neater as I go. Thanks also for the link! I only had one python class in college ages ago so coding is an uphill battle for me. :stuck_out_tongue:

  • Matt

ps: Hmm, this one is doing what mine did. It links the doors correctly one way, but not the other, so you can’t traverse backward through the floors.

that suggests that the problem is not the scripts, but in the module: maybe typos, wonky tags, etc

edit: maybe the return door(s) don’t have the script slotted in their events yet …

edit2: wait a sec. Does your first area even use the tag-suffix “xxx” protocol?

OK, I made new areas just to be safe and it works fine except for one thing: since there can only by one wpAreaEntry, the player comes back to the same spot in an area regardless of which door they use. That’s not a big enough deal to worry about, though I suppose I could do away with the waypoints and jump them to the door instead, but then they may end up stuck in a door or facing a weird direction.

The other thing is that the last door in the dungeon goes nowhere. I wonder if I should add something so that a door with no valid choices locks itself and speaks a one-liner like “you shall not pass” or something.

Thanks again for all your help! This code is so much more elegant than the monstrosity I was cobbling together, I am learning a lot! :slight_smile:

-Matt

doh. you could suffix the suffix : “a” or “b”

append one or the other (randomly perhaps) to the FloorID before getting the dest-wp/return-door

things just got complicated . . . . . . .

Or append “a” to the tag of the start door and use “a” to begin, then alternate (storing the current value as a local on the module)

What about using the door and storing the object on the door instead of just a string? Wouldn’t that keep that unique door regardless of the tag?

it should …

Hmm, let me give that a try, jumping to the door didn’t look weird, and if this works things won’t get complicated. :slight_smile:

i’m not sure what you meant there: storing the destination as a local_object, or bypassing waypoints and simply using the doors as destinations?

they’re not mutually exclusive … in any case, will let ya tinker w/ “options”