Easiest way to get PC to narrate bits of story?

I’m now starting to run before I can walk !

I’m making a module where I’d like the PC to kinda narrate bits of his back story as he makes his way back to his home. By that I suppose I mean basically speaking to himself and the text appears either at the top/bottom of screen. I’m thinking that it would have to be done by triggers, maybe at points along the road ?, but I’m fearing an excursion into scripting which makes my inability to place a simple door look reasonable ! The mere sight of a bracket draws a nose bleed, I’m afraid.

Is there either a set way to achieve this or if scripting is a must is there a script somewhere I could copy or even a very easy tutorial on the matter ?

On a related but less important note . . . there appears to be a (very limited) ability for characters to speak out loud. How does this happen?

1 Like

Fortunately, you are surrounded by people who do know NWScript, and can pass you the bits and pieces of code you need, along with explanations for how they work, wherefore you need never fear the wicked and vicious curse of the brackets. :smiley: WHY RUN WHEN YOU CAN FLY, GOOD FELLOW!

- Setting up OnEnter events for floor triggers.

Draw a new generic floor trigger on the ground.

image

Create a new script in the floor trigger’s OnEnter event.

image

When any object enters the trigger, the code in this event will run. Because of this, you will need to add abort checks to ensure that the specified events only happen for player characters, and possibly only happen once per player character, or only once at all.

Within void main, declare an object, and define it as the entering object:

void main()
{
    // We declare the object oPC, and define it as the entering object.
    object oPC = GetEnteringObject();

    // If the entering object is not a player chacter, abort the script at this point.
    if (!GetIsPC(oPC))
        return;

    // If a local integer has been set on the floor trigger, unique to the entering object, abort the script at this point.
    if (GetLocalInt(OBJECT_SELF, ObjectToString(oPC)))
        return;
    // Store the local integer for this entering object on the floor trigger, so that the trigger will not work next time.
    SetLocalInt(OBJECT_SELF, ObjectToString(oPC), TRUE);

    // Do stuff here. For example, speak a string.
    AssignCommand(oPC, SpeakString("I am apparently a player character who is entering this trigger for the first time!"));
}

Mind, there are many ways to do “once only” checks, and “the best” way to do them varies. A lot of stuff that’s harmless in singleplayer modules can cause problems in multiplayer or PW ones.


You can pick and choose your favourites among the ways to present the conversation:

- Having the PC speak strings.
AssignCommand(oPC, SpeakString("I'm walking around and talking to myself!"));

You can also delay assigning the command a bit:

AssignCommand(oPC, SpeakString("I'm walking around and talking to myself!"));
DelayCommand(1.0, AssignCommand(oPC, SpeakString("Daa daa doo dee doo!")));
- Sending text messages to the PC.
SendMessageToPC(oPC, "I am monologuing internally because I can.");
- Having brief floating text strings appear over the PC and immediately fade away again.
FloatingTextStringOnCreature("You smell cookies. Mmmm!", oPC);
- Having the PC start a conversation with themselves.
// Clear the PC's action queue, aborting whatever they are currently doing.
// The TRUE parameter specifies that the PC should be taken out of combat state as well.
AssignCommand(oPC, ClearAllActions(TRUE));
AssignCommand(oPC, ActionStartConversation(oPC, "thisisthenameoftheconversationtostart"));
- Having the PC start a conversation with a spawned-in "narrator" invisible object.

To do this, you need to create a blueprint for a placeable. The ResRef of the placeable blueprint will be used by CreateObject to spawn in the placeable.

// Here, we are spawning a placeable using the blueprint with the resref "narrator", at the current location of object oPC.
object oNarrator = CreateObject(OBJECT_TYPE_PLACEABLE, "narrator", GetLocation(oPC));
AssignCommand(oPC, ClearAllActions(TRUE));
AssignCommand(oPC, ActionWait(0.5));
AssignCommand(oPC, ActionStartConversation(oNarrator, "narrator001"));


This is how an example OnEnter event script for a one-size-fits-all "When the PC enters this trigger for the first time, they should say something"-trigger could look, in it's entirety*:
void main()
{
    object oPC = GetEnteringObject();
    string sIdentifier = ObjectToString(oPC);

   // Abort if the entering object is not a PC, or if the entering object has passed this trigger already.
   if (!GetIsPC(oPC) || GetLocalInt(OBJECT_SELF, sIdentifier))
       return;

    // Store the entering object on the trigger, to be able to check whether it has already passed the trigger.
    SetLocalInt(OBJECT_SELF, sIdentifier, TRUE);

    // Cycle through local strings stored on the trigger...
    int i;
    string sSpeak = GetLocalString(OBJECT_SELF, "SPEAK_0");
    while (sSpeak != "")
        {
        // ... and make the entering object speak them.
        DelayCommand(IntToFloat(i), AssignCommand(oPC, SpeakString(sSpeak)));
        sSpeak = GetLocalString(OBJECT_SELF, "SPEAK_"+IntToString(++i));
        }
}

The above variant checks through local strings stored on the trigger under “SPEAK_0”, “SPEAK_1”, “SPEAK_2”, and so on, speaking them at a 1 second delay after one another.

image

*Disclaimer: Did not test beyond checking that everything compiles.


You can colour-tint text, too, by including the x3_inc_string library.
#include "x3_inc_string"

void main()
{
    string sColouredString = StringToRGBString("Cake!", "700");
}

You’d make a PC speak coloured text like this:

    AssignCommand(oPC, SpeakString(StringToRGBString("I can alter the COLOUR of my speech! That's pretty amazing, hey?", "717")));

Tarot made a colour catalogue here:
Colours and Codes for StringToRGBString() | The Neverwinter Vault

2 Likes

Pssst, lemme offer you some under-counter stuff! Top-notch!
https://neverwintervault.org/project/nwn1/other/tool/ls-tk-script-generator
the generator allows you to generate (duh!) simple scripts. After a while when you understand more how the script system works, you will be able to alter them or make new ones on your own!

4 Likes

Thanks, Barbarian. your patience is appreciated. Here is my report card. . .

Your first suggestion worked. Drew the trigger, copied in your code, changed the text in the bit suggested and in the game it worked. Dialogue appeared just above players head. Whooppeeee !

Then I got carried away and tried your once and for all suggestion and it went a bit pear shaped. Receive the following when it compiled

02/02/2019 20:39:08: Error. ‘trg_enter_speak’ did not compile.
trg_enter_speak.nss(2): ERROR: UNKNOWN STATE IN COMPILER

Now, of course I ( ahem . . .) completely understood what this meant but thought I’d just check with you anyway :stuck_out_tongue_winking_eye:

I enclose a copy of what must be my error in the hope you can see something wrong with it.

PS. The variables look just like you suggested.

2 Likes

Thanks, Werelynx. I fully intend to try to use the script generator soon but at the moment its a bit like handing me the nuclear codes !

Mind you . . .

1 Like

GleeeeeE :grin: :fireworks: :star_struck: FOR KNOWLEDGS!!

It’s difficult to see, but I think you might have two “close” brackets, there. :thinking: The pixels look suspiciously identical, and it’s throwing an error message on line 2.

(edit) If this is the case, replace the first bracket with an opening bracket: {

You’ll also want to include x3_inc_string if you want to make use of StringToRGBString() (or, otherwise, delete line 3 in your picture; by itself, that’s just declaring and defining a string and colouring it. Unless somebody speaks or sends that string, it won’t be seen by anybody). Crash course info: Libraries are files that contain functions, but are not meant to be executed/run by themselves. By including a library in another script, you can make use of the functions contained in the library. That looks like this:

#include "x3_inc_string"

void main()
{

}

I’ve removed the colour bit till I get this sorted but it’s still not working.
New script . . .

Thought occurs. Have I used the wrong type of opening bracket in line 2 ? Shouldn’t have as I C+P it. I should say it told me it had compiled successfully. Hmm, . . .

::squints:: Still difficult to see, but I think you might have.

There are two relevant types of bracket here; the smooth ones, and the curly ones.

()

{}

It’s pretty suspicious if it compiled like that, though. :thinking: OK, another tip:

If you write…

[code]
Insert your code here.
[/code]

… into the forum here, you should be able to post formatted code. :smiley: Try copypasting yours in like that; if the brackets are wrong, we’ll be able to see it clearly.

void main()
{
    object oPC = GetEnteringObject();
    string sIdentifier = ObjectToString(oPC);

   // Abort if the entering object is not a PC, or if the entering object has passed this trigger already.
   if (!GetIsPC(oPC) || GetLocalInt(OBJECT_SELF, sIdentifier))
       return;

    // Store the entering object on the trigger, to be able to check whether it has already passed the trigger.
    SetLocalInt(OBJECT_SELF, sIdentifier, TRUE);

    // Cycle through local strings stored on the trigger...
    int i;
    string sSpeak = GetLocalString(OBJECT_SELF, "SPEAK_0");
    while (sSpeak != "")
        {
        // ... and make the entering object speak them.
        DelayCommand(IntToFloat(i), AssignCommand(oPC, SpeakString(sSpeak)));
        sSpeak = GetLocalString(OBJECT_SELF, "SPEAK_"+IntToString(i));
        }
}
1 Like

I think I spot the culprit, and it is my fault, not yours! :fireworks:

sSpeak = GetLocalString(OBJECT_SELF, "SPEAK_"+IntToString(i));

needs to be

sSpeak = GetLocalString(OBJECT_SELF, "SPEAK_"+IntToString(++i));

It’s not incrementing the counter (++i means "increment i by 1, then do this). This way, the while loop is infinite, since sSpeak will never be an empty string, as it’s always reading SPEAK_0 (and never _1, _2, _3, and so on).

Editing script in first post. :+1:

Thanks again, but still no cigar for me !

You’ve been a great help. It’s maybe something else I’m doing. I’m not giving up on this. I’m gonna play with what you’ve given me for an hour or so and see if I can get it to work. The first suggestion worked great so I’m confident I can get something going on this. I’ll reply to you if I get it done.

Just for interest I’ll add the current script and variables to see if there’s something obvious but don’t you waste any more time on it till i have a fiddle.

Basic tab on Properties
44

Code

Variables

19

!

The script is checking local strings named “SPEAK_0”, “SPEAK_1”, “SPEAK_2”, and so on. You’re storing your strings under the names “SPEAK_01”, “SPEAK_02”, “SPEAK_03”. Try removing the zeroes, and starting at number zero, see if that does it. :smiley:

YYYEEEESSSS!!!

Thank you so much. I feel a sense of achievement . . . while I suspect you need to go and lie down for a while .

1 Like

Now for the colour and trying other bits from your suggestions .

One final question, I promise. If I wanted to script something longer for each “Speak” would it stay on the screen long enough to read or is that another command?

I’m actually siphoning off your sense of achievement like a particularly creepy sort of achievement-vampire, living vicariously through your efforts, reexperiencing the joy of getting things to work for the first time over and over again. :expressionless::+1: I’m good. Carry on.

Change this line:

DelayCommand(IntToFloat(i), AssignCommand(oPC, SpeakString(sSpeak)));

Here, it’s converting i, (which is getting incremented by 1 each tick of the while loop) into a floating point number. You could increase the value, like this:

DelayCommand(IntToFloat(i)*2.0, AssignCommand(oPC, SpeakString(sSpeak)));

Now it would be two second intervals per line; 0.0 * 2.0 = 0.0 seconds delay. 1.0 * 2.0 = 2.0 seconds delay. 2.0 * 2.0 = 4.0 seconds delay. Increase the value as needed. :slight_smile:

No, that’s not working. Seems same interval and I’ve upped it to 4. Script below plus failed attempt to add colour.

Think I’ve missed out the Assign Command line for the colour to actually get him to speak in colour?

Take the “sColouredString” line back out again, as well as the closing bracket on line 6. :thinking: I think that might be mucking something up.

Yes, exactly. :smiley: oPC is being assigned the command to speak string “sSpeak”, not “sColouredString”, which is the one we’d tinted with a colour. String sSpeak is still colourless.

Here, try this variant:

#include "x3_inc_string"

void main()
{
    object oPC = GetEnteringObject();
    string sIdentifier = ObjectToString(oPC);

   // Abort if the entering object is not a PC, or if the entering object has passed this trigger already.
   if (!GetIsPC(oPC) || GetLocalInt(OBJECT_SELF, sIdentifier))
       return;

    // Store the entering object on the trigger, to be able to check whether it has already passed the trigger.
    SetLocalInt(OBJECT_SELF, sIdentifier, TRUE);

    // Cycle through local strings stored on the trigger...
    int i;
    string sSpeak = GetLocalString(OBJECT_SELF, "SPEAK_0");
    while (sSpeak != "")
        {
        // ... and make the entering object speak them.
        DelayCommand(IntToFloat(i)*3.0, AssignCommand(oPC, SpeakString(StringToRGBString(sSpeak, "700"))));
        sSpeak = GetLocalString(OBJECT_SELF, "SPEAK_"+IntToString(++i));
        }
}

Puurrfect !

Colour red and delay upped. I’m assuming I can now alter the 3 to anything I want?