Edit: Stupid typo (a -
for a =
)
Edit2: Another silly mistake that didn't show up on my first test
Something recently prompted me to look at some of the gamebooks on my bookshelf, and I remembered that you were often turning dozens of pages at once. They never had a link to the next page; maybe because that would encourage cheating, or maybe so you didn't accidentally see a possible next option when flipping through pages.
Now, that's not necessary in an online gamebook. But maybe having page numbers jumping all over the place gives you a feeling of nostalgia or something. So I made a script for Quest that will assign every page a number; attempting to maximise the number of pages that each link jumps over.
To use this code:
pagenumber
int attribute.
pagenumber
but also set number_static
to false
, the page will start where you specify but can still move. This could be used to give the algorithm a hint about what would be a good order, so it starts faster.
?#?
in a page's text part, it will be replaced with the page number.
number_shown
to true?#?
will be replaced by the number of the page it goes to.
(turn to page ?#?)
to each option… like in the gamebooks on my bookshelfmaxnumber*3
and change it to something lower. This may result in some pages being close to their successors, but will finish quicker.This code can go in the enter script, on the game's "Scripts" tab.
firsttime {
allpages = AllObjects()
list remove (allpages, player)
maxnumber = ListCount (allpages)
next_number = 1
page_names_by_number = NewStringDictionary()
if (not HasInt (player.parent, "pagenumber")) {
player.parent.pagenumber = 1
}
foreach (page, allpages) {
if (HasInt (page, "pagenumber")) {
if (not HasBoolean (page, "number_static")) {
page.number_static = true
}
if (page.number_static and page.pagenumber > maxnumber) {
maxnumber = page.pagenumber
}
}
else {
while (FindWithAttribute (allpages, "pagenumber", next_number)) {
next_number = next_number + 1
}
page.pagenumber = next_number
}
dictionary add (page_names_by_number, ""+page.pagenumber, page.name)
if (page.pagenumber > maxnumber) {
maxnumber = page.pagenumber
}
if (not HasAttribute (page, "bi_links")) {
page.bi_links = NewObjectList()
}
if (HasAttribute (page, "options")) {
foreach (name, page.options) {
target = GetObject (name)
if (not target = null) {
if (not HasAttribute (target, "bi_links")) {
target.bi_links = NewObjectList()
}
if (not ListContains (page.bi_links, target)) {
list add (page.bi_links, target)
}
if (not ListContains (target.bi_links, page)) {
list add (target.bi_links, page)
}
}
}
}
}
orders_tried = NewStringList()
currentorder = ""
while (not ListContains (orders_tried, currentorder) and ListCount (orders_tried) < maxnumber*3) {
list add (orders_tried, currentorder)
currentorder = ""
for (i, 1, maxnumber) {
currentorder = currentorder + ">"
if (DictionaryContains (page_names_by_number, ""+i) and GetBoolean (GetObject (StringDictionaryItem (page_names_by_number, ""+i)), "number_static")) {
j = maxnumber + 1
}
else {
j = i + 1
}
while (DictionaryContains (page_names_by_number, ""+j) and GetBoolean (GetObject (StringDictionaryItem (page_names_by_number, ""+j)), "number_static")) {
j = j + 1
}
if (j <= maxnumber) {
pushswap = 0
leftpage = null
rightpage = null
if (DictionaryContains (page_names_by_number, ""+i)) {
leftpage = GetObject (StringDictionaryItem (page_names_by_number, ""+i))
}
if (not leftpage = null) {
foreach (comp, leftpage.bi_links) {
if (comp.pagenumber < i) {
diff = i - comp.pagenumber
while (diff < maxnumber+12) {
pushswap = pushswap + 1
diff = diff * 3/2
if (diff = 1) {
diff = 2
}
}
}
else if (comp.pagenumber > j) {
diff = comp.pagenumber - j
while (diff < maxnumber+12) {
pushswap = pushswap - 1
diff = diff * 3/2
if (diff = 1) {
diff = 2
}
}
}
}
}
if (DictionaryContains (page_names_by_number, ""+j)) {
rightpage = GetObject (StringDictionaryItem (page_names_by_number, ""+j))
}
if (not rightpage = null) {
foreach (comp, rightpage.bi_links) {
if (comp.pagenumber < i) {
diff = i - comp.pagenumber
while (diff < maxnumber+12) {
pushswap = pushswap - 1
diff = diff * 3/2
if (diff = 1) {
diff = 2
}
}
}
else if (comp.pagenumber > j) {
diff = comp.pagenumber - j
while (diff < maxnumber+12) {
pushswap = pushswap + 1
diff = diff * 3/2
if (diff = 1) {
diff = 2
}
}
}
}
}
if (pushswap > 1) {
if (DictionaryContains (page_names_by_number, ""+i)) {
dictionary remove (page_names_by_number, ""+i)
}
if (DictionaryContains (page_names_by_number, ""+(i+1))) {
dictionary remove (page_names_by_number, ""+(i+1))
}
if (not leftpage = null) {
leftpage.pagenumber = i + 1
dictionary add (page_names_by_number, ""+(i + 1), leftpage.name)
}
if (not rightpage = null) {
rightpage.pagenumber = i
dictionary add (page_names_by_number, ""+i, rightpage.name)
}
}
}
if (DictionaryContains (page_names_by_number, ""+i)) currentorder = currentorder + StringDictionaryItem (page_names_by_number, ""+i)
}
JS.console.log ("Arranged order: "+currentorder)
}
// Apply the new numbers to the pages
foreach (page, AllObjects()) {
if (HasString (page, "description") and HasString (page, "pagenumber")) {
if (Instr (page.description, "?#?") > 0 and not HasBoolean (page, "number_shown")) {
page.number_shown = true
}
page.description = Replace (page.description, "?#?", ""+page.pagenumber)
}
if (HasAttribute (page, "options")) {
new_options = NewStringDictionary()
foreach (option, page.options) {
number = "???"
target = GetObject (option)
link = DictionaryItem (page.options, option)
if (not target = null and HasInt (target, "pagenumber")) {
if (Instr (link, "?#?") > 0) {
link = Replace (link, "?#?", target.pagenumber)
}
else {
link = link + " <small>(turn to page " + target.pagenumber + ")</small>"
}
}
dictionary add (new_options, option, link)
}
page.options = new_options
}
}
}
if (HasInt (player.parent, "pagenumber") and not GetBoolean (player.parent, "number_shown")) {
msg ("<h4 align=\"center\">Page " + player.parent.pagenumber + "</h4>")
}
You will also need this utility function:
<function name="FindWithAttribute" type="boolean" parameters="list,attr,value">
foreach (obj, list) {
if (HasAttribute (obj, attr)) {
v = GetAttribute (obj, attr)
if (TypeOf (v) = TypeOf (value)) {
if (value = v) {
return (true)
}
}
}
}
return (false)
</function>
(which checks a list to see if it contains any objects with a specific attribute value)
Note: Yes, it's a spring tensor distribution algorithm implemented using bubble sort. As much as I'd like to do it more efficiently, every method I could think of ends up being way more complex thanks to the interesting quirks of Quest's data scructures.
I banged my head against the wall a few days until I made it worked, and your code turned up to be very useful.
I was expecting the page numbers to be random every time the player starts up the game, Nope!, the page numbers are consistent, making it a true nostalgic returning trip to the golden age of real game books where you actually flip the books.
I have used your code on my game and have credited mrangel.
So, for newcomers who is facing issue of adding this code, you can continue reading the following.
The code 1 of mrangel is straightforward, directly paste into gamebook, script tab as stated by mrangel.
The code 2 is the hard one, no matter how you paste it, it ain't going work.
So, the answer is, right click the pages, + function.
Give it a name of FindWithAttribute
Set the return type to Boolean
Add in the parameters, list, attr, value (You should have 3 parameters in total.)
Copy the simplified code 2 into the function called FindWithAttribute.
foreach (obj, list) {
if (HasAttribute (obj, attr)) {
v = GetAttribute (obj, attr)
if (TypeOf (v) = TypeOf (value)) {
if (value = v) {
return (true)
}
}
}
}
return (false)
The code 2 is the hard one, no matter how you paste it, it ain't going work.
Ah, sorry, didn't think about giving detailed directions. If you're on the desktop editor, you can also paste it in full code view; just putting it outside of any other objects or functions; but I guess that's not something most people know how to do either.
Glad you could figure out how to use it :)
Failed to load game.
The following errors occurred:
Invalid XML: Name cannot begin with the '<' character, hexadecimal value 0x3C. Line 209, position 81.
This is not important, since the code is already working.
But just informing, that copy pasting code 2 into full code view doesn't work.
Okay, maybe I placed in wrong place.
So, I created a fake function.
Then I go to full code view and replace fake function with code 2, which still gives me the same error.
I might have found the error the first line of code 2.
The last letter should be > and not <.
<function name="FindWithAttribute" type="boolean" parameters="list,attr,value"<
foreach (obj, list) {
if (HasAttribute (obj, attr)) {
v = GetAttribute (obj, attr)
if (TypeOf (v) = TypeOf (value)) {
if (value = v) {
return (true)
}
}
}
}
return (false)
</function>