Commit 2ccff52c authored by Leon Arnott's avatar Leon Arnott
Browse files

Temp variables finally work correctly with changers that defer a hook.

Now, you can reference temp variables inside the hook, such as in (link:"Read")[It reads: _engraving], just as you can with other kinds of changers.

Additionally:

 * Error messages should now explain in slightly more detail what kind of lambda a macro requires ("a 'where' macro", for instance).
 * The "undo" and "redo" buttons' CSS opacity has been reduced, to better fit the default dark theme.
 * Fixed a number of macros that shouldn't have been made into commands, like (prompt:) or (save-game:).

As a result of that last point, three macros that used to return { earlyExit: 1 }, (save-game:), (goto-url:) and (reload:), no longer do, because Section.runExpression no longer recognises that as a valid return value outside of attachables with TwineScript_Run() (which (goto:) is a part of). I ought to fix this at some later point, though the low traffic of those three makes this a low priority.
parent 86456639fcc7
......@@ -6,6 +6,7 @@ Rough documentation is at http://twine2.neocities.org/. See below for compilatio
####Bugfixes
* Temp variables finally work correctly with changers that defer a hook until some event occurs, like `(link:)`, `(click:)` and such. Now, you can reference temp variables inside the hook, such as in `(link:"Read")[It reads: _engraving]`, just as you can with other kinds of changers.
* Fixed a bug where supplying multiple shortened `is` or `is not` comparisons, in a form such as `$a is $b and $c`, would produce an incorrect result.
* Fixed a bug where the Debug View's (set:) messages were worded incorrectly when setting global variables.
* A more useful error message is given if you write a link with no passage name (such as `[[Go->]]`).
......@@ -18,8 +19,10 @@ Rough documentation is at http://twine2.neocities.org/. See below for compilatio
* To more clearly separate the concepts of "printing data" and "running commands" in Harlowe, the `(print:)` macro will no longer run commands passed to it (that is, `(print:(go-to:"Foo"))` and `(go-to:"Foo")` will no longer do the same thing - the former will just print out a descriptive string, as if printing out a changer). Commands can now only be run by placing them directly in the passage (either as plain calls, inside variables, or wrapped in strings that (print:) receives).
* Passage links no longer have a `passage-name` attribute indicating which passage they lead to, which the player could inspect using developer tools.
* If links' text contains an error message (for instance, in the case of `(link-replace:"(print:2+true"))[]`), then the link can no longer be clicked (so that the error message can be expanded).
* Error messages should now explain in slightly more detail what kind of lambda a macro requires (`"a "where ..." lambda"`, for instance).
* Now, a `(transition:)` added to `(link:)`s, `(click:)`s, `(mouseover:)`s and other such macros will no longer cause the links or other elements to use the named transition themselves - instead, it will only be applied to the attached hook when it is made to appear.
* `(show:)` will no longer produce an error if it tries to show a hook that's already visible, for consistency with other macros that accept hooknames, like `(click:)`. (Actually, it never did this in the first place, due to a bug.)
* The "undo" and "redo" buttons in the story's sidebar are now brighter by default.
####Additions
......
......@@ -85,7 +85,12 @@ define(['utils', 'utils/operationutils', 'internaltypes/varscope', 'internaltype
expects its lambda to have.
*/
TypeSignature(clauses) {
return { pattern: "lambda", innerType: Lambda, clauses };
return {
pattern: "lambda", innerType: Lambda, clauses,
typeName: "a \""
+ clauses.split().concat('').join(" ...")
+ "\" lambda",
};
},
/*
......@@ -178,12 +183,13 @@ define(['utils', 'utils/operationutils', 'internaltypes/varscope', 'internaltype
This needs to have the macro's section passed in so that its JS code can be eval()'d in
the correct scope.
*/
apply(section, {loop:loopArg, 'with':withArg, making:makingArg, fail:failArg, pass:passArg, ignoreVia}) {
apply(section, {loop:loopArg, 'with':withArg, making:makingArg, fail:failArg, pass:passArg, ignoreVia, tempVariables}) {
/*
We run the JS code of this lambda, inserting the arguments by adding them to a "tempVariables"
object. The tempVariable references in the code are compiled to VarRefs for tempVariables.
We run the JS code of this lambda, inserting the arguments by adding them to a tempVariables
object (if one wasn't provided). The temporary variable references in the code are compiled to
VarRefs for tempVariables.
*/
const tempVariables = Object.create(VarScope);
tempVariables = tempVariables || Object.create(section.stack[0] || VarScope);
[
[this.loop, loopArg],
[this.with, withArg],
......@@ -241,8 +247,10 @@ define(['utils', 'utils/operationutils', 'internaltypes/varscope', 'internaltype
/*
This convenience function is used to run reduce() on macro args using a lambda,
which is an operation common to (find:), (all-pass:) and (some-pass:).
tempVariables is only overridden by certain deferred rendering macros (as of Sep 2018, just (event:)).
*/
filter(section, args) {
filter(section, args, tempVariables = null) {
return args.reduce((result, arg) => {
/*
If an earlier iteration produced an error, don't run any more
......@@ -255,7 +263,7 @@ define(['utils', 'utils/operationutils', 'internaltypes/varscope', 'internaltype
/*
Run the lambda, to determine whether to filter out this element.
*/
const passedFilter = this.apply(section, {loop:arg, pass:true, fail:false, ignoreVia:true});
const passedFilter = this.apply(section, {loop:arg, pass:true, fail:false, ignoreVia:true, tempVariables});
if ((error = TwineError.containsError(passedFilter))) {
return error;
}
......
......@@ -324,7 +324,7 @@ define(['jquery', 'requestAnimationFrame', 'macros', 'utils', 'utils/selectors',
);
}
},
(cd, _, ...labels) => {
(cd, section, ...labels) => {
/*
Often, all the params are labels. But if the first one is actually the optional VarBind,
we need to extract it from the labels array.
......@@ -335,6 +335,11 @@ define(['jquery', 'requestAnimationFrame', 'macros', 'utils', 'utils/selectors',
}
let index = 0;
/*
As this is a deferred rendering macro, the current tempVariables
object must be stored for reuse, as the section pops it when normal rendering finishes.
*/
const [{tempVariables}] = section.stack;
cd.data.clickEvent = (link) => {
/*
Rotate to the next label, cycling around if it's past the end.
......@@ -366,7 +371,7 @@ define(['jquery', 'requestAnimationFrame', 'macros', 'utils', 'utils/selectors',
Since cd2's target SHOULD equal link, passing anything as the second argument won't do anything useful
(much like how the first argument is overwritten by cd2's source). So, null is given.
*/
cd.section.renderInto("", null, cd2);
cd.section.renderInto("", null, cd2, tempVariables);
};
/*
As above, the bound variable, if present, is set to the first label. Errors resulting
......@@ -414,7 +419,7 @@ define(['jquery', 'requestAnimationFrame', 'macros', 'utils', 'utils/selectors',
When one option is selected, the bound variable is set to match the string value of the text.
Example usage:
* `(dropdown: bind $origin, "Abyssal outer reaches", "Gyre's wake", "The planar interstice")`
* `(dropdown: bind _origin, "Abyssal outer reaches", "Gyre's wake", "The planar interstice")`
Rationale:
Dropdown menus offer a more esoteric, but visually and functionally unique way of presenting the player with
......@@ -599,116 +604,14 @@ define(['jquery', 'requestAnimationFrame', 'macros', 'utils', 'utils/selectors',
#live
*/
/*
The existence of this macro is checked by searching for its <tw-expression> DOM element
within a hook.
*/
("stop",
() => {},
() => "",
[], false)
/*d:
(save-game: String, [String]) -> Boolean
This macro saves the current game's state in browser storage, in the given save slot,
and including a special filename. It can then be restored using (load-game:).
Rationale:
Many web games use browser cookies to save the player's place in the game.
Twine allows you to save the game, including all of the variables that were (set:)
or (put:), and the passages the player visited, to the player's browser storage.
(save-game:) is a single operation that can be used as often or as little as you
want to. You can include it on every page; You can put it at the start of each "chapter";
You can put it inside a (link:) hook, such as
```
{(link:"Save game")[
  (if:(save-game:"Slot A"))[
    Game saved!
  ](else: )[
    Sorry, I couldn't save your game.
  ]
]}
```
and let the player choose when to save.
Details:
(save-game:)'s first string is a slot name in which to store the game. You can have as many slots
as you like. If you only need one slot, you can just call it, say, `"A"`, and use `(save-game:"A")`.
You can tie them to a name the player gives, such as `(save-game: $playerName)`, if multiple players
are likely to play this game - at an exhibition, for instance.
Giving the saved game a file name is optional, but allows that name to be displayed by finding it in the
$Saves datamap. This can be combined with a (load-game:)(link:) to clue the players into the save's contents:
```
(link: "Load game: " + ("Slot 1") of Saves)[
  (load-game: "Slot 1")
]
```
(save-game:) evaluates to a boolean - true if the game was indeed saved, and false if the browser prevented
it (because they're using private browsing, their browser's storage is full, or some other reason).
Since there's always a possibility of a save failing, you should use (if:) and (else:) with (save-game:)
to display an apology message in the event that it returns false (as seen above).
This command can't have changers attached - attempting to do so will produce an error.
See also:
(load-game:), (saved-games:)
#saving
*/
("save-game",
() => {},
(_, slotName, fileName) => {
/*
The default filename is the empty string.
*/
fileName = fileName || "";
if (!hasStorage) {
/*
If storage isn't available, that's the unfortunate fault of the
browser. Return false, signifying that the save failed, and
allowing the author to display an apology message.
*/
return false;
}
const serialisation = State.serialise();
if (TwineError.containsError(serialisation)) {
/*
On the other hand, if serialisation fails, that's presumably
the fault of the author, and an error should be given.
*/
return serialisation;
}
/*
In case setItem() fails, let's run this in a try block.
*/
try {
localStorage.setItem(
/*
Saved games are prefixed with (Saved Game <ifid>).
*/
storagePrefix("Saved Game") + slotName, serialisation);
/*
The file name is saved separately from the state, so that it can be retrieved
without having to JSON.parse() the entire state.
*/
localStorage.setItem(
/*
Saved games are prefixed with (Saved Game Filename <ifid>).
*/
storagePrefix("Saved Game Filename") + slotName, fileName);
return true;
} catch(e) {
/*
As above, if it fails, a return value of false is called for.
*/
return false;
}
},
[String, optional(String)], false)
[], false)
/*d:
(load-game: String) -> Command
......@@ -741,7 +644,7 @@ define(['jquery', 'requestAnimationFrame', 'macros', 'utils', 'utils/selectors',
*/
("load-game",
() => {},
(_, slotName) => {
(/* no cd because this is attachable:false */ _, slotName) => {
const saveData = localStorage.getItem(storagePrefix("Saved Game") + slotName);
if (!saveData) {
......@@ -755,7 +658,6 @@ define(['jquery', 'requestAnimationFrame', 'macros', 'utils', 'utils/selectors',
the save itself is drastically incorrect.
*/
requestAnimationFrame(Engine.showPassage.bind(Engine, State.passage, false /* stretchtext value */));
return { earlyExit: 1 };
},
[String], false)
......@@ -786,74 +688,11 @@ define(['jquery', 'requestAnimationFrame', 'macros', 'utils', 'utils/selectors',
*/
("alert",
() => {},
(_, text) => {
(/* no cd because this is attachable:false */ _, text) => {
window.alert(text);
return "";
},
[String], false)
/*d:
(prompt: String, String) -> String
When this macro is evaluated, a browser pop-up dialog box is shown with the first string displayed,
a text entry box containing the second string (as a default value), and an "OK" button to submit.
When it is submitted, it evaluates to the string in the text entry box.
Example usage:
`(set: $name to (prompt: "Your name, please:", "Frances Spayne"))`
Details:
This is essentially identical to the Javascript `prompt()` function in purpose and ability. You can
use it to obtain a string value from the player directly, such as a name for the main character.
But, be aware that as the box uses the player's operating system and browser's styling, it
may clash visually with the design of your story.
When the dialog is on-screen, the entire game is essentially "paused" - no further computations are
performed until it is dismissed.
This command can't have changers attached - attempting to do so will produce an error.
See also:
(alert:), (confirm:)
#popup
*/
("prompt",
() => {},
(_, text, value) => window.prompt(text, value) || "",
[String, String], false)
/*d:
(confirm: String) -> Boolean
When this macro is evaluated, a browser pop-up dialog box is shown with the given string displayed,
as well as "OK" and "Cancel" button to confirm or cancel whatever action or fact the string tells the player.
When it is submitted, it evaluates to the boolean true if "OK" had been pressed, and false if "Cancel" had.
Example usage:
`(set: $makeCake to (confirm: "Transform your best friend into a cake?"))`
Details:
This is essentially identical to the Javascript `confirm()` function in purpose and ability. You can
use it to ask the player a question directly, and act on the result immediately.
But, be aware that as the box uses the player's operating system and browser's styling, it
may clash visually with the design of your story.
When the dialog is on-screen, the entire game is essentially "paused" - no further computations are
performed until it is dismissed.
This command can't have changers attached - attempting to do so will produce an error.
See also:
(alert:), (prompt:)
#popup
*/
("confirm",
() => {},
(_, text) => window.confirm(text),
[String], false)
/*d:
(open-url: String) -> Command
......@@ -882,9 +721,8 @@ define(['jquery', 'requestAnimationFrame', 'macros', 'utils', 'utils/selectors',
*/
("open-url",
() => {},
(_, text) => {
(/* no cd because this is attachable:false */ _, text) => {
window.open(text, '');
return "";
},
[String], false)
......@@ -907,17 +745,11 @@ define(['jquery', 'requestAnimationFrame', 'macros', 'utils', 'utils/selectors',
*/
("reload",
() => {},
() => {
(/* no cd because this is attachable:false */ ) => {
if (State.pastLength < 1) {
return TwineError.create("infinite", "I mustn't (reload:) the page in the starting passage.");
}
window.location.reload();
/*
This technically doesn't need to be an "early exit" command
like (goto:), because reload() halts all execution by itself.
But, when proper error-checking is added, this will be necessary.
*/
return { earlyExit: 1 };
},
[], false)
......@@ -949,29 +781,191 @@ define(['jquery', 'requestAnimationFrame', 'macros', 'utils', 'utils/selectors',
*/
("goto-url",
() => {},
(_, url)=>{
(/* no cd because this is attachable:false */ _, url)=>{
window.location.assign(url);
},
[String], false);
/*
The following couple of macros are not commands, but they are each intrinsically related to some of the macros above.
*/
Macros.add
/*d:
(save-game: String, [String]) -> Boolean
This macro saves the current game's state in browser storage, in the given save slot,
and including a special filename. It can then be restored using (load-game:).
Rationale:
Many web games use browser cookies to save the player's place in the game.
Twine allows you to save the game, including all of the variables that were (set:)
or (put:), and the passages the player visited, to the player's browser storage.
(save-game:) is a single operation that can be used as often or as little as you
want to. You can include it on every page; You can put it at the start of each "chapter";
You can put it inside a (link:) hook, such as
```
{(link:"Save game")[
  (if:(save-game:"Slot A"))[
    Game saved!
  ](else: )[
    Sorry, I couldn't save your game.
  ]
]}
```
and let the player choose when to save.
Details:
(save-game:)'s first string is a slot name in which to store the game. You can have as many slots
as you like. If you only need one slot, you can just call it, say, `"A"`, and use `(save-game:"A")`.
You can tie them to a name the player gives, such as `(save-game: $playerName)`, if multiple players
are likely to play this game - at an exhibition, for instance.
Giving the saved game a file name is optional, but allows that name to be displayed by finding it in the
$Saves datamap. This can be combined with a (load-game:)(link:) to clue the players into the save's contents:
```
(link: "Load game: " + ("Slot 1") of Saves)[
  (load-game: "Slot 1")
]
```
(save-game:) evaluates to a boolean - true if the game was indeed saved, and false if the browser prevented
it (because they're using private browsing, their browser's storage is full, or some other reason).
Since there's always a possibility of a save failing, you should use (if:) and (else:) with (save-game:)
to display an apology message in the event that it returns false (as seen above).
See also:
(load-game:), (saved-games:)
#saving
*/
("save-game",
(_, slotName, fileName) => {
/*
As with (reload:), this early exit signal will be useful once
proper editing is added.
The default filename is the empty string.
*/
return { earlyExit: 1 };
fileName = fileName || "";
if (!hasStorage) {
/*
If storage isn't available, that's the unfortunate fault of the
browser. Return false, signifying that the save failed, and
allowing the author to display an apology message.
*/
return false;
}
const serialisation = State.serialise();
if (TwineError.containsError(serialisation)) {
/*
On the other hand, if serialisation fails, that's presumably
the fault of the author, and an error should be given.
*/
return serialisation;
}
/*
In case setItem() fails, let's run this in a try block.
*/
try {
localStorage.setItem(
/*
Saved games are prefixed with (Saved Game <ifid>).
*/
storagePrefix("Saved Game") + slotName, serialisation);
/*
The file name is saved separately from the state, so that it can be retrieved
without having to JSON.parse() the entire state.
*/
localStorage.setItem(
/*
Saved games are prefixed with (Saved Game Filename <ifid>).
*/
storagePrefix("Saved Game Filename") + slotName, fileName);
return true;
} catch(e) {
/*
As above, if it fails, a return value of false is called for.
*/
return false;
}
},
[String], false);
[String, optional(String)])
/*d:
(page-url:) -> String
/*d:
(prompt: String, String) -> String
This macro produces the full URL of the story's HTML page, as it is in the player's browser.
When this macro is evaluated, a browser pop-up dialog box is shown with the first string displayed,
a text entry box containing the second string (as a default value), and an "OK" button to submit.
When it is submitted, it evaluates to the string in the text entry box.
Example usage:
`(if: (page-url:) contains "#cellar")` will be true if the URL contains the `#cellar` hash.
Example usage:
`(set: $name to (prompt: "Your name, please:", "Frances Spayne"))`
Details:
This **may** be changed in a future version of Harlowe to return a datamap containing more
descriptive values about the URL, instead of a single string.
Details:
This is essentially identical to the Javascript `prompt()` function in purpose and ability. You can
use it to obtain a string value from the player directly, such as a name for the main character.
But, be aware that as the box uses the player's operating system and browser's styling, it
may clash visually with the design of your story.
#url
*/
Macros.add("page-url", () => window.location.href, []);
When the dialog is on-screen, the entire game is essentially "paused" - no further computations are
performed until it is dismissed.
This command can't have changers attached - attempting to do so will produce an error.
See also:
(alert:), (confirm:)
#popup
*/
("prompt",
(_, text, value) => window.prompt(text, value) || "",
[String, String])
/*d:
(confirm: String) -> Boolean
When this macro is evaluated, a browser pop-up dialog box is shown with the given string displayed,
as well as "OK" and "Cancel" button to confirm or cancel whatever action or fact the string tells the player.
When it is submitted, it evaluates to the boolean true if "OK" had been pressed, and false if "Cancel" had.
Example usage:
`(set: $makeCake to (confirm: "Transform your best friend into a cake?"))`
Details:
This is essentially identical to the Javascript `confirm()` function in purpose and ability. You can
use it to ask the player a question directly, and act on the result immediately.
But, be aware that as the box uses the player's operating system and browser's styling, it
may clash visually with the design of your story.
When the dialog is on-screen, the entire game is essentially "paused" - no further computations are
performed until it is dismissed.
This command can't have changers attached - attempting to do so will produce an error.
See also:
(alert:), (prompt:)
#popup
*/
("confirm",
(_, text) => window.confirm(text),
[String])
/*d:
(page-url:) -> String
This macro produces the full URL of the story's HTML page, as it is in the player's browser.
Example usage:
`(if: (page-url:) contains "#cellar")` will be true if the URL contains the `#cellar` hash.
Details:
This **may** be changed in a future version of Harlowe to return a datamap containing more
descriptive values about the URL, instead of a single string.
#url
*/
("page-url", () => window.location.href, []);
});
......@@ -362,6 +362,11 @@ define(['jquery', 'utils', 'utils/selectors', 'utils/operationutils', 'engine',
desc.newTargets = (desc.newTargets || [])
.concat({ target: selector, append: enchantDesc.rerender });
}
/*
As these are deferred rendering macros, the current tempVariables
object must be stored for reuse, as the section pops it when normal rendering finishes.
*/
const [{tempVariables}] = desc.section.stack;
/*
This enchantData object is stored in the descriptor's Section's enchantments
......@@ -439,7 +444,8 @@ define(['jquery', 'utils', 'utils/selectors', 'utils/operationutils', 'engine',
can use them (given that the interaction element did not).
*/
transitionDeferred: false,
})
}),
tempVariables
);
},
},
......
......@@ -201,6 +201,11 @@ define(['jquery', 'macros', 'utils', 'utils/selectors', 'state', 'passages', 'en
*/
desc.append = arr[0] === "link" ? "replace" : "append";
desc.transitionDeferred = true;
/*
As this is a deferred rendering macro, the current tempVariables
object must be stored for reuse, as the section pops it when normal rendering finishes.
*/
const [{tempVariables}] = desc.section.stack;
desc.data.clickEvent = (link) => {
/*
Only (link-reveal:) turns the link into plain text:
......@@ -211,7 +216,7 @@ define(['jquery', 'macros', 'utils', 'utils/selectors', 'state', 'passages', 'en
}
desc.source = desc.innerSource + "";
desc.transitionDeferred = false;
desc.section.renderInto("", null, desc);
desc.section.renderInto("", null, desc, tempVariables);
};
},
[String]
......@@ -444,6 +449,7 @@ define(['jquery', 'macros', 'utils', 'utils/selectors', 'state', 'passages', 'en
}
},
(cd, section, text, ...hooks) => {
const [{tempVariables}] = section.stack;
cd.data.clickEvent = (link) => {
link.contents().unwrap();
hooks.forEach(hook => hook.forEach(section, elem => {
......@@ -452,7 +458,8 @@ define(['jquery', 'macros', 'utils', 'utils/selectors', 'state', 'passages', 'en
return;
}
section.renderInto("", null,
assign({}, cd, { source: hiddenSource, target: elem, transitionDeferred: false })
assign({}, cd, { source: hiddenSource, target: elem, transitionDeferred: false }),
tempVariables
);
}));
};
......@@ -543,6 +550,11 @@ define(['jquery', 'macros', 'utils', 'utils/selectors', 'state', 'passages', 'en
desc.source = '<tw-link tabindex=0 ' + (visited > 0 ? 'class="visited" ' : '') + '>' + text + '</tw-link>';
desc.append = "append";
desc.transitionDeferred = true;
/*
As this is a deferred rendering macro, the current tempVariables
object must be stored for reuse, as the section pops it when normal rendering finishes.