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.
Valkyrie summoned while wielding a melee weapon
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;
}
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.