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.

Smarter Healers & Supports

Postby amv2k9 » Thu Jul 09, 2015 4:17 pm

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 allExpand view
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 allExpand view
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 allExpand view
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 CheckClass function in an expression used by A_JumpIf. 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 allExpand view
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 allExpand view
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 allExpand view
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 allExpand view
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
   }
}
User avatar
amv2k9
That Strife Guy
 
Joined: 10 Jan 2010
Location: Southern California

Re: Smarter Healers & Supports

Postby blood » Wed Dec 02, 2015 10:42 am

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 !
blood
 
Joined: 25 Aug 2011

Re: Smarter Healers & Supports

Postby amv2k9 » Wed Dec 09, 2015 2:37 pm

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 A_CheckProximity exists :p
I need to mess around with that function and retool this tutorial.
User avatar
amv2k9
That Strife Guy
 
Joined: 10 Jan 2010
Location: Southern California

Re: Smarter Healers & Supports

Postby Nash » Wed Dec 09, 2015 8:46 pm

Looking forward to see this adapted for A_CheckProximity. :D
User avatar
Nash
AKA Nash Muhandes! Twitter/Facebook/Youtube: nashmuhandes
 
 
 
Joined: 27 Oct 2003
Location: Kuala Lumpur, Malaysia
Twitch ID: nashmuhandes
Github ID: nashmuhandes

Re: Smarter Healers & Supports

Postby rayburn » Sat Jun 11, 2022 5:03 am

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
 
Joined: 14 Oct 2018

Re: Smarter Healers & Supports

Postby rayburn » Fri Jun 17, 2022 4:24 am

For future generations, if I find any bugs I'll edit, or let me know to, thx:
Code: Select allExpand view
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;
   
  }
}
rayburn
 
Joined: 14 Oct 2018


Return to Tutorials

Who is online

Users browsing this forum: No registered users and 1 guest