Quest Script readable by mere mortals?

For all talk about quest development - creation, testing, and quest system.
BansheeXYZ
Posts: 555
Joined: Fri Oct 23, 2015 8:19 pm

Quest Script readable by mere mortals?

Post by BansheeXYZ »

So... making quests is hard, especially if you're new. Using N0B10Y03 as an example, I've mocked up a new scripting language that I think is a lot more readable and easier to design without mistakes, as it is closer to English syntax. Just a summary of what I did:

-Broke up the script into three separated parts: Definitions, Immediate Tasks, and Triggered Tasks
-Turned triggered tasks into if/then/& indented statements
-took out unnecessary abbreviations to be less cryptic for noobs (pc becomes Player)
-moved spawn intervals/quantities into definitions of enemy variables
-used ##d##h##m format for all duration

Task Objects
---------------------
Chance()
Enemy()
Item()
Message()
Person()
Place()
Affliction()
Player()
Reputation()
Time
Timer()

Object States (the "is" prefix is not required, but helps readability in "If" statements):
--------------------------
Chance can be isA, isB, isC, etc
Enemy can be isHit, isDead, isAlive or have #of in front of it to indicate quantity
Item can be isTaken or isDropped
Time can be is##:## for a time, is##:##to##:## for a timeframe, or is##:##or##:## for multiple periods of time
Timer can be isRunning, isStopped or isExpired
Person can be isClicked or isUnclicked
Player can be isHit, isInside, or isOutside

Actions
-----------
Calculate (Followed by Chance)
Create (Followed by Item)
Define (Followed by things that need preset before quest starts, can only appear under definitions)
Remove (Followed by Player Item or Player Affliction)
Drop (Followed by Player Item)
End (Followed by nothing, ends the quest)
Give (Followed by Player Item or Player Reputation or Player Affliction)
Hide (Followed by Person)
Log (Followed by Message)
Make (Followed by anything that has a changeable adjective status)
Reveal (followed by Place)
Say (Followed by Message)
Show (Followed by Person)
Spawn (followed by Enemy)
Start (Followed by Timer)
Stop (Followed by Timer)
Tag (followed by Item)
Unspawn (followed by Enemy)
Untag (followed by Item)

newquestscript.png
newquestscript.png (41.37 KiB) Viewed 3438 times
I'm sure I've made some oversights. I have no idea what the "create npc at mages guild" part does in the original, or the clearclick stuff, or the variables s.10 and s.13 that have no contents... thoughts?

edit: revising words
Last edited by BansheeXYZ on Fri Sep 20, 2019 11:14 am, edited 9 times in total.

User avatar
Jay_H
Posts: 4061
Joined: Tue Aug 25, 2015 1:54 am
Contact:

Re: Quest Script readable by mere mortals?

Post by Jay_H »

If making quests were hard, I could never do it! I'm the least capable of this entire forum, I can almost guarantee it :lol:

"place" and "create" are two different concepts for the game. For items it's unnecessary; the game automatically creates all items upon quest start, but doesn't place them in the game world. For people creating was necessary in Daggerfall but unnecessary in DFU; DFU creates people at a chosen spawnpoint for them upon quest start.

"clear" is used to shut down a previously activated action. For example, if I hit an enemy, I can set task _spawn_. If I kill the enemy, I can clear task _spawn_. _spawn_ would create enemies while it's activated, until it's deactivated.

Part of the ease you have is that you've renamed all the variables. Working with endless "S.02" and so on while editing the classic quests was the most Medusa-like of all my tasks; overlooking that, in terms of order you're really just using preference here. I've gotten used to the Template structure so I can parse a quest in very little time. It's a matter of familiarity, is all.

JorisVanEijden has shown that the Template compiler wasn't perfect, but we're working to fix its errors. Some tasks will be blank just for no reason, partially due to things like that and partially because of unfinished content -- like the "love" variable in the Knightly Order witch artifact quest.

BansheeXYZ
Posts: 555
Joined: Fri Oct 23, 2015 8:19 pm

Re: Quest Script readable by mere mortals?

Post by BansheeXYZ »

Jay_H wrote: Thu Sep 05, 2019 10:26 pmIf making quests were hard, I could never do it! I'm the least capable of this entire forum, I can almost guarantee it :lol:
You've been at this for a long time and I fear we're not going to get many more people making quests due to how hard it is to decipher the old format. And I mean like REAL new quests, stuff that isn't just an edit or variant of existing ones. Seeing how many errors the original devs made with it and weeding out DFU bugs that have lingered for years makes me think it's just not readable enough for even seasoned programmers. Especially when the quest is somewhat complex and has all these forked paths and conditions.
"place" and "create" are two different concepts for the game. For items it's unnecessary; the game automatically creates all items upon quest start, but doesn't place them in the game world. For people creating was necessary in Daggerfall but unnecessary in DFU; DFU creates people at a chosen spawnpoint for them upon quest start.
I think I understood most of this, but I didn't want to use Place at Place. I thought it would be clearer to use Create at Place. Place is one of those weird words that can either be a noun or verb.
"clear" is used to shut down a previously activated action. For example, if I hit an enemy, I can set task _spawn_. If I kill the enemy, I can clear task _spawn_. _spawn_ would create enemies while it's activated, until it's deactivated.
Yeah, I think this is an oversight on my end not having "Stop" for timers. Spawns in other quests can be infinite. If you hooked up these spawns to an immediate task Timer called _infinite_ that was the duration of the quest, you could just be like

Code: Select all

If Player isInside Place(_house1_) & Timer(_infinite_) isRunning
       Then Spawn Enemy(_rats_)
If 7of Enemy(_rats_) isKilled
       Then Stop Timer(_infinite_)
Although I'm still unclear on what clearclick does for qgiver, or if that's even used by DFU.
Part of the ease you have is that you've renamed all the variables. Working with endless "S.02" and so on while editing the classic quests was the most Medusa-like of all my tasks; overlooking that, in terms of order you're really just using preference here. I've gotten used to the Template structure so I can parse a quest in very little time. It's a matter of familiarity, is all.
Yeah all the unnamed tasks and spawns are nasty. It makes the questfile read like a cipher, where I'm constantly scrolling back to see what S## means and how it's different from S##. Like, why did they call the timers _oneday_ and _S.10_ instead of _dueback_ and _guardshift_? Or _F.00_ instead of _thieves_? Not giving descriptive names makes it harder to reference them later, and you're going to make swap mistakes constantly.

User avatar
Jay_H
Posts: 4061
Joined: Tue Aug 25, 2015 1:54 am
Contact:

Re: Quest Script readable by mere mortals?

Post by Jay_H »

I've been talking to a couple people on Discord who might be interested in writing quests. If they are, perhaps more will join too :) I was able to fix an error in the quest tutorial yesterday from someone following it strictly; I don't think others had followed the tutorial that far.

If someone were to add an alternate input methods for quests, it would have to be third party. I know for a fact Interkarma won't be doing it :lol:

User avatar
JorisVanEijden
Posts: 114
Joined: Mon Aug 12, 2019 5:02 pm

Re: Quest Script readable by mere mortals?

Post by JorisVanEijden »

See relevant comment in thread viewtopic.php?f=29&t=2628&start=10#p30818

And maybe https://stellargames.github.io/Quester/ for something very close to daggerfall classic.

At the moment any approach would need to be able to both compile to the internal DFU quest object format (see savegame json) and decompile from it to preserve the quests we have now.

The visual node based quest editor would be incredibly user friendly and eady to use but would require a lot of work to build and it would not produce copy & pastable text output to read.

A new language would have to be extremely easy to use and write quests in. As long as all the information is in it we can build a parser /compiler to get it in DFU.
The examples in this thread so far are not that much easier to read/use. They require quite a lot of boilerplate to achieve the things that are possible in the engine.
For a textual format I think we'd need a more coroutine-like syntax to represents the concurrency and the on /off states of "tasks", functions, variables or states.

The level of abstraction is also a concern. You need to find some middle ground between writing

Code: Select all

if you have the treasure on you outside the guard period you will get arrested.
and

Code: Select all

 12: >> IfItemPickedUp (i_treasure): set s_stoletreasure
13: >> IfItemDroppedAt (i_treasure, l_magesguild): set not s_stoletreasure
14: >> If (s_stoletreasure and not s_midnight3): set s_persecute
15: s_persecute >> StartTimer (t_000da513); When it expires: set s_000da513
16: s_persecute => CreateLogEntry (1011, 1)
17: s_persecute => AdjustReputationWithNpc (n_qgiver, -30)
18: s_persecute => CreateFoe(m_knights, 60, 10%, 5)
19: >> IfMobHurtByPlayer (m_knights): set s_001a98fb [Msg 1013]

BansheeXYZ
Posts: 555
Joined: Fri Oct 23, 2015 8:19 pm

Re: Quest Script readable by mere mortals?

Post by BansheeXYZ »

At the moment any approach would need to be able to both compile to the internal DFU quest object format (see savegame json) and decompile from it to preserve the quests we have now.
Thanks for the feedback. How hard would it be for DFU to support both the old and a new scripting method by using headers in the quest file to tell DFU which type it is and writing the new quests to its own "QuestData2.txt"? That would also prevent old quests from needing converted.
The visual node based quest editor would be incredibly user friendly and eady to use but would require a lot of work to build and it would not produce copy & pastable text output to read.
Yeah, I'm not sure the interest is there to justify the work involved in something that fancy. Ultimately, the UI for such a program would end up looking like this, procedurally defining, then stringing conditions together to trigger events.

If there was any interest in this, it would be useful to have input from people who don't already have intimate knowledge. It's really hard for programmers or DFU regulars like Jay to pretend to be new again and judge learning curves. So while I value your input, it would be better to throw both methods at some noobs with a bundled tutorial and get opinions on usability.

To clarify some earlier points:

Code: Select all

daily from 00:00 to 03:00 
This isn't a task per se, it's a condition posing as a task that an actual task references backwardly. Everything in classic is jumbled into the same visual hierarchy and called a "task". To me, this is what makes classic's method so hard to read. The word task connotes action, yet here are all these lines that have no action in them. I'd prefer clearer distinction between conditions and tasks, and what conditions belong to what tasks. That is what new people are going to struggle with the most, and what I believe my example overcomes.

The second biggest struggle is that nothing is labelled. The definitions that come at the start aren't called definitions, they almost look at first like they could be part of the task portion. Clearer lines between what is done prior, at the start of, and during the quest are needed. That too is what I tried to solve by dividing things into those labelled categories.

Code: Select all

injured _F.00_ saying 1012
This is pretty much a condition and task rolled into one, with awkward tensing (present participle) for the task (say) that doesn't match the tenses of other commands.

Also, why do things like "Start Timer X" and "Create Foe X", but then "Say X". That's not consistent, it shouldv'e been "Say Message X"

Code: Select all

create foe _F.01_ every 60 minutes 5 times with 10% success 
start timer _S.10_ 
log 1011 step 1 
change repute with _qgiver_ by -30 
These look much better, but we're getting lengthy definitions for spawns and penalties when we already had a place to do that at the start of the QBN. So why muddy up the task area with it when the task area is the hardest to read when looking for errors in your work?

User avatar
JorisVanEijden
Posts: 114
Joined: Mon Aug 12, 2019 5:02 pm

Re: Quest Script readable by mere mortals?

Post by JorisVanEijden »

Hah, you're telling me.
I stumbled upon this project only three weeks ago. I found the "Template" quest format so bewildering that I wrote a new QBN (daggerfall classic quest format) decompiler just to understand what the quests were trying to accomplish.

You can use any language or script that you can think of to write quests in, as long as they transpile to Template (Tipton's QBN decompiler format that DFU uses) or QBN (which can already be decompiled into Template)

Having multiple parsers in DFU is not going to happen because it makes little sense.
What we might do is decouple the parser from the questmachine a bit more so that other external parsers/compilers that produce serialized Quest objects can also be used.

So for a new/improved quest language your best bets are either writing a "newscript" to Template transpiler or building a "newscript" compiler that outputs serialized DaggerfallWorkshop.Game.Questing.Quest objects.
Once someone is committed to doing that we can discuss the syntax and features this new script language should have.

User avatar
TheLacus
Posts: 1305
Joined: Wed Sep 14, 2016 6:22 pm

Re: Quest Script readable by mere mortals?

Post by TheLacus »

BansheeXYZ wrote: Sat Sep 07, 2019 7:35 am

Code: Select all

daily from 00:00 to 03:00 
This isn't a task per se, it's a condition posing as a task that an actual task references backwardly. Everything in classic is jumbled into the same visual hierarchy and called a "task". To me, this is what makes classic's method so hard to read. The word task connotes action, yet here are all these lines that have no action in them. I'd prefer clearer distinction between conditions and tasks, and what conditions belong to what tasks. That is what new people are going to struggle with the most, and what I believe my example overcomes.
Tasks are live objects; forget the name task if you think is confusing, but this is what they are ;)
A task is composed by one or multiple conditions (that are something like an event that trigger the task, not just an if that is checked once) and actions (the actual executive part). See this example from Jay

Code: Select all

_day_ task:
    daily from 5:00 to 20:00
    hide npc _friend_

_night_ task:
    when not _day_
    restore _friend_
If you want to follow an if-else pattern you could write something like this:

Code: Select all

when (from 5:00 to 20:00)
    hide npc _friend_
else
    restore _friend_
The issue is that you lose the ability to reference tasks; How would you change something more advanced like this one? (link to full quest)

Code: Select all

pick one of _dud_ _dud_ _dud_ _dud_ _alt_
pc at _mondung_ set _inside_

_daytime_ task:
	daily from 8:00 to 18:00
	
_slain_ task:
	killed 1 _enemy_ saying 1020

variable _dud_

_altspawn_ task:
	when _inside_ and _alt_
	start task _altinform_
	
_spawn_ task:
	when _inside_ and _daytime_ and not _slain_ and not _alt_
	create foe _wild1_ every 14 minutes indefinitely with 45% success
	create foe _wild2_ every 15 minutes indefinitely with 36% success
	create foe _wild3_ every 17 minutes indefinitely with 34% success
	create foe _wild4_ every 22 minutes indefinitely with 22% success
BansheeXYZ wrote: Sat Sep 07, 2019 7:35 am

Code: Select all

injured _F.00_ saying 1012
This is pretty much a condition and task rolled into one, with awkward tensing (present participle) for the task (say) that doesn't match the tenses of other commands.

Also, why do things like "Start Timer X" and "Create Foe X", but then "Say X". That's not consistent, it shouldv'e been "Say Message X"
There are many conditions that allow an optional saying id for convenience. If you don't like it you can split the condition and the action:

Code: Select all

injured _F.00_
say 1012
BansheeXYZ wrote: Sat Sep 07, 2019 7:35 am

Code: Select all

create foe _F.01_ every 60 minutes 5 times with 10% success 
start timer _S.10_ 
log 1011 step 1 
change repute with _qgiver_ by -30 
These look much better, but we're getting lengthy definitions for spawns and penalties when we already had a place to do that at the start of the QBN. So why muddy up the task area with it when the task area is the hardest to read when looking for errors in your work?
What exactly do you want to move to the definitions section? I don't think moving create foe is a good idea; what if a new action is introduced to deal with enemy spawn in a different way? Personally i see definitions as constructors for resources' symbols, i don't expect them to define data that is only useful for a specific action.

BansheeXYZ
Posts: 555
Joined: Fri Oct 23, 2015 8:19 pm

Re: Quest Script readable by mere mortals?

Post by BansheeXYZ »

Sorry for the late reply, had stuff to do today.
The issue is that you lose the ability to reference tasks; How would you change something more advanced like this one?
I see what you mean. The "pick one" command looks like it's mostly used as a mild randomizer to make quests less repetitive and fork you into different events. The sick cousin quest is another one I looked at doing this: you have a 50% chance of contracting swamp rot when talking to the cousin. Jay is using this to give a 20% chance to spawn no hermit minions and fire a message.

It looks like I simply need to add Chance as a defined object and then use it in the conditions. Also, I used "??d??h??m as letting the engine set the timer (based on dungeon distance). Here's Jay's quest in my format. Please poke holes in it if you can:

Code: Select all

Definitions:
	Define Item(_alch_) as small.plant
	Define Person(_questgiver_) as male.questor
	Define Place(_dungeon_) as remote.dungeon
	Define Timer(_dueback_) as ??d??h??m
	Define Enemy(_hermit_) as healer
	Define Enemy(_grizzlybears_) as grizzly_bear every 00d00h14m indefinitely with 45% success
	Define Enemy(_giantbats_) as giant_bat every 00d00h15m indefinitely with 36% success
	Define Enemy(_giantrats_) as giant_rat every 00d00h17m indefinitely with 34% success
	Define Enemy(_spriggans_) as spriggan every 00d00h22m indefinitely with 22% success
	Define Chance(_minions_) as 20% A 80% B

Immediate Tasks:
	Start Timer(_dueback_) 
	Log Message(1030)
	Calculate Chance(_minions_)
	Create Enemy(_hermit_) at Place(_dungeon_)
	Reveal Place(_dungeon_)

Triggered Tasks:
	If Player isInside Place(_dungeon_) & Chance(_minions_) isA
		Then Say Message(1024)
	If Player isInside Place(_dungeon_) & Chance(_minions_) isB & Enemy(_hermit_) isAlive & Time is08:00to18:00
		Then Spawn Enemy(_grizzlybears_) & Spawn Enemy(_giantbats_) & Spawn Enemy(_giantrats_) & Spawn Enemy(_spriggans_)
	If Enemy(_hermit_) isDead
		Then Say Message(1020)
	If Person(_questgiver_) isClicked & Enemy(_hermit_) isDead
		Then Say Message(1004) & End
	If Timer(_dueback_) isExpired
		Then Say Message(1003) & End
Correct me if I'm wrong, but this seems to expose a shortcoming in classic, which is that making small chances (like a 1% chance to drop a rare item) is not feasible, because you'd have to write in 100 tasks. I mean, you could do that, but it would look ridiculous.
Last edited by BansheeXYZ on Mon Sep 09, 2019 2:37 am, edited 1 time in total.

User avatar
Jay_H
Posts: 4061
Joined: Tue Aug 25, 2015 1:54 am
Contact:

Re: Quest Script readable by mere mortals?

Post by Jay_H »

Eh, sort of. You can cascade it using multiple events:

Code: Select all

_hundredpercent_ task:
pick one of _tenpercent_ _fail_ _fail_ _fail_ _fail_ _fail_ _fail_ _fail_ _fail_ _fail_

Code: Select all

_tenpercent_ task:
pick one of _onepercent_ _fail_ _fail_ _fail_ _fail_ _fail_ _fail_ _fail_ _fail_ _fail_

Code: Select all

_onepercent_ task:
give pc _reward_

Code: Select all

variable _fail_
10% * 10% should be 1%, if I have my statistics properly understood.

Post Reply