├── .gitignore
├── LICENSE
├── README.md
├── assets
├── calico 1200 x 628.png
├── calico 1500 x 500.png
└── calico 630 x 500.png
├── build.sh
├── documentation
├── creating patches.md
├── events.md
├── getting started.md
├── ink esoterics.md
├── ink guide.md
├── ink symbols.md
├── patches.md
└── tags.md
├── engine
├── README.md
├── calico.js
├── ink.js
└── patches
│ ├── audioplayer.js
│ ├── autosave.js
│ ├── dragtoscroll.js
│ ├── eval.js
│ ├── fadeafterchoice.js
│ ├── forcetagsbeforeline.js
│ ├── history.js
│ ├── markdowntohtml.js
│ ├── memorycard.js
│ ├── minwordsperline.js
│ ├── parallaxframes.js
│ ├── preload.js
│ ├── scrollafterchoice.js
│ ├── shortcuts
│ └── choices.js
│ ├── shorthandclasstags.js
│ ├── stepback.js
│ ├── storage.js
│ ├── storylets.ink
│ ├── storylets.js
│ └── template.js
├── package.json
└── templates
├── Calico [Template]
├── README.txt
├── calico.js
├── index.html
├── ink.js
├── patches
│ ├── audioplayer.js
│ ├── autosave.js
│ ├── dragtoscroll.js
│ ├── eval.js
│ ├── forcetagsbeforeline.js
│ ├── history.js
│ ├── markdowntohtml.js
│ ├── memorycard.js
│ ├── minwordsperline.js
│ ├── parallaxframes.js
│ ├── preload.js
│ ├── scrollafterchoice.js
│ ├── shortcuts
│ │ └── choices.js
│ ├── shorthandclasstags.js
│ ├── stepback.js
│ ├── storage.js
│ ├── storylets.ink
│ ├── storylets.js
│ └── template.js
├── project.js
├── story.ink
└── style.css
├── README.md
└── Winter [Sample project]
├── README.txt
├── calico.js
├── fonts
├── LICENSE.txt
├── OpenSans-Bold.ttf
├── OpenSans-BoldItalic.ttf
├── OpenSans-ExtraBold.ttf
├── OpenSans-ExtraBoldItalic.ttf
├── OpenSans-Italic.ttf
├── OpenSans-Light.ttf
├── OpenSans-LightItalic.ttf
├── OpenSans-Regular.ttf
├── OpenSans-SemiBold.ttf
└── OpenSans-SemiBoldItalic.ttf
├── images
├── act4_winter.png
├── act4_winter2.png
├── bedroom2_bg1.png
├── bedroom2_fg1.png
├── bedroom2_fg2.png
├── bedroom2_fg3.png
├── bedroom_bg1.png
├── bedroom_bg2.png
├── bedroom_fg1.png
├── bedroom_fg2.png
├── bedroom_fg3.png
├── bedroom_fg4.png
├── bedroom_fg5.png
├── corridor_bg.png
├── corridor_fg.png
├── corridor_fg2.png
├── corridor_fg3.png
├── cover.png
├── cover_bg1.png
├── cover_bg2.png
├── cover_fg1.png
├── cover_fg2.png
├── hand4.png
├── hands.png
├── hands2.png
├── kiss_red.png
├── kiss_shadow.png
├── kiss_winter.png
├── park_bg.png
├── park_bg2.png
├── park_bg3.png
├── park_bg4.png
├── park_fg.png
├── park_fg2.png
├── park_fg2b.png
├── park_fg3.png
├── party1_bg.png
├── party1_fg.png
├── party2_bg.png
├── party2_fg.png
├── party2_fg2.png
├── party3_bg.png
├── party3_fg1.png
├── party3_fg2.png
├── party3_fg3.png
├── party4_bg.png
├── party4_fg2.png
├── party4_fg3.png
├── party4_fg3_shadow.png
├── phone_fg.png
├── winter16.ico
├── winter256.ico
├── winter32.ico
├── winter48.ico
├── winterlay.png
└── zine2.png
├── index.html
├── ink.js
├── music
├── act1park.mp3
├── act1party.mp3
├── act2.mp3
├── act3.mp3
└── act4.mp3
├── patches
├── audioplayer.js
├── autosave.js
├── dragtoscroll.js
├── eval.js
├── forcetagsbeforeline.js
├── history.js
├── markdowntohtml.js
├── memorycard.js
├── minwordsperline.js
├── parallaxframes.js
├── preload.js
├── scrollafterchoice.js
├── shortcuts
│ └── choices.js
├── shorthandclasstags.js
├── stepback.js
├── storage.js
├── storylets.ink
├── storylets.js
└── template.js
├── style.css
├── winter.ink
└── winter.js
/.gitignore:
--------------------------------------------------------------------------------
1 | release
2 | .DS_Store
3 | node_modules/
4 | package-lock.json
5 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Elliot Herriman
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Calico
2 |
3 | Calico is a web engine developed for use with inkle's narrative scripting language Ink.
4 |
5 | This project has an itch page [here](https://elliotherriman.itch.io/calico).
6 |
7 | Due to browser security issues, you may need to use [Catmint](https://elliotherriman.itch.io/catmint), or run a local server, in order to test your game. These issues won't be present once your game is uploaded to a website like itch.io.
8 |
9 | If you're interested in contributing, please don't hesitate to submit a pull request.
10 |
11 | ## Resources
12 |
13 | ### Using Calico
14 | **[Getting Started](./documentation/getting%20started.md)**
15 |
16 | How to set up Calico, import patches, build a game, and upload everything to itch.io.
17 |
18 | **[Tags](./documentation/tags.md)**
19 |
20 | A list of tags offered out of the box by Calico, and an explanation of how to create your own.
21 |
22 | **[Patches](./documentation/patches.md)**
23 |
24 | A guide covering some of the more complicated patches provided by Calico. If a patch isn't working as expected for you, check in here!
25 |
26 | ### Using Ink
27 |
28 | **[A Beginner's Guide To Ink](./documentation/ink%20guide.md)**
29 |
30 | A condensed introduction to ink, the narrative scripting language that powers Calico.
31 |
32 | **[Ink Symbols](./documentation/ink%20symbols.md)**
33 |
34 | A list of symbols used in ink (and their corresponding function), with links to the official documentation. Designed to be easy to control-F through.
35 |
36 | ### Advanced Features
37 |
38 | **[Creating Patches](./documentation/creating%20patches.md)**
39 |
40 | A guide covering how to create patches yourself.
41 |
42 | **[Events](./documentation/events.md)**
43 |
44 | An explanation and list of the custom events that Calico offers, which allow you to easily tweak the engine's functionality.
45 |
46 |
47 | **[Ink Esoterics](./documentation/ink%20esoterics.md)**
48 |
49 | A list of some *very* obscure ink features. In all honesty, these are mostly here for when I forget.
50 |
51 | ## License
52 |
53 | Calico and all patches are released under the MIT license. Additionally, a small number of patches rely on other MIT licensed code.
54 |
55 | At runtime, Calico will automatically generate and compactly print all necessary licenses to the browser console.
56 |
--------------------------------------------------------------------------------
/assets/calico 1200 x 628.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/assets/calico 1200 x 628.png
--------------------------------------------------------------------------------
/assets/calico 1500 x 500.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/assets/calico 1500 x 500.png
--------------------------------------------------------------------------------
/assets/calico 630 x 500.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/assets/calico 630 x 500.png
--------------------------------------------------------------------------------
/build.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | cd templates
4 | for folder in *
5 | do echo "$folder"
6 | rm -rf "$folder"/patches
7 | mkdir "$folder"/patches
8 | cp ../engine/{calico,ink}.js "$folder"/
9 | cp -r ../engine/patches/* "$folder"/patches
10 | mkdir ../release
11 | zip -r ../release/"$folder" "$folder"/*
12 | done
13 |
--------------------------------------------------------------------------------
/documentation/events.md:
--------------------------------------------------------------------------------
1 | # Events
2 |
3 | Calico offers a large number of custom events that you can use to tweak the engine's functionality. If you're unfamiliar with Javascript events, you can read more [here](https://developer.mozilla.org/en-US/docs/Web/Events).
4 |
5 | ## Responding To Events
6 |
7 | To listen for an event, you can use `addEventListener`, and any other related Javascript functions, on the expected DOM target.
8 |
9 | By default, events will be sent to the `window` object, but most events manually override this to target their parent story's `outerdiv`.
10 |
11 | If your code isn't working, make sure you're using the right target.
12 |
13 | As long as your code doesn't contain any asynchronous code, Calico will wait until your code has finished execution before continuing.
14 |
15 | ## Arguments
16 |
17 | Many events include arguments that can be accessed through `event.detail`, like so.
18 |
19 | ```js
20 | Patches.add(function()
21 | {
22 | this.outerdiv.addEventListener("passage line", (event) =>
23 | {
24 | console.log(event.detail.line);
25 | });
26 | });
27 | ```
28 |
29 | As a general rule, if an argument is an object, then any changes you make to it will persist in Calico's core loop. For example, this would allow you to alter any or every line of text that Calico processes.
30 |
31 | Conversely, if an argument is not an object — a string, a number, a boolean, and so forth — then any changes you make will not persist when Calico resumes execution.
32 |
33 | ## Targeting `outerdiv`
34 |
35 | If you look over existing patches, you may find that Calico targets a story's `outerdiv` using both `this.outerdiv` and `story.outerdiv`. These are functionally the same thing — `this` is used when inside a story's function, and `story` is used when a story has been passed into the current function as an argument.
36 |
37 | # List Of Events
38 |
39 | All the events contained in the core Calico engine. I'll try to fill in descriptions of each event and their arguments as soon as possible.
40 |
41 | ### Story Events
42 |
43 | Events that concern your story overall, and its inner and outer divs.
44 |
45 | **Name**: "story ready"
46 |
47 | **Arguments**: story
48 |
49 | **Target**: `outerdiv`
50 |
51 | **Name**: "story clearing"
52 |
53 | **Arguments**: story, delay, howMany
54 |
55 | **Target**: `outerdiv`
56 |
57 | **Name**: "story restarting"
58 |
59 | **Arguments**: story
60 |
61 | **Target**: `outerdiv`
62 |
63 | **Name**: "story setheight"
64 |
65 | **Arguments**: story, old, new
66 |
67 | **Target**: `outerdiv`
68 |
69 | ### Passage Events
70 |
71 | Events that concern the current ink passage — all text and data from the start of the story or the player's last input, until the end of the story, or the next available choice.
72 |
73 | **Name**: "passage start"
74 |
75 | **Arguments**: story
76 |
77 | **Target**: `outerdiv`
78 |
79 | **Name**: "passage line"
80 |
81 | **Arguments**: story, line
82 |
83 | **Target**: `outerdiv`
84 |
85 | **Name**: "passage line element"
86 |
87 | **Arguments**: story, element, line
88 |
89 | **Target**: `outerdiv`
90 |
91 | **Name**: "passage choice"
92 |
93 | **Arguments**: story, choice
94 |
95 | **Target**: `outerdiv`
96 |
97 | **Name**: "passage choice element"
98 |
99 | **Arguments**: story, choice, element
100 |
101 | **Target**: `outerdiv`
102 |
103 | **Name**: "passage end"
104 |
105 | **Arguments**: story, choice
106 |
107 | **Target**: `outerdiv`
108 |
109 | ### Queue Events
110 |
111 | Events that concern the queue of elements to be rendered.
112 |
113 | **Name**: "queue clear"
114 |
115 | **Arguments**: queue, old
116 |
117 | **Target**: `outerdiv`
118 |
119 | **Name**: "queue setdelay"
120 |
121 | **Arguments**: new, old, queue
122 |
123 | **Target**: `outerdiv`
124 |
125 | **Name**: "queue setlinebyline"
126 |
127 | **Arguments**: value, queue
128 |
129 | **Target**: `outerdiv`
130 |
131 | **Name**: "queue push"
132 |
133 | **Arguments**: element, queue
134 |
135 | **Target**: `outerdiv`
136 |
137 | ### Element Events
138 |
139 | Events that concern individual lines of text, images, and other HTML elements.
140 |
141 | **Name**: "element addclass"
142 |
143 | **Arguments**: element, class, queue
144 |
145 | **Target**: `outerdiv`
146 |
147 | **Name**: "element adddelay"
148 |
149 | **Arguments**: element, delay, story, queue
150 |
151 | **Target**: `window`
152 |
153 | **Name**: "element setproperty"
154 |
155 | **Arguments**: element, story, property, new, old, queue
156 |
157 | **Target**: `outerdiv`
158 |
159 | **Name**: "element added"
160 |
161 | **Arguments**: element, story, queue
162 |
163 | **Target**: `outerdiv`
164 |
165 | **Name**: "element show"
166 |
167 | **Arguments**: element, story, queue
168 |
169 | **Target**: `outerdiv`
170 |
171 | **Name**: "element rendered"
172 |
173 | **Arguments**: element, story, queue
174 |
175 | **Target**: `outerdiv`
176 |
177 | **Name**: "element hide"
178 |
179 | **Arguments**: element, story, queue
180 |
181 | **Target**: `outerdiv`
182 |
183 | **Name**: "element remove"
184 |
185 | **Arguments**: element, story, queue
186 |
187 | **Target**: `outerdiv`
188 |
189 | ### Render Events
190 |
191 | Events that concern the render process, performed by the queue.
192 |
193 | **Name**: "render start"
194 |
195 | **Arguments**: story, queue, target
196 |
197 | **Target**: `outerdiv`
198 |
199 | **Name**: "render finished"
200 |
201 | **Arguments**: story, queue, target
202 |
203 | **Target**: `outerdiv`
204 |
205 | **Name**: "render interrupted"
206 |
207 | **Arguments**: story, queue, index, target
208 |
209 | **Target**: `outerdiv`
210 |
211 | ### Tags Events
212 |
213 | Events that concern ink tags.
214 |
215 | **Name**: "tags add"
216 |
217 | **Arguments**: name, function
218 |
219 | **Target**: `window`
220 |
221 | **Name**: "tags process"
222 |
223 | **Arguments**: story, tag
224 |
225 | **Target**: `outerdiv`
226 |
227 | **Name**: "tags matched"
228 |
229 | **Arguments**: story, tag, property
230 |
231 | **Target**: `outerdiv`
232 |
233 | **Name**: "tags option"
234 |
235 | **Arguments**: story, variable, new, old
236 |
237 | **Target**: `outerdiv`
238 |
239 | **Name**: "tags unhandled"
240 |
241 | **Arguments**: tag, property
242 |
243 | **Target**: `outerdiv`
244 |
245 | ### Parser Events
246 |
247 | Events that concern inline ink tags, and text parsing.
248 |
249 | **Name**: "parser add tag"
250 |
251 | **Arguments**: tag, function
252 |
253 | **Target**: `window`
254 |
255 | **Name**: "parser add pattern"
256 |
257 | **Arguments**: pattern, function
258 |
259 | **Target**: `window`
260 |
261 | **Name**: "parser process"
262 |
263 | **Arguments**: line
264 |
265 | **Target**: `outerdiv`
266 |
267 | **Name**: "parser matched tag"
268 |
269 | **Arguments**: tag, arguments, function, line
270 |
271 | **Target**: `outerdiv`
272 |
273 | **Name**: "parser matched pattern"
274 |
275 | **Arguments**: pattern, function, line, tags
276 |
277 | **Target**: `outerdiv`
278 |
279 | ### Text Animation Events
280 |
281 | Events that concern text animations.
282 |
283 | **Name**: "textanimation create"
284 |
285 | **Arguments**: name, effect
286 |
287 | **Target**: `window`
288 |
289 | **Name**: "textanimation apply"
290 |
291 | **Arguments**: element, name, effect
292 |
293 | **Target**: `outerdiv`
294 |
295 | ### Shortcuts Events
296 |
297 | Events that concern keyboard shortcuts.
298 |
299 | **Name**: "shortcuts add"
300 |
301 | **Arguments**: string, callback, type
302 |
303 | **Target**: `window`
304 |
305 | **Name**: "shortcuts remove"
306 |
307 | **Arguments**: string, callback, type
308 |
309 | **Target**: `window`
310 |
311 | **Name**: "shortcuts process"
312 |
313 | **Arguments**: type, input, event
314 |
315 | **Target**: `window`
316 |
317 | ### Window Events
318 |
319 | Events that concern the browser window.
320 |
321 | **Name**: "window resized"
322 |
323 | **Target**: `window`
324 |
--------------------------------------------------------------------------------
/documentation/ink esoterics.md:
--------------------------------------------------------------------------------
1 | A quick reference for obscure features in ink.
2 |
3 | ## WEB PLAYER TAGS
4 |
5 | A list of tags that can be used with Inky's native web player.
6 |
7 | Some of these tags expect a property, in the form of `#TAG: property`.
8 |
9 | **Capitalisation matters.** These tags will not work if you don't capitalise them as they appear here.
10 |
11 | ```
12 | theme
13 | author
14 | AUDIO
15 | AUDIOLOOP
16 | IMAGE
17 | LINK
18 | LINKOPEN
19 | BACKGROUND
20 | CLASS
21 | CLEAR
22 | RESTART
23 | ```
24 |
25 | ## NATIVE FUNCTION CALLS
26 |
27 | These aren't actually functions, despite the name. These are operations you can use on variables and lists to modify or compare them.
28 |
29 | ```
30 | Add : +
31 | Subtract : -
32 | Divide : /
33 | Multiply : *
34 | Mod : %
35 | Negate : _
36 |
37 | Equal : ==
38 | Greater : >
39 | Less : <
40 | GreaterThanOrEquals : >=
41 | LessThanOrEquals : <=
42 | NotEquals : !=
43 | Not : !
44 |
45 | And : &&
46 | Or : ||
47 |
48 | Min : MIN
49 | Max : MAX
50 |
51 | Pow : POW
52 | Floor : FLOOR
53 | Ceiling : CEILING
54 | Int : INT
55 | Float : FLOAT
56 |
57 | Has : ?
58 | Hasnt : !?
59 | Intersect : ^
60 |
61 | ListMin : LIST_MIN
62 | ListMax : LIST_MAX
63 | All : LIST_ALL
64 | Count : LIST_COUNT
65 | ValueOfList : LIST_VALUE
66 | Invert : LIST_INVERT
67 | ```
68 |
69 | ## OPERATIONS WITH PLAIN TEXT ALTERNATIVES
70 |
71 | You can use the plain text version of these operations, rather than the symbol.
72 |
73 | It's recommended that you use "not" over "!", since "!" can perform other functions in ink, and may lead to syntax errors.
74 |
75 | ```
76 | && : and
77 | || : or
78 | % : mod
79 | ! : not
80 | ? : has
81 | !? : hasnt
82 | ```
83 |
84 | ## OTHER FUNCTIONS
85 |
86 | These must be called as functions. Will return a value.
87 |
88 | ```
89 | CHOICE_COUNT
90 | TURNS_SINCE
91 | TURNS
92 | RANDOM
93 | SEED_RANDOM
94 | LIST_VALUE
95 | LIST_RANDOM
96 | READ_COUNT
97 | ```
98 |
99 | ## CONTROL COMMAND TYPES
100 |
101 | Not necessary unless you're doing some truly wild things with the ink runtime. Current as of inkjs 2.2.0.
102 |
103 | ```
104 | -1 : NotSet
105 | 0 : EvalStart
106 | 1 : EvalOutput
107 | 2 : EvalEnd
108 | 3 : Duplicate
109 | 4 : PopEvaluatedValue
110 | 5 : PopFunction
111 | 6 : PopTunnel
112 | 7 : BeginString
113 | 8 : EndString
114 | 9 : NoOp
115 | 10 : ChoiceCount
116 | 11 : Turns
117 | 12 : TurnsSince
118 | 13 : ReadCount
119 | 14 : Random
120 | 15 : SeedRandom
121 | 16 : VisitIndex
122 | 17 : SequenceShuffleIndex
123 | 18 : StartThread
124 | 19 : Done
125 | 20 : End
126 | 21 : ListFromInt
127 | 22 : ListRange
128 | 23 : ListRandom
129 | 24 : BeginTag
130 | 25 : EndTag
131 | 26 : TOTAL_VALUES
132 | ```
133 |
--------------------------------------------------------------------------------
/documentation/ink symbols.md:
--------------------------------------------------------------------------------
1 | # Ink Symbols
2 |
3 | ## Introduction
4 |
5 | This is a simple, incomplete guide to inkle's ink. Although the ink documentation is comprehensive, it's also hard to navigate, and borderline impossible to just search for a symbol to figure out what it means. This is a mostly complete list of all those symbols, which you can easily search for here, and then click to expand for more details.
6 |
7 | Right now, most of the details you'll find will simply be links to the original documentation. My plan is to expand that shortly, but... sleep beckons, and I'd rather let people use a half finished version than nothing at all.
8 |
9 | ## Content
10 |
11 | - [Includes `INCLUDE`](https://github.com/inkle/ink/blob/master/Documentation/WritingWithInk.md#script-files-can-be-combined)
12 | - [Text](https://github.com/inkle/ink/blob/master/Documentation/WritingWithInk.md#the-simplest-ink-script)
13 | - [Tags `#` `:`](https://github.com/inkle/ink/blob/master/Documentation/WritingWithInk.md#tags)
14 | - [Choices `*` `**` `***`](https://github.com/inkle/ink/blob/master/Documentation/WritingWithInk.md#2-choices)
15 | - [Sticky choices `+` `++` `+++`](https://github.com/inkle/ink/blob/master/Documentation/WritingWithInk.md#sticky-choices)
16 | - [Fallback choices `*` `->`](https://github.com/inkle/ink/blob/master/Documentation/WritingWithInk.md#fallback-choices)
17 | - [Suppress choice text `*` `[]`](https://github.com/inkle/ink/blob/master/Documentation/WritingWithInk.md#suppressing-choice-text)
18 | - [Glue `<>`](https://github.com/inkle/ink/blob/master/Documentation/WritingWithInk.md#glue)
19 | - [Gathers `-` `--` `---`](https://github.com/inkle/ink/blob/master/Documentation/WritingWithInk.md#1-gathers)
20 | - [Alternatives `{}` `|` `&` `!` `~`](https://github.com/inkle/ink/blob/master/Documentation/WritingWithInk.md#sequences-cycles-and-other-alternatives)
21 | have to "*\ " choices if you want to start them with a sequence
22 | - Sequences `{}` `|`
23 | - Cycles `{}` `|` `&`
24 | - Once-only sequences `{}` `|` `!`
25 | - Shuffles `{}` `|` `~`
26 | - [Knots `==` `===`](https://github.com/inkle/ink/blob/master/Documentation/WritingWithInk.md#pieces-of-content-are-called-knots)
27 | - [Stitches `=`](https://github.com/inkle/ink/blob/master/Documentation/WritingWithInk.md#knots-can-be-subdivided)
28 | - [Labelled gathers `-` `()`](https://github.com/inkle/ink/blob/master/Documentation/WritingWithInk.md#gathers-and-options-can-be-labelled)
29 | - [Labelled choices `*` `()`](https://github.com/inkle/ink/blob/master/Documentation/WritingWithInk.md#advanced-all-options-can-be-labelled)
30 | - [Diverts `->`](https://github.com/inkle/ink/blob/master/Documentation/WritingWithInk.md#4-diverts)
31 | - [Tunnels `->` `->`](https://github.com/inkle/ink/blob/master/Documentation/WritingWithInk.md#1-tunnels)
32 | - [Threads `<-`](https://github.com/inkle/ink/blob/master/Documentation/WritingWithInk.md#2-threads)
33 | - [Done `-> DONE`](https://github.com/inkle/ink/blob/master/Documentation/WritingWithInk.md#using---done)
34 | - [End `-> END`](https://github.com/inkle/ink/blob/master/Documentation/WritingWithInk.md#advanced-a-knottier-hello-world)
35 | - [Comments `//` `/**/`](https://github.com/inkle/ink/blob/master/Documentation/WritingWithInk.md#comments)
36 | - Escape character `\`
37 |
38 | ---
39 |
40 | ## Variables
41 |
42 | - [Variables `VAR` `=`](https://github.com/inkle/ink/blob/master/Documentation/WritingWithInk.md#defining-global-variables)
43 | distinguish types: integer, floating point (decimal), content, or a story address.
44 | - [Constants `CONST` `=`](https://github.com/inkle/ink/blob/master/Documentation/WritingWithInk.md#global-constants)
45 | - [Lists `LIST`](https://github.com/inkle/ink/blob/master/Documentation/WritingWithInk.md#1-basic-lists)
46 | - [Print variable as text `{}`](https://github.com/inkle/ink/blob/master/Documentation/WritingWithInk.md#printing-variables)
47 | - [Code lines`~`](https://github.com/inkle/ink/blob/master/Documentation/WritingWithInk.md#2-logic)
48 | - Set variable to `~` `=`
49 | - [Temporary variables `~` `temp` `VAR` `=`](https://github.com/inkle/ink/blob/master/Documentation/WritingWithInk.md#4-temporary-variables)
50 | - [Mathematical operations `~` `=` `+` `-` `+=` `-=` `*` `/` `%` `mod` `++` `--` `INT()` `FLOAT()` `FLOOR()` `POW()` `RANDOM()`](https://github.com/inkle/ink/blob/master/Documentation/WritingWithInk.md#mathematics)
51 | - Add `~` `+`
52 | - Subtract `~` `-`
53 | - Add and assign `~` `+=`
54 | - Subtract and assign `~` `-=`
55 | - Multiply `~` `*`
56 | - Divide `~` `/`
57 | - Modulo, remainder `~` `%` `mod`
58 | - Increment variable `~` `++`
59 | - Decrement variable `~` `--`
60 | - [Round numbers `INT()` `FLOAT()` `FLOOR()`](https://github.com/inkle/ink/blob/master/Documentation/WritingWithInk.md#advanced-int-floor-and-float)
61 | - X to the power of Y `POW()`
62 | - [Generate a random number `RANDOM()`](https://github.com/inkle/ink/blob/master/Documentation/WritingWithInk.md#randommin-max)
63 |
64 | ---
65 |
66 | ## Conditionals
67 |
68 | - Conditional content `{}` `:` `|` `-` `else`
69 | - [one line if](https://github.com/inkle/ink/blob/master/Documentation/WritingWithInk.md#conditional-text)
70 | - [one line if else](https://github.com/inkle/ink/blob/master/Documentation/WritingWithInk.md#conditional-text)
71 | - [multi line if](https://github.com/inkle/ink/blob/master/Documentation/WritingWithInk.md#a-simple-if)
72 | - [if, else](https://github.com/inkle/ink/blob/master/Documentation/WritingWithInk.md#a-simple-if)
73 | - [if, else if, else](https://github.com/inkle/ink/blob/master/Documentation/WritingWithInk.md#extended-ifelse-ifelse-blocks)
74 | - [switch statements](https://github.com/inkle/ink/blob/master/Documentation/WritingWithInk.md#switch-blocks)
75 | - Conditional choices `*` `{}`
76 | - [Basic](https://github.com/inkle/ink/blob/master/Documentation/WritingWithInk.md#conditional-choices)
77 | - [Advanced](https://github.com/inkle/ink/blob/master/Documentation/WritingWithInk.md#advanced-multiple-conditions)
78 |
79 | ---
80 |
81 | ## Comparisons
82 |
83 | - Is equal to `==`
84 | - Is less than `<`
85 | - Is greater than `>`
86 | - Is less than or equal to `<=`
87 | - Is greater than or equal to `>=`
88 | - [Not `!` `not`](https://github.com/inkle/ink/blob/master/Documentation/WritingWithInk.md#logical-operators-and-and-or)
89 | use `not` instead of `!`, otherwise compiler may see text as once only list
90 | - [And `&&` `and`](https://github.com/inkle/ink/blob/master/Documentation/WritingWithInk.md#logical-operators-and-and-or)
91 | - [Or `||` `or`](https://github.com/inkle/ink/blob/master/Documentation/WritingWithInk.md#logical-operators-and-and-or)
92 |
93 | ---
94 |
95 | ## Functions
96 |
97 | - [Functions `==` `===` `FUNCTION` `()`](https://github.com/inkle/ink/blob/master/Documentation/WritingWithInk.md#5-functions)
98 | - Return value from function `~` `return`
99 | - Game queries `()` `CHOICE_COUNT()` `TURNS()` `TURNS_SINCE()` `->` `SEED_RANDOM()`
100 | - [Count available choices `CHOICE_COUNT()`](https://github.com/inkle/ink/blob/master/Documentation/WritingWithInk.md#choice_count)
101 | - [Count turns played so far `TURNS()`](https://github.com/inkle/ink/blob/master/Documentation/WritingWithInk.md#turns)
102 | - [Turns since labelled content `TURNS_SINCE()` `->`](https://github.com/inkle/ink/blob/master/Documentation/WritingWithInk.md#turns_since--knot)
103 | - [Set the random number generator's seed `SEED_RANDOM()`](https://github.com/inkle/ink/blob/master/Documentation/WritingWithInk.md#seed_random)
104 | - [External functions `EXTERNAL` `()`](https://github.com/inkle/ink/blob/master/Documentation/RunningYourInk.md#external-functions)
105 |
--------------------------------------------------------------------------------
/documentation/patches.md:
--------------------------------------------------------------------------------
1 | # Patches
2 |
3 | Patches are simple (or complex) tweaks that you can import into a project to change how the core engine behaves. You can read more [here](https://github.com/elliotherriman/calico/blob/main/documentation/getting%20started.md#importing-patches).
4 |
5 | Generally, patches require minimal configuration after you've imported them. That said, a select few require some extra tweaking to run properly. This document aims to cover all of these cases, but if you're having difficulty getting a patch to work, feel free to contact me on [Twitter](https://twitter.com/elliotherriman).
6 |
7 | ## memorycard.js
8 |
9 | If you're using `memorycard.js` in conjunction with any tags that make persistent changes — for example, tags that trigger audio, or tags that change the background of your game — then those changes will normally be lost if the player reloads the window.
10 |
11 | To avoid this, you can add persistent tags to `options.memorycard_applymostrecenttag` from your `project.js`. This will attempt to find the most recent instance of each given tag, and process it.
12 |
13 | ```js
14 | options.memorycard_applymostrecenttag.push("play", "resume", "pause", "stop");
15 | ```
16 |
17 | ## parallaxframes.js
18 |
19 | While `parallaxframes.js` will run without configuration, it won't display properly without some custom CSS code.
20 |
21 | The following code was used in Winter. Feel free to edit the `height` and `min-height` variables to suit.
22 |
23 | ```css
24 |
25 | .frame
26 | {
27 | width: 100%;
28 | height: calc(100vw * 0.2);
29 | overflow: hidden;
30 | position: relative;
31 | }
32 |
33 | .frameLayer
34 | {
35 | position: absolute;
36 | width: 120%;
37 | display: block;
38 | top: -9999px;
39 | bottom: -9999px;
40 | left: -9999px;
41 | right: -9999px;
42 | margin: auto;
43 | max-width: auto;
44 | }
45 |
46 | /* css for mobile */
47 | @media (hover:none), (hover:on-demand)
48 | {
49 | .frame
50 | {
51 | min-height: calc(100vmax * 0.25);
52 | }
53 | .frameLayer
54 | {
55 | width: 150%;
56 | }
57 | }
58 | ```
59 |
60 | ## preload.js
61 |
62 | ### Finding files
63 | `preload.js` will automatically attempt to preload files detected in `image` and `background` tags. To preload files found in other tags, you'll need to add those tags yourself.
64 |
65 | The following code will allow preloading of files from a number of audio and image tags.
66 |
67 | ```js
68 | options.preload_tags.audio.push("play", "pause", "resume", "stop");
69 | options.preload_tags.image.push("frame");
70 | ```
71 |
72 | You can also add tags to `options.preload_tags.other` through the same method.
73 |
74 | The distinction between audio, image, and other types of files is used to simplify project structure (allowing you to put the relevant files in `defaultaudiolocation` and `defaultimagelocation`), and to simplify your ink (unspecified audio and image file types will default to `defaultaudioformat` and `defaultimageformat` respectively).
75 |
76 | ### Loading bar
77 | In order to show the loading bar, you'll need to add some CSS. Winter uses the following rules.
78 |
79 | ```css
80 | .progressbar
81 | {
82 | position: absolute;
83 | overflow: hidden;
84 | top: 0;
85 | background: #7690ac;
86 | width: 0%;
87 | height: 1vh;
88 | }
89 | ```
90 |
91 | ## shortcuts/choices.js
92 |
93 | `choices.js` is a patch that simplifies binding shortcuts to choices. Impossible inputs (such as trying to select the fourth choice when there are only two) will be ignored.
94 |
95 | The following code will allow the player to select choices via the number keys. `1` corresponds to the first choice, `2` the second, and so forth.
96 |
97 | ```js
98 | for (var i = 0; i < 9; i++)
99 | {
100 | choices.add((i+1).toString(), i, true);
101 | }
102 | ```
103 |
104 | You can also use the following code to allow the player to continue the story via the `spacebar`, but only if there's a single choice.
105 |
106 | ```js
107 | choices.add(" ", 0, true, true);
108 | ```
109 |
110 | ## shorthandclasstags.js
111 |
112 | To create a shorthand class tag, you'll need to add it to `options.shorthandclasstags_tags`.
113 |
114 | ```js
115 | options.shorthandclasstags_tags = ["red"];
116 | ```
117 |
118 | You'll also need to add a corresponding CSS class.
119 |
120 | ```css
121 | .red
122 | {
123 | color: #f76e6a !important;
124 | }
125 | ```
126 |
127 | ## stepback.js
128 |
129 | While `stepback.js` does add the ability to step forwards and backwards, the central functions `stepBack()` and `stepForwards()` won't run unless you call them from elsewhere, generally by binding them to a keypress or GUI element.
130 |
131 | The following code binds the keys "Q" to `stepBack()` and "E" to `stepForwards()`.
132 |
133 | ```js
134 | Patches.add(function()
135 | {
136 | Shortcuts.add("q", this.stepBack);
137 | Shortcuts.add("e", this.stepForwards);
138 | });
139 | ```
140 |
--------------------------------------------------------------------------------
/documentation/tags.md:
--------------------------------------------------------------------------------
1 | # Tags
2 |
3 | Calico contains several functional tags you can use by default, and makes it easy to define and add more.
4 |
5 | A tag indicates to Calico that you want the engine to do something - maybe you want it to set a background image, delay the appearance of a particular line, clear the screen, or something else.
6 |
7 | ## Options
8 | More information on how to use tags can be found [here](https://github.com/elliotherriman/calico/blob/main/documentation/getting%20started.md#tags).
9 |
10 | Some tags can be customised via options, which by convention are separated with a ">>". For example, in `musicplayer.js`, you can delay a track by using the `delay` option.
11 |
12 | ```
13 | #play: act4 >> delay: 500
14 | ```
15 |
16 | ## Default Tags
17 |
18 | A list of tags offered by Calico's core engine.
19 |
20 | ### #class
21 | This tag allows you to add a CSS class or classes to the attached line. If you attach CSS styling, this will allow you to customize the appearance of specific lines in the text using a simple tag in ink.
22 |
23 | The class tag can be after text on the same line, or on the line after text.
24 |
25 | You can apply several classes by adding them separated by spaces.
26 |
27 | ```
28 | I ran out of the door #class: center
29 | Down the street
30 | #class: rocky center
31 | And tripped over the package #class goblin falcon mango
32 | ```
33 |
34 | ### #image
35 |
36 | Adds an image to your story as its own paragraph.
37 |
38 | Calico will look for images in `defaultimagelocation` — by default, a folder called "images" at the root of your project. You can override this by starting your image's name with "./".
39 |
40 | If no image format is given, Calico will fall back to `defaultimagetype`, which by default is ".png".
41 |
42 | ```
43 | I ran out of the door.
44 | #image: door.png // ./images/door.png
45 | #image: package // ./images/package.png
46 | #image: postman.gif // ./images/postman.gif
47 | #image: ./street // ./street.png
48 | ```
49 |
50 | ### #background
51 |
52 | Works very similarly to `#image`, but instead of placing the image as a paragraph, it sets it as the background of your story's outerdiv.
53 |
54 | Example:
55 |
56 | ```
57 | I ran out of the door #background: door.png
58 | ```
59 |
60 | ### #clear
61 |
62 | Gracefully removes all text from the screen. It will clean inline images, but not the background image.
63 |
64 | Example:
65 |
66 | ```
67 | I ran out of the door
68 | #image: door.png
69 | Down the street
70 | And tripped over the package
71 | Falling until I skinned my knees
72 | * Everything went black
73 | #clear
74 | ```
75 |
76 | The clear tag is typically used immediately following a choice.
77 |
78 | If the clear tag is included on the same line as a choice, Calico will clear the page's text and the choice at the same time.
79 |
80 | ```
81 | * choice #clear
82 | -
83 | ```
84 |
85 | If the clear tag appears on a line after the choice, Calico will first clear the choice, pause, and then clear the rest of the text.
86 |
87 | ```
88 | * choice
89 | -
90 | #clear
91 | ```
92 |
93 | You may wish to use `[` and `]` to wrap your choice's text.
94 |
95 | You can also use `#clear` in the middle of a passage. Upon reaching a clear tag, Calico will briefly pause, then remove any text on screen, and continue with the rest of the passage.
96 |
97 | ### #restart
98 |
99 | Starts the story over from the beginning. Clears all text and images, but does not clear background images.
100 |
101 | Example:
102 |
103 | ```
104 | I ran out of the door
105 | #image: door.png
106 | Down the street
107 | And tripped over the package
108 | Falling until I skinned my knees
109 | * Everything went black#background: door
110 | ** Again?
111 | #restart
112 | ```
113 |
114 | ### #delay
115 |
116 | This tag will add a delay before the text it's attached to appears.
117 |
118 | If a delay tag appears on its own line, or on a line with only other tags, it will add a delay before the next line of text appears.
119 |
120 | The delay is counted in milliseconds.
121 |
122 | ```
123 | I ran out the door #delay: 500
124 | Down the street
125 | #delay: 1000
126 | And tripped over the package
127 | ```
128 |
129 | ### #linebyline
130 |
131 | `#linebyline` will cause Calico to halt after rendering each line, and wait until ``queue.render`` is called again before continuing.
132 |
133 | If no argument is provided, line-by-line mode will be toggled. With a ``true`` or ``false`` argument, you can control its behaviour directly.
134 |
135 | ## Custom Tags
136 |
137 | You can define custom tags by calling `Tags.add`. Tags are shared globally, which means two stories running in one game would share the same tags.
138 |
139 | `Tags.add` expects two arguments — the tag's name as a string, and the callback to be executed if Calico finds that tag.
140 |
141 | If that tag is matched via `Tags.process`, your callback will be executed with the following arguments.
142 |
143 | ```js
144 | Story story // the story this tag was found in.`
145 | string property // any text found after a ":" in the tag
146 | boolean isAfterText // whether this tag is being executed before or after the current text line
147 | ```
148 |
149 | Your callback can contain a return value, but it won't be processed. Instead, if you want to make changes to a line, you can access it through the story object's queue.
150 |
151 | This is the current code for Calico's class tag.
152 |
153 | ```js
154 | Tags.add("class", function(story, property)
155 | {
156 | // don't do anything if the class is empty
157 | if (!property)
158 | return;
159 |
160 | // if there's multiple classes separated by spaces,
161 | // add all of them to the element
162 | story.queue.addClass(property.split(" "));
163 | });
164 | ```
165 |
--------------------------------------------------------------------------------
/engine/README.md:
--------------------------------------------------------------------------------
1 | # Calico
2 |
3 | These are the files that make Calico run.
4 |
5 | To make a project with Calico, you'll need to create an HTML file to import these into. Alternatively, you can get started using one of Calico's templates.
--------------------------------------------------------------------------------
/engine/patches/autosave.js:
--------------------------------------------------------------------------------
1 | import memorycard from "./memorycard.js"
2 |
3 | // -----------------------------------
4 | // persistent saves
5 | // -----------------------------------
6 |
7 | var credits = {
8 | emoji: "💽",
9 | name: "Autosave",
10 | author: "Elliot Herriman",
11 | version: "1.0",
12 | description: "Automatically save the story's state.",
13 | licences: {
14 | self: "2021",
15 | }
16 | }
17 |
18 | var options =
19 | {
20 | autosave_enabled: true,
21 | };
22 |
23 | Patches.add(function()
24 | {
25 | this.outerdiv.addEventListener("passage start", (event) =>
26 | {
27 | if (this.options.autosave_enabled) memorycard.save(event.detail.story);
28 | });
29 |
30 | this.outerdiv.addEventListener("story restarting", (event) =>
31 | {
32 | if (this.options.autosave_enabled) memorycard.save(event.detail.story);
33 | });
34 |
35 | this.outerdiv.addEventListener("story ready", (event) =>
36 | {
37 | if (this.options.autosave_enabled)
38 | {
39 | memorycard.load(event.detail.story);
40 |
41 | this.outerdiv.addEventListener("render start", (event) =>
42 | {
43 | event.detail.queue.contents[0].delay = 0;
44 | }, {once: true});
45 | }
46 | });
47 |
48 | }, options, credits);
49 |
50 | export default {options: options, credits: credits};
--------------------------------------------------------------------------------
/engine/patches/dragtoscroll.js:
--------------------------------------------------------------------------------
1 | // -----------------------------------
2 | // click and drag to scroll
3 | // -----------------------------------
4 |
5 | var credits =
6 | {
7 | emoji: "🐁",
8 | name: "Drag to scroll",
9 | version: "1.1",
10 | description: ["Click and drag the page to scroll."],
11 | licences: {
12 | self: "2021 Elliot Herriman",
13 | }
14 | }
15 |
16 | var options =
17 | {
18 | dragtoscroll_loadatstart: true,
19 | // if false, will prevent dragging by scrolling vertically
20 | dragtoscroll_vertical: true,
21 | // if false, will prevent dragging by scrolling horizontally
22 | dragtoscroll_horizontal: false,
23 | // modifies how far the page scrolls relative to the mouse distance
24 | dragtoscroll_verticalmodifier: 0.9,
25 | dragtoscroll_horizontalmodifier: 0.9,
26 | }
27 |
28 | // fired when the player clicks, telling the page to allow drag scrolling
29 | function dragMouseClick(target, options, event)
30 | {
31 | // set initial positions
32 | var divStartPos = {x: target.scrollLeft, y: target.scrollTop};
33 | target.mouseStartPos = {x: event.clientX, y: event.clientY};
34 |
35 | // define the function here so we can remove it later
36 | var dragMouse = dragMouseMove.bind(null, target, options, divStartPos);
37 |
38 | // update things when we move the mouse
39 | document.addEventListener('mousemove', dragMouse);
40 | // stop doing things when we release the drag
41 | document.addEventListener('mouseup', function()
42 | {
43 | document.removeEventListener('mousemove', dragMouse);
44 | });
45 | };
46 |
47 | // update scroll position each time the mouse moves
48 | // removed once the mouse is unclicked
49 | function dragMouseMove(target, options, divStartPos, event)
50 | {
51 | if (!event.buttons == 1)
52 | {
53 | target.removeEventListener('mousemove', dragMouseMove);
54 | return;
55 | }
56 |
57 | if (options.dragtoscroll_vertical)
58 | {
59 | target.scrollTop = (divStartPos.y - options.dragtoscroll_verticalmodifier * (event.clientY - target.mouseStartPos.y));
60 | }
61 |
62 | if (options.dragtoscroll_horizontal)
63 | {
64 | target.scrollLeft = (divStartPos.x - options.dragtoscroll_horizontalmodifier * (event.clientX - target.mouseStartPos.x));
65 | }
66 | };
67 |
68 | function Bind(target, options)
69 | {
70 | target.mouseStartPos = {};
71 |
72 | // bind handler for when you click
73 | target.addEventListener('mousedown', (event) =>
74 | {
75 | dragMouseClick(target, options, event)
76 | });
77 | }
78 |
79 | Patches.add(function()
80 | {
81 | if (!this.options.dragtoscroll_loadatstart) return;
82 |
83 | Bind(this.outerdiv, this.options);
84 |
85 | }, options, credits);
86 |
87 | export default {options: options, credits: credits, bind: Bind};
--------------------------------------------------------------------------------
/engine/patches/eval.js:
--------------------------------------------------------------------------------
1 | // -----------------------------------
2 | // eval
3 | // -----------------------------------
4 |
5 | var credits = {
6 | emoji: "🤖",
7 | name: "eval()",
8 | author: "Elliot Herriman",
9 | version: "1.1",
10 | description: ["Allows you to execute Javascript directly from your ink.", "This patch is highly irresponsible in like four different ways."],
11 | licences: {
12 | self: "2021",
13 | }
14 | }
15 |
16 | var options = {};
17 |
18 | // runs everything after the ":" as javascript
19 | // this wasn't included by default, as it provides more
20 | // than a few opportunities for mischief. let's call it
21 | // a feature for Advanced Users.
22 | //
23 | // no, but, like, seriously. there is almost always a better way
24 | // to do what you're trying to do than eval. unless you understand
25 | // that, unless you're sure, please go ask someone for help. this
26 | // is almost Definitely the wrong solution for your problem
27 |
28 | // eval.bind(this) or eval.call(this) doesn't work (that's the specification) so we wrap eval in a function so we can change "this".
29 | function evalWithThis(code) {
30 | eval(code);
31 | }
32 | Tags.add("eval",
33 | function(story, property)
34 | {
35 | if (!story.options.eval_enabled) return;
36 |
37 | // make sure we have something to execute
38 | if (property.trim())
39 | {
40 | evalWithThis.call(story, property);
41 | }
42 | });
43 |
44 | Patches.add(null, options, credits);
45 |
46 | export default {options: options, credits: credits};
47 |
--------------------------------------------------------------------------------
/engine/patches/fadeafterchoice.js:
--------------------------------------------------------------------------------
1 | // -----------------------------------
2 | // fade after choice
3 | // -----------------------------------
4 |
5 | var credits = {
6 | emoji: "⚪",
7 | name: "Fade after choice",
8 | author: "Michael Gutman",
9 | version: "1.0",
10 | description: ["After choosing a choice, make previous text partially fade out."],
11 | licences: {
12 | self: "2021",
13 | }
14 | }
15 |
16 | var options = {
17 | // if set to true will start the fade (delay timer) immediately after clicking the choice
18 | // by default (false) fadeafterchoice waits until the next passage starts to render
19 | fadeafterchoice_onchoice: false,
20 | // the target % opacity to fade to between 0 and 100. 100 does nothing. lower numbers make it less visible.
21 | fadeafterchoice_fadelevel: 30.0,
22 | // how long it takes to reach the target opacity (fadelevel)
23 | fadeafterchoice_fadespeed: 200.0,
24 | // the delay between the new text starting to render (or, if onchoice is true, the choice being made)
25 | // and the old text starting to fade
26 | fadeafterchoice_fadedelay: 0.0,
27 | }
28 |
29 | Patches.add(function() {
30 | if (this.options.fadeafterchoice_onchoice) {
31 | // passage end -> a choice has been made, so we fade out the queue
32 | this.outerdiv.addEventListener("passage end", (event) => {
33 | event.detail.story.queue.contents.forEach(p => {
34 | transition(p, "opacity", this.options.fadeafterchoice_fadelevel + "%", this.options.fadeafterchoice_fadespeed, this.options.fadeafterchoice_fadedelay);
35 | });
36 | })
37 | } else {
38 | // the queue is cleared when a choice is made, so save that old queue to fade it out
39 | this.outerdiv.addEventListener("queue clear", (event) => { event.detail.queue.fadeafterchoice_queue = event.detail.old });
40 |
41 | // when we start to render the next pasasage, fade out everything in the old queue
42 | this.outerdiv.addEventListener("render start", (event) => {
43 | if (!event.detail.queue.fadeafterchoice_queue) return;
44 | event.detail.queue.fadeafterchoice_queue.forEach(p => {
45 | transition(p, "opacity", this.options.fadeafterchoice_fadelevel + "%", this.options.fadeafterchoice_fadespeed, this.options.fadeafterchoice_fadedelay);
46 | });
47 | })
48 | }
49 |
50 |
51 |
52 | }, options, credits)
53 |
54 | export default {options: options, credits: credits};
55 |
--------------------------------------------------------------------------------
/engine/patches/forcetagsbeforeline.js:
--------------------------------------------------------------------------------
1 | // -----------------------------------
2 | // force tags before line
3 | // -----------------------------------
4 | // if you have an ink story that was written for the vanilla web player,
5 | // then you've probably written your tags in a way that assumes
6 | // they'll all be processed before any text on that line.
7 | // if that's the case, and you don't want to edit your ink to run it in here,
8 | // then this will force all tags to be processed before the line
9 |
10 | var credits = {
11 | emoji: "🧳",
12 | name: "Always process tags before text",
13 | author: "Elliot Herriman",
14 | version: "1.0",
15 | description: "Forces all tags to execute before each line, to ensure compatibility with stories written for the vanilla web player.",
16 | licences: {
17 | self: "2021",
18 | }
19 | }
20 |
21 | var options = { };
22 |
23 | Patches.add(function()
24 | {
25 | this.outerdiv.addEventListener("passage line processed", (event) =>
26 | {
27 | event.detail.line.tags.before = event.detail.line.before.concat(event.detail.line.tags.after);
28 | event.detail.line.tags.after = [];
29 | });
30 |
31 | }, options, credits);
32 |
33 | export default {options: options, credits: credits};
--------------------------------------------------------------------------------
/engine/patches/history.js:
--------------------------------------------------------------------------------
1 | // -----------------------------------
2 | // keep track of the story's history
3 | // -----------------------------------
4 |
5 | var credits = {
6 | emoji: "📚",
7 | name: "History",
8 | author: "Elliot Herriman",
9 | version: "1.0",
10 | description: ["Helper patch that store a record of the player's choices as they play, allowing other patches to do things like save, rewind, or reload the game."],
11 | licences: {
12 | self: "2021",
13 | }
14 | }
15 |
16 | var options = {};
17 |
18 | // go back one passage
19 | function load(story, index, el, callback)
20 | {
21 | // make suer we have a history object
22 | story.history.choices = story.history.choices || [];
23 |
24 | var missedTags = [];
25 |
26 | // make sure we have choices
27 | // for some reason length was Always returning true
28 | // so we just check if the first element exists
29 | if (story.history.choices)
30 | {
31 | // cancel if we don't have a state to load
32 | if (index < 0 || index > story.history.choices.length) return false;
33 |
34 | // reset the story to the start
35 | story.ink.ResetState();
36 |
37 | // and restore the story's original seed, so any randomness
38 | // this time will be the same as randomness last time
39 | story.ink.state.storySeed = story.history.initialSeed;
40 |
41 | // quickly catch up to where we were
42 | var choice;
43 | for (var i = 0; i < index; i++)
44 | {
45 | while (story.ink.canContinue)
46 | {
47 | story.ink.Continue();
48 | story.ink.state.currentTags.forEach((t) => missedTags.push(t));
49 | }
50 |
51 | choice = story.ink.currentChoices[story.history.choices[i]];
52 |
53 | if (!choice) break;
54 |
55 | story.ink.ChooseChoiceIndex(choice.index);
56 | }
57 |
58 | notify("story loaded state", {story: story, state: story.ink.state, lastChoice: choice, tags: missedTags}, story.outerdiv);
59 |
60 | // if it doesn't exist, cancel and start the story
61 | if (!el)
62 | {
63 | story.state = Story.states.idle;
64 | story.continue();
65 | return;
66 | }
67 |
68 | // make sure the story can't continue until we're ready
69 | story.state = Story.states.locked;
70 |
71 | // set up a callback
72 | Element.addCallback(el, "onRemove", () =>
73 | {
74 | // reset our queue
75 | story.queue.reset();
76 | // mark that we can start a new loop
77 | story.state = Story.states.idle;
78 | // and continue
79 | story.continue();
80 | });
81 |
82 | callback();
83 | }
84 | }
85 |
86 | Patches.add(function()
87 | {
88 | // create our history object if it doesn't already exist
89 | this.history = this.history || {};
90 | // create a container for our history
91 | this.history.choices = [];
92 | // back up our story's initial seed
93 | this.history.initialSeed = this.ink.state.storySeed;
94 |
95 | this.outerdiv.addEventListener("story restarting", (event) =>
96 | {
97 | this.history.choices = [];
98 | });
99 |
100 | // at the end of each passage,
101 | this.outerdiv.addEventListener("passage end", (event) =>
102 | {
103 | // if we're in a rewound state,
104 | if (this.history.choices.length - 1 > this.ink.state.currentTurnIndex)
105 | {
106 | // remove all states from the end that we no longer need,
107 | this.history.choices.splice(this.ink.state.currentTurnIndex+1);
108 | }
109 | // then store it in our history
110 | this.history.choices.push(event.detail.choice.index);
111 | });
112 |
113 | }, options, credits);
114 |
115 | export default {options: options, credits: credits, load: load};
--------------------------------------------------------------------------------
/engine/patches/memorycard.js:
--------------------------------------------------------------------------------
1 | import history from "./history.js";
2 | import storage from"./storage.js";
3 |
4 | // -----------------------------------
5 | // save story state across refreshes
6 | // -----------------------------------
7 |
8 | var credits = {
9 | emoji: "💾",
10 | name: "Memory Card (8 MB)",
11 | author: "Elliot Herriman",
12 | version: "1.0",
13 | description: "Enables saving and loading the game.",
14 | licences: {
15 | self: "2021",
16 | }
17 | }
18 |
19 | var options = {
20 | memorycard_applymostrecenttag: [],
21 | memorycard_format: "session",
22 | }
23 |
24 | function save(story, id = "save", format = story.options.memorycard_format)
25 | {
26 | var save = Object.assign({}, story);
27 |
28 | save.history.turnIndex = story.ink.state.currentTurnIndex;
29 | save.ink = undefined;
30 | save.options = undefined;
31 | save.queue = undefined;
32 | save.innerdiv = undefined;
33 | save.outerdiv = undefined;
34 | save.watcher = undefined;
35 | save.externalFunctions = undefined;
36 | save.state = undefined;
37 |
38 | storage.set(id, JSON.stringify(save), format, story);
39 | }
40 |
41 | function load(story, id = "save", format = story.options.memorycard_format)
42 | {
43 | var save = storage.get(id, format, story);
44 | if (save)
45 | {
46 | save = JSON.parse(save);
47 |
48 | Object.assign(story, save);
49 |
50 | story.ink.state.storySeed = save.history.initialSeed;
51 |
52 | story.ink.state.currentTurnIndex = Math.min(save.history.turnIndex, story.history.choices.length - 1);
53 |
54 | story.outerdiv.addEventListener("story loaded state", (event) =>
55 | {
56 | applymostrecenttags(story, event.detail.tags);
57 | }, {once: true});
58 |
59 | history.load(story, story.ink.state.currentTurnIndex+1);
60 | }
61 | }
62 |
63 | function applymostrecenttags(story, input)
64 | {
65 | input = input.join(" #");
66 |
67 | for (var tag of story.options.memorycard_applymostrecenttag)
68 | {
69 | var i = input.lastIndexOf(tag);
70 | if (i === -1) return;
71 |
72 | tag = input.substr(i);
73 | tag = (tag.split("#")[0]);
74 |
75 | Tags.process(story, tag.trim());
76 | };
77 | }
78 |
79 | window.addEventListener("story patch", () => { if (storage.options.storage_format === "cookies") credits.name = "Memory Card (4 KB)"}, {once: true});
80 |
81 | Patches.add(function()
82 | {
83 |
84 | }, options, credits);
85 |
86 | export default {options: options, credits: credits, save: save, load: load};
--------------------------------------------------------------------------------
/engine/patches/minwordsperline.js:
--------------------------------------------------------------------------------
1 | // -----------------------------------
2 | // minimum words per line
3 | // -----------------------------------
4 | // ensures that if a line would a widow make (character from overwatch),
5 | // or an orphan make (a single word on the final line of a multi-line
6 | // paragrhaph), it no longer does that.
7 |
8 | // ensures that if a line would break (i.e. what happens when you hit enter in
9 | // a text editor), it will always have more than one word on the second line.
10 | //
11 | // so if a line break was inserted || there
12 | // this ensures that the two lines would become
13 | // so if a line break was
14 | // inserted there
15 | // just because it looks a little nicer, honestly. i mean, it doesn't matter,
16 | // not really, but these sort of little aesthetic considerations are what
17 | // set apart good developers from great ones
18 |
19 | var credits = {
20 | emoji: "📝",
21 | name: "Minimum words per line",
22 | author: "qt-dork and Elliot Herriman",
23 | version: "1.1",
24 | description: "Prevent lines from breaking in a way that only leaves one (or more) word(s) on the next line.",
25 | licences: {
26 | self: "2021",
27 | }
28 | }
29 |
30 | var options = {
31 | // the minimum number of words per line
32 | minwordsperline_length: 2,
33 | };
34 |
35 | function noOrphans(textItems, length) {
36 | // Find the second to last word
37 | // Stick a span right before the second to last word
38 | textItems[textItems.length - length] = `` + textItems[textItems.length - 2];
39 | // Stick a closing span right after the last word
40 | textItems[textItems.length - 1] = textItems[textItems.length - 1] + ``;
41 |
42 | return textItems;
43 | }
44 |
45 | function applyMinLength(story, line)
46 | {
47 | // Rough function layout
48 |
49 | let replacement = '';
50 |
51 | // Split words/tags into array
52 | // NOTE: This trims leading/trailing whitespace, so if you're
53 | // using that intentionally then whoops
54 | let textItems = line.text.trim().replace(/ /g, ' ').split(/ (?=[^>]*(?:<|$))/);
55 |
56 | // Check if the array is shorter than the length
57 | if (textItems.length < story.options.minwordsperline_length) {
58 | return;
59 | }
60 |
61 | // Maybe check if the array already has the span???????
62 |
63 | // Run orphan function
64 | textItems = noOrphans(textItems, story.options.minwordsperline_length);
65 |
66 | // Recombine the array
67 | replacement = textItems.join(' ');
68 |
69 | // Set the line equal to the new line
70 | line.text = replacement;
71 | }
72 |
73 | Patches.add(function()
74 | {
75 | // trigger this in response to us finding a text line,
76 | this.outerdiv.addEventListener("passage line", (event) =>
77 | {
78 | applyMinLength(event.detail.story, event.detail.line);
79 | });
80 |
81 | // or a choice line
82 | this.outerdiv.addEventListener("passage choice", (event) =>
83 | {
84 | applyMinLength(event.detail.story, event.detail.choice);
85 | });
86 |
87 | }, options, credits);
88 |
89 | export default {options: options, credits: credits};
--------------------------------------------------------------------------------
/engine/patches/parallaxframes.js:
--------------------------------------------------------------------------------
1 | // -----------------------------------
2 | // parallax frames
3 | // -----------------------------------
4 | // as used in winter (https://pizzapranks.itch.io/indiepocalypse-15)
5 |
6 | // creates a box with one or more images layered inside. the images will move
7 | // around as the mouse does, and if there's more than one layer, it will create
8 | // a parallax effect. also works with touchscreen-y devices!
9 | //
10 | // #frame: image:6, image2, image3.gif:4.5 | height:0.2
11 |
12 | var credits = {
13 | emoji: "💀",
14 | name: "Parallax frames",
15 | author: "Elliot Herriman",
16 | version: "1.0",
17 | description: "Binds a tag that creates a parallax effect from layered images.",
18 | licences: { self: "2021" }
19 | }
20 |
21 | var options = {}
22 |
23 | // tracks the last known location of the mouse
24 | var lastMousePosition;
25 | // a list of all our frames
26 | var framelist = [];
27 |
28 | // necessary for frames to play nicely with ios when hosted on itch.io
29 | // has something to do with the fact that itch hosts all games in a
30 | // virtual webpage within a webpage (called an iframe)
31 | if (window.isMobile) { document.addEventListener("touchmove", {}); }
32 |
33 | // start watching mouse movements so we can parallax frames
34 | window.addEventListener(window.isMobile ? "touchmove" : "mousemove", (event) =>
35 | {
36 | // store mouse position for later
37 | lastMousePosition = event;
38 |
39 | // if we have frames, move them
40 | if (framelist.length)
41 | {
42 | // get half page width and height
43 | let width = window.innerWidth / 2;
44 | let height = window.innerHeight / 2;
45 |
46 | // update all the frames
47 | framelist.forEach((frame) => { updateFrame(frame, width, height) });
48 | }
49 | });
50 |
51 | function updateFrame(frame, width = window.innerWidth / 2, height = window.innerHeight / 2)
52 | {
53 | // make sure the player's moved the mouse ever
54 | if (lastMousePosition)
55 | {
56 | // get our mouse position as a percentage
57 | var targetX = (width - lastMousePosition.pageX) / width;
58 | var targetY = (height - lastMousePosition.pageY) / height;
59 |
60 | for (var i = 0; i < frame.layers.length; i++)
61 | {
62 | // offset functions are expensive, and values only change when window resizes, so we keep track of whether we need to do this
63 | if (frame.dirty)
64 | {
65 | // get the maximum distance we can travel on each axis
66 | var x = Math.abs(frame.layers[i].offsetWidth - frame.div.offsetWidth) / 2;
67 | var y = Math.abs(frame.layers[i].offsetHeight - frame.div.offsetHeight) / 2;
68 |
69 | // somehow a necessary check? it ensures that the frame
70 | // won't be fed incorrect values (i.e. 9999-1) if this is
71 | // called before the frame is added to the story container
72 | if ((x != 0 || y != 0) && x > -9998 && y < 9998)
73 | {
74 | // subtract one so the edge of the image doesn't show up
75 | frame.stepX = x - 1;
76 | frame.stepY = y - 1;
77 | frame.dirty = false;
78 | }
79 | }
80 |
81 | // apply the offset
82 | // make targetx or targety negative to invert directions
83 | frame.layers[i].style.transform = "translateX(" + (targetX * frame.stepX * frame.layers[i].step) + "px) translateY(" + (targetY * frame.stepY * frame.layers[i].step) + "px)";
84 | };
85 | }
86 | }
87 |
88 | // tell each frame to update its layers if you resize the window
89 | window.addEventListener("window resized", () =>
90 | {
91 | framelist.forEach((frame) => { frame.dirty = true; })
92 | });
93 |
94 | // create tag handler for frame
95 | Tags.add("frame", function(story, property)
96 | {
97 | // create the frame, a list of its layers, a div to store it in,
98 | // and mark that we want to update it next loop
99 | var frame = {
100 | // list of all images in frame
101 | layers: [],
102 | // html element corresponding to this frame
103 | div: document.createElement('p'),
104 | // whether we should update values (expensive) next loop
105 | dirty: true,
106 | // how far we can move in each direction
107 | step: 0,
108 | };
109 |
110 | frame.div.classList.add("frame");
111 |
112 | property = getTagOptions(property);
113 |
114 | if (property.options.height)
115 | {
116 | frame.div.style.height = property.options.height;
117 | }
118 |
119 | // split the text into layer names
120 | for (let i = 0; i < property.value.length; i++)
121 | {
122 | // create the image element
123 | var layer = document.createElement('img');
124 |
125 | // set how much each layer moves so we have a parallax
126 | if (property.value[i][1] && !isNaN(property.value[i][1]))
127 | {
128 | layer.step = 1 / parseFloat(property.value[i][1]);
129 | }
130 | else
131 | {
132 | layer.step = 1 / (i + 1);
133 | }
134 |
135 | // stop the player from being able to drag the image, since css only
136 | // solutions don't seem to work on firefox?
137 | layer.addEventListener("mousedown", (event) => event.preventDefault());
138 |
139 | // tell the CSS to style this as a frame layer
140 | layer.classList.add("frameLayer");
141 |
142 | // tell the layer to use our provided image, making sure it has a file type
143 | layer.src = addFileType(property.value[i][0], story.options.defaultimageformat, story.options.defaultimagelocation);
144 |
145 | // make sure it's rendered above the last layer
146 | layer.style.zIndex = i+1;
147 |
148 | // files away layer to handle later
149 | frame.layers.push(layer);
150 |
151 | // and add it to the frame's div
152 | frame.div.appendChild(layer);
153 | }
154 |
155 | // add it to our manager
156 | framelist.push(frame);
157 |
158 | // add the div to the queue
159 | story.queue.push(frame.div);
160 | // and tell the queue to update the frame's starting position
161 | // once the div is added to the page
162 | story.queue.onAdded(() => { updateFrame(frame); });
163 | // todo test if this even works
164 | // when it's cleared, we remove it from the frame manager
165 | story.queue.onRemove(() => { removeFromArray(frame, framelist); });
166 | });
167 |
168 | Patches.add(null, options, credits);
169 |
170 | export default {options: options, credits: credits}
171 |
--------------------------------------------------------------------------------
/engine/patches/scrollafterchoice.js:
--------------------------------------------------------------------------------
1 | // -----------------------------------
2 | // scroll after choice
3 | // -----------------------------------
4 |
5 | var credits = {
6 | emoji: "⤵️",
7 | name: "Scroll after choice",
8 | author: "Elliot Herriman",
9 | version: "1.1",
10 | description: ["After choosing a choice, the story will automatically scroll to show the new content."],
11 | licences: {
12 | self: "2021",
13 | mit: {"Original scroll down code" : "2016 inkle Ltd."}
14 | }
15 | }
16 |
17 | // honestly the effect is sometimes choppy
18 | // particularly when printing messages to the console,
19 | // or even when the console's open?
20 | // i'm not totally sure what the exact cause is-- the
21 | // calculation each frame probably adds up, but also
22 | // maybe the core engine code is ineffecient?
23 | // (chances are, it's both)
24 | // anyway, the effect's mostly fine, just occasionally
25 | // gets a tiny bit choppy. i've found a multiplier of
26 | // two is usually just about perfect for minimising it
27 |
28 | var options = {
29 | // stop scrolling down (or up) if the user scrolls manually
30 | // it's probably better left on, but it will sometimes
31 | // cause issues on mac trackpads specifically, since there's
32 | // an OS wide setting to add inertia to your scrolling, but
33 | // that inertia presents itself as regular scrolling
34 | // so if you scroll down and hit the bottom and then take a choice,
35 | // the scroll down here might break because it thinks you're still
36 | // scrolling regularly?
37 | scrollafterchoice_breakonuserscroll: true,
38 | // enable scrolling up, for cases where there are a lot of choices,
39 | // and the start of the new content ends up being up instead of down
40 | scrollafterchoice_scrollup: true,
41 | // minimum duration of scroll animation
42 | scrollafterchoice_durationbase: 500,
43 | // how long the scroll will take, relative to the distance
44 | scrollafterchoice_durationmultiplier: 3,
45 | // longest possible scroll in ms
46 | scrollafterchoice_maxduration: 1250,
47 | // how much space you want above the scroll target
48 | // (with 0.2 being 20% of the div's height)
49 | scrollafterchoice_scrollTargetPadding: 0.2,
50 | }
51 |
52 | Story.prototype.scrollAfterChoice = function()
53 | {
54 | let lastText = this.queue.contents[0].previousSibling || null;
55 |
56 | var endOfText = (lastText ? lastText.offsetTop + lastText.offsetHeight : 0);
57 |
58 | var div = this.outerdiv;
59 | var start = div.scrollTop;
60 | var target = endOfText - window.innerHeight * this.options.scrollafterchoice_scrollTargetPadding;
61 |
62 | if (this.innerdiv.scrollHeight - window.innerHeight - target < 20)
63 | {
64 | target = this.innerdiv.scrollHeight - window.innerHeight;
65 | }
66 |
67 | if (!this.options.scrollafterchoice_scrollup && target < start) return;
68 |
69 | var duration = Math.min(this.options.scrollafterchoice_durationbase + this.options.scrollafterchoice_durationmultiplier * Math.abs(target - start), this.options.scrollafterchoice_maxduration);
70 |
71 | var pos = div.scrollTop;
72 | var startTime = null;
73 |
74 | var t;
75 | var lerp;
76 |
77 | notify("story scroll to new content", {story: this, previous: start, target: target});
78 |
79 | var game = this;
80 |
81 | function step(time)
82 | {
83 | // make sure start time is defined, if not, use the current time
84 | startTime = startTime || time;
85 |
86 | // check how far through the animation we are
87 | t = (time-startTime) / duration;
88 |
89 | // if the user's manually scrolled, break
90 | if (t > 1 || game.options.scrollafterchoice_breakonuserscroll && pos != div.scrollTop)
91 | {
92 | return;
93 | }
94 |
95 | // do some lerping with that value
96 | lerp = 3*t*t - 2*t*t*t;
97 |
98 | // calculate the new target and scroll to the result
99 | div.scrollTop = (1.0 - lerp) * start + lerp * target;
100 |
101 | // set the target for later
102 | pos = div.scrollTop;
103 |
104 | // keep going unless it's done
105 | game.scrollDownAnimation = requestAnimationFrame(step);
106 | }
107 |
108 | if (!game.scrollDownAnimation)
109 | {
110 | game.scrollDownAnimation = requestAnimationFrame(step);
111 | }
112 | }
113 |
114 | Patches.add(function()
115 | {
116 | this.outerdiv.addEventListener("render start", function(event)
117 | {
118 | cancelAnimationFrame(event.detail.story.scrollDownAnimation);
119 | event.detail.story.scrollDownAnimation = undefined;
120 | event.detail.queue.onShow(event.detail.story.scrollAfterChoice, 0);
121 | });
122 | }, options, credits);
123 |
124 | export default {options: options, credits: credits};
--------------------------------------------------------------------------------
/engine/patches/shortcuts/choices.js:
--------------------------------------------------------------------------------
1 | var credits = {
2 | emoji: "🔢",
3 | name: "Choice shortcuts",
4 | version: "1.0",
5 | description: ["A template for binding shortcuts to choices."],
6 | licences: {
7 | self: "2021 Elliot Herriman",
8 | }
9 | }
10 |
11 | var options = {
12 | choices_keys: [],
13 | choices_mustbeonscreen: true,
14 | choices_onlyifnomodifierkeys: true,
15 | }
16 |
17 | function chooseChoice(event, story, num, onlyIfNoModifierKeys = story.options.choices_onlyifnomodifierkeys, onlyIfOnlyChoice = false)
18 | {
19 | if (onlyIfNoModifierKeys &&
20 | (event.getModifierState("Control") || event.getModifierState("Alt") ||
21 | event.getModifierState("OS") || event.getModifierState("Meta") ||
22 | event.getModifierState("Win") || event.getModifierState("Fn")))
23 | {
24 | return;
25 | }
26 |
27 | var currentChoices = story.innerdiv.querySelectorAll(".choice > a");
28 |
29 | if (!currentChoices || currentChoices.length - 1 < num || onlyIfOnlyChoice && currentChoices.length > 1)
30 | {
31 | return;
32 | }
33 |
34 | // choose the choice by simulating click on link
35 | var el = currentChoices[num];
36 |
37 | // make sure the element is at least... half on screen?
38 | // technically this doesn't account for the element being off
39 | // and Above the screen, but... that's fine
40 | if (story.options.choices_mustbeonscreen || (el.offsetTop + el.offsetHeight / 2 < story.outerdiv.scrollTop + window.innerHeight))
41 | {
42 | el.click();
43 | }
44 | }
45 |
46 | // simple function that lets you bind a shortcut to choose choices
47 | // for a story. if a story isn't provided, and the patch hasn't been
48 | // applied yet, then the shortcuts you add here will be bound then
49 | function add(story, key, num, onlyIfNoModifierKeys, onlyIfOnlyChoice)
50 | {
51 | if (arguments.length < 5)
52 | {
53 | return options.choices_keys.push(arguments);
54 | }
55 | Shortcuts.add(key, (event) =>
56 | {
57 | chooseChoice(event, story, num, onlyIfNoModifierKeys, onlyIfOnlyChoice);
58 | });
59 | }
60 |
61 | // bind all the number keys to select the corresponding choices
62 | Patches.add(function()
63 | {
64 | this.options.choices_keys.forEach((shortcut) =>
65 | {
66 | add(this, shortcut[0], shortcut[1], shortcut[2], shortcut[3])
67 | })
68 |
69 | }, options, credits);
70 |
71 | export default {options: options, credits: credits, add: add};
--------------------------------------------------------------------------------
/engine/patches/shorthandclasstags.js:
--------------------------------------------------------------------------------
1 | // -----------------------------------
2 | // style tags
3 | // -----------------------------------
4 | // adding "x" to this list will let you use the tag "#x" in your ink, which
5 | // acts as shorthand for "#class: x". my only use for these so far has been
6 | // tagging lines of character dialogue to theme them with unique colours and
7 | // fonts, but you can use these for anything at all, really.
8 |
9 | var credits = {
10 | emoji: "🏷",
11 | name: "Shorthand class tags",
12 | author: "Elliot Herriman",
13 | version: "1.0",
14 | description: "Create shorthand tags to quickly add CSS classes to a line. For all tags specified, \"#tag\" will function identically to \"#class: tag\".",
15 | licences: {
16 | self: "2021",
17 | }
18 | }
19 |
20 | var options = {
21 | // "#tag" will function identically to "#class: tag"
22 | // it's probably better to use [imported object].options.tags.push("tag")
23 | // in your project file than it is to add tags here
24 | shorthandclasstags_tags: [],
25 | };
26 |
27 | Patches.add(function()
28 | {
29 | this.options.shorthandclasstags_tags.forEach(function(tag)
30 | {
31 | // don't do anything if the tag's empty
32 | if (!tag || typeof tag !== "string")
33 | {
34 | return;
35 | }
36 |
37 | // binds a function to the tag handler
38 | Parser.tag(tag, function(line, tag, property)
39 | {
40 | // add the tag to the class list
41 | line.classes.push(tag);
42 | });
43 | });
44 | }, options, credits);
45 |
46 | export default {options: options, credits: credits};
--------------------------------------------------------------------------------
/engine/patches/stepback.js:
--------------------------------------------------------------------------------
1 | import history from "./history.js"
2 |
3 | var credits = {
4 | emoji: "⏳",
5 | name: "Rewind story",
6 | author: "Elliot Herriman",
7 | version: "1.0",
8 | description: "Allow the player to rewind the story to a previous passage.",
9 | licences: {
10 | self: "2021",
11 | }
12 | }
13 |
14 | var options = {
15 | // let the player go forwards as well as backwards
16 | // (you can go forward once for every time you've
17 | // called backwards, basically sugarcube's behaviour)
18 | stepback_enabled: true,
19 | stepback_stepforwards: true,
20 | };
21 |
22 | Story.prototype.stepForwards = function()
23 | {
24 | if (this.state != Story.states.waiting ||
25 | !this.options.stepback_stepforwards) return;
26 |
27 | history.load(this, this.ink.state.currentTurnIndex + 2,
28 | this.innerdiv.querySelector(".choice"), this.clear);
29 | }
30 |
31 | Story.prototype.stepBack = function()
32 | {
33 | if (this.state != Story.states.waiting ||
34 | !this.options.stepback_enabled) return;
35 |
36 | history.load(this, this.ink.state.currentTurnIndex,
37 | this.innerdiv.firstElementChild, () =>
38 | {
39 | this.outerdiv.addEventListener("render start", (event) =>
40 | {
41 | event.detail.queue.contents[0].delay = 0;
42 | }, {once: true});
43 |
44 | this.clear();
45 | });
46 | }
47 |
48 | Patches.add(function()
49 | {
50 |
51 | }, options, credits);
52 |
53 | export default {options: options, credits: credits};
--------------------------------------------------------------------------------
/engine/patches/storage.js:
--------------------------------------------------------------------------------
1 | // -----------------------------------
2 | // save story state across refreshes
3 | // -----------------------------------
4 |
5 | var credits = {
6 | emoji: "📦",
7 | name: "Storage",
8 | author: "Elliot Herriman",
9 | version: "1.1",
10 | description: "Enables saving semi-persistent data to the browser's storage.",
11 | licences: {
12 | self: "2021",
13 | }
14 | }
15 |
16 | // ================================================
17 | // FORMATS
18 | // ================================================
19 | //
20 | // "cookies"
21 | // cookies have a size limit of ~4kb,
22 | // not recommended for anything large
23 | //
24 | // "session"
25 | // session data is bound to the window or tab, so it'll
26 | // exist across page refreshes, but closing the game and
27 | // opening it in another tab will clear the saved data
28 | // max of around 5mb, browser depending
29 | //
30 | // "local"
31 | // local storage just won't get cleared. ever, i guess?
32 | // unless the user manually does it. persists across tabs,
33 | // and also totals around 5mb, browser depending
34 |
35 | var options = {
36 | storage_defaultformat: "session",
37 | storage_ID: "",
38 | }
39 |
40 | function get(id, format = options.storage_defaultformat, story = this)
41 | {
42 | var data = undefined;
43 | id = story.options.storage_ID + id;
44 |
45 | switch (format)
46 | {
47 | case "cookies":
48 | if (id)
49 | {
50 | try
51 | {
52 | data = document.cookie.split('; ').find(row => row.startsWith(id + "=")).split('=')[1];
53 | } catch (e) { data = ""; }
54 | break;
55 | }
56 |
57 | data = document.cookie;
58 | break;
59 |
60 | case "session":
61 | data = sessionStorage.getItem(id);
62 | break;
63 |
64 | case "local":
65 | data = localStorage.getItem(id);
66 | break;
67 | }
68 |
69 | data = JSON.parse(data) || data;
70 | data = (isNaN(data) ? data : parseFloat(data));
71 | return data || (data == 0 ? data : false);
72 | }
73 |
74 | function set(id, data, format = options.storage_defaultformat, story = this)
75 | {
76 | data = JSON.stringify(data) || data;
77 | id = story.options.storage_ID + id;
78 |
79 | switch (format)
80 | {
81 | case "cookies":
82 | if (id)
83 | {
84 | document.cookie = id+"="+data;
85 | }
86 | else
87 | {
88 | this.clear("cookies");
89 | document.cookie = data;
90 | }
91 | break;
92 |
93 | case "session":
94 | sessionStorage.setItem(id, data);
95 | break
96 |
97 | case "local":
98 | localStorage.setItem(id, data);
99 | break;
100 | }
101 | }
102 |
103 | function remove(id, format = options.storage_defaultformat, story = this)
104 | {
105 | id = story.options.storage_ID + id;
106 |
107 | switch (format)
108 | {
109 | case "cookies":
110 | document.cookie = id+"=;expires="+new Date().toUTCString();
111 | break;
112 |
113 | case "session":
114 | sessionStorage.removeItem(id);
115 | break
116 |
117 | case "local":
118 | localStorage.removeItem(id);
119 | break;
120 | }
121 | }
122 |
123 | function clear(format = options.storage_defaultformat)
124 | {
125 | switch (format)
126 | {
127 | case "cookie":
128 | document.cookie.split("; ").forEach(function(cookie)
129 | {
130 | document.cookie = cookie+";expires="+new Date().toUTCString()
131 | });
132 | break;
133 |
134 | case "session":
135 | sessionStorage.clear();
136 | break
137 |
138 | case "local":
139 | localStorage.clear();
140 | break;
141 | }
142 | }
143 |
144 | ExternalFunctions.add("get", get);
145 | ExternalFunctions.add("set", set);
146 |
147 | Patches.add(function()
148 | {
149 | // if you haven't set an ID, just use the URL
150 | if (!this.options.storage_ID)
151 | this.options.storage_ID = window.location.pathname;
152 |
153 | }, options, credits);
154 |
155 | export default {options: options, credits: credits, get: get, set: set, remove: remove, clear: clear};
--------------------------------------------------------------------------------
/engine/patches/storylets.ink:
--------------------------------------------------------------------------------
1 | /**
2 |
3 | ========================================
4 | STORYLET FUNCTIONS
5 | ========================================
6 | An extension library for Ink.
7 |
8 | If you have any issues, feel free to hit me up on Twitter.
9 |
10 | https://twitter.com/elliotherriman
11 |
12 | WHAT IS A STORYLET?
13 |
14 | To quote Em Short, "Storylet systems are a way of organizing narrative content with more flexibility than the typical branching narrative."
15 |
16 | You can read more here: https://emshort.blog/2019/11/29/storylets-you-want-them/
17 |
18 | CREATING STORYLETS
19 |
20 | Storylets are knots that are tagged with the tag "#storylet". Storylets must contain a stitch named "text", and may also contain additional stitches.
21 |
22 | If you're not familiar with those things, that's *very* fair. I'd suggest reading this introduction to Ink.
23 |
24 | https://www.inklestudios.com/ink/web-tutorial/
25 |
26 | STORYLET CATEGORIES
27 |
28 | Instead of using "#storylet" to create a storylet, you can create a categorised storylet using a tag in the form of--
29 |
30 | #storylet: yourCategory
31 |
32 | ACCESSING STORYLETS
33 |
34 | Storylets can be accessed using tunnels, like so.
35 |
36 | -> openStorylets ->
37 |
38 | Alternatively, you can use the following to only search for storylets within a certain category.
39 |
40 | -> filteredStorylets(category) ->
41 |
42 | CONTINUING AFTERWARDS
43 |
44 | If you end a storylet's "text" stitch with "->->", then once chosen, it will show all the content in the "text" stitch, and then continue to the line after the tunnel that called it.
45 |
46 | Alternatively, you can end the "text" stitch with "-> DONE" if you want the story to end there.
47 |
48 | STORYLET STRUCTURE
49 |
50 | === storyletName
51 |
52 | #storylet
53 |
54 | or
55 |
56 | #storylet: category
57 |
58 | = open
59 | Determines whether the storylet is available.
60 |
61 | Must end with an expression wrapped in curly brackets that evaluates to true or false. Like so--
62 |
63 | {x && y}
64 |
65 | or
66 |
67 | {x + y > z}
68 |
69 | or
70 |
71 | {x()}
72 |
73 | = urgency
74 | Determines the priority of this storylet. When you tell the engine to visit a storylet, it will randomly select from amongst all storylets with the highest priority found.
75 |
76 | Must end with a number wrapped in curly brackets. Like so--
77 |
78 | {x}
79 |
80 | or
81 |
82 | {x - 3}
83 |
84 | or
85 |
86 | {x()}
87 |
88 | = exclusivity
89 | Functions similarly to urgency, but it has a higher priority. A knot with exclusivity 3 and urgency 1 will be chosen over a knot with exclusivity 0 and urgency 9.
90 |
91 | Like urgency, must end with a number wrapped in curly brackets.
92 |
93 | = text
94 | The content of the stitch. If this stitch doesn't exist, then the storylet won't be imported.
95 |
96 | The last line of this stitch should either be "->->" or "-> DONE". Choosing neither may cause the Ink compiler to complain.
97 |
98 | OPTIONAL STITCHES
99 |
100 | "text" is the only mandatory stitch. All others are optional.
101 |
102 | If a storylet doesn't have a stitch called "urgency" or "exclusivity", then it will have an urgency or exclusivity of 0. If a storylet doesn't have a stitch called "open", it will always be considered available.
103 |
104 | Excluding "text", none of a storylet's stitches should contain diverts or choices or text. They also shouldn't contain more than one instance of a value inside those curly brackets.
105 |
106 | USEFUL METHODS FOR CHECKING OPENNESS
107 |
108 | These are things that you can include in a storylet's "open" stitch that may be useful.
109 |
110 | To check if you've visited a storylet's content before.
111 |
112 | {storylet.text}
113 |
114 | To check if you've visited a storylet more than x times.
115 |
116 | {storylet.text > x}
117 |
118 | To check if you've visited a storylet within the past x turns.
119 |
120 | {TURNS_SINCE(-> storylet.text) <= x}
121 |
122 | JUMPING TO A SPECIFIC STORYLET
123 |
124 | You can jump directly to a storylet's content by diverting or tunneling to its address. For example, you can jump to a storylet called "ocean" with the line...
125 |
126 | -> ocean.text ->
127 |
128 | Unfortunately, I don't think you can evaluate another storylet's openness or urgency or exlusivity from inside Ink. But if you figure that out, please let me know!
129 | */
130 |
131 | == openStorylets
132 | /**
133 | Call with...
134 |
135 | -> openStorylets ->
136 | */
137 | ~ temp divert = _openStorylets()
138 | -> divert ->
139 | ->->
140 |
141 | == filteredStorylets(category)
142 | /**
143 | Call with...
144 |
145 | -> filteredStorylets(category) ->
146 | */
147 | ~ temp divert = _filteredStorylets(category)
148 | -> divert ->
149 | ->->
150 |
151 | EXTERNAL _openStorylets()
152 | EXTERNAL _filteredStorylets(category)
153 |
154 | /** FALLBACK FUNCTIONS
155 |
156 | Since Inky doesn't support external functions, your storylets won't show up in Inky's preview player. That's just an inherent limitation of Inky as an app, and it's one that I can't easily hack away — despite trying.
157 |
158 | So we have to use fallback functions. Rather than fetching and displaying a storylet, Inky will just... not do that. It'll continue as if you never called a storylet.
159 |
160 | The obvious issue here is playtesting. You can't do that organically. So, if you want to test out your storylets, you'll need to either...
161 |
162 | Run the HTML file in your browser.
163 |
164 | Use Catmint to run the HTML file.
165 |
166 | https://elliotherriman.itch.io/catmint
167 |
168 | Replace the contents of these functions with something that roughly approximates the storylet selection process.
169 |
170 | A very simple version that doesn't support availability, urgency, or exclusivity could look like...
171 |
172 | {shuffle:
173 | - -> storylet1 ->
174 | - -> storylet2 ->
175 | - -> storylet3 ->
176 | }
177 |
178 | A more comprehensive version would require you to implement all that logic inside the function. And that wouldn't even account for urgency and exclusivity. Which sucks, and it's why I built this tool in the first place.
179 | */
180 |
181 | == function _openStorylets()
182 | ~ return -> _storyletsEmptyDivert
183 |
184 | == function _filteredStorylets(category)
185 | ~ return -> _storyletsEmptyDivert
186 |
187 | == _storyletsEmptyDivert
188 | ->->
189 |
190 | /* ========================================== */
--------------------------------------------------------------------------------
/engine/patches/storylets.js:
--------------------------------------------------------------------------------
1 | // -----------------------------------
2 | // patch template
3 | // -----------------------------------
4 |
5 | var credits = {
6 | emoji: "🧶",
7 | name: "Storylets",
8 | author: "Elliot Herriman",
9 | version: "1.1",
10 | description: "Enables storylets, as seen in Twine's Harlowe.",
11 | licences: {
12 | self: "2021",
13 | }
14 | }
15 |
16 | var options = {
17 | storylets_knotTag: "storylet",
18 |
19 | storylets_openStitch: "open",
20 | storylets_urgencyStitch: "urgency",
21 | storylets_exclusivityStitch: "exclusivity",
22 | storylets_contentStitch: "text",
23 |
24 | storylets_function_open: "_openStorylets",
25 | storylets_function_filtered: "_filteredStorylets",
26 | };
27 |
28 | function InitialiseStorylets(tag)
29 | {
30 | tag = tag.toLowerCase().trim();
31 |
32 | this.ink.storylets = {};
33 |
34 | this.ink.mainContentContainer.namedContent.forEach((container) =>
35 | {
36 | let tags = this.ink.TagsForContentAtPath(container.name);
37 |
38 | if (!tags) return;
39 |
40 | for (var i = 0; i < tags.length; i++)
41 | {
42 | tags[i] = tags[i].split(":", 2);
43 |
44 | if (tag == tags[i][0].toLowerCase().trim())
45 | {
46 | let category = (tags[i][1] || "global").toLowerCase().trim();
47 |
48 | if (!container.namedContent.get(this.options.storylets_contentStitch))
49 | {
50 | console.error("Couldn't find a stitch named \"" + this.options.storylets_contentStitch + "\" in storylet \"" + container.name + "\".");
51 | continue;
52 | }
53 |
54 | this.ink.storylets[category] = this.ink.storylets[category] || [];
55 | this.ink.storylets[category].push(container);
56 |
57 | break;
58 | }
59 | }
60 | });
61 |
62 | console.log("Loaded storylets!", this.ink.storylets);
63 | }
64 |
65 | function OpenStorylets(category = null)
66 | {
67 | if (this.ink.storylets)
68 | {
69 | let search;
70 | if (category)
71 | {
72 | search = this.ink.storylets[category];
73 | }
74 | else if (Object.keys(this.ink.storylets).length)
75 | {
76 | search = [];
77 | Object.keys(this.ink.storylets).forEach(group => search = search.concat(this.ink.storylets[group]));
78 | }
79 |
80 | if (search && search.length)
81 | {
82 | let storylets = [];
83 | let currentUrgency = 0;
84 | let currentExclusivity = 0;
85 |
86 | search.forEach((storylet) =>
87 | {
88 | if (!storylet.namedContent.get(this.options.storylets_openStitch) || this.ink.EvaluateContainer(storylet.namedContent.get(this.options.storylets_openStitch)))
89 | {
90 |
91 | let exclusivity = this.ink.EvaluateContainer(storylet.namedContent.get(this.options.storylets_exclusivityStitch)) || 0;
92 | let urgency = this.ink.EvaluateContainer(storylet.namedContent.get(this.options.storylets_urgencyStitch)) || 0;
93 |
94 | if (exclusivity < currentExclusivity)
95 | {
96 | return;
97 | }
98 | else if (exclusivity > currentExclusivity)
99 | {
100 | storylets = [];
101 | currentExclusivity = exclusivity;
102 | currentUrgency = urgency;
103 | }
104 | else if (urgency < currentUrgency)
105 | {
106 | return;
107 | }
108 | else if (urgency > currentUrgency)
109 | {
110 | storylets = [];
111 | currentUrgency = urgency;
112 | }
113 |
114 | storylets.push(storylet);
115 | }
116 | });
117 |
118 | if (storylets.length)
119 | {
120 | for (var i = storylets.length - 1, j, temp; i > 0; i--)
121 | {
122 | j = Math.floor(Math.random()*(i+1));
123 | temp = storylets[j];
124 | storylets[j] = storylets[i];
125 | storylets[i] = temp;
126 | }
127 |
128 | let stitch = storylets[0].namedContent.get(this.options.storylets_contentStitch);
129 | if (stitch) return stitch.path;
130 | }
131 | }
132 | }
133 |
134 | this.state.callStack.PopThread();
135 | }
136 |
137 | Patches.add(function()
138 | {
139 | InitialiseStorylets.bind(this)(this.options.storylets_knotTag);
140 |
141 | ExternalFunctions.add(this.options.storylets_function_open, OpenStorylets.bind(this));
142 | ExternalFunctions.add(this.options.storylets_function_filtered, (category) => OpenStorylets.bind(this)(category));
143 |
144 | }, options, credits);
145 |
146 | export default {options: options, credits: credits};
--------------------------------------------------------------------------------
/engine/patches/template.js:
--------------------------------------------------------------------------------
1 | // -----------------------------------
2 | // patch template
3 | // -----------------------------------
4 |
5 | var credits = {
6 | emoji: "",
7 | name: "name",
8 | author: "author",
9 | version: "1.0",
10 | description: "description",
11 | licences: {
12 | self: "2021",
13 | }
14 | }
15 |
16 | var options = {
17 | patchname_variable: true,
18 | };
19 |
20 | Patches.add(function(content)
21 | {
22 |
23 | }, options, credits);
24 |
25 | export default {options: options, credits: credits};
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Calico",
3 | "version": "2.0.1",
4 | "description": "An interactive fiction engine for the web",
5 | "scripts": {
6 | "build": "./build.sh"
7 | },
8 | "author": "Elliot Herriman",
9 | "license": "MIT",
10 | "devDependencies": {
11 | "@babel/cli": "^7.14.5",
12 | "@babel/core": "^7.14.6"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/templates/Calico [Template]/README.txt:
--------------------------------------------------------------------------------
1 | This is a template to help you build games using Calico.
2 |
3 | A basic guide to help you get started can be found here: https://github.com/elliotherriman/calico/blob/master/documentation/getting%20started.md
--------------------------------------------------------------------------------
/templates/Calico [Template]/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |