Complex A_Chase replacement in ZScript

Ask about ACS, DECORATE, ZScript, or any other scripting questions here!

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.

Complex A_Chase replacement in ZScript

Postby SanyaWaffles » Wed Apr 08, 2020 12:46 am

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.
User avatar
SanyaWaffles
Now I've awoken, and I'm taking back control.
 
Joined: 25 Apr 2013
Location: Eastern Ohio
Discord: SanyaWaffles#5095
Twitch ID: sanyawaffles
Operating System: Windows 10/8.1/8/201x 64-bit
OS Test Version: No (Using Stable Public Version)
Graphics Processor: nVidia with Vulkan support

Re: Complex A_Chase replacement in ZScript

Postby Misery » Wed Nov 25, 2020 2:24 pm

I found a ZScript for A_SmartChase which supposedly modifies A_Chase behavior when player is out of line of sight. Is that what you're looking for?
User avatar
Misery
 
Joined: 04 Nov 2018

Re: Complex A_Chase replacement in ZScript

Postby MartinHowe » Thu Nov 26, 2020 7:07 am

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
Code: Select allExpand view
// 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;
    }
}


Jump/Leap
Code: Select allExpand view
// 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;
    }
}
User avatar
MartinHowe
In space, no-one can hear you KILL an ALIEN
 
Joined: 11 Aug 2003
Location: Waveney, United Kingdom

Re: Complex A_Chase replacement in ZScript

Postby SanyaWaffles » Thu Nov 26, 2020 9:18 am

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)
User avatar
SanyaWaffles
Now I've awoken, and I'm taking back control.
 
Joined: 25 Apr 2013
Location: Eastern Ohio
Discord: SanyaWaffles#5095
Twitch ID: sanyawaffles
Operating System: Windows 10/8.1/8/201x 64-bit
OS Test Version: No (Using Stable Public Version)
Graphics Processor: nVidia with Vulkan support


Return to Scripting

Who is online

Users browsing this forum: No registered users and 0 guests