Author Secret Doors: Overdone Solution
Old Man Coyote Posted: Wednesday, 26 June 02:35AM
Secret Door Kludge
Userlevel: Moderate to Advanced
Version: 0.1, working beta

PROBLEM:
no true D&D secret doors in game.

NEEDED FUNCTIONALITY:
1. Truly hidden from view (not clickable, not selectable, don't glow when TAB is held)
2. Emulate 3E/NWN rules for detection:
Skill check is Search vs Hide.
Skill check is halved if not in 'active search'.
Elves are always considered to be in 'active search' mode.
3. Make secret door access available to all players after being found.

WHAT I DIDN'T/COULDN'T ACHIEVE (or, what I'm still wrestling with)
1. An elegant solution. My secret doors are anything but. This is a total kludge.
2. Non-placeable dependant secret doors. Mine currently rely on an object (boulder, bookshelf) as cover.
3. Resetting of secret door.
4. Locked or trapped secret doors. Haven't looked too deeply into changing this.
5. Finding the door/travelling through door animations/sounds/effects. Haven't gotten to this yet.
6. Henchman/animal companion/etc being able to follow PC through door.

FINAL BEHAVIOR:
Character sees non-clickable background object. If the character is nearby and a search roll beats the DC of the secret door, the character receives a message: "You found a secret door!" The previously non-usable object transforms into a selectable object that ANYONE can click on. A message floats over the object: "Secret door found by (playername)".

On clicking the newly obvious secret door, you are transitioned to the other side of the door (which can be anywhere in the module, but likely is nearby).

This doesn't sound too hard, but becomes difficult to achieve because of some lacking functionality in NWNScript. Biggest problem is I can't seem to find out if someone is in 'Active Search' modal state. I've tried everything... everything. If anyone knows of a way that actually WORKS (I've worn out theory), please let me know. Without knowing that, I was forced to use a fake Trap to emulate the searching functionality. Also, not having events like onDetected or onHeartbeat for trap objects was problematic. Not having access to set something USEABLE/UNUSEABLE via script didn't help. All in all, I hurt my head.

But it does work. I love this game.

OK. Making these babies requires some patience. Here's step by step.

=============

SETUP: Making CUSTOM TEMPLATES.

Open an area in the toolset, then make the following custom templates. Do not place any of these into the area yet.

1. Make new CUSTOM WAYPOINT:
(Template for the destination of secret doors.)
Set name and tag to SDOOR_W_ (The BlueprintResRef will be SDOOR_W_ and you shouldn't need to change it.)
Note: I put Appearance Type to RED to distinguish from my normal WPs.

2. Make new CUSTOM PLACEABLE:
(Template for the undetected secret door object.)
Set name and tag to SDOOR_X_
Set the default Appearance Type for your secret doors. Since this is configurable later, I just use Boulder for default.
PLOT must be off; USEABLE must be off; STATIC should be on. Crank the Hardness/HP/Saves of the object to 200 or so.
Nothing else needs to be changed. No description needed.

3. Make new CUSTOM PLACEABLE:
(Template for the detected secret door object.)
Set name and tag to SDOOR_Y_
Set the default Appearance Type. Note this could be different from the one used by SDOOR_X_ but generally is the same object.
PLOT must be off; USEABLE must be on; STATIC will be greyed out.
Everything should be off in LOCK and TRAP. Setting these will break the secret doors!
Feel free to add a simple description along the lines of "This object conceals a secret door"
In the scripts tab, you need to enter the following script under the onUsed event:

Script Name: sdoor_y_use
onUse event for SDOOR_Y_
Purpose: send user to secret door's destination when object is clicked.
NWScript:
void main()

{

    // When used, the sdoor_YObj will send the invoker to the destination.

    object oPlayer = GetLastUsedBy();

    string sSecretDoorID = GetStringRight(GetTag(OBJECT_SELF), 3);

    // Announce in float-text that we're being used.

    SpeakString("Used by " + GetName(oPlayer), TALKVOLUME_TALK);

    // we're sending the player in question (or at this point, an NPC

    // could use the secret door as well) to its SDOOR_W waypoint.

    object oWaypoint = GetObjectByTag("SDOOR_W_" + sSecretDoorID);

    location lWaypoint = GetLocation(oWaypoint);

    AssignCommand(oPlayer, JumpToLocation(lWaypoint));

}



4. Make new CUSTOM TRIGGER -- TRAP
(This template is used to emulate the PC finding the secret door by D&D rules.)
Set Name and Tag to SDOOR_T_
Leave trap type alone (probably set to 'Spike Trap, Average'. It's moot.)
DETECTION DC is important: this is the difficulty of spotting your Secret Door. It can be changed for each secret door you make, so set it to something easy as the default, like 15 or so.
ONE SHOT must be off; DETECTABLE must be on; DISARMABLE should be off.
No scripts are needed.

5. Make new CUSTOM TRIGGER -- GENERIC TRIGGER
(This the template for a "zone" around the secret door that provides needed onEnter/onExit events.)
Name and Tag need to be SDOOR_Z_
The following scripts need to be added:

Script Name: sdoor_z_enter
onEnter event for SDOOR_Z_
Purpose: see if entering PC has detected the fake trap yet.

NWScript:
void main()

{

    // We only care about PCs for now. No NPC support yet, maybe later.

    object oPC = GetEnteringObject();

    if (GetIsPC(oPC))

        {

        // This pulls the three-digit ID (001, 002, ..., 999) for the secret door.

        // This will match the blueprints you made for this trap instance.

        // For this reason alone you should avoid using the stock blueprints

        // SDOOR_W_, SDOOR_T_, SDOOR_Z_, etc. Instead you should copy them so

        // that the tags AND the blueprint ref are SDOOR_W_00n.

        string sSecretDoorID = GetStringRight(GetTag(OBJECT_SELF), 3);

        // Remember the player. We'll use this in the sdoor_z_heart script

        // to keep checking to see if the player has seen the trap yet, and

        // we'll clear it in sdoor_z_exit when the player leaves.

        // Obviously this provides no multiplayer logic, which might be very

        // unworkable. This is a kludge and very very beta.

        SetLocalObject(OBJECT_SELF, "lastEntered", oPC);

        // GetNearestTrapToObject gets the trap from perspective of the

        // player, meaning that it will return OBJECT_INVALID if the PC

        // is unable to see the trap. Therefore, if it returns the correct

        // SDOOR_T_00n sequence, we know the PC found the secret door.

        object oTrap = GetNearestTrapToObject(oPC);

        if ( GetTag(oTrap) == "SDOOR_T_" + sSecretDoorID )

        {

            // Add functionality later to load in text strings.

            SendMessageToPC(oPC, "You found a secret door!");

            // Ditch the Trap, the XObj, and the Trigger Zone (OBJECT_SELF);

            // bring in the YObj.

            object oXObj = GetNearestObjectByTag("SDOOR_X_" + sSecretDoorID, oPC);

            location lHere = GetLocation(oXObj);

            DestroyObject(oTrap);

            DestroyObject(oXObj);

            CreateObject(OBJECT_TYPE_PLACEABLE, "sdoor_y_" + sSecretDoorID,lHere);

            DestroyObject(OBJECT_SELF);

        }

    }

}



Script Name: sdoor_z_exit
onExit event for SDOOR_Z_
Purpose: see if exiting PC detected the fake trap.

NWScript:
void main()

{

    // We only care about PCs for now. No NPC support yet, maybe later.

    object oPC = GetExitingObject();

    if (GetIsPC(oPC))

        {

        // This pulls the three-digit ID (001, 002, ..., 999) for the secret door.

        // This will match the blueprints you made for this trap instance.

        // For this reason alone you should avoid using the stock blueprints

        // SDOOR_W_, SDOOR_T_, SDOOR_Z_, etc. Instead you should copy them so

        // that the tags AND the blueprint ref are SDOOR_W_00n.

        string sSecretDoorID = GetStringRight(GetTag(OBJECT_SELF), 3);

        // Clear the player from the local object "lastEntered" so

        // the sdoor_z_heart script doesn't keep checking.

        DeleteLocalObject(OBJECT_SELF, "lastEntered");

        object oTrap = GetNearestTrapToObject(oPC);

        if ( GetTag(oTrap) == "SDOOR_T_" + sSecretDoorID )

        {

            // Add functionality later to load in text strings.

            SendMessageToPC(oPC, "You found a secret door!");

            // Ditch the Trap, the XObj, and the Trigger Zone (OBJECT_SELF);

            // bring in the YObj.

            object oXObj = GetNearestObjectByTag("SDOOR_X_" + sSecretDoorID, oPC);

            location lHere = GetLocation(oXObj);

            DestroyObject(oTrap);

            DestroyObject(oXObj);

            CreateObject(OBJECT_TYPE_PLACEABLE,"sdoor_y_" + sSecretDoorID, lHere);

            DestroyObject(OBJECT_SELF);

        }

    }

}



Script Name: sdoor_z_heart
onHeartbeat event for SDOOR_Z_
Purpose: see if PC in area has detected the fake trap yet.

NWScript:
void main()

{

    object oPC = GetLocalObject(OBJECT_SELF,"lastEntered");

    if (oPC!=OBJECT_INVALID)

    {

        // This pulls the three-digit ID (001, 002, ..., 999) for the secret door.

        // This will match the blueprints you made for this trap instance.

        // For this reason alone you should avoid using the stock blueprints

        // SDOOR_W_, SDOOR_T_, SDOOR_Z_, etc. Instead you should copy them so

        // that the tags AND the blueprint ref are SDOOR_W_00n.

        string sSecretDoorID = GetStringRight(GetTag(OBJECT_SELF), 3);

        object oTrap = GetNearestTrapToObject(oPC);

        if ( GetTag(oTrap) == "SDOOR_T_" + sSecretDoorID )

        {

            // Add functionality later to load in text strings.

            SendMessageToPC(oPC,"You found a secret door!");

            // Ditch the Trap, the XObj, and the Trigger Zone (OBJECT_SELF);

            // bring in the YObj.

            object oXObj = GetNearestObjectByTag("SDOOR_X_" + sSecretDoorID, oPC);

            location lHere = GetLocation(oXObj);

            DestroyObject(oTrap);

            DestroyObject(oXObj);

            CreateObject(OBJECT_TYPE_PLACEABLE,"sdoor_y_" + sSecretDoorID, lHere);

            DestroyObject(OBJECT_SELF);

        }

    }

}



Whew. OK, the setup is done. These templates are done... you shouldn't have to be touching them directly again.

=============

PLACING: Placing usable secret doors. This sequence must be done for each secret door used in an module.

1. Go to your premade SDOOR_W_ waypoint, and do an "Edit Copy" on it.
Change the NAME and TAG to SDOOR_W_001
(For the next secret door made within the same module, use 002 instead; then 003, etc.)
The BlueprintResRef should automatically read 'SDOOR_W_001' -- check to be sure.

2. Go to the SDOOR_X_ placeable, and do an "Edit Copy".
Change NAME and TAG to SDOOR_X_001 (or 002, 003, etc.)

3. Go to the SDOOR_Y_ placeable, do "Edit Copy"
Change TAG to SDOOR_Y_001
Change NAME to 'Secret Door'

4. Go to the SDOOR_T_ trap, do "Edit Copy"
Change TAG/NAME to SDOOR_T_001
Change the Detect DC to reflect your secret door's DC

5. Go to the SDOOR_Z_ trigger, do "Edit Copy"
Change the TAG/NAME to SDOOR_Z_001

6. Put a SDOOR_W_001 waypoint into your area. Place it where you want your secret door's destination to be.

7. Put a SDOOR_X_001 object into your area. Place it where the secret door is to be found.

8. Draw a SDOOR_T_001 trap so that the SDOOR_X_001 object entirely covers it. I generally just make a tiny square trap.

9. Draw a SDOOR_Z_001 trigger zone in a circle around the SDOOR_X_001 object. As for size, it should have a radius of about 3 in-game meters or so... not too large.

You do not have to place a SDOOR_Y_001 object; it's generated automatically.

=============

And you're done. Build, save, load, use!

Hope this isn't too bizarre & kludgey. I'll be working on improvements and welcome comments.