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;
}