Secret Door Solution

Ok so, while building my module, I realized that there was a bit of an issue with how secret doors function, and not a whole lot of answers out there to clearly address the subject. Well, after long hours of tinkering, and asking for help within the community, we’ve finally managed to put together a working system for secret doors, that functions much better than the original. Ok so, I realized that the original system for secret doors, while it functioned, would only send one, maybe two henchmen through the door with you, and any others would get stuck behind the wall. This was sort of an issue for me, as I’m building a module designed for a full party of up to six. Anyway, here is the full solution I’ve put together along with the help of our awesome community.

Ok so, I’m using the invisible object method, not the built in floor trigger method. I believe I got the hiddenwalldoor and the hiddenwalldoortrigger from the hakpak that the original bioware tutorials suggest. However, this hakpak ripped out alot of cool placeables I was using from my module, so I had to get rid of it. Incredibly though, the secret doors and their triggers remained, allowing me to still use this system. And actually, You probably wouldn’t even need to download the hakpak at all, because it’s the scripts that count. And you could probably create an invisible object of your own to spawn your own custom doors as well.

So, with that being said, here are the scripts I used to make all of this happen.

Here is the script that is placed within the OnHeartbeat of the invisible object to spawn the door of your choice once detected. Keep in mind, this script requires the resref of the door you want to spawn, not the tag.

//::///////////////////////////////////////////////
//:: nw_o2_dtwalldoor.nss
//:: Copyright (c) 2001-2 Bioware Corp.
//:://////////////////////////////////////////////
/*
    This script runs on either the Hidden Trap Door
    or Hidden Wall Door Trigger invisible objects.
    This script will do a check and see
    if any PC comes within a radius of this Trigger.

    If the PC has the search skill or is an Elf then
    a search check will be made.

    It will create a Trap or Wall door that will have
    its Destination set to a waypoint that has
    a tag of DST_<tag of this object>

    The radius is determined by the Reflex saving
    throw of the invisible object

    The DC of the search stored by the Willpower
    saving throw.

*/
//:://////////////////////////////////////////////
//:: Created By  : Robert Babiak
//:: Created On  : June 25, 2002
//::---------------------------------------------
//:: Modifyed By : Robert, Andrew, Derek
//:: Modifyed On : July - September
//:://////////////////////////////////////////////

void main()
{
    // get the radius and DC of the secret door.
    float fSearchDist = IntToFloat(GetReflexSavingThrow(OBJECT_SELF));
    int nDiffaculty = GetWillSavingThrow(OBJECT_SELF);

    // what is the tag of this object used in setting the destination
    string sTag = GetTag(OBJECT_SELF);

    // has it been found?
    int nDone = GetLocalInt(OBJECT_SELF,"D_"+sTag);
    int nReset = GetLocalInt(OBJECT_SELF,"Reset");

    // ok reset the door is destroyed, and the done and reset flas are made 0 again
    if (nReset == 1)
    {
        nDone = 0;
        nReset = 0;

        SetLocalInt(OBJECT_SELF,"D_"+sTag,nDone);
        SetLocalInt(OBJECT_SELF,"Reset",nReset);

        object oidDoor= GetLocalObject(OBJECT_SELF,"Door");
        if (oidDoor != OBJECT_INVALID)
        {
            SetPlotFlag(oidDoor,0);
            DestroyObject(oidDoor,GetLocalFloat(OBJECT_SELF,"ResetDelay"));
        }

    }


    int nBestSkill = -50;
    object oidBestSearcher = OBJECT_INVALID;
    int nCount = 1;

    // Find the best searcher within the search radius.
    object oidNearestCreature = GetNearestCreature(CREATURE_TYPE_PLAYER_CHAR, PLAYER_CHAR_IS_PC);
    int nDoneSearch = 0;
    int nFoundPCs = 0;

    while ((nDone == 0) &&
           (nDoneSearch == 0) &&
           (oidNearestCreature != OBJECT_INVALID)
          )
    {
        // what is the distance of the PC to the door location
        float fDist = GetDistanceBetween(OBJECT_SELF,oidNearestCreature);

        if (fDist <= fSearchDist)
        {
            int nSkill = GetSkillRank(SKILL_SEARCH,oidNearestCreature);

            if (nSkill > nBestSkill)
            {
                nBestSkill = nSkill;
                oidBestSearcher = oidNearestCreature;
            }
            nFoundPCs = nFoundPCs +1;
        }
        else
        {
            // If there is no one in the search radius, don't continue to search
            // for the best skill.
            nDoneSearch = 1;
        }
        nCount = nCount +1;
        oidNearestCreature = GetNearestCreature(CREATURE_TYPE_PLAYER_CHAR, PLAYER_CHAR_IS_PC, OBJECT_SELF ,nCount);
    }

    if ((nDone == 0) &&
        (nFoundPCs != 0) &&
        (GetIsObjectValid(oidBestSearcher))
       )
    {
       int nMod = d20();

            // did we find it.
       if ((nBestSkill +nMod > nDiffaculty))
       {
            location locLoc = GetLocation (OBJECT_SELF);
            object oidDoor;
            // yes we found it, now create the appropriate door
            oidDoor = CreateObject(OBJECT_TYPE_PLACEABLE,"hiddendoorresref",locLoc,TRUE);

            SetLocalString( oidDoor, "Destination" , "DST_"+sTag );
            // make this door as found.
            SetLocalInt(OBJECT_SELF,"D_"+sTag,1);
            SetPlotFlag(oidDoor,1);
            SetLocalObject(OBJECT_SELF,"Door",oidDoor);

       } // if skill search found
    } // if Object is valid
}

And here is the script to place in the OnUsed of your chosen door.
This script uses the tag of your waypoint destination.

//::///////////////////////////////////////////////
//:: Secret door that takes you to a waypoint that
//:: is stored into the Destination local string.
//:: FileName x2_use_secrtdoor
//:: Copyright (c) 2001 Bioware Corp.
//:://////////////////////////////////////////////
/*

*/
//:://////////////////////////////////////////////
//:: Created By: Robert Babiak
//:: Created On: June 25, 2002
//:://////////////////////////////////////////////
//:: Modified By: Andrew Nobbs
//:: Modified On: September 23, 2002
//:: Modification: Removed unnecessary spaces.
//:://////////////////////////////////////////////

void MoveFollowers(object oPC, location lTarget)
{
    int nType, nCount;
    object oNPC;
    for(nType = 1; nType <= 5; nType++)
    {
        // More than one
        if(nType == ASSOCIATE_TYPE_SUMMONED ||
           nType == ASSOCIATE_TYPE_HENCHMAN)
        {
            nCount = 1;
            oNPC = GetAssociate(nType, oPC, nCount);
            while(GetIsObjectValid(oNPC))
            {
                SendMessageToPC(oPC, "Moving your associate: " + GetName(oNPC));
                AssignCommand(oNPC, ClearAllActions());
                AssignCommand(oNPC, JumpToLocation(lTarget));
                MoveFollowers(oNPC, lTarget);
                nCount++;
                oNPC = GetAssociate(nType, oPC, nCount);
            }
        }
        // Just one
        else
        {
            oNPC = GetAssociate(nType, oPC);
            if(GetIsObjectValid(oNPC))
            {
                SendMessageToPC(oPC, "Moving your associate: " + GetName(oNPC));
                AssignCommand(oNPC, ClearAllActions());
                AssignCommand(oNPC, JumpToLocation(lTarget));
            }
        }
    }
}

void main()
{
    object oidUser;
    object oidDest;
    string sDest;

    if(!GetLocked(OBJECT_SELF))
    {
        if(GetIsOpen(OBJECT_SELF))
        {
            sDest = GetLocalString(OBJECT_SELF,"Destination");

            oidUser = GetLastUsedBy();
            oidDest = GetObjectByTag("DST_TagofWaypoint");
            SendMessageToPC(oidUser, "Moving you: " + GetName(oidUser) + " to destination " + GetName(oidDest));
            SignalEvent(oidDest, EventUserDefined(101)); // reveal the secret door on the other side
            AssignCommand(oidDest, ActionOpenDoor(oidDest));
            AssignCommand(oidUser,ActionJumpToObject(oidDest,FALSE));

            MoveFollowers(oidUser, GetLocation(oidDest));
        }
        else
        {
            PlayAnimation(ANIMATION_PLACEABLE_OPEN);
        }
    }
}

So basically, what helps me to remember this method is thinking “Trigger spawns door, door jumps player”.
And I think that’s about it! Hopefully this clears up any confusion anyone is having with secret doors, cause I know it was a doozy for me. Anyway, good luck. And happy modding!

-Aura

2 Likes

Recently I’ve been using a minimal method:

  • Relocate a No Visible Door transition in the wall
  • Hide it with the cutscene invisibility effect
  • Remove the effect on detection
1 Like

Hello,

sounds intersting, but a few more details would be nice. How do you get a “No Visible Door transition” to any wall. Is that EE-Only?? A special placeable?

If you wish for your code to be readable on this forum, post it as code - like this:

[code]
paste your code here
[/code]

You can edit your post. Note: this message is a canned reply.

Additional code posting guidelines (click here to expand).
  1. Split different scripts into separate [code] sections for extra readability.
  2. You can use ``` instead of [code] and [/code] (also in separate lines).
  3. If NWScript is not detected by syntax highlighting, force it with ```nwscript in first line.
  4. Write inline code like this: `short code fragment` ([code] works too).
  5. Hide details function (:gear: icon of the post editor) can collapse code sections, significantly reducing the post size. Select text (including code tags), then choose that option. Result:
my_script
void main()
{
    // this is code of my_script
}

Ad rem

I also have a piece of code that handles hidden door detection & spawning. Made it as an exercise, but it’s fully functional and quite robust.

Unlike the BW method, it allows all members of PC faction to detect secret doors (read: PCs and their assocs), avoids running with no PCs in the area and prevents detection from behind walls, while staying compatible with vanilla transport script (nw_o2_trapdoor).

I can share it if anyone is interested.

We also have a few (well, at least one) solutions on the forum for intra-area party transition.

Thank you. I was wondering about how to do that. And yes, that sounds very interesting. I’d love to see it.

I’m not sure. I havn’t messed with those types of transitions very much yet. But I’ll tinker around with it and see. If I find anything out about them, I’ll be sure and let you know. For now though at least, full party transition through secret doors has been demystified :slight_smile:

@Mmat Check out Hidden Wall Door and Hidden Wall Door Trigger placeables in the toolset (misc interior category). Second spawns the first.

@AuraShift Yeah, the [code] tag is sadly not present in the post editor, causing some trouble with the script code display.

Here's the code (save as HDT's OnHeartbeat).
// By NWShacker, released 2021-04-03 (please credit me if you use this code)
// OnHeartbeat for a hidden wall door trigger
// Detectable by PCs and their associates
// Script compatible with nw_o2_trapdoor
// Reflex save - detection radius
// Will save - detection DC

// placeable ResRef (created after detection)
const string HIDDEN_PLC = "nw_pl_hiddendr01";
// transition variable name (for above placeables)
const string HIDDEN_DST = "Destination";
// detection VFX style (set to VFX_NONE to ignore)
const int    HIDDEN_VFX = VFX_DUR_GLOW_LIGHT_YELLOW;
// detection VFX duration (set to 0.0 to ignore)
const float  HIDDEN_DUR = 3.0;

void main()
{
    object oPC;
    object oCreature;
    object oDoor1;
    object oDoor2;
    string sTag;
    float fRadius;
    int iDC;

    // get door detection radius from its reflex saving throw
    fRadius = IntToFloat(GetReflexSavingThrow(OBJECT_SELF));

    // if radius is 0.0, this door is not detectable
    // (useful for creation of passive exit node)
    if(fRadius == 0.0)
    {
        return;
    }

    // find any PC in the area, quit if there are none for performance
    oPC = GetNearestCreature(CREATURE_TYPE_PLAYER_CHAR, PLAYER_CHAR_IS_PC);
    if(!GetIsObjectValid(oPC))
    {
        return;
    }

    // get door detection DC from its will saving throw
    iDC = GetWillSavingThrow(OBJECT_SELF);

    // iterate all seen creatures in a sphere around the door, check if any
    // of them is a member of PC faction (a PC or its associate), is detecting
    // and can successfully detect the door
    oCreature = GetFirstObjectInShape(SHAPE_SPHERE, fRadius,
        GetLocation(OBJECT_SELF), TRUE);

    // this loop is ugly, but it works
    while(GetIsObjectValid(oCreature) && !(
        GetFactionEqual(oCreature, oPC) &&
        GetActionMode(oCreature, ACTION_MODE_DETECT) &&
        GetSkillRank(SKILL_SEARCH, oCreature) + d20() >= iDC))
    {
        oCreature = GetNextObjectInShape(SHAPE_SPHERE, fRadius,
            GetLocation(OBJECT_SELF), TRUE);
    }

    // exit if nobody has detected the door
    if(!GetIsObjectValid(oCreature))
    {
        return;
    }

    // make the detector say something to get player's attention
    AssignCommand(oCreature, PlayVoiceChat(VOICE_CHAT_LOOKHERE));

    // get tag of self
    sTag = GetTag(OBJECT_SELF);

    // replace two objects with above tag with new door placeables, connect
    // these placeables with tags and local variables, and play detection VFX
    // (iDC variable is recycled here for an insignificant performance gain)
    for(iDC = 1; iDC >= 0; iDC--)
    {
        oDoor1 = GetObjectByTag(sTag, iDC);
        oDoor2 = CreateObject(OBJECT_TYPE_PLACEABLE, HIDDEN_PLC,
            GetLocation(oDoor1), FALSE, sTag + "_" + IntToString(iDC));
        SetLocalString(oDoor2, HIDDEN_DST, sTag + "_" + IntToString(!iDC));
        ApplyEffectToObject(DURATION_TYPE_TEMPORARY,
            EffectVisualEffect(HIDDEN_VFX), oDoor2, HIDDEN_DUR);
        DestroyObject(oDoor1);
    }
}

I use Get*ObjectInShape and GetFactionEqual to filter the possible detectors. This code can also spawn a single door for one-way transfer.

2 Likes

That is a very nice piece of code there. I’ll for sure try this out.

One more thing - how to use this.

You will need to spawn two triggers with same tag, say HIDDEN. They will be replaced with hidden doors with tags HIDDEN_1 and HIDDEN_0 respectively. To create a one-way door, spawn one HIDDEN trigger and create any other object (a waypoint for example) with HIDDEN_1 tag.

Giving any trigger a 0 reflex save prevents it from being detected, like in vanilla NWN, but it can still become a door. This could be replaced with a local variable to add some script-based control over the detection status.

1 Like

@Mmat to relocate a door anywhere, drop it in a doorway tile (which can be temporary) then Adjust Location by right-clicking in the list of area objects on the left hand side of the toolset.

This works in all versions of NWN.

Thanks, now it is clear. I’ll try that.

Another trick I learned from the Discord channel ( Jasperre ) is the new script command SetObjectVisualTransform(). I use it on placeable NPCs resting on beds and cots to appear at night and wake up during the day. I stopped using CutSceneInvisibility to hide the placeables during the day as it was flaky on placeables I found.

Just a snippet of the command.

// this loop would fire during the nighttime to make placeables appear where they are placed in the toolset.

  while (GetIsObjectValid(oInvisDayNPC))
  {
    SetObjectVisualTransform(oInvisDayNPC, OBJECT_VISUAL_TRANSFORM_TRANSLATE_Z, 0.0);
    oInvisDayNPC = GetObjectByTag("SLEEP_DAY250B9", d++);
  }

// and this loop would hide the npcs on the z-axis during the day to simulate them being awake and elsewhere.

while (GetIsObjectValid(oSleepNPCDay))
  {
    SetObjectVisualTransform(oSleepNPCDay, OBJECT_VISUAL_TRANSFORM_TRANSLATE_Z, -5.0);
    oSleepNPCDay=GetObjectByTag("SLEEP_DAY250B9", e++);
  }

Just another way to do secret stuff. I also use this idea on triggers and do DC spot checks against the PC’s Spot to see if they notice anything out of the ordinary and reveal it using the same method when they pass the check.

FP!

1 Like

@Fester_Pot As it happens, cutscene invisibility works fine for doors and creatures.

You’re right, there is an issue with placeables, which is documented (with solution) under Known Bugs with EffectVisualEffect.

Another trick to hide a placeable is to apply VFX_COM_UNLOAD_MODEL to it.

This is useful to have a visible plc in toolset but invisible in runtime. Naturally it should applied to incorporeal plcs, like arrows (although their facing vector is towards model’s back :man_facepalming:).

1 Like

@NWShacker
Interested by your suggestion, I tried this.
Yet it unloads the model, but I haven’t got any success on removing the effect…

effect eEffect = EffectVusualEffect(VFX_UNLOAD_MODEL); ApplyEffectToObject(DURATION_TYPE_INSTANT, eEffect, OBJECT_SELF);

I tried all duration types and added variants of durations floats to ApplyEffectToObject, but none of them could be removed after a successful search.

Is there a trick, or does it not work like I try it?

Thx for tips, dunahan

Sorry, I forgot to add that the effect is unremovable via script (it is inaccessible by effect enumeration functions regardless of duration type). However it auto-removes itself when you reload the area (or load a savegame - same thing).

This means you’d need to apply it to all recipients every time area’s OnEnter fires for a PC if you want them to stay hidden.

It is also useful when you want to DestroyObject something without the fadeout animation. Just apply the effect first and you have an “instant deletion” :ghost: :dash:

1 Like