Creating a log of visited passages and their content that goes in order

Twine Version: 2.9.1

I’ve been using Twine for five whole days and what I’m asking might be impossible, but I figured I’d never know until I tried :slight_smile: And yes, I spent much time with Dr. Google before asking.

I’m translating a Choose Your Own Adventure book from Japanese. Overall this is great because most of the structure is decided for me. I liked Choose Your Own Adventure Books a lot as a kid, but there was one thing I didn’t really enjoy: all the flipping around. That’s part of why I chose to make it a Twine game when I translated it :slight_smile:

Now to the question: I would like to create a combined log of all passages visited, in order, so that once I finish a route I can read the story again over from the beginning. Harlowe has the (history: ) macro and sugarcube has ways of setting variables ($history and the hasVisited function), but there’s nothing I’ve seen that would allow me to call all of the content of the visited passages in order of when they were visited into a single log file.

I tried using the existing solution of creating an Adventure Log, but that is extremely cumbersome and wouldn’t log items longer than a certain number of characters no matter how I tweaked the settings.

So that’s it! Thanks in advance to anyone who can help! :slight_smile:

Untested, but I believe that in Harlowe you should be able to loop over all the (history:) passages and (display:) them?

(for: each _passage, ...(history:))[(display: _passage)]

For SugarCube, you want the <<include>> macro, perhaps like this:

<<for _i to 0; _i lt State.length; ++_i>>\
    <<include State.index(_i).title>>\
<</for>>

Note that Config.history.maxStates defaults to capping the history at 40 “moments.”

Note: I’m assuming that since you’re translating a physical book, each passage will never change or have any variable content. If your passages might look different at different times based on variables that change over the course of the game, then it can get a lot more complicated.

1 Like

Thanks so much for the quick reply!

Fortunately for me, variations to scenes are all tracked in different passages. The content in my passages is all static. :slight_smile:

I’m using Sugarcube, and I’m a totally new at this, so forgive me if I try to spitball a solution here. (It might be wrong.)

For the separate log file, add the rule, then the list of passages to include if they’re visited:

<<for _i to 0; _i lt State.length; ++_i>>\
    <<include State.index(_i).title>>\
<</for>>
<<include Scene 1 >>
<<include Scene 2 >>
<<include Scene 3 >>

etc.

Then it’s just a matter of linking to the log.

Edit: I tried an experiment with this setup. It seems like it almost works.

The text is logged, but it’s logged in order of how the scenes were listed, not in order of when they were visited.

I had to change the state to a number that wasn’t 0 in order for it to run without errors.

Is there a way to alter the cap on Config.history.maxStates? Some routes are that short but most of them are quite a bit longer.

Edit: Never mind, I found this in my JS and fixed it :slight_smile:

Ok so I actually did this to a degree in my game. What I would do is, instead of checking the History after the fact, add each passage() name as it’s visited to an array (this can save far more than 40 moments).

Then at the end, use a <<for>> loop to through this array of passage names with <<include>>.

1 Like

That sounds great, but I wouldn’t know how to do that :sweat_smile: I’m pretty new at this.

I assume adding the visited passages in order to an array is a JS function.

I don’t really understand <<include>>, the documentation is clear as mud to me. And what would I use the <<for>> loop for? (That was a weird sentence to write.)

The solution above does log everything successfully, just not in order. Seems like getting the order right is the hard part.

Something like this (untested)

:: StoryInit
<<set $passagesVisited = []>>

:: PassageFooter [nobr]
    <<set $passagesVisited.push(passage())>>

:: Log
<<for _passageName range $passagesVisited>>\
    <<include _passageName>>\
<</for>>

<<include>> renders the contents of a passage with the provided name.

<<for>> is to loop through every passage name in the $passagesVisited array (here I named it _passageName which is a temporary variable) and run the code within (i.e. <<include _passageName>> If you don’t know what a For loop is in coding I might look it up if I were you.

The $passagesVisited array will save things in the order they’re added (“pushed”) to the array. I would also look up what a JavaScript array is if this concept is unclear to you.

PassageFooter will put the code to push to the array at the end of every passage, so it’ll run every time (think like how a document footer in MS Word will show up at the bottom of every page)

edited to rename _value to _passageName so that part’s a little clearer

Got a working version up. The main thing I changed was making sure the footer doesn’t show up in the log passage cus that causes glitches. I also put the links to other passages in the footer cuz if you don’t then the log’s <<include>> will pick up every single link and repeat it, which also causes problems.

If you want to try this in your code, the :: Title is the title of the passage and then you just stick the content in the text part.

:: PassageFooter
<<if passage() != "log">>
[[passage 1]]
[[passage 2]]
[[passage 3]]
[[log]]
<<set $passagesVisited.push(passage())>>
<</if>>

:: Start
Starting passage

:: StoryInit
<<set $passagesVisited = []>>

:: log
<<for _passageName range $passagesVisited>>\
    <<include _passageName>>\
<</for>>

:: passage 1
The 1st passage text

:: passage 2
The 2nd passage text

:: passage 3
The 3rd passage text

edited to rename _value to _passageName so that part’s a little clearer

2 Likes

Thanks so much for the detailed explanation! :slight_smile:

I think I’m close but nothing is being logged. No errors either so I can’t quite figure out what I’m missing.

I’ve tried putting in several values for .push{passage()) (like 10, 100, etc.) but this doesn’t seem to make a difference.

All three of my passages have text and links.

Nothing gets logged. When I link to the Log passage after visiting all my passages with text, it’s blank.

It’s possible that I’m missing something small… I’ll try again when I get home :slight_smile:

Uhh, just copy exactly what I put. <<include>> only takes one argument and _passageName is literally that argument.

Let me explain a little better…

Let’s say you have the first passage and second passage

:: passage 1
The 1st passage text

:: passage 2
The 2nd passage text

And the footer

:: PassageFooter
<<if passage() != "log">>
[[passage 1]]
[[passage 2]]
[[passage 3]]
[[log]]
<<set $passagesVisited.push(passage())>>
<</if>>

Note that this <<if>> statement checks conditions. So in this case, IF passage() [i.e. the passage name] != [<-- this syntax means “is not”] "log" then include the code within the IF block (that’s anything between <<if>> and its closing tag, <</if>>. Because neither the text “passage 1” or “passage 2” are the same as the text “log”, the stuff within the <<if>> blocks shows up. Please look up what if statements are in code to find more explanation.

In the actual code of the page, Sugarcube will interpret everything here so passage 1 and passage 2 look like this. Do not copy this code, the code I put above does exactly the same thing.

:: passage 1
The 1st passage text
[[passage 1]]
[[passage 2]]
[[passage 3]]
[[log]]
<<set $passagesVisited.push(passage())>>

:: passage 2
The 2nd passage text
[[passage 1]]
[[passage 2]]
[[passage 3]]
[[log]]
<<set $passagesVisited.push(passage())>>

This means the links and the <<set>> code show up in both passages.

passage() gets the name of the passage you’re on.

So what it ACTUALLY looks like in the code is essentially this:

:: passage 1
The 1st passage text
[[passage 1]]
[[passage 2]]
[[passage 3]]
[[log]]
<<set $passagesVisited.push("passage 1")>>

:: passage 2
The 2nd passage text
[[passage 1]]
[[passage 2]]
[[passage 3]]
[[log]]
<<set $passagesVisited.push("passage 2")>>

$passagesVisited is an array. Please look up what a JavaScript array is. Twine is just simplified JavaScript.

If you visit both passages in order, then the inside of $passagesVisited will look like this: ["passage 1", "passage 2"]

Now, the log passage.

:: log
<<for _passageName range $passagesVisited>>\
    <<include _passageName>>\
<</for>>

Please look up what a JavaScript for loop is!! The <<for>> loop will run the <<include>> text as many times as there are values in the $passagesVisited array, sticking the values in the code provided. Since the array has the values “passage 1” and “passage 2” in it, it will run 2 times, with “passage 1” first and “passage 2” second.

This is equivalent to:

:: log
<<include "passage 1">>
<<include "passage 2">>

If you don’t have basic programming knowledge of arrays, variables, if conditions, and for loops I’m not sure how to explain this any better.

ETA:

because <<include>> just includes the contents of the passage with that name, it means that :: log ACTUALLY looks like this

:: log
The 1st passage text
The 2nd passage text
1 Like

Ah, I think the confusion is that you don’t need to list the passages that should get included in the log. So Aster’s code has [[passage 1]] [[passage 2]] [[passage 3]] in the code, but if you put those in your example to tell the computer which passages should be logged when the player sees them then you should leave them out: that’s not how this works.

Also, Aster’s code has the PassageFooter as a convenience, to add every passage to the log (except the log passage itself). This lets you write the code once, in one place (less opportunity to make mistakes).

But if it’s easier for you to think about, you could forget about PassageFooter and write one line of code at the bottom of every scene (hmm, or at the top, position doesn’t matter here):

<<set $passagesVisited.push(passage())>>

You could instead write the name of each passage:

<<set $passagesVisited.push("Scene 1")>>

But passage() gets the current passage name so you don’t have to change it each time you write it.

1 Like

I got it working! Thanks so much.

The actual confusion was here:

:: PassageFooter
<<if passage() != "log">>
[[passage 1]]
[[passage 2]]
[[passage 3]]
[[log]]
<<set $passagesVisited.push(passage())>>
<</if>>

Changing it to this cleared an “undefined” error:

:: PassageFooter
<<if passage() != "log">>
<<set $passagesVisited.push(passage())>>
<</if>>

I did try putting it in exactly as you had it in a test project, but it errored out on me so I started messing with it (badly, as you saw :sweat_smile:). Now that I’ve revised PassageFooter, it’s working! I got it to work in my Choose Your Own Adventure game, too!

Thank you so much for being patient with my ignorance, @pieartsy and @JoshGrams! I’ve still got a lot to learn!

1 Like

@JoshGrams
You will likely need to use Macro Argument markup to force the State.index(_i) function call and the accessing of the relate title property to occur before the <<include>> macro is called, so the result is passed as an argument to the macro.
eg.

 <<include `State.index(_i).title`>>\