Commit 6dffb2b1 authored by Leon Arnott's avatar Leon Arnott
Browse files

Made (goto:) and (undo:) into HookCommands, and fixed a bug in clickLinkEvent.

parent 33efe054c921
......@@ -64,6 +64,10 @@ define(['jquery', 'utils', 'renderer', 'datatypes/hookset'], ($, {assertOnlyHas,
// Used by enchantment macros to determine where to register
// their enchantments to.
section: null,
// {Boolean} [earlyExit] A boolean that specifies further <tw-expression>s in this section shouldn't be executed,
// because this is a (goto:) command or something like that.
earlyExit: false,
/*
This method produces a short list of properties this descriptor has, which were altered by the changers
......
......@@ -15,7 +15,7 @@ define(['requestAnimationFrame', 'macros', 'utils', 'state', 'passages', 'engine
/*d:
HookCommand data
TBW
As you know, certain macros like (display:) and (print:)
*/
const {Any, rest, optional} = Macros.TypeSignature;
const {assign} = Object;
......@@ -133,115 +133,7 @@ define(['requestAnimationFrame', 'macros', 'utils', 'state', 'passages', 'engine
[Any])
/*d:
(cycling-link: Bind, ...String) -> Command
A command that, when evaluated, creates a cycling link - a link which does not go anywhere, but changes its own text
to the next in a looping sequence of strings, and sets the bound variable to match the string value of the text.
Example usage:
`(cycling-link: bind $head's hair, "Black", "Brown", "Blonde", "Red", "White")`
Rationale:
The cycling link was an interaction idiom popularised in Twine 1 which combined the utility of a dial input element with
the discovery and visual consistency of a link: the player can only discover that this is a cycling link by clicking it,
and can then only discover the full set of labels by clicking through them. This affords a number of subtle dramatic and humourous
possibilities, and moreover allows the link to sit comfortably among passage prose without standing out as an interface element.
The addition of a variable bound to the link, changing to match whichever text the player finally dialed the link to, allows
cycling links to affect subsequent passages, and thus for the link to be just as meaningful in affecting the story's course as any
other, even though no hooks and (set:)s can be attached to them.
Details:
TBW
#input
*/
("cycling-link",
() => {},
(cd, _, bind, ...labels) => {
// NEED evaluateTwineMarkup() here...
return assign(cd, { source: '<tw-link>' + labels[0] + '</tw-link>' });
},
[String]);
Macros.addCommand
/*d:
(show: ...HookName) -> Command
Reveals hidden hooks, running the code within.
Example usage:
```
|fan)[The overhead fan spins lazily.]
(link:"Turn on fan")[(show:?fan)]
```
Rationale:
The purpose of hidden hooks is, of course, to eventually show them - and this macro is
how you show them. You can use this command inside a (link:), trigger it in real-time with
a (live:) macro, or anywhere else.
Using (show:) vs (replace:):
There are different reasons for using hidden hooks and (show:) instead of (replace:). For your stories,
think about whether the prose being revealed is part of the "main" text of the passage, or is just an aside.
In neatly-coded stories, the main text should appear early in a passage's code, as the focus of the
writer's attention.
When using (replace:), the replacement prose is written far from its insertion point. This can improve
readability when the insertion point is part of a long paragraph or sentence, and the prose is a minor aside
or amendment, similar to a footnote or post-script, that would clutter the paragraph were it included inside.
Additionally, (replace:) can be used in a "header" or "footer" tagged passage to affect certain named hooks
throughout the story.
```
You turn away from her, facing the grandfather clock, its [stern ticking]<1| filling the tense silence.
(click-replace: ?1)[echoing, hollow ticking]
```
When using (show:), the hidden hook's position is fixed in the passage prose. This can improve
readability when the hidden hook contains a lot of the "main" text of a passage, which provides vital context
and meaning for the rest of the text.
```
I don't know where to begin... |1)[The weird state of my birth, the prophecy made centuries ago,
my first day of school, the day of the meteors, the day I awoke my friends' powers... so many strands in
the tapestry of my tale, and no time to unravel them.] ...so for now I'll start with when we fell down the hole.
(link:"Where, indeed?")[(show:?1)]
```
But, there aren't any hard rules for when you should use one or the other. As a passage changes in the writing, you should feel free to change between one or the other, or leave your choice as-is.
Details:
(show:) will reveal every hook with the given name. To only reveal a specific hook, you can use the
possessive syntax, as usual: `(show: ?shrub's 1st)`.
If you provide to (show:) a hook which is already visible, an error will be produced.
See also:
(hidden:), (replace:)
#showing and hiding
*/
("show",
() => {},
(section, ...hooks) => {
hooks.forEach(hook => hook.forEach(section, elem => {
const hiddenSource = elem.data('hiddenSource');
if (hiddenSource === undefined) {
return TwineError.create("operation",
"I can't reveal a hook which is already visible.");
}
section.renderInto(hiddenSource, elem);
}));
return '';
},
[rest(HookSet)])
/*d:
(go-to: String) -> Command
(go-to: String) -> HookCommand
This command stops passage code and sends the player to a new passage.
If the passage named by the string does not exist, this produces an error.
......@@ -262,6 +154,11 @@ define(['requestAnimationFrame', 'macros', 'utils', 'state', 'passages', 'engine
(put:) or (save-game:).
Details:
As this is a HookCommand, you can attach changers like (t8n-depart:) and (t8n-arrive:) to
alter the transition animation used when (go-to:) activates. Other kinds of changers
won't do anything, though.
If it is performed, (go-to:) will "halt" the passage and prevent any macros and text
after it from running. So, a passage that contains:
```
......@@ -293,7 +190,7 @@ define(['requestAnimationFrame', 'macros', 'utils', 'state', 'passages', 'engine
);
}
},
(_, name) => {
(cd, _, name) => {
/*
When a passage is being rendered, <tw-story> is detached from the main DOM.
If we now call another Engine.goToPassage in here, it will attempt
......@@ -301,7 +198,7 @@ define(['requestAnimationFrame', 'macros', 'utils', 'state', 'passages', 'engine
So, the change of passage must be deferred until just after
the passage has ceased rendering.
*/
requestAnimationFrame(()=> Engine.goToPassage(name));
requestAnimationFrame(()=> Engine.goToPassage(name, { transitionOut: cd.data.t8nDepart, transitionIn: cd.data.t8nArrive }));
/*
But how do you immediately cease rendering the passage?
......@@ -310,12 +207,12 @@ define(['requestAnimationFrame', 'macros', 'utils', 'state', 'passages', 'engine
(goto: "X")(set: $y to 1)
the (set:) will not run because it is after the (goto:)
*/
return { earlyExit: 1 };
return assign(cd, { earlyExit: true });
},
[String])
/*d:
(undo:) -> Command
(undo:) -> HookCommand
This command stops passage code and "undoes" the current turn, sending the player to the previous visited
passage and forgetting any variable changes that occurred in this passage.
......@@ -333,6 +230,10 @@ define(['requestAnimationFrame', 'macros', 'utils', 'state', 'passages', 'engine
a header tagged passage.
Details:
As this is a HookCommand, you can attach changers like (t8n-depart:) and (t8n-arrive:) to
alter the transition animation used when (undo:) activates. Other kinds of changers
won't do anything, though.
If this is the first turn of the game session, (undo:) will produce an error. You can check which turn it is
by examining the `length` of the (history:) array.
......@@ -346,7 +247,7 @@ define(['requestAnimationFrame', 'macros', 'utils', 'state', 'passages', 'engine
*/
("undo",
() => {},
() => {
(cd) => {
if (State.pastLength < 1) {
return TwineError.create("macrocall", "I can't (undo:) on the first turn.");
}
......@@ -355,15 +256,123 @@ define(['requestAnimationFrame', 'macros', 'utils', 'state', 'passages', 'engine
just after the passage has ceased rendering, to avoid <tw-story> being
detached twice.
*/
requestAnimationFrame(()=> Engine.goBack());
requestAnimationFrame(()=> Engine.goBack({ transitionOut: cd.data.t8nDepart, transitionIn: cd.data.t8nArrive }));
/*
As with the (goto:) macro, this returned object signals to
As with the (goto:) macro, earlyExit signals to
Section's runExpression() to cease evaluation.
*/
return { earlyExit: 1 };
return assign(cd, { earlyExit: true });
},
[])
/*d:
(cycling-link: Bind, ...String) -> Command
A command that, when evaluated, creates a cycling link - a link which does not go anywhere, but changes its own text
to the next in a looping sequence of strings, and sets the bound variable to match the string value of the text.
Example usage:
`(cycling-link: bind $head's hair, "Black", "Brown", "Blonde", "Red", "White")`
Rationale:
The cycling link was an interaction idiom popularised in Twine 1 which combined the utility of a dial input element with
the discovery and visual consistency of a link: the player can only discover that this is a cycling link by clicking it,
and can then only discover the full set of labels by clicking through them. This affords a number of subtle dramatic and humourous
possibilities, and moreover allows the link to sit comfortably among passage prose without standing out as an interface element.
The addition of a variable bound to the link, changing to match whichever text the player finally dialed the link to, allows
cycling links to affect subsequent passages, and thus for the link to be just as meaningful in affecting the story's course as any
other, even though no hooks and (set:)s can be attached to them.
Details:
TBW
#input
*/
("cycling-link",
() => {},
(cd, _, bind, ...labels) => {
// NEED evaluateTwineMarkup() here...
return assign(cd, { source: '<tw-link>' + labels[0] + '</tw-link>' });
},
[String]);
Macros.addCommand
/*d:
(show: ...HookName) -> Command
Reveals hidden hooks, running the code within.
Example usage:
```
|fan)[The overhead fan spins lazily.]
(link:"Turn on fan")[(show:?fan)]
```
Rationale:
The purpose of hidden hooks is, of course, to eventually show them - and this macro is
how you show them. You can use this command inside a (link:), trigger it in real-time with
a (live:) macro, or anywhere else.
Using (show:) vs (replace:):
There are different reasons for using hidden hooks and (show:) instead of (replace:). For your stories,
think about whether the prose being revealed is part of the "main" text of the passage, or is just an aside.
In neatly-coded stories, the main text should appear early in a passage's code, as the focus of the
writer's attention.
When using (replace:), the replacement prose is written far from its insertion point. This can improve
readability when the insertion point is part of a long paragraph or sentence, and the prose is a minor aside
or amendment, similar to a footnote or post-script, that would clutter the paragraph were it included inside.
Additionally, (replace:) can be used in a "header" or "footer" tagged passage to affect certain named hooks
throughout the story.
```
You turn away from her, facing the grandfather clock, its [stern ticking]<1| filling the tense silence.
(click-replace: ?1)[echoing, hollow ticking]
```
When using (show:), the hidden hook's position is fixed in the passage prose. This can improve
readability when the hidden hook contains a lot of the "main" text of a passage, which provides vital context
and meaning for the rest of the text.
```
I don't know where to begin... |1)[The weird state of my birth, the prophecy made centuries ago,
my first day of school, the day of the meteors, the day I awoke my friends' powers... so many strands in
the tapestry of my tale, and no time to unravel them.] ...so for now I'll start with when we fell down the hole.
(link:"Where, indeed?")[(show:?1)]
```
But, there aren't any hard rules for when you should use one or the other. As a passage changes in the writing, you should feel free to change between one or the other, or leave your choice as-is.
Details:
(show:) will reveal every hook with the given name. To only reveal a specific hook, you can use the
possessive syntax, as usual: `(show: ?shrub's 1st)`.
If you provide to (show:) a hook which is already visible, an error will be produced.
See also:
(hidden:), (replace:)
#showing and hiding
*/
("show",
() => {},
(section, ...hooks) => {
hooks.forEach(hook => hook.forEach(section, elem => {
const hiddenSource = elem.data('hiddenSource');
if (hiddenSource === undefined) {
return TwineError.create("operation",
"I can't reveal a hook which is already visible.");
}
section.renderInto(hiddenSource, elem);
}));
return '';
},
[rest(HookSet)])
/*d:
(stop:) -> Command
This macro, which accepts no arguments, creates a (stop:) command, which is not configurable.
......
......@@ -38,11 +38,10 @@ define(['jquery', 'macros', 'utils', 'utils/selectors', 'state', 'passages', 'en
*/
expression = link.closest('tw-expression'),
/*
This could be a (link:) command. Such links' events
are, due to limitations in the ChangeDescriptor format,
attached to the <tw-expression> containing it.
Links' events are, due to limitations in the ChangeDescriptor format,
attached to the <tw-hook> or <tw-expression> containing the element.
*/
event = expression.data('clickEvent');
event = link.closest('tw-expression, tw-hook').data('clickEvent');
if (event) {
event(link);
......
......@@ -320,7 +320,7 @@ define(['jquery', 'utils/naturalsort', 'utils', 'utils/operationutils', 'datatyp
// Return the function to enable "bubble chaining".
return add;
},
/*
Takes two functions, and registers them as a live Changer macro.
......
......@@ -224,7 +224,7 @@ define([
*/
}
/*
If instead of a +, it's another kind of expression, we attempt to determine if it's a Hook Expression (a (print:) macro or somesuch).
If instead of a +, it's another kind of expression, we attempt to determine if it's a HookCommand.
If not, then the changer's attempted attachment fails and an error results, and it doesn't matter if the expression
is dropped (by us executing its js early) as well.
*/
......@@ -320,16 +320,16 @@ define([
if (TwineError.containsError(result)) {
expr.replaceWith(result.render(expr.attr('title'), expr));
}
/*
If TwineScript_Run returns an object of the form { earlyExit },
then that's a signal to cease all further expression evaluation
immediately.
*/
if (result.earlyExit) {
return "earlyexit";
}
if (ChangeDescriptor.isPrototypeOf(result)) {
this.renderInto('', nextElem, result);
/*
If TwineScript_Run returns a ChangeDescriptor with earlyExit,
then that's a signal to cease all further expression evaluation
immediately.
*/
if (result.earlyExit) {
return "earlyexit";
}
}
/*
This should be refactored out soon (May 2018)...
......
......@@ -203,6 +203,22 @@ describe("style changer macros", function() {
// TODO: Add .css() tests of output.
if (name !== "transition") {
it("changes the passage transitions of (goto:)", function(done) {
createPassage("foo","grault");
var p = runPassage("("+name+":'pulse')(goto:'grault')");
setTimeout(function() {
expect($('tw-story tw-transition-container[data-t8n="pulse"]').length).toBe(1);
done();
});
});
it("changes the passage transitions of (undo:)", function(done) {
runPassage("foo","grault");
var p = runPassage("("+name+":'pulse')(undo:)");
setTimeout(function() {
expect($('tw-story tw-transition-container[data-t8n="pulse"]').length).toBe(1);
done();
});
});
it("changes the passage transitions of (link-goto:)", function(done) {
createPassage("foo","grault");
var p = runPassage("("+name+":'pulse')(link-goto:'grault')");
......@@ -232,6 +248,24 @@ describe("style changer macros", function() {
});
if (name === "transition-arrive") {
describe("works when combined with (transition-depart:)", function() {
it("on (goto:)", function(done) {
createPassage("foo","grault");
var p = runPassage("(t8n-depart:'dissolve')+(t8n-arrive:'pulse')(goto:'grault')");
setTimeout(function() {
expect($('tw-story tw-transition-container.transition-out[data-t8n="dissolve"]').length).toBe(1);
expect($('tw-story tw-transition-container[data-t8n="pulse"]').length).toBe(1);
done();
});
});
it("on (undo:)", function(done) {
runPassage("foo","grault");
var p = runPassage("(t8n-depart:'dissolve')+(t8n-arrive:'pulse')(undo:)");
setTimeout(function() {
expect($('tw-story tw-transition-container.transition-out[data-t8n="dissolve"]').length).toBe(1);
expect($('tw-story tw-transition-container.transition-in[data-t8n="pulse"]').length).toBe(1);
done();
});
});
it("on (link-goto:)", function(done) {
createPassage("foo","grault");
var p = runPassage("(t8n-depart:'dissolve')+(t8n-arrive:'pulse')(link-goto:'grault')");
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment