Jump to content
  • 0

how to make an actor that can appear in the next level


chinegun

Question

1 answer to this question

Recommended Posts

  • 0

This is quite hard to do and requires advanced programming knowledge and a source port that supports at least ACS and either DECORATE or DDF/EDF. If this is for a mapset, you can probably get away with it like that; if it must be generic, it is best done using ZScript. The way I did this in the Black Cats of Doom is to have the OnDestroy method in a subclass notify a pure data class that is created as a static thinker (persists across level change) of the health each animal has when destroyed; non-zero will mean it was destroyed other than by being killed, so likely level end. Startup code then checks for valid actors and recreates them.

 

The code for the data object is below and this is just part of it, but I hope it will give you a flavour of the sort of logic required. You can download the project here to see how it all works, rip bits of it if you like, but please bear in mind the project is unfinished (though the "following player across level change" code is final, hopefully).

 


// Martin Howe's "Back Cat" - An NPC mod for ZScript.
// Implements a persistent but partial list of all cats.
// 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

Class BlackCatsToken : Inventory
{
    override void Travelled()
    {
        if (owner && owner.player)
        {
            int playerNumber = PlayerPawn(owner).PlayerNumber();
            BlackCatMapTracker.Get().RecreateCats(playerNumber);
        }
    }
}

class BlackCatMapTrack
{
    // Saved data about each cat
    class <BlackCat> catClass;
    int startHealth;
    int currentHealth;
    int friendPlayer;
    uint catNumber;
    int hubNumber;
    bool followToMap;
    bool followInHub;
    bool followToHub;

    // Only used during recreation; identifies an
    // original instance, identified by global cat
    // number, when returning to a hub level.
    BlackCat original;

    // Initialises a new instance of the class
    BlackCatMapTrack Init(BlackCat aCat)
    {
        catClass = aCat.GetClass();
        startHealth = aCat.StartHealth;
        currentHealth = aCat.Health;
        friendPlayer = aCat.FriendPlayer;
        catNumber = aCat.globalCatNumber;
        hubNumber = (Level.ClusterFlags & CLUSTER_HUB) ? Level.Cluster : -1;
        followToMap = aCat.bFollowPlayerToMap;
        followInHub = aCat.bFollowPlayerInHub;
        followToHub = aCat.bFollowPlayerToHub;
        original = null;
        return self;
    }
}

class BlackCatMapTracker : Thinker
{
    // Global cat tracking ID
    protected uint globalCatID;

    // Gets the next cat tracking ID
    uint GetNextCatTrackingID()
    {
        globalCatID++;
        return globalCatID;
    }

    // List of cats to be tracked
    protected Array<BlackCatMapTrack> cats;

    // Changes to list since last purge
    protected uint changes;

    // Changes in list allowed before next purge
    const purgeThreshold = 8;

    // Level time of last cat destructor call
    int levelTime;

    // Returns the number of cats being tracked
    uint Size()
    {
        return cats.Size();
    }

    // Removes all cats from the collection
    void Clear()
    {
        levelTime = level.maptime;

        let count = cats.size();

        // GC?
        for (uint index = 0; index < count; index++)
        {
            cats[index] = null;
        }

        cats.Clear();
        changes = 0;
    }

    // Checks for trailing null entries in the
    // list and removes any that are found.
    //
    // This is a simple form of GC, used to keep the
    // list size to within reasonable limits.
    protected void Purge()
    {
        // Get the number of cats
        let count = cats.size();

        // If there are none, there's nothing to do
        if (count == 0)
        {
            return;
        }

        // Count null entries
        let removedCount = 0;
        for (uint index = 0; index < count; index++)
        {
            if (!cats[index])
            {
                removedCount++;
            }
        }

        // If all cats were removed then it is done
        if (removedCount == count)
        {
            cats.Clear();
            return;
        }

        // If no cats were removed then it is done
        if (!removedCount)
        {
            return;
        }

        // We now know that there is at least one each
        // of null and non-null entries and thus that
        // count and removedCount are both non-zero.

        // Find the first null entry. Due to the above,
        // we know this code block will *always* exit
        // with a valid firstNull index in the array.
        let firstNull = 0;
        while (firstNull < count)
        {
            if (!cats[firstNull])
            {
                break;
            }
            firstNull++;
        }
        if (firstNull == count)
        {
            ThrowAbortException("**** FATAL: EINVAL(BlackCatMapTracker.Purge: condition \"\'firstNull\' (%u) == \'count\' (%u)\" should be impossible)", firstNull, count);
        }

        // Purge null entries
        let destination = firstNull;
        let source = firstNull + 1;
        while (source < count)
        {
            // Find the next non-null entry (if any)
            while ((source < count) && !cats[source])
            {
                source++;
            }

            // If reached the end then it is completed
            if (source == count)
            {
                break;
            }

            // Move the entry down
            cats[destination] = cats[source];
            destination++;
            source++;
        }

        // Destination is the *index* of the next
        // *empty* entry (if any); so at the end, it
        // is also the *count* of *non-empty* entries.
        if (destination != (count - removedCount))
        {
            ThrowAbortException("**** FATAL: EINCON(BlackCatMapTracker.Purge: condition \"\'destination\' (%u) != \'count\' (%u) - \'removedCount\' (%u)\" should be impossible)", destination, count, removedCount);
        }

        // Shrink the array and exit
        cats.Resize(destination);
        return;
    }

    // Cat uses this to notify the tracker that a cat
    // is about to be destroyed and that enough of its
    // properties should be saved to recreate it when
    // the next level is started. This method treats
    // dead cats as a fatal error; only live cats
    // should be trying to follow the player across
    // level changes (i.e., via end-level teleporter).
    //
    // The time of the cat notification is checked, to
    // avoid doing this for cats manually removed from
    // the map via a script; cats destroyed with a
    // higher level time remove previous timed cats,
    // so that ultimately only cats destroyed due to
    // level end remain; if there is any possibility
    // that a cat destroyed by a script could be the
    // last cat destroyed, the script should clear
    // the bFollowPlayerToLevels flag on the cat.
    //
    // This also means that by the time Travelled() is
    // called on any CatsToken, the list is finalised
    // all of the cats in it are from the same level.
    //
    void NotifyCatDestroying(BlackCat aCat)
    {
        if (!aCat) { ThrowAbortException("**** FATAL: EISNUL(BlackCatMapTracker.NotifyCatDestroying: argument \'aCat\' (null) should specify an \'Actor\'"); }

        if (!(aCat is "BlackCat")) { ThrowAbortException("**** FATAL: ERANGE(BlackCatMapTracker.NotifyCatDestroying: argument \'aCat\' (of class: \"%s\")should specify an actor derived from \'BlackCat\'", aCat.GetClassName()); }

        if (aCat.Health < 1) { ThrowAbortException("**** FATAL: ERANGE(BlackCatMapTracker.NotifyCatDestroying: argument \'aCat\' (of health: %d) should specify a cat that is alive", aCat.Health); }

        if (level.maptime > levelTime)
        {
            self.Clear();
        }

        cats.Push(new("BlackCatMapTrack").Init(aCat));
        changes++;

        if (changes > purgeThreshold)
        {
            self.Purge();
            changes = 0;
        }
    }

    // Spawns the cats in a specific cats list in
    // front of the player, spreading them around as
    // far as necessary to spawn them, while taking
    // care to ensure they do not spawn in the void.
    const yspread = 256;
    void SpawnCats(actor player, array<BlackCatMapTrack> catsToSpawn)
    {
        uint count = catsToSpawn.Size();

        if (!count)
        {
            return;
        }

        int yofs = -(yspread / 2);
        int ystep = yspread / count;

        double a = player.angle;
        double s = sin(a);
        double c = cos(a);
        double z = player.pos.z;

        // Offset code adapted from that in A_SpawnItemEx

        for (int cat = 0 ; cat < count ; cat++)
        {
            BlackCatMapTrack track = catsToSpawn[cat];

            // Destroy the original cat, if any, so
            // that the "clone" seems in-game to have
            // followed the player back to the level.
            if (track.original)
            {
                BlackCat oCat = track.original;
                oCat.bFollowPlayerToMap = false;
                oCat.bFollowPlayerInHub = false;
                oCat.bFollowPlayerToHub = false;
                oCat.Destroy();
            }

            // Recreate the cat
            for (int attempt = 0; attempt < 32; attempt++)
            {
                int xofs = 32 * attempt;

                vector3 newpos = player.Vec2OffsetZ(xofs * c + yofs * s, xofs * s - yofs * c, z);
                if (Level.IsPointInLevel(newpos))
                {
                    bool wasSpawned;
                    actor whatWasSpawned;
                    [wasSpawned, whatWasSpawned] = player.A_SpawnItemEx(track.catClass, xofs, yofs, 0);
                    if (wasSpawned && whatWasSpawned)
                    {
                        BlackCat aCat = BlackCat(whatWasSpawned);
                        aCat.StartHealth = track.startHealth;
                        aCat.Health = track.currentHealth;
                        aCat.FriendPlayer = track.FriendPlayer;
                        aCat.bSpawnWithTeleportFog = true;
                        aCat.globalCatNumber = track.catNumber;
                        break;
                    }
                }
            }

            yofs = yofs + ystep;
        }
    }

    // Clones each cat belonging to the specified
    // player; in game it looks like the same cats;
    // the data is removed from the list after use.
    void RecreateCats(int playerNumber)
    {
        if (playerNumber >= MAXPLAYERS) { ThrowAbortException("**** FATAL: ERANGE(BlackCatMapTracker.RecreateCats: argument \'playerNumber\' (index %d) should be less than MAXPLAYERS (%d)", playerNumber, MAXPLAYERS); }

        Purge();

        uint count = cats.Size();

        if (!count)
        {
            return;
        }

        // If any cats on this new level have the same
        // global cat number as any in the collection,
        // means we are returning to a level in a hub
        // from one in the same hub and their original
        // instances have been recreated by the game.
        //
        // This cannot happen between hubs or non-hub
        // levels, as everything on the previous level
        // is already discarded by the game; thus this
        // only needs checking if this is a hub level.
        //
        // Strictly, we should check the cat for the
        // same hub number as well, but since we would
        // still need to check the global cat number,
        // which is sufficient by itself, doing so is
        // pointless and incurs a performance penalty.
        //
        // In these cases, the original instances must
        // be destroyed, so that the "clones" seem to
        // have followed the player back to the level.
        if (Level.ClusterFlags & CLUSTER_HUB)
        {
            ThinkerIterator catScanner = ThinkerIterator.Create("BlackCat", STAT_DEFAULT);
            let somePerson = actor(catScanner.Next());
            while (somePerson)
            {
                BlackCat aCat = BlackCat(somePerson);
                for (uint i = 0; i < count; i++)
                {
                    BlackCatMapTrack track = cats[i];
                    if (track.catNumber == aCat.globalCatNumber)
                    {
                        track.original = aCat;
                        break;
                    }
                }
                somePerson = actor(catScanner.Next());
            }
        }

        array <BlackCatMapTrack> catsToSpawn;
        for (uint i = 0; i < count; i++)
        {
            BlackCatMapTrack cat = cats[i];
            if (cat.friendPlayer == 1 + playerNumber)
            {
                if ( (cat.followToMap && (cat.hubNumber == -1)) ||
                     (cat.followInHub && (cat.hubNumber == Level.Cluster)) ||
                     (cat.followToHub)
                    )
                {
                    catsToSpawn.Push(cats[i]);
                    cats[i] = null;
                }
            }
        }

        Purge();

        SpawnCats(players[playerNumber].mo, catsToSpawn);
    }

    // Initialises a new instance of the class
    protected BlackCatMapTracker Init()
    {
        globalCatID = 0;
        ChangeStatNum(STAT_STATIC);
        self.Clear();
        return self;
    }

    // Gets the (only) instance of the class;
    // creates it if it does not exist, unless
    // only required to check if it has been.
    static BlackCatMapTracker Get(bool checkOnly = false)
    {
        ThinkerIterator i = ThinkerIterator.Create("BlackCatMapTracker", STAT_STATIC);
        let p = BlackCatMapTracker(i.Next());
        if (!p && !checkOnly)
        {
            p = new("BlackCatMapTracker").Init();
        }
        return p;
    }
}

 

Share this post


Link to post

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Answer this question...

×   Pasted as rich text.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...