Author Topic: NESS Spawn Group Alteration  (Read 305 times)

Legacy_henesua

  • Hero Member
  • *****
  • Posts: 6519
  • Karma: +0/-0
NESS Spawn Group Alteration
« on: March 10, 2014, 01:28:51 pm »


               

I did not like the NESS Spawn Group configuration script (spawn_cfg_group) so I rewrote it.


 


I posted my version to PasteBin in case anyone is interested.


 


The main difference is that lists of creatures in a group are created in a more straightforward manner which makes group handling easier for non-scripters, but you now have to initialize the group lists in OnModuleLoad.


 


Here's the initialization function where people can put their lists:



// user configurable: Add groups and their creatures inside this function [FILE: spawn_cfg_group]
// this function needs to be called in the module's onload event.
void NESSInitializeGroups()
{
// for each creature you add to a group use the following line:
//  NESSAddCreatureToGroup("CreatureResRef", "GroupName", #);
// more explanation:
// "CreatureResRef" - is the resource reference (resref) of the creature blueprint to spawn.
//                    Put the resref between quotes.
// "GroupName"      - is the name you will use to identify this list of creatures.
//                    Put the name between quotes.
// #                - is an integer value of the creature's challenge rating.
//                    CRs of 1 and below should be listed as 1. Do not use fractions or decimal points. Positive, whole numbers only.


    // gobsnboss
    NESSAddCreatureToGroup("nw_goblina", "gobsnboss", 1);
    NESSAddCreatureToGroup("nw_goblinb", "gobsnboss", 1);
    NESSAddCreatureToGroup("nw_gobwiza", "gobsnboss", 3);
    NESSAddCreatureToGroup("nw_gobwizb", "gobsnboss", 3);
    NESSAddCreatureToGroup("nw_gobchiefb", "gobsnboss", 3);
    NESSAddCreatureToGroup("nw_gobchiefa", "gobsnboss", 4);


}

As you can see I maintained and expanded one of the NESS example groups, gobsnboss, to show what can be done with this change.


 


 


Beyond just the interest of sharing, I posted this to get feedback as I am putting the lists of creatures into pseudo arrays of strings stored on the module. Is there a problem doing this?


 


Ultimately I plan to hook this to NWNX so that the lists can be stored and called from a database, while data entry is removed from the script itself and moved to a web application. But in the implementation I posted is there a problem with storing a ton of lists on the module object? Would it be better to make a seperate object for each group type and store the lists on these?


 


For those to lazy to bother with pastebin here is the script in total:



//
// Spawn Groups
//
//
// nChildrenSpawned
// : Number of Total Children ever Spawned
//
// nSpawnCount
// : Number of Children currently Alive
//
// nSpawnNumber
// : Number of Children to Maintain at Spawn
//
// nRandomWalk
// : Walking Randomly? TRUE/FALSE
//
// nPlaceable
// : Spawning Placeables? TRUE/FALSE
//
//
//int ParseFlagValue(string sName, string sFlag, int nDigits, int nDefault);
//int ParseSubFlagValue(string sName, string sFlag, int nDigits, string sSubFlag, int nSubDigits, int nDefault);

object GetChildByTag(object oSpawn, string sChildTag);
object GetChildByNumber(object oSpawn, int nChildNum);
object GetSpawnByID(int nSpawnID);
void DeactivateSpawn(object oSpawn);
void DeactivateSpawnsByTag(string sSpawnTag);
void DeactivateAllSpawns();
void DespawnChildren(object oSpawn);
void DespawnChildrenByTag(object oSpawn, string sSpawnTag);

// user configurable: Add groups and their creatures inside this function [FILE: spawn_cfg_group]
// this function needs to be called in the module's onload event.
void NESSInitializeGroups();
// this function is called by NESSInitializeGroups() [FILE: spawn_cfg_group]
// for each creature you add to a group use the following line:
// NESSAddCreatureToGroup("CreatureResRef", "GroupName", #);
// more explanation:
// "CreatureResRef" - is the resource reference (resref) of the creature blueprint to spawn.
//                    Put the resref between quotes.
// "GroupName"      - is the name you will use to identify this list of creatures.
//                    Put the name between quotes.
// #                - is an integer value of the creature's challenge rating.
//                    CRs of 1 and below should be listed as 1. Do not use fractions or decimal points. Positive, whole numbers only.
void NESSAddCreatureToGroup(string CreatureResRef, string GroupName, int CreatureChallengeRating);
// this function is called by SpawnGroup() [FILE: spawn_cfg_group]
// the return value is a resref for a creature
string NESSGetCreatureFromGroupList(object oSpawn, string sGroupName, int nCR=0);



// user configurable: Add groups and their creatures inside this function [FILE: spawn_cfg_group]
// this function needs to be called in the module's onload event.
void NESSInitializeGroups()
{
// for each creature you add to a group use the following line:
//  NESSAddCreatureToGroup("CreatureResRef", "GroupName", #);
// more explanation:
// "CreatureResRef" - is the resource reference (resref) of the creature blueprint to spawn.
//                    Put the resref between quotes.
// "GroupName"      - is the name you will use to identify this list of creatures.
//                    Put the name between quotes.
// #                - is an integer value of the creature's challenge rating.
//                    CRs of 1 and below should be listed as 1. Do not use fractions or decimal points. Positive, whole numbers only.

    // gobsnboss
    NESSAddCreatureToGroup("nw_goblina", "gobsnboss", 1);
    NESSAddCreatureToGroup("nw_goblinb", "gobsnboss", 1);
    NESSAddCreatureToGroup("nw_gobwiza", "gobsnboss", 3);
    NESSAddCreatureToGroup("nw_gobwizb", "gobsnboss", 3);
    NESSAddCreatureToGroup("nw_gobchiefb", "gobsnboss", 3);
    NESSAddCreatureToGroup("nw_gobchiefa", "gobsnboss", 4);

}


void NESSAddCreatureToGroup(string CreatureResRef, string GroupName, int CreatureChallengeRating)
{
    object oMod         = GetModule();

    // sanity checks
    if(CreatureResRef=="" || GroupName=="") return;
    if(CreatureChallengeRating<1) CreatureChallengeRating = 1;


    // add creature to master list
    int nIndex  =   GetLocalInt(oMod,"NESS_COUNT_"+GroupName)+1;
                    SetLocalInt(oMod,"NESS_COUNT_"+GroupName,nIndex);

    SetLocalString(oMod, "NESS_"+GroupName+"_"+IntToString(nIndex),CreatureResRef);

    if(CreatureChallengeRating>0)
    {
        // add creature to CR list
        string CRLabel  = IntToString(CreatureChallengeRating);
        int nCRIndex    = GetLocalInt(oMod, "NESS_COUNT_CR"+CRLabel+"_"+GroupName)+1;
                          SetLocalInt(oMod, "NESS_COUNT_CR"+CRLabel+"_"+GroupName, nCRIndex);

        SetLocalString(oMod, "NESS_"+GroupName+"_"+IntToString(CreatureChallengeRating)+"_"+IntToString(nCRIndex),CreatureResRef);
    }
}



string NESSGetCreatureFromGroupList(object oSpawn, string sGroupName, int nCR=0)
{
    string sResRef;

    // set to TRUE below if you want to prevent a spawn
    int bNoSpawn;
    if(sGroupName!="")
    {
        // USER CONFIGURABLE ---------------------------------------------------
        if (sGroupName == "gobsnboss")
        {
            if(GetLocalInt(oSpawn, "IsBossSpawned"))
            {
                // Find the Boss
                object oBoss = GetChildByTag(oSpawn, GetLocalString(oSpawn,"BossTag"));

                // Check if Boss is Alive
                if(oBoss==OBJECT_INVALID || GetIsDead(oBoss))
                {
                    // He's dead, Deactivate Camp!
                    SetLocalInt(oSpawn, "SpawnDeactivated", TRUE);
                    bNoSpawn    = TRUE;
                }
                else
                {


                }
            }
            else
            {
                // No Boss, so Let's Spawn Him
                if(!nCR || nCR>=11)
                {
                    sResRef = "nw_goblinboss";
                    if(nCR)
                        SetLocalInt(oSpawn, "NESS_LAST_SPAWN_CR",11);
                }
                else if(nCR>=4)
                {
                    sResRef = "nw_gobchiefa";
                    SetLocalInt(oSpawn, "NESS_LAST_SPAWN_CR",4);
                }
                else
                {
                    sResRef = "nw_gobchiefb";
                    SetLocalInt(oSpawn, "NESS_LAST_SPAWN_CR",3);
                }

                SetLocalString(oSpawn,"BossTag", GetStringUpperCase(sResRef));
                SetLocalInt(oSpawn, "IsBossSpawned", TRUE);
            }
        }
        // END USER CONFIGURABLE -----------------------------------------------

        // we have yet to receive a creature resref so lets randomly select one from our list...
        if(sResRef=="" && !bNoSpawn)
        {
            object oMod = GetModule();
            string sGroupLabel; int nListCount;
            // scaled encounters....
            if(nCR)
            {
                    nListCount  = GetLocalInt(oMod, "NESS_COUNT_CR"+IntToString(nCR)+"_"+sGroupName);
                while( !nListCount && nCR>0 )
                    nListCount  = GetLocalInt(oMod, "NESS_COUNT_CR"+IntToString(--nCR)+"_"+sGroupName);

                if(nListCount)
                {
                    sGroupLabel = "NESS_"+sGroupName+"_"+IntToString(nCR)+"_";
                    SetLocalInt(oSpawn, "NESS_LAST_SPAWN_CR",nCR);
                }
                else
                    DeleteLocalInt(oSpawn, "NESS_LAST_SPAWN_CR");
            }
            // randomly from the entire list
            else
            {
                    nListCount  = GetLocalInt(oMod, "NESS_COUNT_"+sGroupName);
                if(nListCount)
                    sGroupLabel = "NESS_"+sGroupName+"_";
            }

            if(sGroupLabel!="")
            {
                // This line ensures the Random function behaves randomly.
                int iRandomize = Random(Random(GetTimeMillisecond()));

                sResRef = GetLocalString(oMod, sGroupLabel+IntToString(Random(nListCount)+1));
            }
        }

    }

    return sResRef;
}

// Convert a given EL equivalent and its encounter level,
// return the corresponding CR
float ConvertELEquivToCR(float fEquiv, float fEncounterLevel)
{
    if (fEquiv == 0.0)
        return 0.0;
    /*
    float fCR, fEquivSq, fTemp;
    fEquivSq = fEquiv * fEquiv;
    fTemp = log(fEquivSq);
    fTemp /= log(2.0);
    fCR = fEncounterLevel + fTemp;
    */

    return fEncounterLevel + (log(fEquiv * fEquiv)/log(2.0));
}

// Convert a given CR to its encounter level equivalent per DMG page 101.
float ConvertCRToELEquiv(float fCR, float fEncounterLevel)
{
    if(     fCR>fEncounterLevel
        ||  fCR<1.0
      )
        return 1.0;
/*
    float fEquiv, fExponent, fDenom;

    fExponent   = (fEncounterLevel - fCR)*0.5;
    fDenom      = pow(2.0, fExponent);
    fEquiv      =  1.0 / fDenom;
*/
    return (1.0 / pow(2.0, ((fEncounterLevel - fCR)*0.5) ) );
}

// - [File: spawn_cfg_group]
string SpawnGroup(object oSpawn, string sTemplate);
string SpawnGroup(object oSpawn, string sTemplate)
{
    // Initialize
    string sRetTemplate;
    int nSpawnNumber    = GetLocalInt(oSpawn, "f_SpawnNumber");

    // BEGIN SCALING -----------------------------------------------------------
    if (GetStringLeft(sTemplate, 7) == "scaled_")
    {
        float fEncounterLevel;
        string sGroupType = GetStringRight(sTemplate, GetStringLength(sTemplate) - 7);

        // First Time in for this encounter?
        if (!GetLocalInt(oSpawn, "ScaledInProgress"))
        {

            // First time in - find the party level
            int nTotalPCs = 0;
            int nTotalPCLevel = 0;

            float fTriggerRadius    = GetLocalFloat(oSpawn, "f_SpawnTrigger");
            if(fTriggerRadius>0.0)
            {
                location lLoc   = GetLocation(oSpawn);
                object oPC  = GetFirstObjectInShape(SHAPE_SPHERE,fTriggerRadius,lLoc);
                while (oPC != OBJECT_INVALID)
                {
                    if(!GetIsDM(oPC) && GetIsPC(oPC))
                    {
                        nTotalPCs++;
                        nTotalPCLevel = nTotalPCLevel + GetHitDice(oPC);
                    }
                    oPC = GetNextObjectInShape(SHAPE_SPHERE,fTriggerRadius,lLoc);
                }
            }
            else
            {
                object oArea = GetArea(OBJECT_SELF);
                object oPC = GetFirstPC();
                while (oPC != OBJECT_INVALID)
                {
                    if(     !GetIsDM(oPC)
                        &&  GetArea(oPC)==oArea
                      )
                    {
                        nTotalPCs++;
                        nTotalPCLevel = nTotalPCLevel + GetHitDice(oPC);
                    }
                    oPC = GetNextPC();
                }
            }

            if (nTotalPCs == 0)
                fEncounterLevel = 0.0;
            else
            {
                float fPCs      = IntToFloat(nTotalPCs);
                fEncounterLevel = (IntToFloat(nTotalPCLevel)/ fPCs)
                                  // *(fPCs/4.0)
                                    ;
            }

            // Save this for subsequent calls
            SetLocalFloat(oSpawn, "ScaledEncounterLevel", fEncounterLevel);

            // We're done when the CRs chosen add up to the desired encounter level
            SetLocalInt(oSpawn, "ScaledCallCount", 0);
            SetLocalInt(oSpawn, "ScaledInProgress", TRUE);
        }

            fEncounterLevel     = GetLocalFloat(oSpawn, "ScaledEncounterLevel");
        int nScaledCallCount    = GetLocalInt(oSpawn, "ScaledCallCount");

        // For simplicity, I'm not supporting creatures with CR < 1.0)
        if (fEncounterLevel < 1.0)
            // We're done... No creatures have CR low enough to add to this encounter
            sRetTemplate = "";
        else
        {
            int nCR;
            if(nScaledCallCount)
            // randomly choose a CR at or below the remaining (uncovered) encounter level
                nCR = Random(FloatToInt(fEncounterLevel)) + 1;
            else
                // on the first call use the largest possible CR
                nCR = FloatToInt(fEncounterLevel);
            sRetTemplate = NESSGetCreatureFromGroupList(oSpawn, sGroupType, nCR);

            // Calculate remaining
            nCR = GetLocalInt(oSpawn, "NESS_LAST_SPAWN_CR");

            float fElRemaining  = 1.0 - ConvertCRToELEquiv(IntToFloat(nCR), fEncounterLevel);

            fEncounterLevel = ConvertELEquivToCR(fElRemaining, fEncounterLevel);
            SetLocalFloat(oSpawn, "ScaledEncounterLevel", fEncounterLevel);
        }

        SetLocalInt(oSpawn, "ScaledCallCount", ++nScaledCallCount);
        if (nScaledCallCount >= nSpawnNumber)
            // reset...
            SetLocalInt(oSpawn, "ScaledInProgress", FALSE);
    }
    // END SCALING -------------------------------------------------------------
    else
    {
        sRetTemplate = NESSGetCreatureFromGroupList(oSpawn, sTemplate);
    }

    return sRetTemplate;
}


               
               

               
            

Legacy_Shadooow

  • Hero Member
  • *****
  • Posts: 7698
  • Karma: +0/-0
NESS Spawn Group Alteration
« Reply #1 on: March 10, 2014, 10:04:33 pm »


               

 


Ultimately I plan to hook this to NWNX so that the lists can be stored and called from a database, while data entry is removed from the script itself and moved to a web application. But in the implementation I posted is there a problem with storing a ton of lists on the module object? Would it be better to make a seperate object for each group type and store the lists on these?



Storing somewhere else might be bit more efficient if you are storing other things on module such as craft recipes etc. But not recognizeable anyway. And I don't think it has a sense to store each group on individual object, one object for NESS will be enough if you want to separate it.



               
               

               
            

Legacy_henesua

  • Hero Member
  • *****
  • Posts: 6519
  • Karma: +0/-0
NESS Spawn Group Alteration
« Reply #2 on: March 10, 2014, 11:19:46 pm »


               

The reason I ask is that I typically create objects to store data on, but I was wondering if its worth it or if I might as well just store a bunch of stings on the module. Each creature in a list uses two strings. So a list of ten creatures would generate 20 entries. Ultimately we are talking about hundreds if not more than a thousand local variables before I get around to pushing this to the database.


 


  • So how many local variables stored on the module is a problem?

  • Is there anything particular about using the module to store this data that would affect performance? Or would all objects be more or less the same in that regard? (I get the sense that there is nothing special about the module as a local variable collector except for the fact that it is so easy to do and so there is a tendency to "over load" it with local variables.)

  • Is it just an issue of RAM? A limit in terms of number of locals you can store? A problem for scripts to set and retrieve a local variable when you have too many?