Commit c5ef4496 authored by Leon Arnott's avatar Leon Arnott
Browse files

Added a debugging command, (mock-visits:), which lets you re-create a game...

Added a debugging command, (mock-visits:), which lets you re-create a game state where certain passages have been visited a certain number of times.

Also tweaked the Colours panel of The Harlowe Toolbar such that an (enchant:?page) call can now be optionally generated from that panel.
parent e848ddef76a5
......@@ -207,6 +207,7 @@ Documentation is at http://twine2.neocities.org/. See below for compilation inst
* Added `(icon-undo:)` and `(icon-redo:)`, two commands that replicate the default sidebar's "undo" and "redo" icons. Use these to reinsert these into the sidebar if you've modified it using `(replace:)` or `(append:)`, or simply place those icons into your passage text if you wish. You can change the icon to use a different Unicode character if you wish. You can also add text labels to these icons, which appear below them.
* Added `(icon-reload:)` and `(icon-fullscreen:)`, two additional commands that can be used, with `(prepend:)` and `(append:)`, to add "fullscreen" and "reload" buttons to the sidebar. The "fullscreen" icon works similarly to the `(link-fullscreen:)` macro described above, while the "reload" icon performs similarly to `(reload:)` when clicked. These offer the same options as `(icon-undo:)` and `(icon-redo:)`.
* Added `(icon-counter:)`, which produces a numeric display element that live-updates to match a bound number variable (rounded to a whole number). The element is roughly the same size as the sidebar icons, and designed to be used in the sidebar alongside them.
* Added a debugging command, `(mock-visits:)`, which lets you re-create a game state where certain passages have been visited a certain number of times, so that the `visits` keyword and the `(history:)` array produce desired results. This command will cause an error unless it's used in debug mode.
##### Custom Macros
......
"use strict";
define(['jquery', 'macros', 'utils', 'state', 'passages', 'renderer', 'engine', 'internaltypes/twineerror',
'internaltypes/twinenotifier', 'datatypes/assignmentrequest', 'datatypes/hookset', 'datatypes/codehook', 'datatypes/lambda', 'datatypes/colour', 'datatypes/gradient', 'internaltypes/varref', 'datatypes/typedvar', 'datatypes/varbind', 'utils/operationutils', 'utils/renderutils'],
($, Macros, Utils, State, Passages, Renderer, Engine, TwineError, TwineNotifier, AssignmentRequest, HookSet, CodeHook, Lambda, Colour, Gradient, VarRef, TypedVar, VarBind, {printBuiltinValue, objectName}, {dialog, geomParse, geomStringRegExp}) => {
($, Macros, Utils, State, Passages, Renderer, Engine, TwineError, TwineNotifier, AssignmentRequest, HookSet, CodeHook, Lambda, Colour, Gradient, VarRef, TypedVar, VarBind, {printBuiltinValue, objectName, clone}, {dialog, geomParse, geomStringRegExp}) => {
/*d:
Command data
......@@ -2318,6 +2318,62 @@ define(['jquery', 'macros', 'utils', 'state', 'passages', 'renderer', 'engine',
},
[String], false)
/*d:
(mock-visits: ...String) -> Command
A macro that can only be used in debug mode, this allows you to mark various passages as "visited", even though the player
actually hasn't. This allows you to quickly test passages that use the `visits` keyword, or the `(history:)` datamap, without
having to play through the whole game from the start.
Example usage:
* `(mock-visits:"Juice Temple", "Milk Temple", "Water Temple")` marks the passages "Juice Temple", "Milk Temple" and "Water Temple" as each having been visited once.
* `(mock-visits:"Lobby","Lobby","Lobby")` marks the "Lobby" passage as having been visited 3 times.
Rationale:
Using the `visits` keyword, or the (history:) array, as a way to track the player's progress instead of using Boolean variables and (set:)
can produce simpler, more understandable code - it's obvious what `(if: visits is > 2)` means just by looking at it. But, it comes
with a cost: when testing your story using the "Play from here" feature in the Twine editor, you may want to pretend that you have visited the
requisite passages a given number of times, so as to examine the resulting prose. If you were using variables, you could add a few temporary
(set:) macros to the passage, or put them in a "debug-header" tagged passage, to adjust the variables to match a game in progress. This macro
provides that same functionality to the `visits` keyword and (history:) array, letting you temporarily adjust it for testing purposes.
Details:
It's critical to understand that these are **mock** visits - the passages listed are not actually visited, and code inside them is not run by this macro.
As stated above, this macro will cause an error if it's used outside debug mode. This is NOT intended for use in a final game - while temporarily
tweaking the meaning of `visits` and (history:) is convenient for testing, the author should be able to trust that in the "real" game, they
correctly report the visits the player has actually made, so that the story's code can be properly understood.
Each occurrence of a passage name given to this macro counts as a single mock visit. Add multiples of the same passage name to register multiple mock
visits to that passage.
If this macro is used multiple times, only the final usage will count - all the rest will be forgotten. `(mock-visits:"A")(mock-visits:"B")`, for instance,
will only cause the "B" passage to be considered visited 1 time. This allows you to remove mock visits in the middle of a story by writing `(mock-visits:)`.
The effects of (mock-visits:) are NOT saved by (save-game:).
If you undo past a passage that used (mock-visits:), the effects of that macro call will be removed, as if it had been a (set:) macro call.
See also:
(history:), (set:)
Added in: 3.2.0
#debugging
*/
("mock-visits",
(...names) => {
if (!Engine.options.debug) {
return TwineError.create('operation', '(mock-visits:) cannot be used outside of debug mode.',
'This macro is not meant to be used outside of debugging your story.');
}
const incorrect = names.find(name => !Passages.hasValid(name));
if (incorrect) {
return TwineError.create('datatype', "I can't mock-visit '" + incorrect + "' because no passage with that name exists.");
}
},
(cd, _, ...names) => { State.mockVisits = clone(names); },
[rest(String)])
/*d:
(dialog: [Bind], String, ...String) -> Command
Also known as: (alert:)
......
......@@ -945,7 +945,8 @@ define([
This returns an array containing the string names of all of the passages
the player has visited up to now, in the order that the player visited them. An optional lambda
can filter the passages, by checking the (passage:) datamap of each.
can filter the passages, by checking the (passage:) datamap of each. The (mock-visits:) macro
can, during debugging, artifically add values to this array to simulate having visited various passages.
Example usage:
* `(history:) contains "Cellar"` is true if the player has visited a passage called
......@@ -958,8 +959,8 @@ define([
Rationale:
Often, you may find yourself using "flag" variables to keep track of whether
the player has visited a certain passage in the past. You can use (history:), along with
data structure operators such as the `contains` operator, to obviate this necessity.
the player has visited a certain passage in the past. In some cases, you can use (history:), along with
data structure operators, such as the `contains` operator, to obviate this necessity.
Details:
The array includes duplicate names if the player has visited a passage more than once, or visited
......@@ -967,7 +968,7 @@ define([
This does *not* include the name of the current passage the player is visiting.
This macro can optionally be given a lambda, which is used to only include passage names from the
This macro can optionally be given a `where` lambda, which is used to only include passage names from the
returned array if they match the lambda. Note that even though this produces an array of strings,
the variable in the lambda is always a **datamap** -
the same datamap as would be returned by (passage:) for that passage name. That datamap contains
......@@ -986,6 +987,12 @@ define([
`(find: _name where (passage: _name)'s tags contains "Forest", ...(history:))`,
which takes the normal (history:) array, and finds only those names for passages whose tags contain "Forest".
If you're testing your story in debug mode using (mock-visits:), then each of the "mock" visits you simulate
using that macro will be added to the front of the returned array (if they match the passed-in lambda).
`(mock-visits:"A","B")` will cause `(history:)` to produce an array starting with `"A","B"`, followed by
passages the tester has actually visited on this playthrough. It will also cause `(history: where its name contains "A")`
to produce an array starting with `"A"`.
See also:
(passage:), (savedgames:), (passages:)
......@@ -993,10 +1000,14 @@ define([
#game state
*/
("history", (section, lambda) => {
/*
Add the mock visits to the front of the pastPassageNames array.
*/
const history = State.mockVisits.concat(State.pastPassageNames());
if (!lambda) {
return State.pastPassageNames();
return history;
}
const passages = lambda.filter(section, State.pastPassageNames().map(p => Passages.get(p)));
const passages = lambda.filter(section, history.map(p => Passages.get(p)));
if (TwineError.containsError(passages)) {
return passages;
}
......
......@@ -1062,7 +1062,7 @@ define(['macros', 'utils', 'utils/operationutils', 'datatypes/colour', 'datatype
a gradient that draws a striped background, with each stripe as wide as the distance, and alternating through the given colours.
Example usage:
* `(enchant: ?page, (background: (gradient: 45, 20, fuchsia+white, white)))` causes the page to have a pink-and-white striped background.
* `(enchant: ?page, (background: (stripes: 45, 20, fuchsia+white, white)))` causes the page to have a pink-and-white striped background.
Rationale:
The (gradient:) macro can be used to dynamically create gradient backgrounds, which smoothly transition between multiple colours. By using
......
......@@ -1313,12 +1313,13 @@
type: 'preview',
text: 'Example text preview',
update(model, panel) {
const changers = model.suppressedChangers || model.changers;
panel.firstChild.setAttribute('style', `${
(model.changers['text-colour'] ? `color:${model.textColour};` : '')
(changers['text-colour'] ? `color:${model.textColour};` : '')
}${
model.stops ? `background:linear-gradient(${model.angle}deg, ${
model.stops.map(stop => stop.getAttribute('data-colour') + " " + (stop.getAttribute('data-pos')*100) + "%")
})` : model.changers.background ? `background:${model.backgroundColour}` : ''
})` : changers.background ? `background:${model.backgroundColour}` : ''
}`);
},
},{
......@@ -1414,7 +1415,24 @@
]
],
},
remainderOfPassageCheckbox,
{
type: 'radios',
name: 'Affect:',
options: ["The attached hook", "The remainder of the passage or hook.", "The entire page."],
model(m, el) {
const v = el[$]('input:checked').value;
const changers = Object.entries(m.changers);
if (v.includes('page') && changers.length) {
m.wrapStart = "(enchant:?page," + changers.map(([k,v]) => `(${k}:${v.join(',')})`).join('+') + ")";
m.wrapEnd = "";
m.suppressedChangers = m.changers;
m.changers = {};
} else if (v.includes('passage')) {
m.wrapStart = "[=\n";
m.wrapEnd = "";
}
},
},
confirmRow),
/*
The [[Link]] button's panel. This configures the link syntax, plus a (t8n-depart:) and/or (t8n-arrive:) macro to attach to it.
......
......@@ -50,6 +50,14 @@ define(['utils', 'passages', 'datatypes/changercommand', 'internaltypes/twineerr
and assigning to its properties does affect game state.
*/
TwineScript_VariableStore: true,
/*
For testing purposes, there needs to be a way to "mock" having visited certain passages a certain number of times.
Because (mock-visits:) calls should be considered modal, and can be undone, their effects need to be tied
to the variable store.
Note that currently, mock visits are NOT saved using (save-game:).
*/
TwineScript_MockVisits: null,
});
/*
......@@ -93,7 +101,7 @@ define(['utils', 'passages', 'datatypes/changercommand', 'internaltypes/twineerr
the player has undone).
Count begins at 0 (the game start).
*/
let timeline = [ ];
let timeline = [];
/*
Index to the game state just when the current passage was entered.
......@@ -200,6 +208,17 @@ define(['utils', 'passages', 'datatypes/changercommand', 'internaltypes/twineerr
return (timeline.length - 1) - recent;
},
/*
Get and set the current mockVisits state.
*/
get mockVisits() {
return present.variables.TwineScript_MockVisits || [];
},
set mockVisits(value) {
present.variables.TwineScript_MockVisits = value;
},
/*
Did we ever visit this passage, given its name?
Return the number of times visited.
......
......@@ -326,11 +326,17 @@ define([
not the contained passage. So, you can't use it to determine how many times the (display:)ed passage
has been (display:)ed.
For testing purposes, it can be convenient to temporarily alter `visits`'s value, so as to recreate a
certain game state. The (mock-visits:) macro, usable only in debug mode, lets you increase the number of
times certain passages have been "visited", so that this keyword produces higher numbers when in those passages.
Added in: 3.1.0
*/
get visits() {
const {stackTop:{speculativePassage}} = section;
return State.pastPassageNames().filter(name => name === (speculativePassage || State.passage)).length
const filter = name => name === (speculativePassage || State.passage);
return State.pastPassageNames().filter(filter).length
+ State.mockVisits.filter(filter).length
// Only add 1 (counting the current visit) if this isn't speculation, or the speculative passage equals State.passage (i.e. we're at that passage now).
+ (!speculativePassage || speculativePassage === State.passage);
},
......
......@@ -530,4 +530,48 @@ describe("basic command macros", function() {
});
});
});
describe("the (mock-visits:) macro", function() {
beforeEach(function() {
var t = "(print:visits)";
createPassage(t, "grault");
createPassage(t, "garply");
createPassage(t, "qux");
});
it("takes any number of passage name strings", function() {
expect("(mock-visits:)").markupToError();
expect("(mock-visits:'bar')").markupToError();
expect("(mock-visits:'bar','baz')").markupToError();
expect("(mock-visits:'qux')").not.markupToError();
expect("(mock-visits:'qux','bar')").markupToError();
expect("(mock-visits:'qux','qux','qux')").not.markupToError();
expect("(mock-visits:'qux','garply','grault','grault')").not.markupToError();
expect("(mock-visits:'qux','garply','grault','foo')").markupToError();
});
// Can't test that it only works in debug mode, unfortunately.
it("alters the 'visits' keyword to mock visits to each of the given passages", function() {
runPassage("(mock-visits:'qux','qux','qux','grault')");
var p = goToPassage("qux");
expect(p.text()).toBe('4');
p = goToPassage("garply");
expect(p.text()).toBe('1');
p = goToPassage("grault");
expect(p.text()).toBe('2');
});
it("each successive (mock-visits:) call overrides the last", function() {
runPassage("(mock-visits:'qux','qux','qux','grault')");
var p = goToPassage("qux");
expect(p.text()).toBe('4');
runPassage("(mock-visits:'garply')");
p = goToPassage("garply");
expect(p.text()).toBe('2');
Engine.goBack();
Engine.goBack();
expect($('tw-passage > :last-child').text()).toBe('4');
});
it("alters the (history:) keyword, adding its strings to the start", function() {
runPassage("(mock-visits:'qux','qux','qux','grault')");
expect('(history:)').markupToPrint('qux,qux,qux,grault,test');
});
});
});
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