Smarter Healers & Supports

Handy guides on how to do things, written by users for users.

Moderators: GZDoom Developers, Raze Developers

Forum rules
Please don't start threads here asking for help. This forum is not for requesting guides, only for posting them. If you need help, the Editing forum is for you.
User avatar
amv2k9
Posts: 2178
Joined: Sun Jan 10, 2010 4:05 pm
Location: Southern California

Smarter Healers & Supports

Post by amv2k9 »

So, you want to make a monster that can heal or buff any allies within a radius? Not a problem; there are a number of ways to do this.

...But what if you want it to check to see if there are allies for it to heal or buff, before entering those states, and to avoid going into them if there's nothing around for it to heal or buff? Now things get a little harder.

Here is a method for a monster to check around it for potential "targets" to heal or buff, without using ACS, and without exhaustive and extensive editing of all your other monsters. This tutorial expects that you have some expertise with DECORATE; I'd rate it somewhere between beginner and intermediate difficulty.

This method relies on the fact that any action functions exectued by a CustomInventory actor in its Pickup state are treated as though the actor picking them up is the one executing them, rather than the pickup. Not only does this mean we don't have to do tons of edits to all the monsters, it comes in handy with some tweaks you may want to do when using the method in your mod.

So, let's start with the monster we want to have the healing ability:

Code: Select all

ACTOR HealingWizard : Wizard
{
	States
	{
	RadiusHeal:
		TNT1 A 0 A_RadiusGive("HealingWizardAlliesCheck",512,RGF_MONSTERS,5)
		WZRD A 4 A_Chase
		TNT1 A 0 A_JumpIfInventory("HealingWizardAlliesTrue",1,"AlliesFound")
		Goto See
	}
}
The first thing the monster does is give an item called HealingWizardAlliesCheck to any living monsters within a 512 mapunit radius. Don't worry about the line checking for the 'HealingWizardAlliesTrue' item; we'll come back to that. Now, let's look at the code for the 'HealingWizardAlliesCheck' item:

Code: Select all

ACTOR HealingWizardAlliesCheck : CustomInventory
{
	+ALWAYSPICKUP
	States
	{
	Pickup:
		TNT1 A 0 A_RadiusGive("HealingWizardAlliesCheckBack",512,RGF_MONSTERS,5)
		Stop
	}
}
Any monster that recieves the above item will immediately give another item, called 'HealingWizardAlliesCheckBack' to, again, any living monsters within a 512 mapunit radius. Now, let's look at the code for this second CustomInventory actor:

Code: Select all

ACTOR HealingWizardAlliesCheckBack : CustomInventory
{
	+ALWAYSPICKUP
	States
	{
	Pickup:
		TNT1 A 0 A_JumpIf(CheckClass("HealingWizard",AAPTR_DEFAULT),"PickupWizard")
		Stop
	 PickupHealerGolem:
		TNT1 A 0 A_GiveInventory("HDRLAHealerGolemAlliesTrue")
		Stop
	}
}
Things are a little more complicated now. You'll notice that the first thing this actor does is use the [wiki]CheckClass[/wiki] function in an expression used by [wiki]A_JumpIf[/wiki]. Remember above when I said that action functions executed in a CustomInventory actor's Pickup state are actually executed by that actor picking them up? It may sound counter-intuitive, but the monster picking up this item will now check its own class, and if the monster happens to be our HealingWizard (as a result of us using the AAPTR_DEFAULT actor pointer which is 'check self'), it will then jump to a new state where it gives yet another item, whose code need not be so complicated, at least for our example:

Code: Select all

ACTOR HealingWizardAlliesTrue : Inventory{+ALWAYSPICKUP}
The only purpose of the above item is so that the HealingWizard 'knows' there was at least one monster in the vicinity at the time in question.

Now, let's check back with our HealingWizard.

Code: Select all

ACTOR HealingWizard : Wizard
{
	States
	{
	RadiusHeal:
		TNT1 A 0 A_RadiusGive("HealingWizardAlliesCheck",512,RGF_MONSTERS,5)
		WZRD A 4 A_Chase
		TNT1 A 0 A_JumpIfInventory("HealingWizardAlliesTrue",1,"AlliesFound")
		Goto See
	AlliesFound:
		WZRD CCCCCC 4 A_FaceTarget
		WZRD D 12 A_RadiusGive("HealingWizardHealth",512,RGF_MONSTERS,5)
		TNT1 A 0 A_TakeInventory("HealingWizardAlliesCheck")
		TNT1 A 0 A_TakeInventory("HealingWizardAlliesCheckBack")
		TNT1 A 0 A_TakeInventory("HealingWizardAlliesTrue")
		Goto See
	}
}

ACTOR HealingWizardHealth : CustomInventory
{
	+ALWAYSPICKUP
	States
	{
	Pickup:
		TNT1 A 0 A_SetHealth(health+random(1,2))
		Stop
	}
}
You'll notice that there's quite a few tics between when it checks for allies, and when it actually executes the healing action. You'll probably want to do another check for allies in the area just to be sure, but for simplicity's sake, I haven't included that here.

And... that's that! There's a lot of flexibility to this method, so put your own spin on it! For example, you could have the monster check for different amounts of the item it gets (HealingWizardAlliesTrue in our example) to determine the strength of its healing ability; if there aren't many monsters in the area, the healer could jump to a state where the healing effect is stronger. Or it could check for multiples of that item to set the priority with which it uses the heal ability. Or, how about checking to see if there are any allies that need healing badly, and prioritizing the healing ability then? You could do something like this:

Code: Select all

ACTOR HealingWizardAlliesCheck : CustomInventory
{
	+ALWAYSPICKUP
	States
	{
	Pickup:
		TNT1 A 0 A_JumpIfHealthLower(40,"Critical")
		TNT1 A 0 A_RadiusGive("HealingWizardAlliesCheckBack",512,RGF_MONSTERS,5)
		Stop
	Critical:
		TNT1 A 0 A_RadiusGive("HealingWizardAlliesCheckBackCritical",512,RGF_MONSTERS,5)
		Stop
	}
}
Or what if you need the healer to ignore certain monsters, like if they're allied with the player? Just do something like this:

Code: Select all

ACTOR HealingWizardAlliesCheck : CustomInventory
{
	+ALWAYSPICKUP
	States
	{
	Pickup:
		TNT1 A 0 A_JumpIf(CheckClass("DoomPlayerRoboBuddy",AAPTR_DEFAULT),"PickupFail")
		TNT1 A 0 A_RadiusGive("HealingWizardAlliesCheckBack",512,RGF_MONSTERS,5)
		Stop
	PickupFail:
		Stop
	}
}
blood
Posts: 134
Joined: Thu Aug 25, 2011 3:17 pm

Re: Smarter Healers & Supports

Post by blood »

Very useful, I've a question could it be possible to make it works with dead bodies like a monster that would raise only his allies and not his ennemies, thanks !
User avatar
amv2k9
Posts: 2178
Joined: Sun Jan 10, 2010 4:05 pm
Location: Southern California

Re: Smarter Healers & Supports

Post by amv2k9 »

blood wrote:Very useful, I've a question could it be possible to make it works with dead bodies like a monster that would raise only his allies and not his ennemies, thanks !
I think you should use the RGF_KILLED flag in addition to RGF_MONSTERS in the A_RadiusGive call. The item being given should then call Thing_Raise, using 0 as the tid.

...Thing is, this implementation is redundant now that [wiki]A_CheckProximity[/wiki] exists :p
I need to mess around with that function and retool this tutorial.
User avatar
Nash
 
 
Posts: 17394
Joined: Mon Oct 27, 2003 12:07 am
Location: Kuala Lumpur, Malaysia

Re: Smarter Healers & Supports

Post by Nash »

Looking forward to see this adapted for A_CheckProximity. :D
rayburn
Posts: 44
Joined: Sun Oct 14, 2018 6:30 pm

Re: Smarter Healers & Supports

Post by rayburn »

Hey, I know I'm necroposting, but can you name any actual example of a mod where one monster can "cast buffs" on other monsters? Thank you!
rayburn
Posts: 44
Joined: Sun Oct 14, 2018 6:30 pm

Re: Smarter Healers & Supports

Post by rayburn »

For future generations, if I find any bugs I'll edit, or let me know to, thx:

Code: Select all

Class MMHealHandler : EventHandler                                // this is how we know when monster is hurt and needs healing
{                                                                                     // doing this instead of MaxHealth to current health comparison 
  override void WorldThingDamaged(WorldEvent e)            // because of other mods possibly affecting those values, namely Wrath of Cronos
  {
    let monst = e.thing;
    if (monst && monst.bISMONSTER && !monst.bCORPSE && monst.CheckInventory("HealableItem", 1) == false)
    {
      monst.GiveInventory("HealableItem", 1);
    }
  }

  override void WorldThingDestroyed(WorldEvent e)
  {
    let monstr = e.thing;
    if (monstr && monstr.bISMONSTER && monstr.CheckInventory("HealableItem", 1) == true)
    {
      monstr.TakeInventory("HealableItem", 1);
    }
  }
}

Class HealableItem : Inventory                                                                 // given to monsters that are "hurt"
{
}

Class MassHealVFX : Actor                                                                        // purely decorative actor to spawn VFX on healed targets
{
	Default
	{
		Radius 9;
		Height 6;
		XScale 1.8;
		YScale 1.5;
		+NOINTERACTION +NOGRAVITY
		//RenderStyle "Add";
		//Alpha 0.5;
	}
	States
	{
		Spawn:
			MHFX ABCDEFGHIJKLMNOPQRST 3 A_Warp(AAPTR_TRACER, 0, 0, 2, 0, WARPF_NOCHECKPOSITION);
			Stop;
	}
}

Class Monk : AirElement2 	                                                                               //healer, can't heal itself or other monks or a bunch of other monsters
{							
	Default
	{
		Health 280;
		Radius 20;
		Height 56;
		Mass 400;
		Speed 12;
		FastSpeed 12;
		PainChance 120;
		XScale 0.36;
		YScale 0.3;
		Monster;
		+FLOORCLIP +DONTHARMCLASS +NOINFIGHTSPECIES +AVOIDMELEE
		Species "Castle";
		PainSound "Monk/pain";
		DeathSound "Monk/death";
		ActiveSound "Monk/active";
		Obituary "%o was exorcised by a Monk.";
		MaxStepHeight 30;
		MaxDropOffHeight 30;
	}
	
	
	static const Class<Actor> MobsCanBeHealed[] =                                                                              // excludes various units that can't be healed for roleplay/balance reasons
	{
		"Troglodyte", "Magog", "Harpy", "Beholder", "MedusaQueen", "MinotaurKing",
		"Manticore", "CentaurArcher", "Halberdier", "Dwarf", "Marksman", "Swordsman", "GrandElf",
		"Griffin", "Champion", "Genie", "Archmage", "Naga", "WarUnicorn", "DireWolf", "Hobgoblin",
		"Corsair", "Lizardman", "Arbalester", "Dragonfly", "Ogre",
		"Basilisk", "BrassGorgon", "OgreMagi", "RocBird", "Navigator", "Wyvern", "Cyclops"
	}; 
	
	int CountHealMobs(int radius)                                                                                                         // function to see if there are healable monsters around
	{
		let  itr = BlockThingsIterator.Create(self, radius);
		int healables;
		while (itr.next())                     
		{
			let mon = itr.thing;
			if (!mon || !mon.bISMONSTER || mon.bCORPSE || mon.CheckInventory("HealableItem", 1) == false || Distance3D(mon) > radius)
			{
				continue;                      
			}
			let cls = mon.GetClass();
			for (int i = 0; i < MobsCanBeHealed.Size(); i++)
			{
				if (cls == MobsCanBeHealed[i])           
				{
				++healables;               
				}
			}
		}
		return healables;
	}
	
	void DoHealMobs(int radius)                                                                                                             // function to check mobs around AGAIN and heal them this time
	{
		let  itrtr = BlockThingsIterator.Create(self, radius);
		while (itrtr.next())                    
		{
			let mons = itrtr.thing;
			if (!mons || !mons.bISMONSTER || mons.bCORPSE || mons.CheckInventory("HealableItem", 1) == false || Distance3D(mons) > radius)
			{
				continue;                      
			}
			let clss = mons.GetClass();
			for (int j = 0; j < MobsCanBeHealed.Size(); j++)
			{
				if (clss == MobsCanBeHealed[j])          
				{
					mons.TakeInventory("HealableItem", 1);
					mons.GiveBody(-100);
					mons.A_SpawnItemEx("MassHealVFX",0,0,2,0,0,0,0,SXF_SETTRACER);        
				}
			}
		}
	}	
	
	States
	{
  
		Spawn:
			MNKS A 10 A_Look;
			Loop;
	
		See:  					
			MNKW ABCDEFG 4
			{
				A_Chase();
				int checkchance = random(1,64);				                                                        // setting very low chance check will happen
				if (checkchance == 64)						
				{
					int healtargets = CountHealMobs(400);
					if (healtargets > 0)
					{
						SetStateLabel("DoMassHeal");
					}
				}
			}
			Loop;
			
		DoMassHeal:			                                                                                                                 //  perform healing spell
			MNKF A 5;
			MNKF B 5
			{
				A_PlaySound("spells/heal");
				DoHealMobs(400);
			}
			MNKF C 5;
			MNKF D 5;
			Goto See;
			
		Missile:
			MNKA ABC 4 A_FaceTarget;
			MNKY D 0 A_PlaySound("Monk/attack");
			MNKA D 4 A_SpawnProjectile("LightBoltB");
			MNKA EFG 4;
			Goto See;
	
		Pain: 
			MNKN A 2 A_Pain;
			MNKN B 2; 
			MNKN CDEF 2;
			Goto See;
	
		Death:
			MNKN A 4;
			MNKN B 4 A_Scream;
			MNKD A 4 A_NoBlocking;
			MNKD B 4; 
			MNKD CD 4;
			MNKX A -1;
			Stop;
	
  }
}

Return to “Tutorials”