I've been toying around with some AI from another project with permission, and it worked for a bit... but I've been toying around with the idea of looking around at various other AI subroutines for chasing/jumping/etc but I have no idea where to look. I was wondering if anyone had any luck with something I could use. The license for the project is dual GPLv3 and BSD, if it helps in that regard.
I've been trying to look for AI that's more complex and acts more like the build engine does, albiet not as janky. I've managed to get jumping to sort of work... sort of... and it doesn't work as well as I'd hope. If anyone could give me pointers or existing examples that'd be great.
Complex A_Chase replacement in ZScript
Moderator: GZDoom Developers
Forum rules
Before asking on how to use a ZDoom feature, read the ZDoom wiki first. If you still don't understand how to use a feature, then ask here.
Please bear in mind that the people helping you do not automatically know how much you know. You may be asked to upload your project file to look at. Don't be afraid to ask questions about what things mean, but also please be patient with the people trying to help you. (And helpers, please be patient with the person you're trying to help!)
Before asking on how to use a ZDoom feature, read the ZDoom wiki first. If you still don't understand how to use a feature, then ask here.
Please bear in mind that the people helping you do not automatically know how much you know. You may be asked to upload your project file to look at. Don't be afraid to ask questions about what things mean, but also please be patient with the people trying to help you. (And helpers, please be patient with the person you're trying to help!)
- SanyaWaffles
- Posts: 840
- Joined: Thu Apr 25, 2013 12:21 pm
- Preferred Pronouns: They/Them
- Operating System Version (Optional): Windows 11 for the Motorola Powerstack II
- Graphics Processor: nVidia with Vulkan support
- Location: The Corn Fields
- Contact:
Re: Complex A_Chase replacement in ZScript
[Deleted]
Last edited by Misery on Fri Feb 28, 2025 10:32 pm, edited 1 time in total.
- MartinHowe
- Posts: 2056
- Joined: Mon Aug 11, 2003 1:50 pm
- Preferred Pronouns: He/Him
- Location: East Suffolk (UK)
Re: Complex A_Chase replacement in ZScript
The stuff I post below is not quite final but does work well enough. You're welcome to use any of it with adaptations. Please note the code for jumping and leaping is based that in NTMAI with permission from the author.
Customised looking/chasing
Jump/Leap
Customised looking/chasing
Code: Select all
// Martin Howe's "Back Cat" - An NPC mod for ZScript.
// Implements finding targets to attack.
// Thanks to the ([GQL])?ZDoom teams for their work :D
// This software is distributed under BSD License 2.0.
// Copyright (c) 2020, Martin Howe; all rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
// * Neither the name of Martin Howe nor the names of any contributors to
// his work may be used to endorse or promote products derived from this
// software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL MARTIN HOWE BE LIABLE FOR ANY DIRECT,
// INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE
mixin class BlackCat_Targeting
{
// Used to find monsters attacking amicable players
ThinkerIterator monsterFinder;
// Used to time pauses during 'strafe around actor'
int strafePauseIterations;
// Used to manage aftermath of finishing a meal
actor foodBeingEaten;
// Determine if this cat can see another actor. If
// the actor is null, then the function returns
// false. If the fov is omitted, or is explicitly
// given as zero, it means use the cat's default
// fov; if given as 360, it means to look all
// around (as in Doom).
bool CanSee(actor somePerson, double fov = 0.0)
{
if (!somePerson)
{
return false;
}
if (fov == 360.0)
{
return IsVisible(somePerson, /*allaround:*/true);
}
LookExParams lep;
lep.fov = (fov == 0.0) ? (bNeutral ? neutralFov : 0.0) : fov;
lep.minDist = 0.0; // unlimited
lep.maxDist = 0.0; // unlimited
lep.maxHeardist = 0.0; // ignored by IsVisible
lep.flags = 0; // ignored by IsVisible
lep.seestate = null; // ignored by IsVisible
return IsVisible(somePerson, /*allaround:*/false, lep);
}
// Determine if this cat can be seen by another
// actor. If the actor is null, then the function
// returns false. If the fov is omitted, or is
// explicitly given as zero, it means 180 degrees
// (as in Doom); if given as 360, it means to look
// all around (as in Doom).
bool CanBeSeenBy(actor somePerson, double fov = 0.0)
{
if (!somePerson)
{
return false;
}
if (fov == 360.0)
{
return somePerson.IsVisible(self, /*allaround:*/true);
}
LookExParams lep;
lep.Fov = fov; // the entire purpose
lep.minDist = 0.0; // unlimited
lep.maxDist = 0.0; // unlimited
lep.maxHeardist = 0.0; // ignored by IsVisible
lep.flags = 0; // ignored by IsVisible
lep.seestate = null; // ignored by IsVisible
return somePerson.IsVisible(self, /*allaround:*/false, lep);
}
// Find a visible player within the field of view
protected actor PlayerInFov(bool amicable = true, bool hostile = true)
{
let ptrSel = AAPTR_PLAYER1;
for (int i = 0; i < MAXPLAYERS; i++)
{
let player = GetPointer(ptrSel);
if ( player &&
(player.health > 0) &&
(
(amicable && !IsHostile(player)) ||
(hostile && IsHostile(player))
) &&
CanSee(player)
)
{
return player;
}
ptrSel = ptrSel << 1;
}
return null;
}
// Find a hostile monster that is attacking the cat
// and is visible in any direction, or is within
// the cat's normal field of view and is attacking
// a visible amicable player that is also in the
// field of view. If none found, then return null.
protected actor AggressingMonsterInFov(bool isAttackingPlayers, bool isAttackingSelf)
{
if (!monsterFinder)
{
monsterFinder = ThinkerIterator.Create("Actor");
}
let somePerson = actor(monsterFinder.Next());
if (!somePerson)
{
monsterFinder.Reinit();
somePerson = actor(monsterFinder.Next());
}
while (somePerson)
{
if (!somePerson.bIsMonster && !somePerson.Player)
{
somePerson = actor(monsterFinder.Next());
continue;
}
if (
(somePerson.health <= 0) ||
somePerson.bDormant ||
(somePerson == self) ||
(!somePerson.target) ||
!IsHostile(somePerson)
)
{
somePerson = actor(monsterFinder.Next());
continue;
}
if (
isAttackingSelf &&
(somePerson.target == self) &&
CanSee(somePerson, 360.0)
)
{
return somePerson;
}
if ( isAttackingPlayers &&
somePerson.target.Player &&
!IsHostile(somePerson.target) &&
CanSee(somePerson) &&
CanSee(somePerson.target)
)
{
return somePerson;
}
somePerson = actor(monsterFinder.Next());
}
return null;
}
// Determine whether an amicable player is under
// attack within the cat's field of view. Update
// the cat's internal state. If not countermanded,
// enter the SEE state to chase after the attacker.
protected void CheckPlayerUnderAttack(bool lookOnly = false)
{
if (bNeutral && bProtectPlayer)
{
actor amicablePlayer;
if (amicablePlayer = PlayerInFov(
/*amicable:*/true,
/*hostile:*/false))
{
actor hostilePlayer;
if (hostilePlayer = PlayerInFov(
/*amicable:*/false,
/*hostile:*/true))
{
fussPerson = null;
fussPossible = false;
fussRequested = null;
playerUnderAttack = true;
target = hostilePlayer;
if (myMob)
{
myMob.NotifyNewTarget(target);
}
if (!lookOnly && (currentState != State_See))
{
SetStateLabel("See");
}
return;
}
actor hostileMonster;
if (hostileMonster = AggressingMonsterInFov(
/*isAttackingPlayers:*/true,
/*isAttackingSelf:*/false))
{
fussPerson = null;
fussPossible = false;
fussRequested = null;
playerUnderAttack = true;
target = hostileMonster;
if (myMob)
{
myMob.NotifyNewTarget(target);
}
if (!lookOnly && (currentState != State_See))
{
SetStateLabel("See");
}
return;
}
}
}
playerUnderAttack = false;
}
// Finds an enemy to attack
//
// 1. RefreshPrey means MUST ignore current target;
// if not possible, then no target is selected.
// 2. EdibleOnly means MUST find edible target;
// if not possible, then no target is selected.
// 3. MaxHealth of 0 means unlimited.
// 4. MaxMass of 0 means unlimited.
//
// The defaults behave like A_LookEx except that
// they prevent the cat from going after anything
// too dangerous.
protected void LookForEnemies(
bool refreshPrey = false,
bool edibleOnly = false,
bool canFly = true, // cats can now leap/jump
bool cantFly = true,
bool canBleed = true,
bool cantBleed = false,
int maxHealth = 150,
int maxMass = 400)
{
// Remember current target
actor currentTarget = target;
// Use A_LookEx first as more efficient
let flags = LOF_DONTCHASEGOAL|LOF_NOSEESOUND|LOF_NOJUMP;
let maxseedist = ( (bNeutral && (chaseCount == -1)) ? alertDistance : 0);
let maxheardist = ( (bNeutral && (chaseCount == -1)) ? (alertDistance * 2) : 0);
let catsFov = (bNeutral ? neutralFov : 0.0);
A_LookEx(flags, 0.0, maxseedist, maxheardist, catsFov);
// If no enemy found then nothing to do
if (!target)
{
return;
}
// If enemy matches criteria then use them
if ( target &&
(!refreshPrey || !currentTarget || (target != currentTarget)) &&
(!edibleOnly || (!target.bNoBlood && IsHostile(target))) &&
(!canFly || target.bFloat) &&
(!cantFly || !target.bFloat) &&
(!canBleed || !target.bNoBlood) &&
(!cantBleed || target.bNoBlood) &&
((maxHealth == 0) || (target.health <= maxHealth)) &&
((maxMass == 0) || (target.mass <= maxMass))
)
{
if (myMob)
{
myMob.NotifyNewTarget(target);
}
return;
}
// Fall back on rougher search method
target = FindEnemyEx(
/*targetToIgnore:*/(refreshPrey ? currentTarget : null),
maxHealth,
maxMass,
canFly,
cantFly,
canBleed,
cantBleed,
maxseedist,
catsFov,
/*ignoreVisibility:*/false,
/*allowInfighting:*/false);
if (target && myMob)
{
myMob.NotifyNewTarget(target);
}
}
// Customised A_Look for the cats; this adjusts the
// defaults for neutrality; if the cat is neutral,
// then they have a smaller radius of attention and
// narrower FOV than the default values; if a fuss
// is currently processing, face the actor involved.
//
// 1. If lookOnly then do not jump states.
// 2. If priorityOnly then only check for players
// under attack.
//
// Special rules apply here for neutral cats.
//
// If no enemy is near enough to threaten the cat,
// check for players being attacked; this means
// that both the player and the attacker must be
// within the cat's field of view and the attacker
// is targeting the player.
//
// If hostile and amicable players are in view at
// the same time, the check assumes that they will
// soon be fighting, even if they aren't now, and
// thus starts attacking the hostile player. This
// is not as accurate as desired, but is the only
// way to support the feature in DeathMatch.
protected virtual void A_BlackCatLook(bool lookOnly = false, bool priorityOnly = false)
{
// If this cat is in a mob, then it should
// attack the mob's target, if there is one.
if (myMob)
{
let mobTarget = myMob.GetCurrentTarget();
if (mobTarget && (!target || mobTargetSwitch))
{
A_ResetFussing();
alertCount = -1;
chaseCount = 0;
panicCount = -1;
target = mobTarget;
if (!lookOnly && (currentState != State_See))
{
SetStateLabel("See");
}
return;
}
}
// If checking high priority threats only, then
// just ensure that friendly players are safe.
if (priorityOnly)
{
CheckPlayerUnderAttack(lookOnly);
return;
}
// Look for enemies using the defaults as for
// A_LookEx, except that there is no jump and
// prevent the cat from going after anything
// too dangerous.
LookForEnemies();
// Use the target if one was found
if (target)
{
if (!lookOnly && (currentState != State_See))
{
SetStateLabel("See");
}
return;
}
// If execution does NOT reach this point, then
// the following conditions have been detected.
// In this context, 'gunfire' means 'hearing',
// but in this case not actually damaged by,
// potentially damaging acts that make noise:
// monster weapons fire, barrels detonated by
// world (not player) script, earthquakes, etc.
// +-----------------+-----------+-----------+
// | CONDITION | -FRIENDLY | +FRIENDLY |
// +-----------------+-----------+-----------+
// | PLAYER GUNFIRE | X | X |
// | MONSTER GUNFIRE | - | - |
// | WORLD GUNFIRE | - | - |
// | DAMAGED | X | X |
// +-----------------+-----------+-----------+
// PROBLEMS
// Monsters do not react LITERALLY to SOUNDS;
// they only notice player gunshots when
// looking for enemies, using special coding
// that checks what players are doing, giving
// the APPEARANCE of reacting to sound.
// Friendly monsters are actually normal ones
// with some checks, so they inherit this and
// react to player 'sounds'; however, as they
// do not attack players, they have no target
// to attack and thus chase around in 'panic'.
// If no enemy is near enough to threaten us
// we must check for players being attacked.
CheckPlayerUnderAttack();
// Face any actor the cat is interacting with;
// the following are mutually exclusive in the
// sense that potential enemies take priority.
if (potentialEnemy)
{
A_Face(potentialEnemy);
}
else if (fussPerson)
{
A_Face(fussPerson);
}
}
// Customised wander function for the cats; they
// will navigate to the specified actor, but by
// default, do so less randomly than as normal.
//
// 1. The argument wanderPerson specifies the actor
// to wander to; the default is null and means
// use the cat's goal.
// 2. The argument wanderSpeed specifies the speed
// with which to wander; the default is zero and
// means use the cat's normal speed.
// 3. The argument wanderDirChance specifies the
// probability out of 256 of the actor changing
// direction towards wanderPerson while
// wandering; this controls how erratically the
// cat wanders; the default is 16.
// 4. The argument wanderDirAngle specifies the
// maximum absolute value (zero to 180) of the
// angle that should be allowed to exist between
// wanderPerson and the cat; the default is 180
// (no limit).
//
protected virtual void A_BlackCatWander(
actor wanderPerson = null,
uint wanderSpeed = 0,
uint wanderDirChance = 16,
double wanderDirAngle = 180)
{
// Determine who we navigate to
let navigatePerson = (wanderPerson ? wanderPerson : goal);
let navigateSpeed = (wanderSpeed ? wanderSpeed : speed);
// Navigate to the actor
let oldGoal = goal;
let oldSpeed = speed;
let oldTarget = target;
goal = navigatePerson;
speed = navigateSpeed;
if ( (wanderDirAngle < 180) && (Abs(AngleTo(navigatePerson)) > wanderDirAngle))
{
target = navigatePerson;
NewChaseDir();
}
target = null;
A_Wander();
if (wanderDirChance && (random[BlackCatWander]() < wanderDirChance - 1))
{
target = navigatePerson;
NewChaseDir();
}
if ( (wanderDirAngle < 180) && (Abs(AngleTo(navigatePerson)) > wanderDirAngle))
{
target = navigatePerson;
NewChaseDir();
}
goal = oldGoal;
speed = oldSpeed;
target = oldTarget;
}
// Customised strafe function for the cats; they
// strafe around the specified actor, randomly
// pausing to allow the calling action to occur.
//
// 1. The argument strafeDirChance specifies the
// probability out of 256 of the actor changing
// direction towards the target while strafing;
// this controls how erratic the strafing is.
// 2. The argument strafePauseChance specifies the
// probability out of 256 of pausing to face the
// target (e.g., to make a sound at them).
// 3. The argument strafePauseCount specifies how
// many iterations of the calling state occur
// before the actor resumes strafing. A random
// value of up to 10% is added to it, to make
// the cat's behaviour less predictable in-game.
// 4. If strafePauseCount exceeds the positive
// subrange of int, it is a fatal error. This is
// required because it is used in calculations
// with int values.
// 5. The argument strafeDirAngle specifies the
// maximum absolute value (zero to 180) of the
// angle that should be allowed to exist between
// the target and the cat; to specify no limit,
// give strafeDirAngle as 180.
//
protected virtual void A_BlackCatStrafe(
actor strafePerson,
uint strafeSpeed,
uint strafeDirChance,
double strafeDirAngle,
uint strafePauseChance,
uint strafePauseCount)
{
// Strafe pause count must fit the positive
// subrange of int.
if (strafePauseCount > (MAX_INT / 2)) { ThrowAbortException("**** FATAL: ERANGE(BlackCat.A_BlackCatStrafe: argument \'strafePauseCount\'(%u) should be within the positive subrange of int (%u))", strafePauseCount, (MAX_INT / 2)); }
// Check the pause count first (if in use)
if (strafePauseIterations > -1)
{
strafePauseIterations++;
int spcDelta = min(
random(0, (strafePauseCount / 10)),
(MAX_INT - strafePauseCount)
);
int spc = strafePauseCount + spcDelta;
if (strafePauseIterations > spc)
{
strafePauseIterations = -1;
}
return;
}
// Pause every so often so that the calling
// action actually has time to take place.
if (strafePauseChance && (random[BlackCatStrafe]() < strafePauseChance - 1))
{
A_Stop();
A_Face(strafePerson);
strafePauseIterations = 0;
return;
}
// Do the actual strafing movement
let oldGoal = goal;
let oldSpeed = speed;
let oldTarget = target;
goal = strafePerson;
speed = strafeSpeed;
if ( (strafeDirAngle < 180) && (Abs(AngleTo(strafePerson)) > strafeDirAngle))
{
target = strafePerson;
NewChaseDir();
}
target = null;
A_Wander();
if (strafeDirChance && (random[BlackCatStrafe]() < strafeDirChance - 1))
{
target = strafePerson;
NewChaseDir();
}
if ( (strafeDirAngle < 180) && (Abs(AngleTo(strafePerson)) > strafeDirAngle))
{
target = strafePerson;
NewChaseDir();
}
goal = oldGoal;
speed = oldSpeed;
target = oldTarget;
}
// Periodically make fuss chase sounds
protected void RandomFussChaseSounds()
{
if (
(random[FussChase]() < 3) &&
!IsActorPlayingSound(CHAN_SPEECH)
)
{
A_Speak(Sound_Fusschase);
}
}
// Find food to eat/hunt
protected void FindSomeFood()
{
// Remember that cats are, like T-Rex,
// almost as much scavengers as hunters,
// so go looking for corpses to eat!
if (foodPerson = FindCorpse())
{
// Ensure food is tagged with its death mass
// if we are the first to target it as food.
if (!foodPerson.FindInventory("M426_DeathMass"))
{
foodPerson.GiveInventory("M426_DeathMass", foodPerson.mass);
}
if (debug) { console.printf("Found a %s with %d calories", foodPerson.GetClassName(), foodPerson.mass); }
// When navigating to food, cancel any
// conditional chase or panic and use
// conventional chase logic.
alertCount = -1;
chaseCount = -1;
panicCount = -1;
// If navigating to food, ignore
// any monsters not attacking us.
if (target && target.target && (target.target != self))
{
target = foodPerson;
}
goal = foodPerson;
return;
}
// No edible corpses, so go hunting
LookForEnemies(/*newTarget:*/false, /*edibleOnly:*/true);
if (target)
{
// Ensure food id is correctly set
foodPerson = target;
// Ensure the food is tagged with
// its death mass if we are the
// first to target it as food.
if (!foodPerson.FindInventory("M426_DeathMass"))
{
foodPerson.GiveInventory("M426_DeathMass", foodPerson.mass);
}
if (debug) { console.printf("Hunting a %s with %d calories", foodPerson.GetClassName(), foodPerson.mass); }
// When hunting for food, cancel
// any conditional panic and use
// conditional chase logic.
alertCount = -1;
chaseCount = 0;
panicCount = -1;
// Signal that we are hunting
// and not merely scavaging.
huntingToEat = true;
// Make the usual SIGHT sound,
// tagged with different ID to
// tell the system cat is not
// hunting demons as enemies,
// but as prey to be consumed!
A_Speak(Sound_Hunt);
goal = foodPerson;
return;
}
// Do not cancel fuss if there is nothing to
// eat; the cat will beg the player for food;
// this is used when the only actors available
// to hunt are too big for a cat to take on.
}
// Customised A_Chase for the cats; they do not
// randomly weave as much and, if chasing to fuss,
// play "chirrup" sounds and not the active sound.
protected virtual void A_BlackCatChase(bool canLeap = true, bool canJump = false)
{
// If the cat is under attack right now, they
// must prioritise that over everything else,
// including their current mob target if any.
actor hostileMonster = (target && (target.target == self)) ? target : null;
if (!hostileMonster)
{
hostileMonster = AggressingMonsterInFov(
/*isAttackingPlayers:*/false,
/*isAttackingSelf:*/true);
}
if (hostileMonster)
{
fussPerson = null;
fussPossible = false;
fussRequested = null;
target = hostileMonster;
if (myMob)
{
myMob.NotifyNewTarget(target);
}
A_ResetFussing();
strafePauseIterations = -1;
foodBeingEaten = null;
A_LeapChase(canLeap, canJump);
return;
}
// Determine if the cat is hungry
let hungry = IsHungry();
// If hungry, the food being eaten right now
// must be reset, as it only applies when the
// cat has satisfied their hunger immediately
// after an EAT state cycle, which in this case
// can obviously not be true. Also, if there is
// no food currently known to be available, try
// to find some; this can modify most of the
// food-related context used by the function.
if (hungry)
{
foodBeingEaten = null;
if (!foodPerson)
{
FindSomeFood();
}
}
// If the cat is chasing to fuss but not also
// doing so to beg for food, and has no target,
// then check for the priority threats, such as
// those to the player's safety; if any are
// found, then abandon fuss in favour of those;
// also clear any strafe related context and
// reset the food being eaten right now; these
// have no meaning during a normal chase cycle.
if (!hungry && chasingToFuss && fussPerson && !target)
{
A_BlackCatLook(/*lookOnly:*/true, /*:priorityOnly:*/true);
if (target)
{
A_ResetFussing();
strafePauseIterations = -1;
foodBeingEaten = null;
A_LeapChase(canLeap, canJump);
return;
}
}
// Determine if the cat is in fuss distance
bool canFuss = (chasingToFuss && fussPerson && CheckInteractionRange(fussPerson));
// If the cat is not hungry then it reduces to
// a simple choice: if trying to fuss and cat
// has reached the fussee, then do the fuss;
// otherwise, perform a normal chasing cycle.
if (!hungry)
{
// If we came from the EAT state, and are
// no longer hungry, strafe slightly away
// from the food for a few cycles, before
// returning to the IDLE state; note that
// just before entering the EAT state,
// strafePauseIterations is set to zero and
// foodBeingEaten is set to foodPerson; the
// setting of foodBeingEaten to a non-null
// value distinguishes this condition from
// other strafe pause types that may occur.
if (strafePauseIterations == 0)
{
if (foodBeingEaten)
{
strafePauseIterations = -3;
}
}
if (strafePauseIterations <= -3)
{
A_BlackCatStrafe(
/*strafePerson:*/foodBeingEaten,
/*strafeSpeed:*/3,
/*strafeDirChance:*/128,
/*strafeDirAngle:*/180,
/*strafePauseChance:*/0,
/*strafePauseCount:*/0);
strafePauseIterations--;
if (strafePauseIterations <= -12 -3)
{
strafePauseIterations = -1;
foodBeingEaten = null;
SetStateLabel("Idle");
}
A_BlackCatLook(/*lookOnly:*/false, /*:priorityOnly:*/true);
return;
}
//////////////////////////////////////////////
// If execution gets here, special cases of //
// non-hungry strafing have been handled. //
//////////////////////////////////////////////
// Cat won't be strafing during this cycle,
// as that action is only used when hungry;
// thus, cancel any current strafe pause,
// including any strafe pause qualifiers.
strafePauseIterations = -1;
foodBeingEaten = null;
// Stop a fuss chase if reached the fussee
if (canFuss)
{
goal = null;
A_Stop();
A_Face(fussPerson);
chaseCount = -1;
SetStateLabel("Idle");
return;
}
// Fail-safe to stop a fuss chase if the
// goal (fussee) has been lost by one of
// the engine's under-the-hood behaviours.
if (chasingToFuss && !goal)
{
A_Stop();
A_Face(fussPerson);
chaseCount = -1;
SetStateLabel("Idle");
return;
}
// If chasing to fuss, navigate to fussee
if (chasingToFuss)
{
A_BlackCatWander();
RandomFussChaseSounds();
}
// No fuss activity, just do normal chasing
A_LeapChase(canLeap, canJump);
return;
}
//////////////////////////////////////////////////
// If execution reaches here, the cat is hungry //
//////////////////////////////////////////////////
// Determine if the cat is in range of food
bool canEat = (foodPerson && CheckInteractionRange(foodPerson, /*factor:*/1));
//////////////////////////////////////////////////
// Chase the fussee/food and be little more //
// direct than the usual random weaving. //
//////////////////////////////////////////////////
// If there is food and cat is near enough to
// eat it, then if a corpse, strafe around it
// taking bites/slurps; otherwise, let A_Chase
// handle the actual close-on-and-melee action.
if (canEat)
{
if (huntingToEat) // attack the prey
{
// Stay with the prey from now on
chaseCount = -1;
// Strafing won't start until later
strafePauseIterations = -1;
// Use default logic to start attack
A_LeapChase(canLeap, canJump);
}
else // scavenge food from a corpse
{
// Corpse food chase already unlimited,
// so not needed here: chaseCount = -1;
// Start strafing around it
A_BlackCatStrafe(
/*strafePerson:*/foodPerson,
/*strafeSpeed:*/3,
/*strafeDirChance:*/192,
/*strafeDirAngle:*/135,
/*strafePauseChance:*/32,
/*strafePauseCount:*/12);
// If in the first strafe pause,
// take a bite/slurp of the food!
if (strafePauseIterations == 0)
{
foodBeingEaten = foodPerson;
SetStateLabel("Eat");
return;
}
// If we are not in a strafe pause
// state now, signal that we were
// strafing to eat food; this is
// required by a check that is later
// made to ensure that we didn't
// get too far away from the food to
// continue eating from it.
if (strafePauseIterations < 0)
{
strafePauseIterations = -2;
}
}
return;
}
//////////////////////////////////////////////////
// If execution reaches here, cat is hungry but //
// no food, or cat is not close enough to it. //
//////////////////////////////////////////////////
// Check and act on 'quick return to food' flag
if (foodPerson && (strafePauseIterations == -2))
{
if (!CheckInteractionRange(foodPerson, /*factor:*/1))
{
VelIntercept(foodPerson, 3);
A_BlackCatWander(
/*wanderPerson:*/foodPerson,
/*wanderSpeed:*/3,
/*wanderDirChance:*/128,
/*wanderDirAngle:*/135);
return;
}
}
// If chasing to fuss, and reached the fussee,
// and there is no food, then as cat is hungry,
// strafe around the fussee and meow for food.
if (canFuss && !foodPerson)
{
// Stay with the fussee from now on
chaseCount = -1;
// Start strafing around them
A_BlackCatStrafe(
/*strafePerson:*/fussPerson,
/*strafeSpeed:*/3,
/*strafeDirChance:*/192,
/*strafeDirAngle:*/135,
/*strafePauseChance:*/32,
/*strafePauseCount:*/48);
if (
(random[FussChase]() < 5) &&
!IsActorPlayingSound(CHAN_SPEECH)
)
{
A_Speak(Sound_Meow);
}
return;
}
//////////////////////////////////////////////////
// If execution reaches here, cat is hungry and //
// 1. Not fuss chasing, or //
// 2. Fuss chasing but not near enough, or //
// 3. There is food, or //
// 4. Any combination of the above. //
//////////////////////////////////////////////////
// Cat won't be strafing during this cycle, as
// that action is only used when cat is near
// enough to the 'target' to fuss or eat them;
// thus, cancel any current strafe pause.
strafePauseIterations = -1;
// If chasing after food, whether navigating to
// a corpse or hunting prey, ignore fussing and
// prioritise acquisition of food. If hunting,
// let A_Chase handle the hunting mechanics.
if (foodperson)
{
if (huntingToEat) // use default logic
{
A_LeapChase(canLeap, canJump);
}
else // going after a corpse to eat
{
A_BlackCatWander(
/*wanderPerson:*/foodPerson,
/*wanderSpeed:*/0 /* use default */,
/*wanderDirChance:*/128,
/*wanderDirAngle:*/135);
}
return;
}
//////////////////////////////////////////////////
// If execution reaches here, cat is hungry and //
// 1. Not fuss chasing, or //
// 2. Fuss chasing but not near enough, or //
// 3. There is no food, or //
// 4. Any combination of the above. //
//////////////////////////////////////////////////
// If chasing to fuss someone, navigate to them
if (chasingToFuss)
{
A_BlackCatWander();
RandomFussChaseSounds();
return;
}
//////////////////////////////////////////////////
// If execution reaches here, all the special //
// cases have been handled and it is just a //
// regular chase iteration. //
//////////////////////////////////////////////////
// Just chase normally
A_LeapChase(canLeap, canJump);
}
// Call from user code SPAWN state
void BlackCat_Targeting_Init()
{
monsterFinder = null;
strafePauseIterations = -1;
foodBeingEaten = null;
}
}
Code: Select all
// Martin Howe's "Back Cat" - An NPC mod for ZScript.
// Implements leaping at prey.
// Thanks to the ([GQL])?ZDoom teams for their work :D
// This software is distributed under BSD License 2.0.
// Contributor acknowledgements:
//
// This code is based on the work of "Local Insomniac", from his mod NTMAI
// (Nobody Told Me About Id); that code is itself based on ZSDuke code by
// ZZYZX, aka jewalky and also author of Ultimate Doom Builder.
//
// At time of writing, ZSDuke had no licence terms attached. Local Insomniac
// has stated to me (Martin Howe) that his licence, such as it is, is thus:
//
// "Everything from NTMAi and my other projects are free to use, I don't mind
// about anything AS long as the relevant people who are responsible for code/
// sprites/etc. get credit."
// Copyright (c) 2020, Martin Howe; all rights reserved.
// Copyright (c) 2019 "Local Insomniac"; rights as stated above.
// Copyright (c) 2017 "XXYZX" aka Jewalky; rights as stated above.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
// * Neither the name of Martin Howe nor the names of any contributors to
// his work may be used to endorse or promote products derived from this
// software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL MARTIN HOWE BE LIABLE FOR ANY DIRECT,
// INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE
mixin class BlackCat_Leaping
{
// Used to manage leaping
bool inLeap;
int leapFail;
double leapDistance;
// Leap direction flags
enum LeapDirections
{
Leap_Forward = 1,
Leap_Up = 2,
Leap_Both = Leap_Forward | Leap_Up
}
// Determine the height of whatever is underneath
// the cat, whether the ground or another actor.
double GetGroundHeight()
{
let floorZ = GetZAt();
bool ok;
actor pOnMobj;
[ok, pOnMobj] = TestMobjZ(/*quick:*/true);
if (!pOnMobj)
{
return floorZ;
}
let actorZ = pOnMobj.pos.z + pOnMobj.height;
return (Max(floorZ, actorZ));
}
// Determine if cat is near the ground/target and
// should end the current leap or not start one.
bool NearTheGround()
{
return Abs(GetGroundHeight() - pos.z) < 1.0;
}
// Start a new leap
void LeapStart()
{
A_FaceTarget();
let vertical = 10.0;
if ((target.pos.z - pos.z > 64) && (ceilingZ - pos.z > 128))
{
vertical = 15.0;
}
let horizontalMult = leapDistance / 2048.0;
let horizontal = 1.0;
for (int i; i < 4; i++)
{
horizontal *= horizontalMult;
}
vel += (Cos(angle) * horizontal, Sin(angle) * horizontal, vertical);
painChance = 0;
inLeap = true;
}
// End the current leap
void LeapEnd()
{
inLeap = false;
painChance = default.painChance;
leapFail = 0;
A_Stop();
SetStateLabel("See");
}
// Leap towards the trarget
void A_LeapToTarget()
{
if (NearTheGround()) // nearly on ground/target
{
if (inLeap) // in a leap now so end it
{
LeapEnd();
}
else // not in a leap so do a new leap
{
LeapStart();
}
}
else // in mid-air so update applicable context
{
if (vel.z > 0)
{
vel += (Cos(angle) * 1.5, Sin(angle) * 1.5, 0);
}
}
// Failsafe to stop the leap if the cat misses
// the target, falls into a pit, or whatever.
leapFail++;
if (leapFail >= 37)
{
LeapEnd();
}
}
// Determine the maximum (2D) distance to the
// target over which the cat can sensibly leap.
double GetLeapDistance()
{
let dst = 2048.0;
if (ceilingZ - pos.z > 128)
{
dst *= 2;
}
return (dst);
}
// Determine if (a) the cat can see the target and
// (b) there is a pit between them and the target.
bool CheckPitBeforeTarget()
{
if (!CheckSight(target) || ceilingZ < (pos.z + height + 32))
{
return (false);
}
int step = int(radius * 0.5);
Vector3 checkDirection = (target.pos - pos).Unit() * step;
int steps = min(Distance2D(target), GetLeapDistance()) / step;
double curZ = pos.z;
SetXYZ((pos.x, pos.y, pos.z + 64));
for (int i; i < steps; i++)
{
double zAt = GetZAt(
pos.x + checkDirection.x * i,
pos.y + checkDirection.y * i,
0,
GZF_AbsolutePos | GZF_AbsoluteAng);
if (curZ - zAt > maxStepHeight * 2 || zAt - curZ > maxStepHeight)
{
SetXYZ((pos.x, pos.y, curZ));
return (true);
}
}
SetXYZ((pos.x, pos.y, curZ));
return (false);
}
// Determine if it is appropriate to attempt to
// leap at the target and, if so, perform the leap.
bool TryLeap(LeapDirections flags)
{
if (!target)
{
return (false);
}
if (inLeap)
{
return false;
}
// Leap if
// * cat is standing
// * random (cat doesn't do this all the time)
// * target has more height difference than maxstepheight
// * or
// * there's a deep pit in front of cat
// * target is within max jump radius
if (
NearTheGround() &&
!Random(0, 16) &&
(
((flags & Leap_Up) && (Abs(target.pos.z - pos.z) > default.maxStepHeight)) ||
((flags & Leap_Forward) && CheckPitBeforeTarget())
) &&
Distance2D(target) < GetLeapDistance()
)
{
leapDistance = Distance2D(target);
SetStateLabel("Leap");
return (true);
}
return (false);
}
// Note that a jump is a (nearly) horizontal leap
// made as an attack on the target, rather than a
// leap that is part of navigation to the target.
// Jump at the target
void A_JumpAtTarget()
{
if (NearTheGround())
// if (((pos.z <= floorZ) || bOnMObj) && (vel.z == 0)) // on ground/target
{
if (inLeap) // in a jump now so end it
{
inLeap = false;
painChance = default.painChance;
A_Stop();
SetStateLabel("See");
}
else // not in a jump so do a new jump
{
A_Speak(Sound_Attack);
A_FaceTarget();
A_ChangeVelocity(vel.x, vel.y, 8);
A_Recoil(-15);
painChance = 0;
inLeap = true;
}
}
else // in mid-air so attack if possible
{
if (CheckMeleeRange())
{
SetStateLabel("Melee");
inLeap = false; // fall after attack
}
}
}
// Determine if it is appropriate to attempt to
// jump at the target and, if so, perform the jump.
bool TryJump()
{
if (!target)
{
return (false);
}
if (inLeap)
{
return false;
}
if ( CheckSight(target) &&
Distance2D(target) <= 256 &&
!CheckPitBeforeTarget() &&
!random(0, 1) &&
ceilingZ > (pos.z + height + 32) &&
(pos.z <= floorZ || bOnMobj) &&
vel.z == 0
)
{
SetStateLabel("Jump");
return true;
}
return false;
}
// Variant of A_Chase that first tries to leap to
// or jump at the target if allowed and possible.
void A_LeapChase(bool canLeap = true, bool canJump = true)
{
if (canLeap && TryLeap(Leap_Both))
{
return;
}
if (canJump && TryJump())
{
return;
}
A_Chase();
}
// Call from user code SPAWN state
void BlackCat_Leaping_Init()
{
inLeap = false;
leapFail = 0;
leapDistance = 0.0;
}
}
- SanyaWaffles
- Posts: 840
- Joined: Thu Apr 25, 2013 12:21 pm
- Preferred Pronouns: They/Them
- Operating System Version (Optional): Windows 11 for the Motorola Powerstack II
- Graphics Processor: nVidia with Vulkan support
- Location: The Corn Fields
- Contact:
Re: Complex A_Chase replacement in ZScript
That code you posted might be useful. I will see about testing it. You got an example of how it is used in States?
Some of it might not be applicable of course, I just want to get an idea for how it'd work as-is. I guess I could look at how your mod works, with your permission of course.
I've played NTMAI before, it was what I was going for, but I was unaware of the licensing and I didn't want to assume.
(BTW I love the concept of your mod - I have a black cat too)
Some of it might not be applicable of course, I just want to get an idea for how it'd work as-is. I guess I could look at how your mod works, with your permission of course.
I've played NTMAI before, it was what I was going for, but I was unaware of the licensing and I didn't want to assume.
(BTW I love the concept of your mod - I have a black cat too)
- MartinHowe
- Posts: 2056
- Joined: Mon Aug 11, 2003 1:50 pm
- Preferred Pronouns: He/Him
- Location: East Suffolk (UK)
Re: Complex A_Chase replacement in ZScript
Sudden interest in this out of nowhere
Sadly, I haven't worked on it for years and given the pace of change in ZScript, it's probably sub-optimal. I don't have the time to do anything with it any more 

