13: 56: 11
SWC uses an in-house language for scripting in game entities (currently only NPCs, items, droids, and quests, but eventually much more) based on a combination of concepts taken from the programming languages LISP and Scheme. LISP was originally described by John McCarthy in 1960. A PDF version of its technical and formal description is available online, but you don't need to (and probably don't want to) read it in order to start using the language.
Most scripts for in game entities are written by the NPC team, but Custom NPCs can have their scripts edited by players.
You can access the NPC script editor through the NPC Inventory, any custom NPCs that you have permission to edit the scripts for will have an Edit Script link available.
This language is designed for syntactical simplicity and clarity. It is composed entirely of S-Expressions, which are defined by either a single "atom" or a list of multiple atoms. An atom is a single value, like a number, string, or variable name (usually called a symbol). Symbols can contain most characters except for spaces, semicolons, single or double quotes, parentheses, and brackets (others may be added to this exception list if they are used for future language extensions). S-Expressions group atoms within parentheses to form lists; therefore all of these are valid S-Expressions:
10 (1 10) x (x 5 "this string is one atom")
Each S-Expression is evaluated in exactly the same way--there is only one syntactical form for instructions in SWC Lisp. If the S-Expression is a single atom, it evaluates to itself or its defined value if it is a symbol. Thus 42 evaluates to 42, while x evaluates to the previously assigned value for x. If it is a list, then the first element of the list determines which operation is to be performed on the rest of the list. Effectively, the first element selects a function to be evaluated with the remaining elements as arguments. For example, to add together some numbers:
(+ 1 2)
This evaluates to 3. S-Expressions can be nested and they will be resolved from the inside out. There is no operator precedence to remember in SWC Lisp, because the grouping of S-Expressions makes the exact sequence of evaluation explicit. Each S-Expression can be evaluated and substituted into an outer S-Expression until a single value is left:
(+ 1 (* (- 2 1) (+ 1 2))) (+ 1 (* 1 3)) (+ 1 3) 4
Although the syntax is completely regular, there are a few special statements that do not correspond to "function" calls: instead they define the necessary language constructs required to make a programming language. We will encounter two of these used to create variables and functions.
SWC Lisp also supports comments, which begin with a ; and continue to the end of the line.
Like other languages, SWC Lisp allows you to define variables and functions. A variable is created by calling defvar with a symbol and a value:
(defvar x 1)
This defines a variable named x with the value 1. It can be used in subsequent statements as a normal number would be:
(+ x 1) 2
Functions are defined using the defun form and two arguments: the function declaration, consisting of a name and possibly argument names, and a function body. For example, a function that adds one to its argument:
(defun (incr x) (+ x 1))
Whenever a linebreak occurs inside an S-Expression, it is customary to indent the following lines by two additional spaces, in order to improve clarity. Trailing parenthesis are included on the final line. Extra whitespace (spaces, tabs, newlines, etc.) are all ignored by the interpreter. This function can now be called with any argument desired, including other lists as before:
(incr 1) 2 (incr x) 2 (incr (* 2 3)) 7
If a function does not take any arguments, it should be defined with an atom for the name. It may seem pointless to define a function without arguments, but when we create functions that do things (such as displaying messages to the player), rather than simply calculate things, it is often useful to group a series of operations into a single function for convenience if they need to be repeated. Additionally, multiple statements can be included in a function body. All of them will be executed, but the value of the final statement will used for the function's value. Examples of both of these situations will be shown below.
Variables can also be "quoted" by placing a single quote before the symbol. This indicates to the interpreter that this specific mention of this symbol should not be evaluated and instead used as a literal string, but if it is evaluated a second time, it will be used to look up a variable name. This has a number of important uses, most of which are beyond the scope of this introduction, but it is necessary in order to access the persistent variables.
SWC Lisp also supports a single type of conditional statement, "cond," which evaluates a series of tests and then executes the body statements for the first test that produces a true value. With some hypothetical helper functions we can produce the next largest even number for any input:
(defun (next-even x) (cond [(even? x) x] [#t (+ x 1)]))
In this case, square brackets are used, rather than parentheses, to denote each of the possible conditions. This is purely cosmetic and done to increase the readability of the language. Each condition list consists of a test S-Expression as the first element. If it produces a true value, all of the remaining S-Expressions in the body of that condition are evaluated. This also introduces the constant "#t" which is always true. A cond statement must always have a condition that always evaluates to true or an error is produced. This statement can be empty if need be.
A set of Boolean functions is also available which allows the user to combine logical expressions into compound conditions: `and`, `not`, and `or`. Comparison operators are also available for equality testing between two atoms (`eq?`) or numerical comparison (`geq?` `leq?` `lt?` `gt?` and the traditional math operator versions `>=`, `<=`, `<`, and `>`).
Now that the language is defined, we can use it to build NPC scripts. At their core, NPC scripts consist of a set of function definitions that print messages to the user and offer opportunities for further interaction, which is done using two built-in functions: `say` and `add-response`. All scripts start with a call to a function named `start` when the "interact" button is pressed on the in-game UI. Let's make a simple NPC script that displays a message and offers the user a chance to respond, and then he introduces himself.
(defun start (say "Hi there.") (add-response "Hello, who are you?" my-name)) (defun my-name (say (concat "My name is " (get-name self))))
The first function, `start`, has two function calls in its body. `say` is used to show the given string to the player. "add-response" creates an option for the player to respond with the string given as its first argument ("Hello, who are you?"), and when the player selects this option, the function given as the second argument is called (the function corresponding to the symbol my-name).
The second function, `my-name`, is called after the player has chosen her response. It concatenates a fixed string with the NPC's name to create a message to be displayed to the user. Because this function does not add any responses, the player only sees the option to terminate the conversation.
Passive conversational NPCs can be built using only the `say` and `add-response` functions. The functions `describe` and `ooc` are also available to display a message as description or as OOC-styled text indicating extra instructions that may be relevant to the player. A non-verbal action to be taken by the player may be created with `add-action`.
There are other primitive operations available to NPCs, depending on script context. Quest-type NPCs have the ability to modify the character's quest status, including distributing rewards. Quests are defined as a series of objectives, where some objectives may be sub-objectives to be completed as part of another quest. Quest progression can be nonlinear and there are no requirements on the order of completion of sub-objectives in order to complete a parent quest. To manipulate the quest data corresponding to the currently interacting character, the procedure is simple. First, obtain references to the quest objectives (by ID) that a particular script will be modifying:
(defvar super-hunter (get-quest 100)) (defvar rancor-stage (get-quest 101)) (defvar strider-stage (get-quest 102)) (defvar slug-stage (get-quest 103))
Then, as part of the NPC's interactions with the player, call one of `quest-start`, `quest-finish`, or `quest-fail` to change the quest's status.
(defun start (say "Are you ready to hunt some big game?") (add-response "Absolutely, I live for slaughter." gogogo) (add-response "No way, I prefer cuddling" pacifist)) (defun pacifist (say "We have no place for your kind here.")) (defun gogogo (say "OK, first I need you to bring me 10 rancor trophies.") (quest-start super-hunter) (quest-start rancor-stage))
Of course, this NPC would ask the player if he wanted to begin hunting every time that the two interacted, which is usually undesirable. Instead, we could use conditions to ask about the player's progress instead if he has already begun the hunting quest, by using a set of predicates that test quest state (`quest-active?`, `quest-failed?` and `quest-finished?`):
; Earlier definitions omitted (defun start (cond [(quest-active? super-hunter) (show-quest-status)] [#t (introduce-quests)])) (defun introduce-quests) (say "Are you ready to hunt some big game?") (add-response "Absolutely, I live for slaughter." gogogo) (add-response "No way, I prefer cuddling" pacifist)) (defun show-quest-status (cond [(quest-active? rancor-stage) (rancor-stats)] [(quest-active? strider-stage) (strider-stats)] [(quest-active? slug-stage) (slug-stats)])) (defun rancor-stats ; ... show progress on the rancor killing quest, using q-vars ; which are described later )
None of these quest interactions so far have dealt with rewards for quest completion--those must be handled explicitly as well. Examples seem tedious, but functions like `add-credits`, `remove-credits`, and `add-xp` are also defined to provide the ability to give players rewards. There are also related predicates, such as `has-credits?`.
A full list of available functions is at the end of this page.
SWC Lisp offers the ability to store data (or state information) between interactions and therefore provide memory of interactions between characters and other entities. This state falls into a number of categories, each of which are handled analogously: global variables, quest variables (two types, q and t), entity variables (two types, e and o), character variables, and session variables. More may be added if the need arises. Global variables are shared by all scripts, entities, and characters. They require the most care to avoid naming conflicts and are only available to admin-curated scripts. Quest variables are specific to a particular quest and the current character (q type) or specific only to a particular quest (t type). Entity variables are specific to the entity being interacted with (e type) or specific to both the entity and the character (o type), character variables are specific to the character interacting, and session variables are specific to the exact conversation happening right now: they reset between conversations. Not all variables are available to all script writers--some may be restricted based on the type of entity and/or script being written.
We begin by creating a reference to a variable of the desired type. For G-, E-, C-, S-, and O- variables, the context is implied (current entity/character/session), but for Q- and T- variables, context must be supplied in the form of a quest object reference. Additionally, a default value is supplied if the variable has not been used or created before:
(defvar rancor-stage (get-quest 101)) ; Tracks how many trophies were turned into this specific NPC (defvar my-trophies (evar 'trophies 0)) (defvar rancor-kills (qvar 'rancor rancor-stage 0))
Later, we can retrieve how many confirmed rancor kills a player has (based on how many trophies they have turned in) in order to update quest status or inform them of progress:
(defun check-rancor-stats (cond [(geq? rancor-kills 10) (quest-finish rancor-stage) (quest-start strider-stage) (say "Great, that's 10 rancors. Now bring me 10 kintan strider trophies!")] [#t (say (concat "You still need to kill " (- 10 rancor-kills) " rancors"))]))
And, of course, we need a way to manipulate variable values, which can be done using `set-var!`. Functions which change a variable's value, rather than merely declaring or using it, (called mutators) are frequently indicated by an exclamation point at the end of the function name (read out loud as bang). Several mutators exist for manipulating the state of SWC game objects beyond taking an "action" like saying something or awarding credits. We can assign a new value to a G-, E-, C-, Q-, or S- variable like so:
(defvar rancor-kills (qvar 'rancor rancor-stage 0)) (set-var! rancor-kills (+ rancor-kills 1))
With this in mind, we can look at a complete set of scripts which implement one of our new player quests: talk to someone, move to another location, talk to someone else, and then get a reward for it.
The quest starts when the player talks to the first NPC, James Walker. He uses conditions to prevent the quest from being assigned multiple times and to change his responses based on the current quest status:
(defvar target (get-npc 10072)) (defvar walking-tutorial (get-quest 2)) (defun start (cond [(quest-finished? walking-tutorial) (say "Sorry, I don't have any more work for you")] [(quest-started? walking-tutorial) (say (concat "Go talk to my brother " (get-name target) " and let him know his weapon shipment came in, please."))] [#t (say (concat "Hello there! You look like you could use some exercise. Why don't you walk over to my brother " (get-name target) " at and let him know his weapon shipment came in? You can walk by selecting the Travel button at the top of your Ground Travel page, which is linked on the right menu. Then enter your coordinates and hit Go!")) (add-response "Hey, who are you calling out of shape? Forget it! You can tell him yourself." decline) (add-response "I guess I can do that. I hope there's something in it for me." accept)])) (defun decline (say "Fine. If you change your mind before someone else comes along, let me know.")) (defun accept (say "Great! I'm sure he'll have a tip for you.") (quest-start walking-tutorial))
Two variables are declared at the top to identify objects he interacts with: a quest and a "target" NPC, both of which are specified directly by ID. In this case, the target is Ryan Walker, his brother, whom we must talk to finish the quest and receive our reward. The quests are created separately (currently managed by admin tools), and the appropriate ID number should be noted to add it to the script.
Ryan also checks for quest status and tailors his response appropriately. When the player elects to turn the quest in, he marks the objective as done and gives the player a reward.
(defvar walking-tutorial (get-quest 2)) (defun start (say "Yeah? What do you want?") (cond [(quest-started? walking-tutorial) (add-response "Um.. There's a weapon shipment for you. I don't know where it is. Or what it is. Maybe I should have gotten more information about this. I sure hope you're not up to something shady." done)] [#t (add-response "N..Nothing. Sorry to bother you." no-response)])) (defun done (say "Really? Well it's about time! Here's something for your trouble. Listen, why don't you use this money to buy some equipment? I hear there's a guy in the Shop (at coords?) who can help you. Walk over to the Shop in this city and use the Board button on your Travel page to enter.") (quest-finish walking-tutorial) (add-xp 25 (concat "Finished quest: " (get-name walking-tutorial))) (add-credits 100000)) ;; This shows no response from the NPC and gives the character no opportunity to react further (defun no-response #t)
Ryan also has an empty response function, `no-response`, which is a technique that can be used when the player should get the last line in a conversation.
Not all functions or language features are available in all contexts (for example, custom NPCs cannot access quests or global variables).
number : 1, 10, 0.01, -123 boolean : #t, #f string : "This is a string" symbol : symbol, another-symbol atom : number, string, boolean, or symbol expr : (atom1 atom2 ... atomN)
These are special forms provided by the language for basic takss like variable declaration, function declaration, and flow control.
(defvar name value-expr) (defun name body-expr1 body-expr2 ... body-exprN) (cond [test-expr1 body-expr1 body-expr2 ... body-exprN] [test-expr2 body-expr1 body-expr2 ... body-exprN] [#t body-expr1 body-expr2 ... body-exprN]) (set-var! name value)
Variables that are always available inside a script:
self : A reference to the entity that the script is running on (current NPC being talked to, current item being talked to, etc.) character : A reference to the character currently interacting with this script empty : A list terminator required for advanced scripts
This list includes only currently implemented and available SWC-related functions. Language-builtin functions are listed separately.
(+ number1 number2 ... numberN) (- number1 number2) (* number1 number2 ... numberN) (/ number1 number2) (concat string1 string2 ... stringN) (ge? number1 number2) (le? number1 number2) (gt? number1 number2) (lt? number1 number2) (and boolean-expr1 boolean-expr2 ... boolean-exprN) (or boolean-expr1 boolean-expr2 ... boolean-exprN) (not boolean-expr) (gvar name default) (qvar name quest-object default) (say string) (describe string) (ooc string) (add-response string next-function) (add-action string next-function) (get-creature-type type-id) (get-droid-type type-id) (get-item-type type-id) (get-npc id) (get-quest id) (get-name entity-object) (get-race entity-object) (get-gender entity-object) (send-message sender-object recipient-object message-string) (quest-started? quest-object) (quest-finished? quest-object) (quest-failed? quest-object) (quest-start quest-object) (quest-fail quest-object) (quest-finish quest-object) (add-credits amount) (remove-credits amount) (has-credits? amount) (add-xp amount reason-string) (holding-item? item-type-object) (holding-item-in-container? item-type-object) (give-item item-type-object name-string useNpcOwner-boolean) (take-item item-type-object) (hatch-egg)
Functions available as part of the language that do useful programming-related things. These might be more useful in more sophisticated scripts. Currently these are of limited use as our library functions don't create data that needs to be manipulated in this way.
(car list) (cdr list) (cons expr1 expr2) (list expr1 expr2 ... exprN) (empty? list) (eq? atom1 atom2) (dbg-dump expr1 expr2 ... exprN)
This language and its integration with SWC are both a work in progress. New features will be added over time and hopefully appropriate documentation will be added here as well. Examples of features we are working on include custom quest management, a script loading/sharing mechanism to create "repositories" of commonly used functions, additional hooks into the SWC engine for obtaining or manipulating game state, and other fundamental language improvements.