_,.---._ ,----. ,-,--. ,--.--------.
,-.' - , `. .--.-. .-.-. ,-.--` , \ ,-.'- _\/==/, - , -\
/==/ , - \ /==/ -|/=/ ||==|- _.-`/==/_ ,_.'\==\.-. - ,-./
|==| - .=. , ||==| ,||=| -||==| `.-.\==\ \ `--`\==\- \
|==| : ;=: - ||==|- | =/ /==/_ , / \==\ -\ \==\_ \
|==|, '=' , ||==|, \/ - |==| .-' _\==\ ,\ |==|- |
\==\ _ - ;|==|- , /==|_ ,`-._/==/\/ _ | |==|, |
'.='. , ; -\/==/ , _ .'/==/ , /\==\ - , / /==/ -/
`--`--'' `--`--`..---' `--`-----`` `--`---' `--`--`
,-,--. _,.----. .=-.-. _ __ ,--.--------.
,-.'- _\ .' .' - \ .-.,.---. /==/_ /.-`.' ,`./==/, - , -\
/==/_ ,_.'/==/ , ,-' /==/ ` \|==|, |/==/, - \==\.-. - ,-./
\==\ \ |==|- | .|==|-, .=., |==| |==| _ .=. |`--`\==\- \
\==\ -\ |==|_ `-' \==| '=' /==|- |==| , '=',| \==\_ \
_\==\ ,\ |==| _ , |==|- , .'|==| ,|==|- '..' |==|- |
/==/\/ _ |\==\. /==|_ . ,'.|==|- |==|, | |==|, |
\==\ - , / `-.`.___.-'/==/ /\ , )==/. /==/ - | /==/ -/
`--`---' `--`-`--`--'`--`-``--`---' `--`--`
█████████ █████████ █████████ █████████ █████████ V.1.1.1 █████████
- Created By Alex "Dev" Earley
- Created on 2/4/25
- Updated on 2/26/25
█████████ █████████ █████████ █████████ █████████ █████████ █████████
2/25/25 I added "INFO & ACTIONS". These will provide similar funcationality to SAY and CHOICES, but will be formatted differently in-game.
It was almost tempting to add a "style" option to SAY and CHOICES but I felt like that would add another step, add friction. I don't mind going wide instead of deep. I think deep is good for normal programming languages, but this is not that. Being a "high-level" scripting language, I prefer to make the script as clear and easy to write as possible. Rolling to 1.1.0 because I am introducing new features. On that topic, here is my logic for the versioning:
X.0.0 - Major (stable) release. Something using QS was shipped. Conceptual changes.
0.X.0 - New or removed Commands.
0.0.X - Bug fixes or tweaks to existing Commands.
2/19/25 NOTE: I am currently implementing this into my own GODOT project. Many more updates to come in the near future! When I feel it is stable, I will roll the version to 2.0.0.
- Introduction
- Commands
- Markers
- Predicates
- Options
- INFO & ACTIONS (new)
- Adding Quest Script to your game
Quest Script is a high-level scripting language. It was initially implemented for a game called BRODUCE written in Godot Script. Quest Script by itself has no official runtime or compiler. Instead, it is a set of standards for the script. At the end of the day, you will need to do most of the programming.
Not having an official release isn’t because of laziness or lack of resources. It is by design. Your game can support Quest Scripts only if you build support for the scripts.
The purpose of this document is to explain the scripting language and also provide examples of how to implement Quest Script support. If you notice, the chapter for “Adding Quest Script to your game” is last. Usually this chapter is the first in a guide like this. The reason it is last here is because it is the most complicated step and requires a full understanding of Quest Script.
Quest Script was designed to enable modding in BRODUCE. Modders embed the Quest Scripts along with their mesh data and the game runs the scripts after the mod is loaded in at runtime.
Consider the following Quest Script:
SAY[Good morning %s!,1,GET_PLAYER_NAME()]
ON[PLAYER CLICKED NEXT, Next#1]
CHOICE[THANKS, GOOD MORNING]
[THANKS]
SAY[Thank you!,0]
GO[END]
[GOOD MORNING]
[Next#1]
SAY[Good Morning!,0]
WAIT[1]
GO[END]
[END]
DO[SHOW_GAME_OVER()]
A Quest script consists of multiple Commands and Markers. Each line is wither a marker or a command. Anything else is treated like a comment and ignored. Commands start with a command name and have their parameters wrapped in square brackets. Markers are wrapped in square brackets and have no command name. They are used almost like function names.
COMMAND[Parameters]
[MARKER]
Quest script suggests the following commands:
- SAY[MESSAGE, CHARACTER_INDEX, REPLACEMENTS_FUNCTION]
- INFO[MESSAGE, REPLACEMENTS_FUNCTION]
- ON[SIGNAL_NAME,MARKER]
- WAIT[WAIT_TIME]
- DO[FUNC_NAME]
- GO[MARKER]
- CHOICE[MARKER_1, MARKER_2, MARKER_3, MARKER_4]
- CHOICE[MARKER_1, MARKER_2, MARKER_3]
- CHOICE[MARKER_1, MARKER_2]
- ACTION[MARKER_1, MARKER_2, MARKER_3, MARKER_4]
- ACTION[MARKER_1, MARKER_2, MARKER_3]
- ACTION[MARKER_1, MARKER_2]
- IF[PREDICATE, MARKER]
- CHOICES as an alias for CHOICE
- ACTIONS as an alias for ACTION
Markers are wrapped in square brackets and have no command name. They are used almost like function names. As a best practice, wrap markers in quotes if you use commas in marker names.
[Hello there.]
[Yes#1]
[Yes#2]
[Yes#Only on tuesdays]
["No, I have not seen them."]
[#4) All of the above#45]
[#4) All of the above#46]
You can make repeat markers identifyable with a #
followed by some identifier. These are only used by the scripting engine to identify markers. The text following the #
should not be displayed to the end-user. If you need to use a hash symbol in your choice name, you may. The script engine should only ignore the last hash in the marker name.
say["Are you feeling alright?", Alex]
choice[Yes#Second time saying yes,No#2]
...
[Yes#First time saying yes]
...
[Yes#Second time saying yes]
...
[No#1]
...
[No#2]
Predicates are used inside the IF command. The predicate is a function that always returns a boolean value. Quest Script is only designed to control program flow. Not math or logical functions. Not data manipulation. For that, use something like LUA, Python or BASIC. The predicates logic should live in your game's code. Not in the Quest Script.
IF[func_call(), MARKER_NAME]
or
IF[The player has the key, Unlock Door#1]
Predicates are any string. In your game logic, set up a match(switch) to handle each predicate that is defined in quest script.
match(predicate_string):
"func_call()":
return MY_GLOBAL_AUTOLOAD.func_call()
"The player has the key":
return GLOBAL_INV.has_key()
If you need to get fancy, you can always parse parameters out of a predicate like so:
#WARNING: UNTESTED CODE
if(predicate_string.ends_with(")"):
var split_predicate = predicate_string.split("(")
var predicate_params = split_predicate[1].trim_end(")").split(",")
match(split_predicate[0]):
"func_call":
func_call(predicate_params)
Replacements are used with the Say
command. The Replacements
are defined in your code. When using the SAY
command, you pass the Replacement Function Name
. This function name should defined be in your code and should return one or more replacements. Godot will add these replacements to the message
with the %s
symbol.
SAY[MESSAGE, CHARACTER_INDEX, REPLACEMENTS_FUNCTION]
Say[Hello %s! Wonderful day we are having. Have you seen %s?, replacements_player_and_missing_npc]
"Hello Alex! Wonderful day we are having. Have you seen Ellie?"
and then in your code...
func replacements_player_and_missing_npc():
return [STATE.PLAYER_NAME,GET_MISSING_NPC()]
The INFO command is similar to SAY. Instead of Speaker Name, there is a Title. Replacements work exactly the same as they do in the SAY command. The INFO box is rendered in a different frame as the SAY command and is not associated with any speaker.
The ACTIONS command is similar to the CHOICES command. like the INFO box, it behaves like the CHOICES command but is rendered in a different frame. It is not associated with a character.
Here is a very simple template for a Quest Script implementation :
extends Node
func run_script(script_contents:String):
pass;
func run_script__say(message:String, character_index:int, replacement_function_name:String):
pass;
func run_script__info(message:String, character_index:int, replacement_function_name:String):
pass;
func run_script__do(func_name_and_value:String):
pass;
func run_script__go(marker_name:String):
pass;
func run_script__wait(wait_time:int):
pass;
func run_script__on(signal_name:String, marker:String):
pass;
func run_script__choice(choices:Array[String]):
pass;
func run_script__actions(actions:Array[String]):
pass;
func run_script__if(predicate:String, marker:String):
pass;
I highly encourage making a second script (or more) for the logic. Limit the scipt above to simply matching the script-line to the functionality.
2/26/25
- Rolled Version 1.1.0 > 1.1.1
- Fixed Info Command definition
2/25/25
- Rolled Version 1.0.2 > 1.1.0
- Added feature: INFO command
- Added feature: ACTIONS command
- Added alias CHOICES for CHOICE
- Added some info on predicates
2/19/25
- Rolled Version 1.0.1 > 1.0.2
- Replaced the
Options
from theSAY
command withReplacements
and theREPLACEMENTS_FUNCTION
- Added some more Marker examples.
- Updated template.
2/17/25
- Renamed "WAIT_FOR_SIGNAL[SIGNAL_NAME]" to "ON[SIGNAL_NAME]"
- Added identifier to markers with the
#
symbol - Removed Buttons.
On[]
should suffice. - Replaced
Functions
section with section onOptions
. - Replaced the
Conditions
section withPredicates
. - Rolled Version 1.0.0 > 1.0.1