How to make waves of baddies?

How do you make npcs attack in waves rather than all at once ?

Is it some sort of on death counting and spawn thing ?

In the past I’ve just used encounter triggers so as you fight forward more arrive but that’s always been outside or in a one way cave system etc. But this would be in a smaller interior and I want to cause chaos with the player running back and forth and all over the place.

Is it possible without getting over complicated ? Can encounter triggers activate encounter triggers ?

yes.

learn groups

ginc_group

intermediate scripting req’d … add creatures to a group when they spawn (there are various functions for this depending on the situation) and set

GroupOnDeathExecuteCustomScript()

that spawns the next etc wave(s)

kevL_s… Thank you, I’ll look into this but I suspect it’s going to get way too complicated for what I had in mind which was a house under attack with baddies coming in from all different places so that’s a lot of waypoints, groups and creatures to scatter about and keep control of in scripts.

Here’s an alternate method if using encounter triggers:

/*
	Place this on the encounter trigger's OnExhausted event.
	This runs when all of the encounter creatures spawned by the trigger are dead, thereby 
	triggering the encounter trigger again. It will stop when the number of nWaves have
	executed.
*/

void main()
{
	object oPC = GetFirstPC(FALSE);
	object oSelf = OBJECT_SELF;
	int n = GetLocalInt(oSelf, "count");
	int nWaves = 3;		// set to how many waves you want.
	
	if (n == nWaves - 1)
	{
//		SendMessageToPC(oPC, IntToString(nWaves) + " waves completed.");
		return;
	}
	
	n += 1;
	SetLocalInt(oSelf, "count", n);
	SetEncounterActive(TRUE);
	TriggerEncounter(oSelf, oPC, 0, -1.0f);
}
5 Likes

KevL_s… I just looked a ginc groups and ran away ! Time for a rethink and the scrapping of my idea it can only go wrong and isn’t worth it. I’ll make something else happen to the poor PC and Co.

No problem, thanks for the info.

oh i don’ know,

may i ask how you’re spawning them? I mean if you’re going to use waypoints (which is better than encounter-triggers if you can script at all)

and you keep your tags recognizeable, such as

sp_house_01
sp_house_02
etc

and your monster tags similar … you can do things like spawn monster w/tag house_01 @ wp sp_house_01 (even in a loop if yer ambitious)

and add each monster to a group w/ GroupAddMember() as it is created …

 
anyway, groups are really handy and are used extensively in the official campaigns

2 Likes

kk :)

I use scripts for dying enemies all the time. I suspect that travus’ way of doing it may be the easiest.

Here’s a script made for me for my first module that I have since modified quite a bit. Maybe you could use something like this? You place it on all the enemies OnDeath of a certain group/wave. Then you make a simlar one for the next wave etc.


#include "ginc_object"


int CheckIfCreaturesWithTagAreAllDead(string tagName)
{
    object oPC = GetFirstPC();
    int i = 1;
    object oEnemy = GetNearestObjectByTag(tagName, oPC, i);
    while(GetIsObjectValid(oEnemy))
    {
        if(!GetIsDead(oEnemy)) {
			return FALSE;
        }
        i++;
        oEnemy = GetNearestObjectByTag(tagName, oPC, i);
    }
	return TRUE;
}

void main()
{
	if (CheckIfCreaturesWithTagAreAllDead("rat1") && CheckIfCreaturesWithTagAreAllDead("rat2")) 
	{
	    
	    object oEnemy1 = SpawnCreatureAtWP("enemy1", "enemy1_wp");
	    object oEnemy2 = SpawnCreatureAtWP("enemy2", "enemy1_wp");

	
	}
}
3 Likes

I also have some scripts made for my first module where I obviously create groups (someone made that for me too) but those scripts, when looking at them now, seem extremely crude, so I don’t know if that would help you at all. I have very little experience with creating enemy groups.

EDIT: Here’s a script (not by me) that I found lying in my first module among the scripts. It was used to assign creatures to a group, making them hostile, and at the same time the main baddie flees (he’s a real coward). Don’t think it will help you in this case, but I post it here anyway:

#include "ginc_group"
 
void CreateEnemyGroup()
{
    ResetGroup("group_badguys");
    object oPC = GetFactionLeader(GetFirstPC());
    int i = 1;
    object oBadGuy = GetNearestObjectByTag("tag_badguy", oPC, i);
    while(GetIsObjectValid(oBadGuy))
    {
        if(!GetIsDead(oBadGuy))
        {
            GroupAddMember("group_badguys", oBadGuy, TRUE);
        }
        i++;
        oBadGuy = GetNearestObjectByTag("tag_badguy", oPC, i);
    }
}
 
void main(int nAction)
{
    object oPC = GetPCSpeaker();
    switch ( nAction )
    {
        //Various conversation options
        case 100:
        break;
 
        case 200: //Enemy attack PC
            CreateEnemyGroup();
            object oCaptain = GetObjectByTag("captainbaddie");
            object oWP =  GetObjectByTag("wp_captain_flees");
            AssignCutsceneActionToObject(oCaptain, ActionMoveToObject(oWP, TRUE));
            DelayCommand(0.0f, GroupGoHostile("group_badguys"));
            break;
    }
}
2 Likes

ps. Here’s the basics of a group (for anyone interested)

#include "ginc_group"
void main()
{
	location loc = GetLocation(GetObjectByTag("waypoint"));
	object oCreature = CreateObject(OBJECT_TYPE_CREATURE, "creature", loc);
	GroupAddMember("wave1", oCreature);

	// add more monsters

	GroupOnDeathExecuteCustomScript("wave1", "script_wave2");
}

The creatures’ deathscripts must respect the “DeathScript” variable ( eg ‘nw_c2_default7’ ).

4 Likes

Here is a complex sample taken from Sarmates! that make waves of baddies

Sarmatians and allies must hold a position for 8 hours, repelling 4 waves of roman soldiers. The beauty of the thing is that if you have repelled a wave you can heal allies and/or ask them to pull back to cover before the following attack starts.

The initialization script (OnEnterConnect) sets the main variables : waves, hours and number of allies. A simple placeable tagged plot carries the key variables.

// enter Carnuntum area, first time : set time, count allied, fire convo

#include "ginc_misc"

void main()
{
	object oPC = GetFirstPC();
	
	if (IsMarkedAsDone()) return;
	
	object oSign = GetObjectByTag("sign_carnuntum");
	SetTime(7, 0, 0, 0);
	SetLocalInt(oSign, "Hour", 8);
	SetLocalInt(oSign, "Countdown", 4);
	
	int  n = 0; 
	object oIazyge = GetObjectByTag("iazyge_carnuntum");
	while (GetIsObjectValid(oIazyge))
	{
		n++;
		oIazyge = GetObjectByTag("iazyge_carnuntum", n);
	}
	SetLocalInt(oSign, "Allied", n);
	
	DelayCommand(0.3f, AssignCommand(oPC, ActionStartConversation(GetObjectByTag("galatos"), "cv_galatos")));
	
	MarkAsDone();
}

The main script, put on the area’s heartbeat, monitors the spawning of waves of baddies and the overall battle. At the end the more surviving allies, the more prestige for you commander. Actual reward is granted in a post combat convo.

// spawn new assaulters at hour change (recurrent - area)

void SpawnAt(string sFromWP, string sToWP)
{
	int n = 0;
	string sRef;
	object oSoldier;
	object oToWP = GetObjectByTag(sToWP);
	object oWP = GetObjectByTag(sFromWP);
	while (GetIsObjectValid(oWP))
	{
		switch (Random(6))
		{
			case 0 :
			case 1 : sRef = "roman_fighter"; break;
			case 2 :
			case 3 : sRef = "roman_ranger"; break;
			case 4 : sRef = "roman_cleric"; break;
			case 5 : sRef = "roman_spadassin"; break;
		}
		oSoldier = CreateObject(OBJECT_TYPE_CREATURE, sRef, GetLocation(oWP));
		AssignCommand(oSoldier, ActionForceMoveToObject(oToWP, TRUE, 8.0f, 24.0f));
		n++;
		oWP = GetObjectByTag(sFromWP, n);
	}
}

void main()
{
	object oPC = GetFirstPC();
	if (GetJournalEntry("q_danube", oPC) != 110) return;		// done or too early

	object oSign = GetObjectByTag("sign_carnuntum");
	int h = GetTimeHour();
	if (h <= GetLocalInt(oSign, "Hour")) return;
	
	SetLocalInt(oSign, "Hour", h+1);
	int c = GetLocalInt(oSign, "Countdown") - 1;
	SetLocalInt(oSign, "Countdown", c);
	
	string s;
	if (c > 0)
	{
		SpawnAt("WPS_carnuntum_left", "WP_carnuntum_left");
		SpawnAt("WPS_carnuntum_right", "WP_carnuntum_right");
		s = (GetGlobalInt("FR")) ? "Nouveaux renforts romains. Heure(s) à, tenir : " : "New Roman reinforcments. Hour(s) left : ";
		SendMessageToPC(GetFirstPC(FALSE), s + IntToString(2*c));
		return; 
	}

	AddJournalQuestEntry("q_danube", 120, oPC);			// all done, journal + compute prestige
	c = 0;
	h = 0;
	object oIazyge = GetObjectByTag("iazyge_carnuntum");
	while (GetIsObjectValid(oIazyge))
	{
		if (!GetIsDead(oIazyge)) c++;
		h++;
		oIazyge = GetObjectByTag("iazyge_carnuntum", h);
	}
	h = GetLocalInt(oSign, "Allied");
	if  ((c * 100) > (h * 50)) c = 2;
	else if ((c * 100) > (h * 25)) c = 1;
		 else c = 0;
	SetLocalInt(oSign, "Allied", c);  
} 

The scripts may look scary at first. If you go in depth you may find them helpful.

5 Likes

Wow ! Thank you everybody for all the information and scripts. Now I know roughly what’s required I’ll have a bit more of a think about this idea as I now feel guilty for asking and quitting before I started !

ps. I don’t think I can tick 5 solutions !

3 Likes

Not sure what you mean by that, kevL_s. I’m looking in the nw_c2_default7 right now…what variable are you talking about? Do you mean that you can’t use the string “DeathScript” since that is already in use…or something like that? Sorry if I’m being stupid here.

Yeah, this, although it seems pretty neat, is above my understanding. I could maybe be able to understand it, but that would probably require me to read your code a hundred times or so.

2 Likes

that’s the one… the critters’ OnDeath handler needs this code

	string sDeathScript = GetLocalString(OBJECT_SELF, "DeathScript");
	if (sDeathScript != "")
		ExecuteScript(sDeathScript, OBJECT_SELF);

and yeh you can’t use it for something else … the group functions would overwrite it

detail: The GroupOnDeath*() functions assign a special script as the DeathScript of creatures in a group. Those scripts count the kills and does whatever needs to happen when the killcount reaches the total count of creatures in the group.

<EDIT>the GroupOnDeath*() functs each assign one of several specific values to the DeathScript string:

gg_death_talk
gg_death_journal
gg_death_custom_script
gg_death_l_var

Those are stock scripts that need to execute auto from creatures’ OnDeath events, for the Group subsystem to maintain integrity.</EDIT>

Eg ->

// 'gg_death_custom_script'
#include "ginc_group"
void main()
{
	string sGroup = GetGroupLabel(OBJECT_SELF);
	if (GroupAddKill(sGroup) == GroupGetObjectCount(sGroup))
	{
		object oModule = GetModule();
		string sScript = GetGroupString(sGroup, "CustomScript");
		AssignCommand(oModule, ExecuteScript(sScript, oModule));
	}
}
1 Like

@kevL_s You will have to forgive me, but right now I’m clueless, and I’m probably embarrassing myself right now. It feels like there’s something on a fundamental level here that I just don’t understand. It wouldn’t be the first time when it comes to scripting and code, I’m afraid. :pensive:
Thankfully I can at least do some simple scripts…

I have had critters die who didn’t have this code in their OnDeath scripts (so apparently they don’t need the code after all…or what?)

string sDeathScript = GetLocalString(OBJECT_SELF, "DeathScript");
	if (sDeathScript != "")
		ExecuteScript(sDeathScript, OBJECT_SELF);

What does this mean? If I did a script called “DeathScript”, the group functions would overwrite it?
Is that it? “DeathScript” is used in the include script ginc_group perhaps?

Ok, looking at your code you have a custom death script for the group. It’s called “CustomScript” in your code as far as I can tell. What does that have to do with “DeathScript” that is used in the stock death script? Is there a script called DeathScript that is hardcoded somewhere?

The only thing that I can make out of this (if it’s even remotely correct) is something like: If you do a custom death script that executes under certain circumstances, please don’t call it “DeathScript” since that is used and hardcoded to do…stuff (whatever that script does).

ok from the top. (so to speak)

the stock OnDeath handler (aka script)  nw_c2_default7  has this code in it:

    string sDeathScript = GetLocalString(OBJECT_SELF, "DeathScript");
    if (sDeathScript != "")
        ExecuteScript(sDeathScript, OBJECT_SELF);

That means you can make a creature run an extra script when it dies. As I think you know you can do this by setting a local string in the toolset on the critter

DeathScript = my_extra_death_code_script

Of course that variable can be set via script anywhere, not only in the toolset’s Creature Properties|Scripts

But if you assign a creature to a group and call GroupOnDeathExecuteCustomScript() for that group, all creatures in that group will have the string-variable DeathScript set to “gg_death_custom_script” ( which would overwrite anything you’d set the string to before calling one of the GroupOnDeath*() functs ). Here’s the ginc_group function that sets the DeathScript variable to “gg_death_custom_script” ->

void GroupOnDeathExecuteCustomScript(string sGroup, string sScript)
{
    GroupSetLocalString(sGroup, "DeathScript", "gg_death_custom_script"); // set group's deathscript
    SetGroupString(sGroup, "CustomScript", sScript);                      // set vars for later use
    SetGroupInt(sGroup, GROUP_GLOBAL_KILLED_COUNT, 0);                    // reset count killed (in case grouplabel was used before)
}

 

that’s not my code. It’s code implemented by Obsidian and used by the ginc_group library.

You can see the CustomScript variable above, and it holds the argument sScript – in my example for Tsongo above, that script would be the  script_wave2

So when a critter in the group dies, the  gg_death_custom_script  counts the kill and iff ( if and only if ) all group has been slain, it runs the script held by CustomScript – which in Tsongo’s case would be set to “script_wave2”

If a creature in the group doesn’t have that initial OnDeath code, it won’t run the  gg_death_custom_script, it won’t be counted when killed, so the tally will never reach its total, hence the CustomScript would never run …

 

you don’t need the code unless the creature needs extra death-code … which, for example, the GroupOnDeath*() functs do need.

 

There’s no script called  DeathScriptDeathScript is the name of the variable that holds the name of a/any script that should run as an extra script OnDeath …

 
 
you’re right, andgalf, it’s not simple. But it is logical ;)

3 Likes

I was going to suggest polymorphing them into water elementals, but this works.

1 Like

@kevL_s - Ok, I think I understand a lot better now. Sometimes the programming vocabulary confuses me. I try to learn but it just won’t stick in my head for some reason. And then I google words like integer and stuff and sometimes I get it and sometimes I get even more confused.

The heart of the matter here, which made me confused in the first place, was that I thought “DeathScript” was a string, but it is apparently the name of a variable. I don’t want to hijack this thread more than necessary but what exactly is a variable. I’ve tried to look this up but it seems it can be so many things and that confuses me. In Properties of a character you have the Variables slot where you can put an integer or string I think. So are integers and strings all varialbes then? Maybe bouleans (is that what it’s called) variables too perhaps?

Edit: I think it’s time for me to look at your Basic Scripting help document for the 20th time.
Edit2: So, to me it seems that integers, objects, floats, vectors all are variables? So when I write object oPC in a script, oPC is the name of the variable? Is that right (I think that’s right)? And in this case DeathScript is a name that holds something…and DeathScript is indeed a string, and that string points to a script? Sometimes I feel like I understand and know how to code a bit of this stuff, and sometimes I don’t. I think what may confuse me also is that an object-variable (is that correct that an object can be a variable) can contain a string, like object oWaypoint = GetObjectByTag("waypoint");.

I hope I won’t confuse you anymore:

  • integers, strings, floats are types of variables. The reason for defining a type is for the compiler to know in advance how much memory is needed to store the content of the typed variable (and also to use optimized calculation methods to speed up the execution of the code).
  • integers (12), strings (“hello”), floats (1.02) are not variables (you can’t change their value).
  • a variable can have its value changed (but the type of the value must remain the same: string --> string; integer --> integer)
object oReader = GetFirstPC();
int iTwelve = 12;
SendMessageToPC(oReader, "The variable iTwelve equals " + IntToString(iTwelve); // will display The variable iTwelve equals 12
iTwelve = 1;
SendMessageToPC(oReader, "The variable iTwelve equals " + IntToString(iTwelve); // will display The variable iTwelve equals 1

Yes, whether you write object oPC = GetFirstPC(); or object oPlayer = GetFirstPC();, both oPC and oPlayer will give access to the main character.

Actually, it may use a string, but it is not a string. The function GetObjectByTag("waypoint") retrieves the object whose tag is “waypoint”. Then the variable oWaypoint is used to store that object in memory.
If I may try an analogy, vehicles have license plates. When a speed camera takes a picture, the information that is used is the license number (let’s say it’s AA 123 BB). So, it’s like having a script “Find the vehicle with license plate AA 123 BB”. In the toolset, you’d write something like

string sLicense = "AA 123 BB";
object oVehicle = GetObjectByTag(sLicense);
SendMessageToPC(GetFirstPC(), "Stop the car " + GetName(oVehicle));
2 Likes

yep.

int x = 1;

“int” is the type (of variable)
“x” is the variable (or the name of the variable)
“1” is the literal value

in Properties|Scripts|Variables it’s the same thing just displayed differently.

int and string and object etc are types – and when you create a variable you must specify its type. (compiler reasons, as 4760 says)

You can create/write variables of those types. (a variable without a type is meaningless, at least at our level of thinking about this). Reverse your statement to say, for example, “This variable is an integer (type)” or “This variable is an object (type)”.

yes.

DeathScript is a variable of type string and it holds a string-literal (which incidentally happens to be the filename of a script).

it depends what you mean. An object can be a variable if you create/code a variable of type object and store that object in it. But from a syntactic point of view it’s more correct to say that a variable can be an object …

uh, those are parameters/arguments … which could be variables but in that case it’s just a string-literal.

1 Like