Random Tidbits: Summons/Allies Ai

Information and updates for all other projects by Black Omen Productions (besides Shadow Empires): Namely Codename: MetalStorm, Anti-Balance, Ultimate Fixer and all versions of XyRAX

Moderators: Nefarius, Joel, Doombreed-x, Vendanna

0
No votes
 
Total votes: 0

User avatar
Nefarius
Retired Admin
Cherub
Posts: 11607
Joined: Sat Jun 15, 2002 8:13 pm
Location: Where the blood forever rains

Hand-picked

Random Tidbits: Summons/Allies Ai

Post by Nefarius » Wed May 12, 2010 2:42 pm

Today I've finally had the time to tackle that horrible pet Ai that vanilla is so notorious for and replace it with what we had in mind for the start, this is both more aggressive (like the monsters you fight) and more usable, as with all ai blocks added to the system these can support scripts if some complex behaviour is needed, for the time being however this won't be needed (except for hirelings, which are one of the next things on my list).

The randomly spawned allies have been discussed before, for all intents and purposes they are treated like summons, the way this works is that they are recruited by the first player near them, if two players happen to be in range, they will pick the player with the higher level, these allies usually spawn with equipment (including potions for their own use), they will not follow you into dungeons (although this isn't really hard to implement), when the player they recruited themselves to dies or isn't anywhere around anymore, they will seek another player ( they will attack enemies at any point, like the Act5 barbs in vanilla, with the exception when when recruited, they'll give you exp from kills).

Pets, along the fact that now any pet can use spells, not needing a specific Ai anymore and generally being more responsive and fully softcoded now, also have gotten some other traits, for now here are some screens of a modified valkyrie.

Image
Valkyrie summoned while wielding a melee weapon

Image
And when wielding a ranged weapon

Thats right, as stated elsewhere, some pets spawn with the same equipment you do (like the shadows do in vanilla), the main difference is that they always spawn with randomly generated claws because their Ai cannot handle anything else, this one quite clearly does not suffer from this problem ;)

Since this is a great opportunity to update the old 'sample code' thread, here are some of the funcs I wrote today (for the vanilla pet mover check the CE forum).

Code: Select all

int __fastcall AIBLOCK_PetIdle( D2GameStrc* pGame, D2UnitStrc* pUnit, AiPar* args, AiInter* ai )
{
	D2UnitStrc* pOwner = SKILLS_GetSummonOwner( pUnit );
	
	if ( !UNITS_Alive( pOwner ) )
	{
		return -1;
	}
	
	D2PlayerDataStrc* pPlayerData = GetPlayerData( pOwner );
	
	if ( pPlayerData == NULL )
	{
		return -1;
	}
	
	D2PathStrc* hOwnerPath		= pOwner->hPath;
	D2PathStrc* hUnitPath		= pUnit->hPath;
	
	if ( hOwnerPath == NULL || hUnitPath == NULL )
	{
		return -1;
	}
	
	D2CoordStrc owner		= { hOwnerPath->x, hOwnerPath->y	};
	D2CoordStrc unit		= { hUnitPath->x, hUnitPath->y		};
	
	DRLGRoom* pRoom = hOwnerPath->pRoom;
	
	if ( pRoom == NULL )
	{
		return -1;
	}
	
	D2CoordStrc prev		= { hOwnerPath->nPrevX, hOwnerPath->nPrevY };
	
	if ( !prev.x || !prev.y )
	{
		prev.x = owner.x;
		prev.y = owner.y;
	}

	AiPath* pAiPath = GetAiPath( pUnit );
	ASSERT( pAiPath );

	int nRadius = COMMON_GetUnitDistSized( pUnit, pOwner );

	int nLimit = ai->range( pUnit, ARG_B, 1 );
	
	int nSteps = nLimit - nRadius;
	
	nLimit = ( ( nLimit >> 3 ) + 1 );
	
	if ( nLimit > PET_MIN_WALK )
	{
		nLimit = PET_MIN_WALK;
	}

	int x, y;

	x = pPlayerData->telex;
	y = pPlayerData->teley;
	
	if ( x && y )
	{
		nLimit = nLimit - COMMON_GetUnitDistCoord( pUnit, x, y );
		
		if ( nLimit < 0 )
		{
			nLimit = ( abs( nLimit ) + PET_MIN_WALK );
			
			FindNearPet t;
			
			t.nCount		= 0;
			t.nDistance		= SINT_MAX;
			t.pSource		= pUnit;
			t.pFoundPet		= NULL;
			
			PETS_WalkPets( pGame, pOwner, PETAI_FindNearPet, &t );
			
			if ( t.nCount > 0 )
			{
				if ( AICORE_MonsterEscape( pGame, pUnit, t.pFoundPet, nLimit, 1 ) )
				{
					return 1;
				}
			}

			if ( AICORE_MonsterWander( pGame, pUnit, nLimit ) )
			{
				return 1;
			}
			
			AICORE_MonsterStall( pGame, pUnit, PET_TELEPORT_WAIT );
			
			return 1;
		}
	}
		
	if ( nRadius > ai->range( pUnit, ARG_A, 1 ) )
	{
		// above the teleport range for the pet
		
		RMCoordList* pCoords = D2GetRMCoordList( pRoom, owner.x, owner.y );
		
		if ( MONSTERS_GetValidSpawnPos( pGame, pRoom, pCoords, pUnit->nIndex, &x, &y, 0 ) )
		{
			pRoom = DRLG_GetRoomMatchingCoords( pRoom, x, y );
			
			if ( pRoom )
			{
				if ( D2GAME_ChangeUnitPos( pGame, pUnit, x, y, pRoom, 0, 0 ) )
				{
					pUnit->dwExUnitFlags |= UNITFLAGEX_TELEPORTED;
					
					UNITMSG_AddToUpdateChain( pUnit );
					
					AICORE_MonsterStall( pGame, pUnit, PET_TELEPORT_WAIT );
					
					D2SetOverlay( pUnit, ai->overlay( 1 ) );
					
					return 1;
				}
			}
		}
		
		LOG_Write( "NEF: AIBLOCK_PetIdle(); couldn't teleport pet!" );
		
		return -1;
	}
	
	int nSpeed = ai->value( pUnit, ARG_C, 1 );

	int eMode = ( pOwner->ePlrMode == PLRMODE_RUN ) ? MONMODE_RUN : MONMODE_WALK;
	
	if ( nRadius > ai->range( pUnit, ARG_D, 1 ) )
	{
		eMode = MONMODE_RUN;
	}
	
	if ( eMode == MONMODE_WALK )
	{
		nSpeed = 0;
	}
		else
	{
		if ( nSpeed <= 0 )
		{
			nSpeed = pUnit->unitSeed.rand( PET_SPEED_MIN, PET_SPEED_MAX );
		}
	}
	
	if ( nSteps < 0 )
	{
		// try and walk to the owner
		
		static const BYTE bDirSubs[] =
		{
			4, 3, 2, 1, 0, 7, 6, 5
		};

		int nListNo = D2GetRMCoordListIdFromPos( pRoom, owner.x, owner.y );
		
		if ( nListNo == D2GetRMCoordListIdFromPos( hUnitPath->pRoom, unit.x, unit.y ) )
		{
			int k = D2GetDir8ths( D2GetDirToPoint( pOwner, prev.x, prev.y ) );
			
			int i = 0;
			
			do
			{
				D2GetAngleXYOffset( bDirSubs[ ( i + k ) & 7 ], &x, &y );
				
				x = ( x * 8 ) + prev.x;
				y = ( y * 8 ) + prev.y;
				
				if ( D2GetRMCoordListIdFromPos( pRoom, x, y ) == nListNo )
				{
					SetAiPathQuick( pGame, pUnit, pAiPath, 0, PET_PATH_SHORT, nSpeed, 0 );
					
					if ( AICORE_MonsterApproach( pGame, pUnit, NULL, eMode, x, y, 0, MOVEFLAG_DELEVENT ) )
					{
						return 1;
					}
					
					SetAiPathQuick( pGame, pUnit, pAiPath, 0, PET_PATH_SHORT, nSpeed, 0 );
					
					x = ( x + unit.x ) / 2;
					y = ( y + unit.y ) / 2;

					if ( AICORE_MonsterApproach( pGame, pUnit, NULL, eMode, x, y, 0, MOVEFLAG_DELEVENT ) )
					{
						return 1;
					}
				}
				
				i++;
			
			} while ( i < 8 );
		}
			else
		{
			x = owner.x;
			y = owner.y;
						
			if ( UnitMoves( UNIT_PLAYER, pOwner->ePlrMode ) )
			{
				SetAiPathQuick( pGame, pUnit, pAiPath, 0, PET_PATH_LONG, nSpeed, 0 );
			
				if ( AICORE_MonsterApproach( pGame, pUnit, NULL, eMode, x, y, 0, MOVEFLAG_DELEVENT ) )
				{
					return 1;
				}
				
				SetAiPathQuick( pGame, pUnit, pAiPath, 0, PET_PATH_LONG, nSpeed, 0 );
				
				x = ( x + unit.x ) / 2;
				y = ( y + unit.y ) / 2;

				if ( AICORE_MonsterApproach( pGame, pUnit, NULL, eMode, x, y, 0, MOVEFLAG_DELEVENT ) )
				{
					return 1;
				}
				
				SetAiPathQuick( pGame, pUnit, pAiPath, 0, 0, 0, 0 );
			}
			
			int i = 0;
			int j = pPlayerData->bPosCount;
			
			do
			{
				if ( j > 19 )
				{
					j = 0;
				}
				
				x = pPlayerData->tPos[j].x;
				y = pPlayerData->tPos[j].y;
				
				j++;
				
				if ( x && y )
				{
					if ( COMMON_GetUnitDistCoord( pOwner, x, y ) <= nLimit )
					{
						SetAiPathQuick( pGame, pUnit, pAiPath, 0, PET_PATH_LONG, nSpeed, 0 );
					
						if ( AICORE_MonsterApproach( pGame, pUnit, NULL, eMode, x, y, 0, MOVEFLAG_DELEVENT ) )
						{
							return 1;
						}
						
						SetAiPathQuick( pGame, pUnit, pAiPath, PATHTYPE_MOTION, PET_PATH_LONG, nSpeed, 0 );
					
						if ( AICORE_MonsterApproach( pGame, pUnit, NULL, eMode, x, y, 0, MOVEFLAG_DELEVENT ) )
						{
							return 1;
						}
						
						SetAiPathQuick( pGame, pUnit, pAiPath, PATHTYPE_MON_OTHER, PET_PATH_LONG, nSpeed, 0 );
					
						if ( AICORE_MonsterApproach( pGame, pUnit, NULL, eMode, x, y, 0, MOVEFLAG_DELEVENT ) )
						{
							return 1;
						}	
					}
				}
				
				i++;
			
			} while ( i < 20 );
		}
		
		x = ( unit.x + owner.x ) / 2;
		y = ( unit.y + owner.y ) / 2;
		
		SetAiPathQuick( pGame, pUnit, pAiPath, 0, PET_PATH_LONG, nSpeed, 0 );
	
		if ( AICORE_MonsterApproach( pGame, pUnit, NULL, eMode, x, y, 0, MOVEFLAG_DELEVENT ) )
		{
			return 1;
		}
	
		SetAiPathQuick( pGame, pUnit, pAiPath, 0, 0, 0, 0 );
	}
	
	return 0;
}

int __fastcall AIBLOCK_PetTarget( D2GameStrc* pGame, D2UnitStrc* pUnit, AiPar* args, AiInter* ai )
{
	args->bMelee	= 0;
	args->nDist		= SINT_MAX;
	args->pTarget	= NULL;

	D2UnitStrc* pOwner = SKILLS_GetSummonOwner( pUnit );
	
	if ( !UNITS_Alive( pOwner ) )
	{
		pOwner = NULL;
	}

	if ( !DUNGEON_CheckTown( PATH_GetRoom( pUnit ) ) )
	{
		D2UnitStrc* pKiller = NULL;
		
		if ( pOwner )
		{
			if ( !DUNGEON_CheckTown( PATH_GetRoom( pOwner ) ) )
			{		
				pKiller = COMBAT_GetAttackerUnit( pOwner );
				
				if ( pKiller )
				{
					if ( !UNITS_Alive( pKiller ) || pKiller->dwUnitFlags & UNITFLAG_PETIGNORE )
					{
						pKiller = NULL;
					}
				}
				
				BOOL bMelee = 0;
				
				int nTargetDist = SINT_MAX;
				int nKillerDist = SINT_MAX;
				
				D2UnitStrc* pTarget = MONAI_PickTarget( pOwner, args->pUnitAi, &nTargetDist, &bMelee );
				
				if ( pTarget )
				{
					if ( !UNITS_Alive( pTarget ) || pTarget->dwUnitFlags & UNITFLAG_PETIGNORE )
					{
						pTarget = NULL;
					}
				}
				
				if ( pKiller )
				{
					nKillerDist = COMMON_GetUnitDist( pOwner, pKiller );
				}
				
				if ( pKiller && pTarget )
				{
					if (
							( nKillerDist > nTargetDist )									||
							( MONAI_GetThreat( pKiller ) < MONAI_GetThreat( pTarget ) )
						)
					{
						pKiller = pTarget;
					}
				}
					else if ( pTarget )
				{
					pKiller = pTarget;
				}
			}
		}
		
		int nDist;
		
		BOOL bMelee;
		
		D2UnitStrc* pTarget = MONAI_PickTarget( pUnit, args->pUnitAi, &nDist, &bMelee );
	
		if ( pTarget && pKiller )
		{
			if ( pTarget != pKiller )
			{
				int nKillerDist = COMMON_GetUnitDist( pUnit, pKiller );
				
				if (
						( nKillerDist <= nDist )												&&
						( MONAI_GetThreat( pKiller ) >= MONAI_GetThreat( pTarget ) )
					)
				{
					pTarget = pKiller;
					
					ai->setAnger( 1 );
				}
			}
		}
			else if ( pKiller )
		{
			pTarget = pKiller;
			
			ai->setAnger( 1 );
		}
		
		if ( pTarget )
		{
			args->nDist		= COMMON_GetUnitDist( pUnit, pTarget );
			args->bMelee	= COMBAT_CheckEngage( pUnit, pTarget, 0 );
			args->pTarget	= pTarget;
			
			return 0;
		}
	}
	
	if ( pOwner )
	{
		if ( ai->chance( pUnit ) )
		{
			AiPath* pAiPath = GetAiPath( pUnit );
			ASSERT( pAiPath );
			
			pAiPath->ePathType = PATHTYPE_DEFAULT;
			
			int nSteps = ai->range( pUnit, ARG_B, 1 );
			
			if ( nSteps > 0 )
			{
				nSteps = GetSteps( pUnit, nSteps );
				
				if ( AICORE_MonsterWalkToBoss( pGame, pUnit, pOwner, nSteps ) )
				{
					return 1;
				}
				
				pAiPath->ePathType = 0;
				
				if ( AICORE_MonsterEscape( pGame, pUnit, pOwner, nSteps, 1 ) )
				{
					return 1;
				}
			}
		}
	}
	
	if ( ai->chance( pUnit, ARG_C ) )
	{
		int nSteps = ai->range( pUnit, ARG_D, 1 );
		
		if ( nSteps > 0 )
		{
			nSteps = GetSteps( pUnit, nSteps );
			
			EVENTS_DeleteEventsInline( pGame, pUnit, 2 );
			
			if ( AICORE_MonsterWander( pGame, pUnit, nSteps ) )
			{
				return 1;
			}
		}
	}
	
	AICORE_MonsterStall( pGame, pUnit, ai->delay( pUnit, ARG_A, 1 ) );
	
	return 1;
}

int __fastcall AIBLOCK_Valkyrie( D2GameStrc* pGame, D2UnitStrc* pUnit, AiPar* args, AiInter* ai )
{
	int nRun = ai->range( pUnit, ARG_D, 1 );
	
	int eMode = MONMODE_WALK;
	
	if ( args->nDist >= nRun || ai->retaliate() )
	{
		if ( ai->flags() & BEHAVE_RUN )
		{
			eMode = MONMODE_RUN;
		}
	}
	
	D2UnitStrc* pTarget = args->pTarget;
	
	int nSpeed = ai->value( pUnit, ARG_C, 1 );
		
	if ( nSpeed <= 0 )
	{
		nSpeed = pUnit->unitSeed.rand( PET_SPEED_MIN, PET_SPEED_MAX );
	}

	int i = ai->firstSK();
	
	BOOL bAttack;
	
	if ( DWORD(i) < 7 )
	{
		if ( args->pMonStats->skill[i] == SKILL_ATTACK )
		{
			if ( D2CheckUseMissile( pUnit ) )
			{
				int nSteps = args->nDist - ai->range( pUnit, ARG_B, 1 );
				
				if ( nSteps < 0 )
				{
					nSteps = GetEscapeSteps( nSteps, 1, RANGE_ESCAPE_MIN, RANGE_ESCAPE_MAX );
					
					if ( MONAI_Escape( pGame, pUnit, pTarget, nSpeed, nSteps, ai->flags(), args ) )
					{
						return 1;
					}
					
					ai->setAnger( 1 );
				}
				
				nSteps = args->nDist - nRun;
				
				if ( ai->eval( pUnit, ARG_A ) )
				{
					if ( nSteps > 0 )
					{
						if ( eMode == MONMODE_WALK )
						{
							if ( MONAI_RangeWalk( pGame, pUnit, pTarget, args, nSteps, nRun ) )
							{
								return 1;
							}
						}
							else
						{
							if ( MONAI_RangeRun( pGame, pUnit, pTarget, args, nSpeed, nSteps, nRun ) )
							{
								return 1;
							}
						}
					}
				}
				
				if ( args->nDist < MAXIMUM_MISSILE_RADIUS )
				{
					if ( ai->offensive( pUnit, bAttack, 1 ) )
					{
						if ( MONAI_CastRange( pGame, pUnit, pTarget, i, 0, args ) )
						{
							return 1;
						}
					}
				}
				
				return -1;
			}
		}
	}
	
	if ( ai->flags() & BEHAVE_COWARD )
	{
		if ( MONAI_Coward( pGame, pUnit, pTarget, args ) )
		{
			return 1;
		}
	}
	
	if ( args->bMelee )
	{
		if ( ai->offensive( pUnit, bAttack, 1 ) )
		{
			BOOL bOtherAttack = ( ( pUnit->unitSeed.eval() ) ? 1 : 0 );

			if ( MONAI_Attack( pGame, pUnit, pTarget, args, bOtherAttack ) )
			{
				return 1;
			}
		}
		
		return -1;
	}
	
	if ( ai->eval( pUnit, ARG_A ) )
	{
		if ( eMode == MONMODE_WALK )
		{
			if ( MONAI_MeleeWalk( pGame, pUnit, pTarget, args, 1 ) )
			{
				return 1;
			}
		}
			else
		{
			if ( MONAI_MeleeRun( pGame, pUnit, pTarget, args, nSpeed, 1 ) )
			{
				return 1;
			}
		}
	}
	
	return -1;
}
This does a few things they intended the vanilla Necropet Ai to do, but ended screwing up in one way or another (we all know how it 'behaves' ingame).

Firstly I retrieve a target near the owner of the pet (they did this too, but ended discarding it mostly, or worse running back and forth between targets) as well as the previous unit that attacked the pet owner, whichever of these is closer to the pet owner and has a higher threat level is selected.

Next the pet's own target is retrieved, if this is closer to the pet and more dangerous then the thing attacking the pet owner this becomes the new target, if this happens the pet goes into a more aggressive mode as well.

EDIT: An addition I just made to the Valkyrie behavior is weapon-based support for any skill assigned, so they're not only capable of using the equipment for normal attacks, but with any assigned skill requiring a melee or ranged weapon.
''(...) The game can basically be considered unhackable. '' - Blizzard Entertainment (30th May 2000)
Black Omen Productions | MetalStorm: Progress Report | Screenshots

Spejs
Posts: 8
Joined: Mon Apr 26, 2010 5:23 pm
Location: RF, Moscow

Re: Random Tidbits: Summons/Allies Ai

Post by Spejs » Wed May 12, 2010 7:54 pm

It was mentioned before, what about commands to your summons/allies/pets? Like deffencife/offencive order, attack concrete target or something more specific.

User avatar
Nefarius
Retired Admin
Cherub
Posts: 11607
Joined: Sat Jun 15, 2002 8:13 pm
Location: Where the blood forever rains

Hand-picked

Re: Random Tidbits: Summons/Allies Ai

Post by Nefarius » Thu May 13, 2010 2:55 am

The Ai class does these things automatically if a command is issued, they have the same AICMD support as enemy units do (which controls if they become more offensive, defensive etc.). In vanilla when a monster fetches a target, it does so anew every single think-tick which often makes monsters walk back and forth, the new system stores a target on the unit itself and only picks a new one if the old target becomes invalid (is too far away, dead, turns invisible, becomes allied etc.) the target overloading will simply override this field. Monsters also select a new target if they get hit, more specifically, they will target the previous unit that managed to hit them, if that unit is close enough.
''(...) The game can basically be considered unhackable. '' - Blizzard Entertainment (30th May 2000)
Black Omen Productions | MetalStorm: Progress Report | Screenshots

Spejs
Posts: 8
Joined: Mon Apr 26, 2010 5:23 pm
Location: RF, Moscow

Re: Random Tidbits: Summons/Allies Ai

Post by Spejs » Thu May 13, 2010 6:54 am

I understand they became good guardians, but i can't command them to attack concrete target or, for example, to make a clear path for me in specific direction.

User avatar
Nefarius
Retired Admin
Cherub
Posts: 11607
Joined: Sat Jun 15, 2002 8:13 pm
Location: Where the blood forever rains

Hand-picked

Re: Random Tidbits: Summons/Allies Ai

Post by Nefarius » Thu May 13, 2010 7:59 am

Yeah, as stated in some of the other threads the commands are changing the general behaviour state of the unit, while we considered adding point-and-click commands as well, the game is way too fast face (and not turn based) for the other approach to have much use (vanilla is slow paced compared to the new intensity of things). However an idea I've had was to add a lure spell that works like an inverted attract that will make allies target that unit instead if it is close enough.
''(...) The game can basically be considered unhackable. '' - Blizzard Entertainment (30th May 2000)
Black Omen Productions | MetalStorm: Progress Report | Screenshots

Spejs
Posts: 8
Joined: Mon Apr 26, 2010 5:23 pm
Location: RF, Moscow

Re: Random Tidbits: Summons/Allies Ai

Post by Spejs » Thu May 13, 2010 8:49 am

It's good to have even a small capability to control your allies. Want to see more info about hirelings when it's done.

User avatar
Rattlecage
Forum Regular
Angel
Posts: 932
Joined: Sun Apr 13, 2003 8:46 pm
Location: The Frozen North
United States of America

Re: Random Tidbits: Summons/Allies Ai

Post by Rattlecage » Fri May 14, 2010 2:32 am

Have you thought about using the audio taunts as commands?
No beating heart I felt. I brought no sights to the silver lips; no warmth from the gold.

User avatar
Nefarius
Retired Admin
Cherub
Posts: 11607
Joined: Sat Jun 15, 2002 8:13 pm
Location: Where the blood forever rains

Hand-picked

Re: Random Tidbits: Summons/Allies Ai

Post by Nefarius » Fri May 14, 2010 3:25 am

Yes, this has been mentioned up to 3 times so far ;), we'll be using those numpad messages to change the state of the active pets to either defensive, offensive (etc).

---

Here are some of the descs for changed Raise Skeleton / Valkyrie (note the damage for the valk shows the base damage added to her weapon, at high level this can easily rack in between 400-700 damage).

Image
Image
''(...) The game can basically be considered unhackable. '' - Blizzard Entertainment (30th May 2000)
Black Omen Productions | MetalStorm: Progress Report | Screenshots

User avatar
Drop_Dead
Junior Member
Paladin
Posts: 141
Joined: Thu Aug 23, 2007 1:26 am

Re: Random Tidbits: Summons/Allies Ai

Post by Drop_Dead » Fri May 14, 2010 4:34 pm

I've never been a big fan of having any summons in diablo 2, but this makes them seem much more appealing.

I've been thinking, its gonna be kinda funny in a couple years. Diablo 3 will be out, but i'll likely be playing this instead, or at least playing both equally. That's the impression i've got anyways, with all the insane amount of work that is being done.

Return to “Mods by Nefarius”