Revealing more text in the same passage more than once

I’ve been trying to look for ways to have it so that the player clicks on a link and rather than go to the next passage, it reveals more text that was in the same passage, along with another link to reveal more text, and so on until the end of the passage. I found one way but it made some of the previous text disappear, when I want all the text revealed to stay on the page until the player leaves it.

Basically, I want it to sort of work like a text adventure game where the screen isn’t cleared when more text is revealed until a certain point. I just thought it would look better than having long pages to scroll through, but not have to make loads and loads of passages. Is it possible?

I think you’ll find the <<linkappend>> macro useful (docs). You could nest multiple of those inside one another.

If you have no choice in that passage, just clicking a link to reveal one thing at a time one below the other, Cycy’s CTP custom macro might be nicer (you wouldn’t need to nest <<linkappend>> inside one another.

(There is also the <<link>> + <<append/replace>> combo if you want to change multiple spots on the page when you click on a link, or if you want the player to have choices, but want to stay on one same page. See this post)

2 Likes

Sidenote, and grumping: This was one of the things I really appreciated about AXMA Story Maker - you could prepend any link with a + to make whatever was in the linked passage add to the current window instead of navigating.

[[+Click this to add more text|screentext]]

The other coolest thing was prepending an asterisk * automatically showed the text in a modal window.

[[*Click to pop up your stats!|statscreen]]

The secret handy thing about this is you could still navigate to those passages in full when necessary without the + or *.

1 Like

Thanks, I’m using the one that doesn’t involve nesting to make things less confusing.

I have another question, though. Is there any way to make the links underlined so it’s more obvious to the player that they need to click on them? I tried using <u> and </u> but it says it can’t find the closing tag even though there is one.

You need to edit the CSS for this and target the link itself a (could be .passage a if you don’t want it to target the links outside the passage) and a:hover for when you hover over the link.

I found a way to underline things by typing out underscores around the words I wanted underlines, although it took me a few tries to get it to work in certain places.

Is there a way to make the button that advances the text disappear once it reaches the end? I don’t remember if it was on that page or not.

Is there also a way to make it so that the revealed text stays if you leave the page using the back button, then come back to it with the forward button? I feel like that would involve a script or variable but I’m not sure exactly how those work. I found a mention of it on that page I was using, but I’m not sure if just entering it in to the Javascript will work or if i need to do something else as well.

If you are using the base UI, then you would need special JavaScript for it to happen. If you have a custom UI, then the arrow could be a regular button opening a popup message where the player confirms the choice to rewind.
You could also choose to remove the back button altogether and not have that problem.
Or make a warning at the start of the game.

What method did you end up using? CTP or a regular link?

I went with CTP because it seemed easier to manage.

You could then use it in combination with <<liveupdate>> (also from Cycy), in something like this:

<<liveupdate>>
    <<if !_var>>
        \*your code of the button to advance the story*\
    <</if>>
<</liveupdate>>

And in the part where you put your last bit of text to be revealead: <<set _var to true>><<update>>

I copied in the Javascript and then added the code to the passage and put the link where the text is, but it keeps saying the macro doesn’t exist or that it can’t find a closing tag even though there is one.

There there was something wrong when you copied the JavaScript. I recommend checking whether all the code there is copied correctly for both macros

I don’t know what could be wrong with the Javascript, I coped all of it, and I don’t know if any other javascripts I have are interfereing with it or not, because some have before but then things worked when I removed them, but I’d rather not have to remove the ones I’m finding useful. Here’s a copy of what I have so far so someone can try to find what’s wrong.

if (document.location.href.toLowerCase().includes("/temp/") || document.location.href.toLowerCase().includes("/private/") || hasOwnProperty.call(window, "storyFormat")) {
	// Change this to the path where the HTML file is
	// located if you want to run this from inside Twine.
	setup.Path = "C:/Stories/AttachmentPart1";  // Running inside Twine application
} else { 
	setup.Path = "";  // Running in a browser
}
setup.SoundPath = setup.Path + "sounds/";

// Volume Slider, by Chapel; for SugarCube 2
// version 1.2.0 (modified by HiEv)
// For custom CSS for slider use: http://danielstern.ca/range.css/#/

/*
	Changelog:
	v1.2.0:
		- Fixed using/storing the current volume level in the settings.
	v1.1.0:
		- Fixed compatibility issues with SugarCube version 2.28 (still
		  compatible with older versions, too).
		- Added settings API integration for SugarCube 2.26.
		- Internal improvements and greater style consistency with my
		  other work.
		- Added a pre-minified version.
		- By default, the slider is now more granular than before
		  (101 possible positions vs 11). Change the 'current' and
		  'rangeMax' options to 10 to restore the old feel.
*/

(function () {
	// Set initial values.
	var options = {
		current  : 50,  // Default volume level.
		rangeMax : 100,
		step	 : 1,
		setting  : true
	};
	Setting.load();
	if (options.setting && settings.volume) {
		options.current = parseInt(settings.volume);
	}
	var vol = {
		last: options.current,
		start: (options.current / options.rangeMax).toFixed(2)
	};

	// Function to update the volume level.
	function setVolume (val) {
		if (typeof val !== 'number') val = Number(val);
		if (Number.isNaN(val) || val < 0) val = 0;
		if (val > 1) val = 1;
		options.current = Math.round(val * options.rangeMax);
		if (options.setting) {
			settings.volume = options.current;
			Setting.save();
		}
		if ($('input[name=volume]').val() != options.current) {
			$('input[name=volume]').val(options.current);
		}
		try {
			if (SimpleAudio) {
				if (typeof SimpleAudio.volume === 'function') {
					SimpleAudio.volume(val);
				} else {
					SimpleAudio.volume = val;
				}
				return val;
			} else {
				throw new Error('Cannot access audio API.');
			}
		} catch (err) {
			// Fall back to the wikifier if we have to.
			console.error(err.message, err);
			$.wiki('<<masteraudio volume ' + val + '>>');
			return val;
		}
	}

	// Fix the initial volume level display.
	postdisplay['volume-task'] = function (taskName) {
		delete postdisplay[taskName];
		setVolume(vol.start);
	};

	// Grab volume level changes from the volume slider.
	$(document).on('input', 'input[name=volume]', function() {
		var change = parseInt($('input[name=volume]').val());
		setVolume(change / options.rangeMax);
		vol.last = change;
	});

	// Create the <<volume>> macro.
	Macro.add('volume', {
		handler : function () {
			var wrapper = $(document.createElement('span'));
			var slider = $(document.createElement('input'));
			var className = 'macro-' + this.name;
			slider.attr({
				id		: 'volume-control',
				type	: 'range',
				name	: 'volume',
				min		: '0',
				max		: options.rangeMax,
				step	: options.step,
				value	: options.current
			});
			// Class '.macro-volume' and ID '#volume-control' for styling the slider
			wrapper.append(slider).addClass(className).appendTo(this.output);
		}
	});

	// Add Setting API integration for SugarCube 2.26 and higher.
	function updateVolume () {
		setVolume(settings.volume / options.rangeMax);
	}
	if (options.setting) {
		if (Setting && Setting.addRange && typeof Setting.addRange === 'function') {
			Setting.addRange('volume', {
				label : 'Volume: ',
				min : 0,
				max : options.rangeMax,
				step : options.step,
				default : options.current,
				onInit : updateVolume,
				onChange : updateVolume
			});
		} else {
			console.error('This version of SugarCube does not include the `Settings.addRange()` method; please try updating to the latest version of SugarCube.');
		}
	}
}());

function revealFirstHidden(ev) { 
	if(ev.target.nodeName === 'A') return
	const hidden = $('.passage .hide')
	if(hidden.length > 0) {
		let show = hidden.first()
		show.addClass('fadein')
		show.removeClass('hide')
		if(show[0].scrollIntoView)
			show[0].scrollIntoView({behavior: 'smooth', block: 'nearest'})
	}
}

(function () {
	"use strict";

	$(document).on(":passageinit", () => {
		CTP.Logs.forEach((_, id) => {
			if (!CTP.Repository.get(id)?.persist) CTP.Logs.delete(id);
		});
		CTP.Repository.forEach(({ persist }, id) => {
			if (!persist) CTP.Repository.delete(id);
		});
	});

	window.CTP = class CTP {
		constructor(id, persist = false) {
			this.stack = [];
			this.clears = [];
			this.options = {};
			if (!id?.trim()) throw new Error(`No ID specified!`);
			this.id = id;
			this.persist = persist;
			CTP.Repository.set(id, this);
		}

		static get Repository() {
			if (!setup["@CTP/Repository"]) setup["@CTP/Repository"] = new Map();
			return setup["@CTP/Repository"];
		}

		static get Logs() {
			if (!variables()["@CTP/Logs"]) variables()["@CTP/Logs"] = new Map();
			return variables()["@CTP/Logs"];
		}

		get log() {
			if (!CTP.Logs.get(this.id)) CTP.Logs.set(this.id, { lastClear: -1, index: -1, seen: -1 });
			return CTP.Logs.get(this.id);
		}

		static getCTP(id) {
			return CTP.Repository.get(id);
		}

		add(content, options = {}) {
			options = {
				...this.options,
				...options
			};
			if (options.clear) this.clears.push(this.stack.length);
			this.stack.push({
				options, content,
				index: this.stack.length,
				element: $()
			});
			return this;
		}

		print(index) {
			const { content, options: iOpts } = this.stack[index];
			const options = {
				...this.options,
				...iOpts
			};
			const element = $(document.createElement(options.element || "span"))
				.addClass("--macro-ctp-hidden")
				.attr({
					"data-macro-ctp-id": this.id,
					"data-macro-ctp-index": index,
				})
				.on("update-internal.macro-ctp", (event, firstTime) => {
					if ($(event.target).is(element)) {
						if (index === this.log.index) {
							if (firstTime) {
								if (typeof content === "string") element.wiki(content);
								else element.append(content);
								element.addClass(options.transition ? "--macro-ctp-t8n" : "");
							}
							element.removeClass("--macro-ctp-hidden");
						} else {
							if (index < this.log.seen) element.removeClass("--macro-ctp-t8n");
							element.toggleClass("--macro-ctp-hidden", index > this.log.index || index < this.log.lastClear);
						}
					}
				});
			this.stack[index].element = element;
			return element;
		}

		output() {
			const wrapper = document.createDocumentFragment();
			for (let i = 0; i < this.stack.length; i++) {
				this.print(i).appendTo(wrapper);
			}
			return wrapper;
		}

		advance() {
			if (this.log.index < this.stack.length - 1) {
				this.log.index++;
				const firstTime = this.log.index > this.log.seen;
				this.log.seen = Math.max(this.log.seen, this.log.index);
				this.log.lastClear = this.clears.slice().reverse().find(el => el <= this.log.index) ?? -1;
				$(document).trigger("update.macro-ctp", ["advance", this.id, this.log.index]);
				this.stack.forEach(({ element }) => element.trigger("update-internal.macro-ctp", [firstTime, "advance", this.id, this.log.index]));
			}
			return this;
		}

		back() {
			if (this.log.index > 0) {
				this.log.index--;
				this.log.lastClear = this.clears.slice().reverse().find(el => el <= this.log.index) ?? -1;
				$(document).trigger("update.macro-ctp", ["back", this.id, this.log.index]);
				this.stack.forEach(({ element }) => element.trigger("update-internal.macro-ctp", [false, "back", this.id, this.log.index]));
			}
			return this;
		}
	}

	Macro.add("ctp", {
		tags: ["ctpNext"],
		handler() {
			const id = this.args[0];
			const persist = this.args.slice(1).includes("persist");
			const ctp = new CTP(id, persist);
			const _passage = passage();
			this.payload.forEach(({ args, name, contents }) => {
				const options = {};
				if (args.includes("clear")) options.clear = true;
				if (args.includesAny("t8n", "transition")) options.transition = true;
				const elementArg = (args.find((el) => el.startsWith("element:")) ?? "");
				if (elementArg) options.element = elementArg.replace("element:", "");
				if (name === "ctp") ctp.options = { ...options };
				ctp.add(contents, options);
			});
			$(this.output).append(ctp.output());
			$(document).one(":passagedisplay", () => {
				if (_passage === passage()) {
					const i = Math.max(ctp.log.index, 0);
					ctp.log.index = -1;
					ctp.log.seen = -1;
					while (ctp.log.index < i) ctp.advance();
				}
			});
		}
	});

	Macro.add("ctpAdvance", {
		handler() {
			const id = this.args[0];
			if (id) {
				const ctp = CTP.getCTP(id);
				if (ctp) ctp.advance();
				else throw new Error(`No CTP with ID '${id}' found!`);
			} else throw new Error(`No ID specified!`);
		}
	});

	Macro.add("ctpBack", {
		handler() {
			const id = this.args[0];
			if (id) {
				const ctp = CTP.getCTP(id);
				if (ctp) ctp.back();
				else throw new Error(`No CTP with ID '${id}' found!`);
			} else throw new Error(`No ID specified!`);
		}
	});
})();

(function () {
    // v1.1.1
    'use strict';

    var characters = new Map();

    function addCharacter (name, displayname, icon) {
		if(icon === undefined && displayname){
			icon = displayname;
			displayname = null;
		}
        if (State.length) {
            throw new Error('addCharacter() -> must be called before story starts');
        }
        if (!name || !icon) {
            console.error('addCharacter() -> invalid arguments');
            return;
        }
        if (characters.has(name)) {
            console.error('addCharacter() -> overwriting character "' + name + '"');
        }
        characters.set(name, {displayName: displayname, image: icon});
    }

    function say ($output, character, text, imgSrc) {
        // 
        var $box = $(document.createElement('div'))
            .addClass(Util.slugify(character) + ' say');

			
        // portrait
        var _img = characters.has(character) ? characters.get(character).image : null;        
        var $img = $(document.createElement('img'))
            .attr('src', imgSrc || _img || '');

        if ($img.attr('src') && $img.attr('src').trim()) {
            $box.append($img);
        }

        // name and content boxes
		var _name =  character.toUpperFirst();
		if (characters.has(character) && characters.get(character).displayName) {
            _name = characters.get(character).displayName;
        }

        $box.append($(document.createElement('p'))
            .wiki(_name))
            .append($(document.createElement('p'))
                .wiki(text));

        if ($output) {
            if (!($output instanceof $)) {
                $output = $($output);
            }
            $box.appendTo($output);
        }

        return $box;
    }

    setup.say = say;
    setup.addCharacter = addCharacter;

    Macro.add('character', {
        // character macro
        handler : function () {
            addCharacter(this.args[0], this.args[1], this.args[2]);
        }
    });

    $(document).one(':passagestart', function () {
        // construct array of character names
        var names = Array.from(characters.keys());
        names.push('say');
        // generate macros
        Macro.add(names, {
            tags : null,
            handler : function () {
                if (this.name !== 'say') {
                    say(this.output, this.name, this.payload[0].contents);
                } else {
                    say(this.output, this.args[0], this.payload[0].contents, this.args[1]);
                }
            }
        });
    });
}());

(function () {
	"use strict";

	$(document).on(":liveupdate", function () {
		$(".macro-live").trigger(":liveupdateinternal");
	});

	Macro.add(['update', 'upd'], {
		handler: function handler() {
			$(document).trigger(":liveupdate");
		}
	});

	Macro.add(['live', 'l', 'lh'], {
		skipArgs: true,
		handler: function handler() {
			if (this.args.full.length === 0) {
				return this.error('no expression specified');
			}
			try {
				var statement = this.args.full;
				var result = toStringOrDefault(Scripting.evalJavaScript(statement), null);
				if (result !== null) {
					var lh = this.name === "lh";
					var $el = $("<span></span>").addClass("macro-live").wiki(lh ? Util.escape(result) : result).appendTo(this.output);
					$el.on(":liveupdateinternal", this.createShadowWrapper(function (ev) {
						var out = toStringOrDefault(Scripting.evalJavaScript(statement), null);
						$el.empty().wiki(lh ? Util.escape(out) : out);
					}));
				}
			} catch (ex) {
				return this.error("bad evaluation: " + (_typeof(ex) === 'object' ? ex.message : ex));
			}
		}
	});

	Macro.add(['liveblock', 'lb'], {
		tags: null,
		handler: function handler() {
			try {
				var content = this.payload[0].contents.trim();
				if (content) {
					var $el = $("<span></span>").addClass("macro-live macro-live-block").wiki(content).appendTo(this.output);
					$el.on(":liveupdateinternal", this.createShadowWrapper(function (ev) {
						$el.empty().wiki(content);
					}));
				}
			} catch (ex) {
				return this.error("bad evaluation: " + (_typeof(ex) === 'object' ? ex.message : ex));
			}
		}
	});
})();

OK I tried and it works fine for me. But I think I know where this might be coming from:

I keep mixing up the macro blocks name (bc the macro is name liveupdate, but the blocks are actually <<liveblock>>


(from the macro’s documentation)
Sorry about that

It worked this time. Thanks, and sorry for not understanding enough about coding, it’s often hard to understand when I try to follow instructions for it.

No need for apologies on your part! It’s all a learning process and we all have to start from somewhere (I never did any coding before I started my Twine journey).
It will get easier with time :wink:

1 Like

Well, I’ve found that the ability to reveal more text is a real game-changer (almost literally) because it works with other codes as well, so I can have the music change within the same passage instead of having to make a new page every time I want to change it.

2 Likes