! GxScript - A library to do OKBScript-style conversations, but with the conversation menu printed in a separate Glulx window.
! By Brendan "BrenBarn" Barnwell, alias OKB (BrenBarn@aol.com)
!!!
! This library reserves 800,000,011-800,000,020 as Glk object rocks
! 800,000,011 -- Dialogue Window rock
! 800,000,012-20 -- Reserved for future expansion
!!!
! Development Log
! Date What the dilly-o
! 2001/03/18 Began!
! 2001/04/06 Mechanisms complete (I think). Time to start doing the window stuff.
! 2001/04/09 Seems to be working fine. I'm not entirely sure whether it's handling save/restore
! correctly, but there are no apparent problems.
! 2001/04/11 After writing Entry.h, made sure this works with it. Fixed a few small bugs.
! 2001/04/13 Fixed a couple bugs with RLine.
! RELEASED
! 2001/05/19 Some substantial changes:
! 1. A new property, switchedTo, has been added to the GxScriptNode class.
! x.switchedTo() is called whenever x becomes the current node.
! 2. Because of this, writing custom chooseNode routines for NPCs is now
! deprecated. Use switchedTo instead.
! 3. The player object can now optionally provide a lastspoke property. If
! provided, this property will hold the number of turns since the player last
! spoke to any NPC (or -1 if she has never spoken to any NPC).
! 2001/05/23 More changes:
! 1. A new property, NPCbefore, has been added to the GxScriptNode class.
! This property can be provided by lines of dialogue, and acts as though it were
! a before routine on the NPC itself.
! 2. NPCs should now use NPCbefore to handle things that before used to
! handle. Objects ofclass Talker MUST NOT provide a before routine.
! 3. NPCbefore routines are executed in a special order. For more information,
! see the comments near the definition of NPCbefore in the GxScriptNode
! class.
! 2001/05/28 Rewrote NPCbefore code to use the newly created BustAFlow. GxScript now
! requires the BustFlow library.
! 2001/05/29 Applied BustAFlow to the forget property as well. This property has been moved
! from the Dialogue class up to the GxScriptNode class.
! RELEASED
!!!!!
!!!!!!!!
! Brief explanation
! Basically, to use this library, you create objects ofclass Talker to represent the NPCs in your game. Each Talker should be the root of an object tree which contains all the lines of dialogue which the player could conceivably say to that NPC. Use class NLine for normal lines of dialogue and PLine for persistent lines (which can be chosen from the menu more than once). (There is also a class RLine for more advanced use.)
! Lines can "adopt" other lines, treating them as their own children -- some lines can have more than one parent. To specify that one line should adopt another (or others), specify it (or them) in the adopting property. A line can also share the children of others -- treating their children as if they were its own. To specify that one line should share with another (like adopting all of its children), specify the sharing parent in the sharing property.
! A simple example:
!!!
! Talker OKB
! with name 'okb' 'brendan' 'barnwell' 'brenbarn',
! description "It is OKB, the author of this library.",
! color [; return $0000FF; ],
! bgcolor [; return $00FFFF; ];
!
! NLine -> Hello "Hello."
! with content "~Hello,~ you say to OKB.^
! ~Hola,~ he says.";
!
! NLine ->-> LoveIt "I love your library."
! with content "~I love your library,~ you say.^
! ~Thanks,~ says OKB. ~Glad to hear it.~";
!
! NLine ->-> YouSuck "You suck!"
! with content "~You suck!~ you scream at OKB.^
! He gestures toward you with his hand.",
! after [; deadflag=2; ];
!
! NLine -> WhoAreYou "Who are you?"
! with adopting YouSuck,
! content "~Who are you?~ you ask.^
! ~I'm OKB,~ he says. ~I wrote this library.~";
!
! NLine ->-> Really "Really? Cool."
! with content "~Really?~ you say. ~Cool.~^
! OKB chuckles.";
!
! NLine -> HeyHey "Hey hey hey!"
! with sharing Hello,
! content "~Hey hey hey!~ you exclaim.^
! ~Hey yourself,~ OKB replies.";
!
! NLine ->-> HoHo "Ho ho ho!"
! with content "~Ho ho ho!~ you say.^
! ~Oho!~ says OKB.";
!!!
! In the conversation tree above, the Talker object OKB is the NPC, and is the root of the tree. Its immediate children are Hello, WhoAreYou, and HeyHey. Hello has two children: LoveIt and YouSuck. WhoAreYou also has two children: Really and YouSuck (an adopted child). HeyHey has three children: LoveIt and YouSuck (shared children from Hello) and HoHo.
! When a menu option is chosen, it becomes "current", and the options for the next menu are its children. For example, if the player were to choose "Who are you?" from the first menu, he would see two options on the next menu: "I love your library." and "You suck!".
! The color and bgcolor properties of the NPC, if provided, must be routines that return hex color values specifying the text and background color, respectively. These color values will be used in the dialogue window to print the menu. (For more information, see the comments for GxScriptWindowPolicy below.)
! And, of course, HTML- or TeX-style tags can be included in any text to style it (as in LoveIt, where the word "love" will appear in the Emphasized style).
!!!!!
! Brief description of the interface
! A dialogue menu is displayed in a separate window (by default at the bottom of the screen). The current options for dialogue are displayed therein, with the NPC's name centered at the top of the window. The player can choose to activate one of the displayed options by typing "say X", where X is a menu option; this is a normal verb, like Take or Get, and as such can be undone. Since conversation is at the same "level" of gameplay as other actions, the player is free to perform other actions while the menu is displayed. If the player chooses a menu option, the menu options will change to display the new set of choices (unless special code by the game author says to do otherwise).
! The menu displayed will always contain the dialogue choices for the most recently referred-to NPC. This means that any command referring to an NPC (like >X MALCOLM or >GIVE FOOD TO BUM) will cause the move that NPC "into focus", causing his/her/its menu options to be displayed. If the player wishes to shift conversation focus without using up game time, he can use the verb >SWITCHTO NPC. ("Switchto" can be abbreviated to "st".)
!!!!!!!!
!!!!!!!!
!
! Documentation
!
!!!
! Documentation is provided throughout this file in the form of comments about the various classes, objects, properties, etc. This section is a brief overview of this library in its entirety.
!!!!!
! What's in this file (in brief):
! 1. This documentation
! 2. Includes, etc.
! 3. Default window object (in case the game author doesn't provide one)
! 4. GxScriptManager object (manages the whole process)
! 5. GxScriptNode and its subclasses (what you use to make NPCs)
! 6. General-purpose routines for use with the GxScriptNode classes
! 7. Grammar extensions
!!!
! What to Include and when to Include it
! You must Include GxScript after including Grammar. GxScript requires OKBStyle (by yours truly), which requires OKBGlulx (also by yours truly), which in turn requires infglk (by John "katre" Cater); however, GxScript will automatically Include OKBStyle if it is not included, OKBStyle will automatically Include OKBGlulx if it is not included, and OKBGlulx will automatically Include infglk if it is not included. GxScript also requires BustFlow (again by yours truly), and will include it if it isn't already included. This means including GxScript will be sufficient, as long as you have all the necessary library files.
! You must call GxScriptManager.intialize() from your Initialise routine, unless you are using Entry, in which case this will be done automatically. You must also properly initialize OKBStyle, and you must be sure to make appropriate calls to OKBStyle and to your OKBGlulx window(s) (including GxScriptWin) from IdentifyGlkObject -- again, this will done automatically if you are using Entry.
!!!
! The GxScriptWin object
! GxScript requires an object called GxScriptWin. This will be the window where dialogue menus are printed. If you do not define one yourself, a default will be used.
! GxScriptWin should be an object ofclass Window (as defined in OKBGlulx), and should be of type BufferWin. Its rock value should be 800000011 (there is a constant GxScriptRock==800000010, so you can use GxScriptRock+1 as the rock). The size can theoretically be anything>1, and the window could conceivably be horizontal or vertical, but if it is too narrow dialogue menu options will word-wrap, which might look awkward. It is recommended that you make it a horizontal window as wide as the screen (this is the default behavior, and the library was designed with this configuration in mind).
! If the constant GxScriptWindowPolicy is positive, the window will retain the size which you specify (or a default of 6 lines if you don't specify); it is then your job to make sure that your menu options fit within that space. If it is negative, it will be closed and reopened repeatedly at various sizes during the game, to accomodate varying numbers of menu options. If it is zero (which is the default), it will remain at the size you initially specify, unless there are too many menu options to fit in the window, in which case it will expand just enough to accomodate all the options.
! (The absolute maximum number of options that can be on any one window defaults to 18, but you can set this limit to whatever you desire by defining a constant GxScriptMaxChoices whose value is the limit you want. Of course, how many choices will actually fit on the screen, or in the window, is a different matter.)
! You can specify stylehints in GxScriptWin, but color and background color can also be defined separately for each NPC. NPC-specific colors override those specified in GxScriptWin, unless GxScriptWindowPolicy is positive, in which case the window will always be the same color.
!!!
! GxScriptManager
! You must call GxScriptManager.initialize() from your Initialize routine. It will start its own daemon and handle all dialogue window functions.
!!!
! Class outline
! This is a listing of all the NPC-related classes provided by this library, in outline form. Classes in parentheses are "abstract" (i.e., you shouldn't make objects that inherit directly from them). More detailed information can be found in the comments which accompany the definitions of these classes.
!
! (GxScriptNode) ! Top-level class, contains all others
! (Talker) ! For NPC objects
! (Person) ! For human NPCs
! Man ! For male NPCs
! Woman ! For female NPCs
! NonHuman ! For "inhuman" NPCs
! (Dialogue) ! Lines of dialogue, and groups of lines
! (Line) ! For lines of dialogue
! NLine ! Normal (non-persistent) lines of dialogue
! PLine ! Persistent lines of dialogue
! RLine ! Lines of dialogue that redirect conversation
! (SilentLine) ! Lines of dialogue where the NPC says nothing
! SNLine* ! Normal (but silent) lines of dialogue
! SPLine* ! Persistent (and silent) lines of dialogue
! SRLine* ! Redirecting (and silent) lines of dialogue
! Holder ! For organizing sub-trees of conversations
!
! * Each subclass of SilentLine also inherits from the corresponding immediate subclass of Line (e.g., SNLine inherits from NLine as well as from SilentLine).
!!!
! General-purpose routines
! 5 general-purpose routines are provided for getting information about conversation trees. The most useful of these are Speaker(line), which returns the NPC who speaks a given line of dialogue, and InTree(root, line) which returns true if the specified line of dialogue is part of the conversation tree whose root is given (this may be either a line of dialogue or an NPC). More detailed information about these functions (and two other, less-useful ones, Adopts and Shares) can be found in the comments accompanying the routines.
!!!
! Grammar extensions
! GxScript handles conversation menus using the normal Inform grammar mechanism. The dialogue window displays the possible choices for dialogue, such as:
! 0. "Hey there."
! 1. "Goodbye."
! 2. "Busta busta busta!"
! The player can say a given line by typing "say X", where X is the number of that line in the dialogue window. In this example, "say 2" would cause the player to say "Busta busta busta!".
! This is implemented by extending the verb "say". The old functionality of "say" ("say blah blah blah to NPC") still exists, but the new form takes precedence.
!!!
! More information
! The various classes, routines, etc. in this library are liberally commented where they occur. These comments describe the syntax and usage of all the tools in the library. A general description of the "philosophy" of the library as a whole can be found at the end of this file.
! If you have any questions, comments, or other things to say, please do contact me at BrenBarn@aol.com .
!
! --OKB
!
!!!!!!!!
System_file;
#IfNDef GxScript;
#IfNDef OKBGlulx;
Include "OKBGlulx";
#EndIf;
#IfNDef OKBStyle;
Include "OKBStyle";
#EndIf;
#IfNDef BustAFlow;
Include "BustFlow";
#EndIf;
Message "[Including ]";
#IfDef Enterer;
Enterer GxScript;
EnterInitialise -> GxScriptInitialize
with follows OKBGlulx OKBStyle Game,
execute [; GxScriptManager.initialize(); ];
#IfNot;
Constant GxScript;
#EndIf;
Constant GxScriptRock 800000010;
Default GxScriptWindowPolicy 0; ! By default, dialogue window is resizable
! GxScriptWindowPolicy --
! < 0 -- Dialogue window will always be just large enough to contain the menu
! ==0 -- Dialogue window will retain the specified size, unless the menu is too big to fit
! inside, in which case it will be enlarged just enough to contain the whole menu.
! Exception: If GxScriptWin.closeempty~=0, the window will be closed if it is
! completely empty (i.e., there is no NPC in the room).
! > 0 -- Dialogue window will retain the specified size no matter what. The window will be
! opened once at the beginning of the game, and never closed. If you use this
! setting, the window will always have the same color scheme -- colors specified by
! individual NPCs will be ignored.
! (The specified size is the value of GxScriptWin.size)
#IfNDef GxScriptWin;
Message " GxScript: No GxScriptWin object found, using default.";
! GxScriptWin -- This should be a normal OKBGlulx Window. You can, however, specify two additional (optional) properties:
! defaultbgcolor -- This should be a routine which returns a hex color value. This color will be used to fill the dialogue window whenever it is totally blank -- in other words, when no NPC is in the room.
! closeempty -- If this is nonzero, the dialogue window will be closed if it is blank (i.e., there is no NPC in the room), even if GxScriptWindowPolicy==0. (It has no effect if GxScriptWindowPolicy~=0)
Window GxScriptWin "[Dialogue Window]"
with rock GxScriptRock+1, type BufferWin,
splitfrom MainWindow, splitdirection SplitBelow, splittype SplitFixed, size 6,
indent [a; return MainWindow.indent(a); ], paragraph [a; return MainWindow.paragraph(a); ],
justify [a; if (a~=StyleSubheader) return MainWindow.justify(a); else return JustifyCenter; ],
fontsize [a; return MainWindow.fontsize(a); ],
weight [a; if (a~=StyleSubheader) return MainWindow.weight(a); else return 1; ],
oblique [a; return MainWindow.oblique(a); ],
proportional [a; return MainWindow.proportional(a); ],
color [a; if (self.NPCcolor==self.NPCbgcolor) return MainWindow.color(a);
else return self.NPCcolor; ],
bgcolor [a; if (self.NPCcolor==self.NPCbgcolor) return MainWindow.bgcolor(a);
else return self.NPCbgcolor; ],
reverse [a; return MainWindow.reverse(a); ],
winRestore [; GxScriptManager.initialize(); ],
defaultbgcolor [; return MainWindow.bgcolor(StyleNormal); ],
closeempty 0,
NPCcolor 0, NPCbgcolor 0;
#EndIf;
! GxScriptManager
! This object manages the dialogue window.
Default GxScriptMaxChoices 18; ! Maximum number of options on any one menu
Array GxScriptChoices --> GxScriptMaxChoices; ! Holds the options currently on the menu
Object GxScriptManager "[GxScriptManager]"
with
recent 0, ! Most recently referred-to NPC
choices 0, ! Number of choices currently on menu
spokento 0, ! NPC spoken to this turn, if any
originalSize 0, ! Original size of GxScriptWin
debugging 0, ! Debugging-mode flag
initialize [;
StartDaemon(self); ! Start our own daemon so we can run every turn
self.originalSize=GxScriptWin.size; ! We might need the original size later
if (player==0) player=selfobj; ! Our daemon needs to know who the player is
if (player notin location) move player to location; ! . . .and where he is
! Don't open the window unless we must
if (GxScriptWindowPolicy>0) GxScriptWin.winOpen();
self.daemon(); ! Get the jump on things
],
playerSays [choice who;
! This is called when the player types "say X" to activate one of the menu options
if (GxScriptChoices-->0==0) "There's nothing to be said.";
if (noun>self.choices || noun<0) "That's not one of the menu options.";
choice=GxScriptChoices-->noun; ! Which node did the player choose?
who=Speaker(choice);
self.spokento=who;
who.node=choice; ! That node is now the NPC's current node
choice.switchedTo(); ! Let it know it's the current node
! Now set choice to the value of the current node -- this is necessary because switchedTo may have redirected the conversation to another node.
choice=who.node;
who.lastheard=0; ! Reset the NPC's lastheard counter
if (player provides lastspoke) player.lastspoke=0;
if (self.debugging) { ! For debugging, print to another window
GxScriptDebugWin.winGoto();
}
choice.after( ! Run the before, content, and after routines
choice.content ( ! in that order, passing each return value to
choice.before() ! the next routine
)
);
if (self.debugging) {
GlkWinSet(gg_mainwin);
}
if (~~(choice ofclass SilentLine)) who.lastspoke=0; ! If it's not a SilentLine, reset lastspoke
],
daemon [a;
! This makes sure the dialogue menu contains the right options, but it farms out all of the work to other property routines.
! First need to update the lastheard properties of all NPCs who the player did not speak to this turn, and run the forget routines of ALL the NPCs.
! If this is a meta-verb (almost certainly 'switchto'), then we'd better not mess with lastheard
if (~~meta) {
objectloop (a ofclass Talker) {
if (a~=self.spokento && a.lastheard~=-1) (a.lastheard)++; ! Increment the lastheards
BustAFlow(forget, a.node(), 0, modifiedClosestParent, a); ! Bust a flow on the forget routines
}
self.spokento=0; ! Reset spokento
if (player provides lastspoke && player.lastspoke>-1) player.lastspoke++;
}
! If we're in debugging mode, don't mess with the "most recent NPC" definiton at all
if (self.debugging) {
self.windowReset();
self.windowSetup();
rtrue;
}
! If the player has referred to an NPC this turn, make that the most recent NPC
if (second ofclass Talker && second~=self.recent) self.recent=second;
if (noun ofclass Talker && noun~=self.recent) self.recent=noun;
! If there is no most recent NPC, or if the most recent NPC is not present, look for nearby NPCs
if ( (self.recent==0) || (~~(TestScope(self.recent))) ) {
objectloop (a ofclass Talker) {
if (TestScope(a)) { ! If we find one, make that the most recent.
self.recent=a; break;
}
}
}
! Even if there is a most recent NPC, it may not be here (in the room)
if (self.recent) {
if (TestScope(self.recent)) self.windowSetup(); ! If the NPC is present, set up the window
else self.windowReset(); ! If not, reset the window (to prevent printing
} ! menu options for an NPC who isn't here).
else self.windowReset(); ! If there is no most recent NPC, reset the window
! At this point, the window is either blank, full of the proper menu options, or closed. In any case,
! we're done.
],
windowReset [;
self.clearChoices(); ! Clear out the old menu options
if (GxScriptWindowPolicy<0) { ! If we're supposed to keep the window as small as
GxScriptWin.winClose(); ! possible, then we close it entirely.
}
if (GxScriptWindowPolicy==0) { ! If we had to enlarge the window last time, we can
GxScriptWin.winClose(); ! bring it back to normal size now, since it's empty.
GxScriptWin.size=GxScriptManager.originalSize; ! Also, reset the window color.
GxScriptWin.NPCbgcolor=GxScriptWin.defaultbgcolor();
if (~~closeempty) GxScriptWin.winOpen();
}
if (GxScriptWindowPolicy>=0) { ! If it's at the size we want, just clear it
GxScriptWin.winClear();
}
],
windowSetup [cnode;
self.clearChoices(); ! Clear out old menu options
cnode=(self.recent).node(); ! Get the conversation node
! If NPC is its own cnode, let it choose a different node (if it wants to)
if (cnode==self.recent) {
(self.recent).switchedTo();
cnode=(self.recent).node(); ! switchedTo may have changed the nodes
}
self.addChildren(cnode); ! Collect all menu options
self.printMenu(); ! Print the menu options
],
printMenu [i;
! First make sure the window is the right size
if (GxScriptWindowPolicy<=0) { ! First we close the window (unless
GxScriptWin.winClose(); ! told to keep it a constant size)
! If we're supposed to keep it as small as possible, do so
if (GxScriptWindowPolicy<0) {
GxScriptWin.size=self.choices+2;
}
! The reason we use self.choices+2 is that we must add 1 to compensate for the 0-based
! index of the GxScriptChoices array, and we still need 1 more line to print the NPC's
! name on a line by itself.
if (GxScriptWindowPolicy==0) { ! If we have a minimum specified size
if ((self.choices+2)>GxScriptManager.originalSize) { ! set it to that minimum, unless. . .
GxScriptWin.size=(self.choices+2);
}
else {
GxScriptWin.size=GxScriptManager.originalSize; ! we need more space.
}
}
if (GxScriptWindowPolicy<=0) { ! Get the colors from the NPC, if allowed
GxScriptWin.NPCcolor=(self.recent).color(); ! to change colors.
GxScriptWin.NPCbgcolor=(self.recent).bgcolor();
}
GxScriptWin.winOpen(); ! Then reopen it, if we closed it before;
}
else { ! If we're not supposed to close and reopen it, we'll
GxScriptWin.winClear(); ! just clear it.
}
! First, let's print the NPCs name across the top of the window
GxScriptWin.winGoto(); ! Shift focus to the dialogue window
print "",(The) self.recent,"^";
! Now time to print the menu choices to the window
for (i=0 : i<(self.choices) : i++) { ! For each menu option, we. . .
if ((GxScriptChoices-->i).active) {
print " ",i,": ",(name) (GxScriptChoices-->i),"^"; ! Print it, along with an identifying number.
}
else {
print " ",i,": {",(name) (GxScriptChoices-->i),"}^"; ! For debugging purposes
}
}
GlkWinSet(gg_mainwin);
],
clearChoices [i;
self.choices=0; ! Reset the number of menu options to 0
for (i=0 : ii=0; ! Clear the old menu options out of the array
}
],
addChildren [cnode;
! There are three types of children to add:
! biological -- actually contained inside the parent object
! adopted -- mentioned in the parent's adopting property
! shared -- children of an object mentioned in the parent's sharing property
! This routine calls three others, one for each type of child.
! addShared may recursively call addChildren (since a shared parent may have adopted or shared children of its own).
self.addBiological(cnode);
self.addAdopted(cnode);
self.addShared(cnode);
],
addBiological [cnode x;
! The simplest type of child
for (x=child(cnode) : x : x=sibling(x)) { ! Loop through all the children of the node
! Take all active lines (or ALL lines, if we're in debugging mode)
if (x ofclass Line && (x.active || GxScriptManager.debugging) ) {
GxScriptChoices-->(self.choices)=x; (self.choices)++; ! Add it to the list of choices
}
}
],
addAdopted [cnode length i x;
! A bit more complex
length=(cnode.#adopting)/WORDSIZE;
for (i=0 : ii;
! Take all active lines (or ALL lines, if we're in debugging mode)
if (x ofclass Line && (x.active || GxScriptManager.debugging) ) {
GxScriptChoices-->(self.choices)=x; (self.choices)++; ! Add it to the list of choices
}
}
],
addShared [cnode length i x;
! Potentially the most complex type of child, since it can be recursive
length=(cnode.#sharing)/WORDSIZE;
for (i=0 : ii;
if (x ofclass Dialogue) self.addChildren(x); ! If it's a Line or Holder, we don't have to add it, we have
} ! to add it's children, so we go back to square 1.
];
!!!!!
! These classes define the various types of objects used in GxScript. Each class is laid out like so:
! Class ! Brief description of class
! class
! has
! ! what this attribute means
!
! ! what this attribute means
! etc. . .
! with
! ,
! ! What this property means
! ! What type of value this property should contain
! ! -- Additional comments about this property
! ! Default value, if it is meaningful
! ! Miscellaneous comments
! ;
! ! etc. . .
! ! Miscellaneous comments about this class
!!!!!
Class GxScriptNode ! Top-level class; contains all possible dialogue nodes
with
adopting 0,
! Means: Nodes which should be considered children of this node
! Type: An object ofclass GxScriptNode, or a list of such objects
! Default is 0 -- Adopts no nodes
sharing 0,
! Nodes whose children should be considered children of this node
! An object ofclass GxScriptNode, or a list of such objects
! Default is 0 -- Shares no children of other objects
switchedTo [; rfalse; ],
! Called when this node becomes the current node
! Routine
! Default is to do nothing
! This routine can be used to have lines of dialogue (or, more usefully, Holders) redirect
! the conversation BEFORE anything is printed, or to have lines which "do something" (i.e.,
! alter the game state in some way) when the player chooses them.
! Note that this routine is called IMMEDIATELY when this node becomes the current
! node, BEFORE any other routines (such as before, content, or after).
! A particularly common use of switchedTo is on a Talker object. A Talker object becomes
! the current node if: A) it becomes the most recently referred-to NPC (for example, if the player
! LOOKs at it); AND B) it is its own current node. In other words, when NPC becomes the
! most recently referred-to NPC, and NPC.node()==NPC, NPC.switchedTo() is called. This
! allows NPCs to do special things when the player first tries to talk to them.
reset [flag test a;
if (test==0) test=StartsOn;
objectloop (a ofclass Dialogue) {
if (InTree(self,a)) {
if (test(a)) a.activate(); else { if (flag) a.deactivate(); }
}
}
],
! Activates or deactivates nodes in this node's conversation tree which pass the given test
! This routine
! This routine
! Do not alter this property.
! This routine is provided as a convenient means of activating and/or deactivating nodes
! based on user-specified criteria. It accepts 2 arguments, flag and test, both of which are
! optional.
! flag should be 1 or 0.
! test, if provided, should be a routine which takes one argument, an object ofclass
! Dialogue. It should return true if this "passes the test" which the routine is applying, or
! false if the object fails. If test is omitted (or is 0), the default is whether the object has the on
! attribute.
! All objects (nodes) which pass the test will be activated (provided they are children of the
! calling node). If flag==0, nodes which fail the test will be left as they are. If flag~=0, nodes
! which fail the test will be deactivated.
! BustAFlowing properties
! GxScript makes use of 2 BustAFlowing properties: NPCbefore and forget. These
! properties are executed in a special way. The examples below refer to NPCbefore, but the
! process is the same for both NPCbefore and forget. (If you know what BustAFlow is and what
! it does, you can probably skip this description.)
! BustAFlowing properties are provided to give the you, the game author, finer control over
! NPC behavior. They allow you to easily define what happens in "special cases". For example,
! you may want an NPC to respond to >KISS NPC in one way if the player has just asked about
! aphordisiacs, and in another way if the player has just been talking about sorting algorithms.
! Using NPCbefore as an example, instead of doing this:
!
! Human SomeNPC "NPC"
! with before [;
! if (self.node()==ThisNode) {
! ! #1: Do a certain thing if we are at this particular point in the conversation
! }
! if ( InTree(ThisHolder,self.node()) ) {
!! #2: Do something else if we are at any node which is part of this larger conversation. . .
! }
! ! #3: Do something different if we're not at either of those places in the conversation
! ],
! . . .;
!
! . . .you can do this:
!
! Human SomeNPC "NPC"
! with NPCbefore [;
! ! What to do if there are no special conversation circumstances (#3 above)
! ],
! . . .;
!
! NLine -> ThisNode "node"
! with NPCbefore [;
! ! What to do if this is the current node (#1 above)
! ],
! . . .;
!
! Holder -> ThisHolder;
! with NPCbefore [;
! ! What to do if we're at any point in the conversation which this Holder holds (#2 above)
! ],
! . . .;
!
! ! Dialogue Lines go here that are part of the conversation that ThisHolder holds
!
! It works like this. When the player performs an action on an NPC, the object which gets
! "first dibs" on saying what happens is not the NPC itself but the Dialogue object which is the
! current node of conversation. So if the player types >EAT BOB and Bob.node()==Banana,
! then Banana.NPCbefore() will be called first.
! If Banana.NPCbefore returns false (or if Banana doesn't provide an NPCbefore routine),
! the NPCbefore of parent of Banana will be called. (So if parent(Banana)==Fruit,
! Fruit.NPCbefore() will be called. Usually. There are some pro quos, which see below.) If
! Fruit.NPCbefore returns false (or if Fruit doesn't provide an NPCbefore routine), then Fruit's
! parent's NPCbefore routine will be run, and that returns false then ITS parent's NPCbefore will
! be run, and so on until finally the NPC's own NPCbefore routine is run. (The effect is the
! same as if each object in the tree were a class, and NPCbefore were an additive property.)
! This chain of execution can end in two ways. First, if any of the NPCbefore routines
! returns true (actually, returns anything other than false), then that's it -- no more NPCbefore
! routines are run. Second, if ALL of the routines return false (including the NPC's own), then
! the default behavior will take place (a LibraryMessage printed, or whatever).
! Now, I mentioned above that if an NPCbefore routine returns false, it "passes the buck" to
! it's parent. However, it's not quite that simple. Since, in GxScript conversation trees, nodes
! can have more than one parent, finding an object's "parent" is a little different than usual. The
! bottom line is this: if the object has a biological parent (i.e., parent(obj)~=0), control is
! passed to that object. Failing that, if the node has an adopting parent (an object which
! adopts it), control is passed to that. Finally, if an object has neither a biological nor an
! adopting parent, but it does have a sharing parent (one which shares the children of its parent)
! then control is passed to that. (If an object has none of these, it is the NPC, and the chain of
! execution ends. If it has none of these and isn't an NPC, things may go awry -- so make sure
! that all your Dialogue objects have some kind of parent.)
! A pro quo for ALL of the above is that the parent object MUST BE A DESCENDANT OF
! THE NPC. (In other words, InTree(NPC,parentobj)==true.) So if parent(obj) exists, but
! InTree(NPC,parent(obj))==false, control is NOT passed to parent(obj). (The same applies to
! adoping and sharing parents.) This means that the conversation trees of multiple NPCs can
! intertwine (although it is probably not a good idea to actually do this -- there may be strange
! side-effects).
! Even with this restriction, it is not guaranteed that the flow of execution will follow the
! same path through the conversation tree that the player took to get to this point in the
! conversation. For example, if one node has no biological parent, but is adopted by several
! other nodes, there is no telling which one's NPCbefore will be run. However, if this is
! a problem, the conversation tree is probably laid out in a peculiar way; if you want different
! behavior for different paths through the conversation tree, make two separate objects instead
! of having one be adopted by two parents.
! It is important to remember that the "self" variable only refers to the NPC if the routine
! being run is defined by the NPC. In BustAFlow property routines defined by lines of dialogue
! (and Holders), you should use Speaker(self) to refer to the NPC. (Since Speaker(x)==x when
! x is an NPC, it is good practice to ALWAYS use Speaker(x) to refer to the NPC in NPCbefore
! and forget routines.
! Remember that these rules of execution apply to the forget property as well.
NPCbefore [; rfalse; ],
! A before routine for the NPC (even if this object is not actually the NPC, but a descendant)
! Routine
! -- It should return true to indicate that it has taken appropriate action, false if not.
! Default is to return false, never doing anything
! NPCbefore is run in a BustAFlow chain of execution. See the description of BustAFlowing
! properties above.
! NPCbefore is provided to make it easier to program NPCs which respond to the same
! stimuli differently depending on the conversational context. It allows a line of dialogue to
! define special circumstances in which it overrides the NPC's normal behavior. (See the
! description of BustAFlow properties above.)
! In essence, an NPCbefore routine should do exactly what a "before" routine would
! normally do. The major difference is that the code specified in an NPCbefore routine always
! applies to the NPC, not necessarily the object whose NPCbefore is being run. (That is, if
! there is an NPC called Joe which contains a Line called JoeCoffee, JoeCoffee.NPCbefore is
! relevant to Joe, not JoeCoffee.)
forget [; return (Speaker(self)).node(); ];
! A routine to handle the degree to which an NPC remembers where it is in a conversation
! Routine which returns the NPC's current node
! Default is elephant memory -- the NPC always remembers where it left off
! forget routines are run in a chain of execution just like NPCbefore routines are. See the
! comments on NPCbefore (above) for more information.
! The forget routine is called every turn. Its job is to decide, based on lastheard and/or
! lastspoke, how well the NPC remembers where it was in the conversation. It should then use
! chooseNode to set the node accordingly. For example, if you wanted the NPC to entirely
! forget that a conversation had ever happened if it remained silent for 5 turns, you would do
! something like:
! forget [a;
! a=Speaker(self);
! if (a.lastspoke>=5) {
! a.reset(0,1); a.chooseNode(a); } ],
! This would reset all nodes to their original state and reset the conversation node to the
! Talker itself, effectively "brainwashing" it.
! More complex effects are possible. For example, by careful arrangement of conversation
! trees, and the use of a sophisticated forget routine, you could create an NPC which
! progressively remembers less and less about the conversation as time passes (by moving the
! conversation node up the tree), and even, conceivably, one that ocassionally remembers bits of
! it (by having a random chance of moving back down the tree).
! Unfortunately, the library does not attempt to actually DO any of this. The default forget
! routine provides "elephant memory" -- the NPC always knows where it is in the conversation,
! no matter how long you leave it alone. It is up to the game author to work the magic.
! Don't use this class directly
Class Talker ! Someone (or. . . some-THING! :-) who can speak lines
class GxScriptNode
has animate static
with
color [; return GxScriptWin.color(); ],
! Text color of the dialogue choices for this character
! Routine to return hex color value.
! Defaults to same color as existing GxScriptWin
bgcolor [; return GxScriptWin.bgcolor(); ],
! Background color of dialogue choices for this character
! Routine to return hex color value.
! Defaults to same background color as existing GxScriptWin
lastheard -1,
! How long since this Talker was last spoken to by the player
! The number of turns since the player last spoke to this Talker, or -1
! -- -1 means the player has never spoken to this Talker
! Default is -1 -- the player has never spoken to this NPC
! lastheard is incremented on every turn in which the player does not talk to the NPC,
! unless lastheard==-1, in which case it is left alone. Whenever the player talks to the NPC,
! lastheard is set to 0. Other than that, the library ignores the value of lastheard. It is provided
! for use by the game author in forget routines.
lastspoke -1,
! How long since this Talker last spoke to the player
! The number of turns since this Talker last spoke to this Talker, or -1
! -- -1 means the player has never spoken to this Talker
! Default is -1 -- this NPC has never spoken to the player
! The library sets lastspoke to 0 whenever any content property (which see below in class
! Line) is executed during a conversation with this Talker, unless that property is part of an
! object ofclass SilentLine (which also see). Other than that, the library ignores lastspoke
! entirely -- you can use it however you want.
node [; return self; ],
! The conversation node of the conversation this Talker is engaged in
! An object ofclass GxScriptNode
! Default is self -- Starting point is the Talker object itself
! You should only alter the value of node from a chooseNode routine, if at all.
chooseNode [a;
self.node=a;
a.switchedTo();
return a;
],
! Selects a child node of this Talker, and moves the conversation to that node
! This routine
! Default is this routine
! This routine is provided as way for your code to change the current node. You should
! always call NPC.chooseNode(x) rather than simply setting NPC.node=x;
! It is best to leave this routine as it is. If you really must write your own chooseNode
! routine, be SURE to call (self.node).switchedTo() whenever your routine changes the current
! node.
forget [; return self.node(); ],
! A routine to handle the degree to which an NPC remembers where it is in a conversation
! Routine which returns self.node()
! Default is elephant memory -- the NPC always remembers where it left off
! The forget routine is called every turn. Its job is to decide, based on lastheard and/or
! lastspoke, how well the NPC remembers where it was in the conversation. It should then use
! chooseNode to set the node accordingly. For example, if you wanted the NPC to entirely
! forget that a conversation had ever happened if it remained silent for 5 turns, you would do
! something like:
! forget [a;
! if (lastspoke>=5) {
! self.reset(0,1); self.chooseNode(self); } ],
! This would reset all nodes to their original state and reset the conversation node to the
! Talker itself, effectively "brainwashing" it.
! More complex effects are possible. For example, by careful arrangement of conversation
! trees, and the use of a sophisticated forget routine, you could create an NPC which
! progressively remembers less and less about the conversation as time passes (by moving the
! conversation node up the tree), and even, conceivably, one that ocassionally remembers bits of
! it (by having a random chance of moving back down the tree).
! Unfortunately, the library does not attempt to actually DO any of this. The default forget
! routine provides "elephant memory" -- the NPC always knows where it is in the conversation,
! no matter how long you leave it alone. It is up to the game author to work the magic.
before [worknode;
if (worknode==0) worknode=self.node();
return BustAFlow(NPCbefore, worknode, 0, modifiedClosestParent, self);
! This is pre-BustAFlow code which I'm keeping around just in case
! Note: For this to work, I must also define an extra local variable "a"
! while ( worknode && (~~(worknode provides NPCbefore)) ) worknode=ClosestParent(worknode);
! if (worknode==0) rfalse;
! a=worknode.NPCbefore();
! if (a) return a;
! a=ClosestParent(worknode);
! if (a) return self.before(a);
];
! Your NPCs must not provide a before property. Use NPCbefore instead.
! This before routine makes the whole NPCbefore scheme work. Do not mess with it.
! Don't use this class directly, use its subclasses (Man, Woman, NoGender)
Class Human ! Human beings
class Talker
has proper;
! Better to use Man or Woman than to use this directly. It is provided to be used as a base class for "types" of people with similar characteristics (e.g., boys, girls, soldiers, etc.)
Class Man ! Male Talker
class Human
has male;
Class Woman ! Female Talker
class Human
has female;
Class NonHuman ! It's -- it's not HUMAN!
class Talker
has neuter;
! Provided for talkable-to creatures that aren't human -- aliens, talking animals, whatever.
Class Dialogue ! Lines of dialogue and containers for lines of dialogue
class GxScriptNode
has on
! Nodes which are initially active should have on. Nodes which are initially inactive
! should have ~on. This makes it easy to reset any given node to its original state. You should
! define each Dialogue object with either on or ~on (on is implied if you do not specify); do not
! toggle the attribute at run-time.
with
active true,
! Indicates whether this node is active
! true or false
! -- true means this node is active
! -- false means this node is inactive
! Default is true -- node is active
activate [; self.active=true; ],
! Activates this node
! This routine should always be [; self.active=true; ]
! Default is [; self.active=true; ]
! Don't set this property to anything
deactivate [; self.active=false; ],
! Deactivates this node
! This routine should always be [; self.active=false; ]
! Default is [; self.active=false; ]
! Don't set this property to anything
toggle [; if (self.active()==true) self.active=false; else self.active=true; ];
! Toggles this node (if active, deactivates it; if inactive, activates it)
! This routine should always be [; if (self.active()==true) self.active=false; else self.active=true; ]
! Default is [; if (self.active()==true) self.active=false; else self.active=true; ]
! Don't set this property to anything
! Don't use this class directly
Class Holder ! Container for lines of dialogue
class Dialogue;
! Objects ofclass Holder should be used sparingly. They are provided for situations in which you want to redirect conversation flow to another node. For example, you can create a Talker which contains several Holders, each of which is the root of a different conversation tree. Then use the switchedTo property of the Talker to select (at random, or based on some criteria) one of the Holders. The conversation will be begin with that Holder as the conversation node.
! Put another way, a Holder can be used as the root of a sub-tree of a conversation. They can contain somewhat distinct segments of a conversation which you might need to refer to separately from the conversation as a whole.
! You could conceivably use "placeholder" Lines instead of Holders, but using a separate class clarifies the difference between the two uses: Lines are objects which contain text which is intended to be read by the player, whereas Holders are programming shortcuts for organizing those objects.
Class Line ! A line of dialogue
class Dialogue
with
! short_name
! The text which will appear on the dialogue menu
! String, or routine to print one
! -- If a routine, it should not do anything but print text
! Default is "hardware" object name.
! If a string, this property's value will be printed in the dialogue menu (if you want
! quotation marks around the text, you must include ~s in the string). If a routine, it will be
! run.
! If a string, this can also be specified at the head of the object definition as with all Inform
! objects, as in:
!
! Line SomeLine "~Hi there.~"
! . . .;
!
! . . .rather than. . .
!
! Line SomeLine
! with short_name "~Hi there.~",
! . . .;
NPCbefore [; rfalse; ],
! A before routine for the NPC who speaks this line, to be run if this is the conversation node
! Routine
! -- It should return true to indicate that it has taken appropriate action, false if not.
! Default is to return false, never doing anything
! NPCbefore is provided to make it easier to program NPCs which respond to the same
! stimuli differently depending on the conversational context. Instead of doing this in your
! NPC's before routine:
! Human SomeNPC "NPC"
! with before [;
! if (self.node()==ThisNode) {
! ! #1: Do a certain thing if we are at this particular point in the conversation
! }
! if ( InTree(ThisHolder,self.node()) ) {
!! #2: Do something else if we are at any node which is part of this larger conversation. . .
! }
! ! #3: Do something different if we're not at either of those places in the conversation
! ],
! . . .;
!
! . . .you can do this:
!
! Human SomeNPC "NPC"
! with NPCbefore [;
! ! What to do if there are no special conversation circumstances (#3 above)
! ],
! . . .;
!
! NLine -> ThisNode "node"
! with NPCbefore [;
! ! What to do if this is the current node (#1 above)
! ],
! . . .;
!
! Holder -> ThisHolder;
! with NPCbefore [;
! ! What to do if we're at any point in the conversation which this Holder holds (#2 above)
! ],
! . . .;
!
! ! Dialogue Lines go here that are part of the conversation that ThisHolder holds
!
! This allows a line of dialogue to define special circumstances in which it overrides the
! NPC's normal behavior.
! It works like this. When the player performs an action on an NPC, the object which gets
! "first dibs" on saying what happens is not the NPC itself but the Dialogue object which is the
! current node of conversation. So if the player types >EAT BOB and Bob.node()==Banana,
! then Banana.NPCbefore() will be called first.
! If Banana.NPCbefore returns false (or if Banana doesn't provide an NPCbefore routine),
! the NPCbefore of parent of Banana will be called. (So if parent(Banana)==Fruit,
! Fruit.NPCbefore() will be called. Usually. There are some pro quos, which see below.) If
! Fruit.NPCbefore returns false (or if Fruit doesn't provide an NPCbefore routine), then Fruit's
! parent's NPCbefore routine will be run, and that returns false then ITS parent's NPCbefore will
! be run, and so on until finally the NPC's own NPCbefore routine is run. (The effect is the
! same as if each object in the tree were a class, and
! NPCbefore were an additive property.)
! This chain of execution can end in two ways. First, if any of the NPCbefore routines
! returns true (actually, returns anything other than false), then that's it -- no more NPCbefore
! routines are run. Second, if ALL of the routines return false (including the NPC's own), then
! the default behavior will take place (a LibraryMessage printed, or whatever).
! Now, I mentioned above that if an NPCbefore routine returns false, it "passes the buck" to
! it's parent. However, it's not quite that simple. Since, in GxScript conversation trees, nodes
! can have more than one parent, finding an object's "parent" is a little different than usual. The
! bottom line is this: if the object has a biological parent (i.e., parent(obj)~=0), control is
! passed to that object. Failing that, if the node has an adopting parent (an object which
! adopts it), control is passed to that. Finally, if an object has neither a biological nor an
! adopting parent, but it does have a sharing parent (one which shares the children of its parent)
! then control is passed to that. (If an object has none of these, it is the NPC, and the chain of
! execution ends. If it has none of these and isn't an NPC, things may go awry -- so make sure
! that all your Dialogue objects have some kind of parent.)
! A pro quo for ALL of the above is that the parent object MUST BE A DESCENDANT OF
! THE NPC. (In other words, InTree(NPC,parentobj)==true.) So if parent(obj) exists, but
! InTree(NPC,parent(obj))==false, control is NOT passed to parent(obj). (The same applies to
! adoping and sharing parents.) This means that the conversation trees of multiple NPCs can
! intertwine (although it is probably not a good idea to actually do this -- there may be strange
! side-effects).
! Even with this restriction, it is not guaranteed that the flow of execution will follow the
! same path through the conversation tree that the player took to get to this point in the
! conversation. For example, if one node has no biological parent, but is adopted by several
! other nodes, there is no telling which one's NPCbefore will be run. However, if this is
! a problem, the conversation tree is probably laid out in a peculiar way; if you want different
! behavior for different paths through the conversation tree, make two separate objects instead
! of having one be adopted by two parents.
before NULL,
! Stuff to do before the text of the dialogue is printed
! NULL or routine
! -- Its return value will be passed as an argument to the content
! property (if that is a routine) -- EVEN if before==NULL.
! Default is NULL -- do nothing
content [; "BUG: This line of dialogue (",(name) self,") has no content."; ],
! Text of this dialogue, or routine to print it
! String or routine
! -- It will receive as an argument the return value of the before property. You can use
! this to alter the text displayed.
! -- Note that if before==NULL, the argument passed to content will be 0 (since this
! is the value the null function always returns). This means you should not rely on
! any other meaning for content(0).
! -- Its return value will be passed as an argument to the after property -- EVEN if
! content is a string.
! Default is error message
after [; rfalse; ];
! Stuff to do after the text of the dialogue is printed
! NULL or routine
! -- It will receive as an argument the return value of the content property. You can use
! this to alter behavior. One possible use would be to activate other nodes (opening
! up new conversation topics).
! -- Note that if content is a string (not a routine), the argument passed to after will be
! 1 (since that is return value of printing a string). This means you shouldn't rely on
! any other meaning for after(1).
! Default is NULL -- do nothing
! Do not use this class directly, use its subclasses (NLine, PLine, RLine)
Class NLine ! Normal line of dialogue (can only be chosen once)
class Line
with
after [; self.deactivate(); ];
! Note that this class makes use of an after routine. If your NLines have after routines, they must return false (or explicitly run the superclass routine), or else the NLine class after routine will not take effect and the line will not be deactivated.
Class PLine ! Persistent line of dialogue (can be chosen indefinitely)
class Line;
Class RLine ! Redirecting line of dialogue (moves to another node)
class Line
with
newnode 0,
! The node to redirect conversation to
! An object ofclass Dialogue, or a routine to return one
! -- If a routine, it will be passed the return value of the content property, if that was
! a routine.
! No meaningful default
after [a goto;
if (metaclass(self.newnode)==Routine) goto=self.newnode(a); else goto=self.newnode;
(Speaker(self)).node=goto;
];
! Note that this class makes use of an after routine. If your RLines have after routines, they must return false (or explicitly run the superclass routine), or else the RLine class after routine will not take effect and the conversation will not be redirected.
! It is better to use the subclasses RNLine and RPLine
Class RNLine ! Normal redirecting line
class RLine NLine;
Class RPLine ! Persistent redirecting line
class RLine PLine;
Class SilentLine
class Line;
! Use this class for lines of dialogue where the NPC doesn't actually say anything. The only difference from regular Lines is that lastspoke will not be updated.
! The subclasses of SilentLine use multiple inheritance, inheriting not only from SilentLine but also from direct subclasses of Line. (SNLine inherits from NLine, SPLine inherits from PLine, and SRLine inherits from RLine. This ensures that any future modifications to NLine, PLine, or RLine will "automatically" affect the SilentLine classes as well, and makes it unnecessary to cut-and-paste code.
! Do not use this class directly, use its subclasses (SNLine, SPLine, SRLine);
Class SNLine ! Normal line of silent dialogue (can only be chosen once)
class SilentLine NLine;
Class SPLine ! Persistent line of silent dialogue (can be chosen indefinitely)
class SilentLine PLine;
Class SRLine ! Redirecting line of silent dialogue (moves to another node)
class SilentLine RLine;
!
! These are general-purpose routines for getting information about the conversation tree
!
! Speaker [line]
! ofclass GxScriptNode
! Returns: ofclass Talker
! Returns the Talker who speaks the given Line
! If no argument given, returns the talker of the calling object. Do not call this routine from
! objects that are not ofclass GxScriptNode.
[Speaker spoken a;
if (spoken==0) spoken=sender;
if (~~(spoken ofclass GxScriptNode)) QuitWithError("GxScript Error: Tried to find the Speaker of something which is not ofclass GxScriptNode.",spoken);
if (spoken ofclass Talker) return spoken;
if (parent(spoken) ofclass Talker) return parent(spoken);
if (parent(spoken) ofclass Dialogue) return Speaker(parent(spoken));
objectloop (a ofclass GxScriptNode) {
if ( IsInProp(a,adopting,spoken) && a ofclass Talker) return a;
if ( IsInProp(a,adopting,spoken) && a ofclass Dialogue) return Speaker(a);
}
if (~~(spoken ofclass Talker)) QuitWithError("GxScript Error: Tried to find the Speaker of a Dialogue object, but it has none.",spoken);
];
! InTree root leaf
! ofclass GxScriptNode ofclass Dialogue
! Returns: true or false
! Returns true if leaf is a member of the conversation tree based at root, false if not. For these purposes, all objects are considered to be children of the "nothing" object, so InTree(0,x) will always return true.
[InTree root leaf length i x;
#IfDef Debug;
if (debug_flag & 1 ~=0) {
print "InTree(",(name) root,",",(name) leaf,")^";
KeyCharPrimitive();
}
#EndIf;
if (IndirectlyContains(root,leaf)) rtrue;
if (Adopts(root,leaf)) rtrue;
if (Shares(root,leaf)) rtrue;
for (x=child(root) : x : x=sibling(x)) {
if (InTree(x,leaf)) rtrue;
}
length=(root.#adopting)/WORDSIZE;
for (i=0: ii;
if (x==0) rfalse;
if (InTree(x,leaf)) rtrue;
}
length=(root.#sharing)/WORDSIZE;
for (i=0 : ii;
if (x==0) rfalse;
if (InTree(x,leaf)) rtrue;
}
rfalse;
];
! Adopts parent child
! ofclass GxScriptNode ofclass Dialogue
! Returns: true or false
! Returns true if child is an adopted child of parent, false otherwise.
[Adopts padre hijo length i x;
#IfDef Debug;
if (debug_flag & 1 ~=0) {
print "Adopts(",(name) padre,",",(name) hijo,")^";
KeyCharPrimitive();
}
#EndIf;
if (IsInProp(padre, adopting, hijo)) rtrue;
length=(padre.#sharing)/WORDSIZE;
for (i=0 : ii;
if (x==0) rfalse;
if (Adopts(x, hijo)) rtrue;
}
rfalse;
];
! Shares parent child
! ofclass GxScriptNode ofclass Dialogue
! Returns: true or false
! Returns true if parent is sharing child from another object, false if not.
[Shares padre hijo length i x;
#IfDef Debug;
if (debug_flag & 1 ~=0) {
print "Shares(",(name) padre,",",(name) hijo,")^";
KeyCharPrimitive();
}
#EndIf;
length=(padre.#sharing)/WORDSIZE;
for (i=0 : ii;
if (x==0) rfalse;
if (parent(hijo)==x) rtrue;
if (Adopts(x, hijo)) rtrue;
if (Shares(x, hijo)) rtrue;
}
rfalse;
];
! ClosestParent node [root=sender]
! ofclass GxScriptNode ofclass GxScriptNode
! Returns: object ofclass GxScriptNode, or 0
! Returns the most closely-related parent of this node. Biological parents are considered most closely related, followed by adopting parents, with shared parents the most distant. Returns 0 if the node has no parent of any kind. This is protected for node==0. ClosestParent(0,x) will always return 0.
! A second argument, root, may optionally be given. If given, this places a constraint on returned nodes: they must be members of the dialogue tree of which root is the root. 0 will be returned if the object has a parent, but it is not in the specified tree. If root is omitted, sender will be used.
[ClosestParent hijo root a;
if (hijo==0) rfalse;
if (root==0) root=sender;
if ( InTree(root,parent(hijo)) ) return parent(hijo);
objectloop (a ofclass Dialogue) {
if ( Adopts(a,hijo) && InTree(root,a) ) return a;
}
objectloop (a ofclass Dialogue) {
if ( Shares(a,hijo) && InTree(root,a) ) return a;
}
rfalse;
];
! modifiedClosestParent
! This is equivalent to ClosestParent, but its arguments are rearranged so that it can be called by BustAFlow.
[modifiedClosestParent worknode test NPC;
return ClosestParent(worknode,NPC);
];
! StartsOn node
! ofclass GxScriptNode
! Returns true or false
! Returns true if this node is on by default (node has on), false otherwise (node has ~on)
! This is used as the default test by GxScriptNode::reset
[StartsOn x;
if (x has on) rtrue;
else rfalse;
];
! IsActive node
! ofclass Dialogue
! Returns true or false
! Returns true if this node is active, false if not
! This is provided for use with GxScriptNode::reset
[IsActive x;
if (x.active) rtrue;
else rfalse;
];
! IsInactive
! Equivalent to ~~IsActive
[IsInactive x;
return ( ~~(IsActive(x)) );
];
! Grammar
[GxScriptSaySub;
GxScriptManager.playerSays();
];
[SwitchConversationSub;
if (~~(noun ofclass Talker)) "You can't talk to ",(ThatOrThose) noun,".";
GxScriptManager.recent=noun;
GxScriptManager.daemon();
];
Extend 'say' first
* number -> GxScriptSay;
Verb meta 'switch conversation' 'switchto' 'st'
* noun -> SwitchConversation;
Verb meta 'talk' 't' 'tt'
* noun -> SwitchConversation
* 'to' noun -> SwitchConversation;
#IfDef Debug;
Window GxScriptDebugWin
with rock GxScriptRock+2, type BufferWin,
splitfrom MainWindow, splitdirection SplitLeft, splittype SplitProportional,
size 20;
[DebugGxScriptOnSub;
GxScriptDebugWin.winOpen();
GxScriptManager.debugging=true;
];
[DebugGxScriptOffSub;
GxScriptDebugWin.winClose();
GxScriptManager.debugging=false;
];
[DebugGxScriptNodeSub;
DebugGxScriptOnSub();
GxScriptManager.recent=Speaker(noun);
(Speaker(noun)).node=noun;
GxScriptManager.daemon();
];
[IsGxScriptNode x;
if (scope_stage==1) rfalse;
if (scope_stage==2) {
objectloop (x) {
if (x ofclass GxScriptNode) PlaceInScope(x);
}
rtrue;
}
if (scope_stage==3) "[That is not a GxScriptNode.]";
];
Verb meta 'gxdebug' 'gxd'
* 'on' -> DebugGxScriptOn
* 'off' -> DebugGxScriptOff
* scope=IsGxScriptNode -> DebugGxScriptNode;
#EndIf;
#EndIf;
!!!
! General comments about the use of GxScript
! GxScript conversations are laid out as trees. The root of each tree is the NPC with whom the conversation takes place. Below that are the various lines of dialogue the player can say to the NPC, laid out in a tree with the earliest possibly lines highest, and going down so that lines at the extreme "leaves" of the tree can only be reached via a long conversation. There may be "placeholder" objects in the tree, which do not themselves represent lines of dialogue, but contain objects that do.
! Each branching point on the tree, be it an NPC, a Holder, or a Line of dialogue, is called a node. The "root node" of the tree is the NPC itself. Each node object can contain others, which are then "child nodes" of their "parent node".
! The conversation tree need not have a strict tree structute; nodes can have more than one parent. Nodes can "adopt" child nodes that are not strictly their own children (that is, the child node object is not actually contained in the parent node object), and they can "share" the children of other nodes (that is, Node A can treat Node B's children as though they were Node A's own children).
! For any given NPC, the node where the conversation currently "is" (that is, the most recent line of dialogue which has been said) is called the conversation node. As the conversation progresses, the conversation node moves around the tree, and different nodes become the conversation node. Initially, each NPC is its own conversation node; in other words, the conversation begins with the NPC as the conversation node, and moves on from there.
! Normally, when the player chooses a line of dialogue from the menu, that line becomes the conversation node for the next player input. You can use the classes RLine and SRLine to create lines that redirect the conversation to another node. You can also use a switchedTo routine to shunt the conversation to a different node before it happens.
! A note about the difference between the before/content/after properties and the switchedTo property is in order here. A node's before, content, and after contain the actual mechanics of the conversation for that node -- in other words, what happens when that line of dialogue is spoken by the player; if any of these properties move the conversation node, the only effect will be to change what is displayed in the conversation menu on the following turn. In contrast, a node's switchedTo property handles things that should happen BEFORE the player actually "commits" to saying the line of dialogue; if switchedTo moves the conversation node, the effect will be the same as if the player had actually chosen the new node off the menu. (Why is it called switchedTo and not simply before? Because it may need to be used for NPCs as well as lines of dialogue, and NPCs may need a "normal" before routine to handle normal things like KISS NPC or HIT NPC WITH SABER OF D00D.)
! You can control many things about the conversation tree. Most basic is its actual shape: which nodes are children of others, which are adopted children, and which are shared children. (You define this by how you arrange the objects in your code.) You can also find out from any NPC what its current conversation node is (by using NPC.node()), and you can define how well and how long the NPC will remember that node before forgetting where it was in the conversation. You can even move the conversation node around yourself, but you have to be careful to keep the dialogue running smoothly.
System_file;