├── .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 | 4 | 5 | 6 | Calico 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 |
21 | 22 | -------------------------------------------------------------------------------- /templates/Calico [Template]/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}; -------------------------------------------------------------------------------- /templates/Calico [Template]/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}; -------------------------------------------------------------------------------- /templates/Calico [Template]/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 | -------------------------------------------------------------------------------- /templates/Calico [Template]/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}; -------------------------------------------------------------------------------- /templates/Calico [Template]/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}; -------------------------------------------------------------------------------- /templates/Calico [Template]/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}; -------------------------------------------------------------------------------- /templates/Calico [Template]/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}; -------------------------------------------------------------------------------- /templates/Calico [Template]/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 | -------------------------------------------------------------------------------- /templates/Calico [Template]/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}; -------------------------------------------------------------------------------- /templates/Calico [Template]/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}; -------------------------------------------------------------------------------- /templates/Calico [Template]/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}; -------------------------------------------------------------------------------- /templates/Calico [Template]/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}; -------------------------------------------------------------------------------- /templates/Calico [Template]/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}; -------------------------------------------------------------------------------- /templates/Calico [Template]/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 | /* ========================================== */ -------------------------------------------------------------------------------- /templates/Calico [Template]/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}; -------------------------------------------------------------------------------- /templates/Calico [Template]/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}; -------------------------------------------------------------------------------- /templates/Calico [Template]/project.js: -------------------------------------------------------------------------------- 1 | // ----------------------------------- 2 | 3 | // always attempt to break to a new line in a way that 4 | // preserves a minimum number of words per line 5 | import "./patches/minwordsperline.js"; 6 | 7 | // click and drag to scroll the page 8 | import "./patches/dragtoscroll.js"; 9 | 10 | // convert markdown to HTML tags 11 | import "./patches/markdowntohtml.js" 12 | 13 | // ----------------------------------- 14 | 15 | // import helper patch for binding shortcuts to choices 16 | import choices from "./patches/shortcuts/choices.js"; 17 | 18 | // bind the number keys to choices 19 | for (var i = 0; i < 9; i++) 20 | { 21 | choices.add((i+1).toString(), i, true); 22 | } 23 | 24 | // bind z, x, and c to the first three shortcuts respectively 25 | ["z", "x", "c"].forEach((key, index) => { choices.add(key, index, true) }) 26 | 27 | // bind spacebar to progress the story, 28 | // provided there's only one choice available 29 | choices.add(" ", 0, true, true); 30 | 31 | // ----------------------------------- 32 | 33 | // create our game 34 | var story = new Story("story.ink"); -------------------------------------------------------------------------------- /templates/Calico [Template]/story.ink: -------------------------------------------------------------------------------- 1 | Once upon a time... 2 | 3 | * There were two choices. 4 | * There were four lines of content. 5 | 6 | - They lived happily ever after. 7 | -> END -------------------------------------------------------------------------------- /templates/Calico [Template]/style.css: -------------------------------------------------------------------------------- 1 | body 2 | { 3 | font-family: 'Open Sans', sans-serif; 4 | font-weight: 300; 5 | background: black; 6 | color: white; 7 | -webkit-touch-callout: none; 8 | -webkit-user-select: none; 9 | -khtml-user-select: none; 10 | -moz-user-select: none; 11 | -ms-user-select: none; 12 | user-select: none; 13 | -webkit-user-drag: none; 14 | -khtml-user-drag: none; 15 | -moz-user-drag: none; 16 | -o-user-drag: none; 17 | } 18 | 19 | /* Seems necessary to make iframes work on itch.io on mobile iOS :-( */ 20 | #container 21 | { 22 | position: absolute; 23 | display: block; 24 | margin: 0; 25 | padding: 0; 26 | -webkit-overflow-scrolling: touch; 27 | overflow: scroll; 28 | overflow-x: hidden; 29 | height: 100%; 30 | width: 100%; 31 | top: 0; 32 | left: 0; 33 | background-size: cover; 34 | background-repeat: no-repeat; 35 | z-index: 10; 36 | } 37 | 38 | #story 39 | { 40 | position: relative; 41 | margin: 0 auto; 42 | overflow: hidden; 43 | height: 100%; 44 | width: 700px; 45 | min-width: 60vw; 46 | max-width: 80vw !important; 47 | transition: opacity 2s ease; 48 | padding: 0em 1em 0em 1em; 49 | } 50 | 51 | @media (hover:none), (hover:on-demand) 52 | { 53 | #story 54 | { 55 | max-width: 90%; 56 | } 57 | } 58 | 59 | #story > *:first-child { 60 | margin-top: 10vh !important; 61 | } 62 | 63 | #story > *:last-child 64 | { 65 | margin-bottom: 15vh !important; 66 | } 67 | 68 | p 69 | { 70 | margin-block-start: 0px !important; 71 | font-size: 16pt; 72 | line-height: 1.4; 73 | } 74 | 75 | p, p > * 76 | { 77 | -webkit-user-drag: none; 78 | -khtml-user-drag: none; 79 | -moz-user-drag: none; 80 | -o-user-drag: none; 81 | } 82 | 83 | a 84 | { 85 | text-decoration: none !important; 86 | transition: all 0.5s linear; 87 | } 88 | 89 | a:hover 90 | { 91 | color: white; 92 | } 93 | 94 | 95 | .choice 96 | { 97 | opacity: 0.5; 98 | transition: all 1s; 99 | color: white; 100 | } 101 | 102 | .choice:hover 103 | { 104 | color: white; 105 | opacity: 1; 106 | transition: all 1s; 107 | } 108 | 109 | .choice > a 110 | { 111 | color: inherit; 112 | } 113 | 114 | .choice > a:active 115 | { 116 | color:red; 117 | } 118 | 119 | .chosen 120 | { 121 | color: white !important; 122 | opacity: 1 !important; 123 | } 124 | 125 | /* ================================================================= */ 126 | /* PATCH SPECIFIC CSS */ 127 | /* ================================================================= */ 128 | 129 | /* --------------- */ 130 | /* parallax frames */ 131 | /* --------------- */ 132 | .frame 133 | { 134 | width: 100%; 135 | height: calc(100vw * 0.2); 136 | overflow: hidden; 137 | position: relative; 138 | } 139 | 140 | .frameLayer 141 | { 142 | position: absolute; 143 | width: 120%; 144 | display: block; 145 | top: -9999px; 146 | bottom: -9999px; 147 | left: -9999px; 148 | right: -9999px; 149 | margin: auto; 150 | max-width: auto; 151 | } 152 | 153 | /* css for mobile */ 154 | @media (hover:none), (hover:on-demand) 155 | { 156 | .frame 157 | { 158 | min-height: calc(100vmax * 0.25); 159 | } 160 | .frameLayer 161 | { 162 | width: 150%; 163 | } 164 | } 165 | 166 | /* ------- */ 167 | /* preload */ 168 | /* ------- */ 169 | 170 | .progressbar 171 | { 172 | position: absolute; 173 | overflow: hidden; 174 | top: 0; 175 | background: #7690ac; 176 | width: 0%; 177 | height: 1vh; 178 | } -------------------------------------------------------------------------------- /templates/README.md: -------------------------------------------------------------------------------- 1 | # Calico templates 2 | 3 | These are sample projects you can use as a template for your game. 4 | 5 | Due to browser security issues, you may need to use [Catmint](https://elliotherriman.itch.io/catmint), or run a local server if you know how, in order to test your game. These issues won't be present once your game is uploaded to a website like itch.io. 6 | 7 | The scripts inside each template — specifically `calico.js`, `ink.js`, and the entire patches folder — are updated each time a new version of Calico is released. If you're running into issues, it may help to copy the latest versions of each from the root of this repository into your folder. -------------------------------------------------------------------------------- /templates/Winter [Sample project]/README.txt: -------------------------------------------------------------------------------- 1 | This is a sample game built using Calico. 2 | 3 | Enclosed is a (slightly updated) copy of the code used for Winter (https://communistsister.itch.io/winter). All art and audio by Freya Campbell (communistsister.itch.io). 4 | 5 | A basic guide to help you get started can be found here: https://github.com/elliotherriman/calico/blob/master/documentation/getting%20started.md -------------------------------------------------------------------------------- /templates/Winter [Sample project]/fonts/OpenSans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/fonts/OpenSans-Bold.ttf -------------------------------------------------------------------------------- /templates/Winter [Sample project]/fonts/OpenSans-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/fonts/OpenSans-BoldItalic.ttf -------------------------------------------------------------------------------- /templates/Winter [Sample project]/fonts/OpenSans-ExtraBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/fonts/OpenSans-ExtraBold.ttf -------------------------------------------------------------------------------- /templates/Winter [Sample project]/fonts/OpenSans-ExtraBoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/fonts/OpenSans-ExtraBoldItalic.ttf -------------------------------------------------------------------------------- /templates/Winter [Sample project]/fonts/OpenSans-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/fonts/OpenSans-Italic.ttf -------------------------------------------------------------------------------- /templates/Winter [Sample project]/fonts/OpenSans-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/fonts/OpenSans-Light.ttf -------------------------------------------------------------------------------- /templates/Winter [Sample project]/fonts/OpenSans-LightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/fonts/OpenSans-LightItalic.ttf -------------------------------------------------------------------------------- /templates/Winter [Sample project]/fonts/OpenSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/fonts/OpenSans-Regular.ttf -------------------------------------------------------------------------------- /templates/Winter [Sample project]/fonts/OpenSans-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/fonts/OpenSans-SemiBold.ttf -------------------------------------------------------------------------------- /templates/Winter [Sample project]/fonts/OpenSans-SemiBoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/fonts/OpenSans-SemiBoldItalic.ttf -------------------------------------------------------------------------------- /templates/Winter [Sample project]/images/act4_winter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/images/act4_winter.png -------------------------------------------------------------------------------- /templates/Winter [Sample project]/images/act4_winter2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/images/act4_winter2.png -------------------------------------------------------------------------------- /templates/Winter [Sample project]/images/bedroom2_bg1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/images/bedroom2_bg1.png -------------------------------------------------------------------------------- /templates/Winter [Sample project]/images/bedroom2_fg1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/images/bedroom2_fg1.png -------------------------------------------------------------------------------- /templates/Winter [Sample project]/images/bedroom2_fg2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/images/bedroom2_fg2.png -------------------------------------------------------------------------------- /templates/Winter [Sample project]/images/bedroom2_fg3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/images/bedroom2_fg3.png -------------------------------------------------------------------------------- /templates/Winter [Sample project]/images/bedroom_bg1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/images/bedroom_bg1.png -------------------------------------------------------------------------------- /templates/Winter [Sample project]/images/bedroom_bg2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/images/bedroom_bg2.png -------------------------------------------------------------------------------- /templates/Winter [Sample project]/images/bedroom_fg1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/images/bedroom_fg1.png -------------------------------------------------------------------------------- /templates/Winter [Sample project]/images/bedroom_fg2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/images/bedroom_fg2.png -------------------------------------------------------------------------------- /templates/Winter [Sample project]/images/bedroom_fg3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/images/bedroom_fg3.png -------------------------------------------------------------------------------- /templates/Winter [Sample project]/images/bedroom_fg4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/images/bedroom_fg4.png -------------------------------------------------------------------------------- /templates/Winter [Sample project]/images/bedroom_fg5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/images/bedroom_fg5.png -------------------------------------------------------------------------------- /templates/Winter [Sample project]/images/corridor_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/images/corridor_bg.png -------------------------------------------------------------------------------- /templates/Winter [Sample project]/images/corridor_fg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/images/corridor_fg.png -------------------------------------------------------------------------------- /templates/Winter [Sample project]/images/corridor_fg2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/images/corridor_fg2.png -------------------------------------------------------------------------------- /templates/Winter [Sample project]/images/corridor_fg3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/images/corridor_fg3.png -------------------------------------------------------------------------------- /templates/Winter [Sample project]/images/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/images/cover.png -------------------------------------------------------------------------------- /templates/Winter [Sample project]/images/cover_bg1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/images/cover_bg1.png -------------------------------------------------------------------------------- /templates/Winter [Sample project]/images/cover_bg2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/images/cover_bg2.png -------------------------------------------------------------------------------- /templates/Winter [Sample project]/images/cover_fg1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/images/cover_fg1.png -------------------------------------------------------------------------------- /templates/Winter [Sample project]/images/cover_fg2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/images/cover_fg2.png -------------------------------------------------------------------------------- /templates/Winter [Sample project]/images/hand4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/images/hand4.png -------------------------------------------------------------------------------- /templates/Winter [Sample project]/images/hands.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/images/hands.png -------------------------------------------------------------------------------- /templates/Winter [Sample project]/images/hands2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/images/hands2.png -------------------------------------------------------------------------------- /templates/Winter [Sample project]/images/kiss_red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/images/kiss_red.png -------------------------------------------------------------------------------- /templates/Winter [Sample project]/images/kiss_shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/images/kiss_shadow.png -------------------------------------------------------------------------------- /templates/Winter [Sample project]/images/kiss_winter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/images/kiss_winter.png -------------------------------------------------------------------------------- /templates/Winter [Sample project]/images/park_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/images/park_bg.png -------------------------------------------------------------------------------- /templates/Winter [Sample project]/images/park_bg2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/images/park_bg2.png -------------------------------------------------------------------------------- /templates/Winter [Sample project]/images/park_bg3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/images/park_bg3.png -------------------------------------------------------------------------------- /templates/Winter [Sample project]/images/park_bg4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/images/park_bg4.png -------------------------------------------------------------------------------- /templates/Winter [Sample project]/images/park_fg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/images/park_fg.png -------------------------------------------------------------------------------- /templates/Winter [Sample project]/images/park_fg2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/images/park_fg2.png -------------------------------------------------------------------------------- /templates/Winter [Sample project]/images/park_fg2b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/images/park_fg2b.png -------------------------------------------------------------------------------- /templates/Winter [Sample project]/images/park_fg3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/images/park_fg3.png -------------------------------------------------------------------------------- /templates/Winter [Sample project]/images/party1_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/images/party1_bg.png -------------------------------------------------------------------------------- /templates/Winter [Sample project]/images/party1_fg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/images/party1_fg.png -------------------------------------------------------------------------------- /templates/Winter [Sample project]/images/party2_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/images/party2_bg.png -------------------------------------------------------------------------------- /templates/Winter [Sample project]/images/party2_fg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/images/party2_fg.png -------------------------------------------------------------------------------- /templates/Winter [Sample project]/images/party2_fg2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/images/party2_fg2.png -------------------------------------------------------------------------------- /templates/Winter [Sample project]/images/party3_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/images/party3_bg.png -------------------------------------------------------------------------------- /templates/Winter [Sample project]/images/party3_fg1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/images/party3_fg1.png -------------------------------------------------------------------------------- /templates/Winter [Sample project]/images/party3_fg2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/images/party3_fg2.png -------------------------------------------------------------------------------- /templates/Winter [Sample project]/images/party3_fg3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/images/party3_fg3.png -------------------------------------------------------------------------------- /templates/Winter [Sample project]/images/party4_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/images/party4_bg.png -------------------------------------------------------------------------------- /templates/Winter [Sample project]/images/party4_fg2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/images/party4_fg2.png -------------------------------------------------------------------------------- /templates/Winter [Sample project]/images/party4_fg3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/images/party4_fg3.png -------------------------------------------------------------------------------- /templates/Winter [Sample project]/images/party4_fg3_shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/images/party4_fg3_shadow.png -------------------------------------------------------------------------------- /templates/Winter [Sample project]/images/phone_fg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/images/phone_fg.png -------------------------------------------------------------------------------- /templates/Winter [Sample project]/images/winter16.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/images/winter16.ico -------------------------------------------------------------------------------- /templates/Winter [Sample project]/images/winter256.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/images/winter256.ico -------------------------------------------------------------------------------- /templates/Winter [Sample project]/images/winter32.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/images/winter32.ico -------------------------------------------------------------------------------- /templates/Winter [Sample project]/images/winter48.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/images/winter48.ico -------------------------------------------------------------------------------- /templates/Winter [Sample project]/images/winterlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/images/winterlay.png -------------------------------------------------------------------------------- /templates/Winter [Sample project]/images/zine2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/images/zine2.png -------------------------------------------------------------------------------- /templates/Winter [Sample project]/index.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | Calico 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 |
21 | 22 | -------------------------------------------------------------------------------- /templates/Winter [Sample project]/music/act1park.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/music/act1park.mp3 -------------------------------------------------------------------------------- /templates/Winter [Sample project]/music/act1party.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/music/act1party.mp3 -------------------------------------------------------------------------------- /templates/Winter [Sample project]/music/act2.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/music/act2.mp3 -------------------------------------------------------------------------------- /templates/Winter [Sample project]/music/act3.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/music/act3.mp3 -------------------------------------------------------------------------------- /templates/Winter [Sample project]/music/act4.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotherriman/calico/bbdb0dd4ec7d3902a61858ba549a94af17c9ff0d/templates/Winter [Sample project]/music/act4.mp3 -------------------------------------------------------------------------------- /templates/Winter [Sample project]/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}; -------------------------------------------------------------------------------- /templates/Winter [Sample project]/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}; -------------------------------------------------------------------------------- /templates/Winter [Sample project]/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 | -------------------------------------------------------------------------------- /templates/Winter [Sample project]/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}; -------------------------------------------------------------------------------- /templates/Winter [Sample project]/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}; -------------------------------------------------------------------------------- /templates/Winter [Sample project]/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}; -------------------------------------------------------------------------------- /templates/Winter [Sample project]/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}; -------------------------------------------------------------------------------- /templates/Winter [Sample project]/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 | -------------------------------------------------------------------------------- /templates/Winter [Sample project]/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}; -------------------------------------------------------------------------------- /templates/Winter [Sample project]/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}; -------------------------------------------------------------------------------- /templates/Winter [Sample project]/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}; -------------------------------------------------------------------------------- /templates/Winter [Sample project]/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}; -------------------------------------------------------------------------------- /templates/Winter [Sample project]/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}; -------------------------------------------------------------------------------- /templates/Winter [Sample project]/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 | /* ========================================== */ -------------------------------------------------------------------------------- /templates/Winter [Sample project]/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}; -------------------------------------------------------------------------------- /templates/Winter [Sample project]/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}; -------------------------------------------------------------------------------- /templates/Winter [Sample project]/style.css: -------------------------------------------------------------------------------- 1 | body 2 | { 3 | font-family: 'Open Sans', sans-serif; 4 | font-weight: 300; 5 | background: black; 6 | color: white; 7 | -webkit-touch-callout: none; 8 | -webkit-user-select: none; 9 | -khtml-user-select: none; 10 | -moz-user-select: none; 11 | -ms-user-select: none; 12 | user-select: none; 13 | -webkit-user-drag: none; 14 | -khtml-user-drag: none; 15 | -moz-user-drag: none; 16 | -o-user-drag: none; 17 | } 18 | 19 | /* Seems necessary to make iframes work on itch.io on mobile iOS :-( */ 20 | #container 21 | { 22 | position: absolute; 23 | display: block; 24 | margin: 0; 25 | padding: 0; 26 | -webkit-overflow-scrolling: touch; 27 | overflow: scroll; 28 | overflow-x: hidden; 29 | height: 100%; 30 | width: 100%; 31 | top: 0; 32 | left: 0; 33 | background-size: cover; 34 | background-repeat: no-repeat; 35 | z-index: 10; 36 | } 37 | 38 | #story 39 | { 40 | position: relative; 41 | margin: 0 auto; 42 | overflow: hidden; 43 | height: 100%; 44 | width: 700px; 45 | min-width: 60vw; 46 | max-width: 80vw !important; 47 | transition: opacity 2s ease; 48 | padding: 0em 1em 0em 1em; 49 | } 50 | 51 | @media (hover:none), (hover:on-demand) 52 | { 53 | #story 54 | { 55 | max-width: 90%; 56 | } 57 | } 58 | 59 | #story > *:first-child { 60 | margin-top: 10vh !important; 61 | } 62 | 63 | #story > *:last-child 64 | { 65 | margin-bottom: 15vh !important; 66 | } 67 | 68 | /* ================================================================= */ 69 | 70 | p 71 | { 72 | margin-block-start: 0px !important; 73 | font-size: 16pt; 74 | line-height: 1.4; 75 | } 76 | 77 | p, p > * 78 | { 79 | -webkit-user-drag: none; 80 | -khtml-user-drag: none; 81 | -moz-user-drag: none; 82 | -o-user-drag: none; 83 | } 84 | 85 | a 86 | { 87 | color: #f16d69; 88 | text-decoration: none !important; 89 | transition: all 0.5s linear; 90 | } 91 | 92 | a:hover 93 | { 94 | color: #ffffff; 95 | } 96 | 97 | .choice 98 | { 99 | opacity: 0.5; 100 | transition: all 1s; 101 | color: white; 102 | } 103 | 104 | .choice:hover 105 | { 106 | color: white; 107 | opacity: 1; 108 | transition: all 1s; 109 | } 110 | 111 | .choice > a 112 | { 113 | color: inherit; 114 | } 115 | 116 | .choice > a:active 117 | { 118 | color:red; 119 | } 120 | 121 | .chosen 122 | { 123 | color: white !important; 124 | opacity: 1 !important; 125 | } 126 | 127 | /* ================================================================= */ 128 | 129 | .progressbar 130 | { 131 | position: absolute; 132 | overflow: hidden; 133 | top: 0; 134 | background: #7690ac; 135 | width: 0%; 136 | height: 1vh; 137 | } 138 | 139 | .frame 140 | { 141 | width: 100%; 142 | height: calc(100vw * 0.2); 143 | overflow: hidden; 144 | position: relative; 145 | } 146 | 147 | .frameLayer 148 | { 149 | position: absolute; 150 | width: 120%; 151 | display: block; 152 | top: -9999px; 153 | bottom: -9999px; 154 | left: -9999px; 155 | right: -9999px; 156 | margin: auto; 157 | max-width: auto; 158 | } 159 | 160 | /* css for mobile */ 161 | @media (hover:none), (hover:on-demand) 162 | { 163 | .frame 164 | { 165 | min-height: calc(100vmax * 0.25); 166 | } 167 | .frameLayer 168 | { 169 | width: 150%; 170 | } 171 | } 172 | 173 | /* ================================================================= */ 174 | 175 | .winter 176 | { 177 | color: #7690ac !important; 178 | } 179 | 180 | .red 181 | { 182 | color: #f76e6a !important; 183 | } 184 | 185 | 186 | img 187 | { 188 | -ms-interpolation-mode: nearest-neighbor; 189 | image-rendering: crisp-edges; 190 | image-rendering: pixelated; 191 | } 192 | 193 | /* ================================================================= */ 194 | 195 | .frame.menu 196 | { 197 | width: 60vh; 198 | height: 48vh; 199 | margin: auto; 200 | margin-bottom: revert; 201 | min-height: revert; 202 | max-height: revert; 203 | } 204 | 205 | .menu, .menu ~ p 206 | { 207 | width: 60vh; 208 | margin: auto; 209 | margin-bottom: revert; 210 | } 211 | 212 | @media (hover:none), (hover:on-demand) 213 | { 214 | .frame.menu 215 | { 216 | width: 80vmin; 217 | height: 64vmin; 218 | } 219 | 220 | .menu, .menu ~ p 221 | { 222 | width: 80vmin; 223 | } 224 | } 225 | 226 | .menu > .frameLayer 227 | { 228 | width: 120%; 229 | height: 120%; 230 | } 231 | 232 | .menu > .frameLayer:nth-child(n+3) 233 | { 234 | width: 100%; 235 | height: 100%; 236 | } 237 | 238 | /* ================================================================= */ 239 | 240 | /* open-sans-300 - latin */ 241 | @font-face { 242 | font-family: 'Open Sans'; 243 | font-style: normal; 244 | font-weight: 300; 245 | src: url('./fonts/OpenSans-Light.ttf'); 246 | } 247 | /* open-sans-300italic - latin */ 248 | @font-face { 249 | font-family: 'Open Sans'; 250 | font-style: italic; 251 | font-weight: 300; 252 | src: url('./fonts/OpenSans-LightItalic.ttf'); 253 | } 254 | /* open-sans-regular - latin */ 255 | @font-face { 256 | font-family: 'Open Sans'; 257 | font-style: normal; 258 | font-weight: 400; 259 | src: url('./fonts/OpenSans-Regular.ttf'); 260 | } 261 | /* open-sans-italic - latin */ 262 | @font-face { 263 | font-family: 'Open Sans'; 264 | font-style: italic; 265 | font-weight: 400; 266 | src: url('./fonts/OpenSans-Italic.ttf'); 267 | } 268 | /* open-sans-700 - latin */ 269 | @font-face { 270 | font-family: 'Open Sans'; 271 | font-style: normal; 272 | font-weight: 700; 273 | src: url('./fonts/OpenSans-Bold.ttf'); 274 | } 275 | /* open-sans-700italic - latin */ 276 | @font-face { 277 | font-family: 'Open Sans'; 278 | font-style: italic; 279 | font-weight: 700; 280 | src: url('./fonts/OpenSans-BoltItalic.ttf'); 281 | } 282 | -------------------------------------------------------------------------------- /templates/Winter [Sample project]/winter.js: -------------------------------------------------------------------------------- 1 | // ----------------------------------- 2 | // attempt to preload all files before starting the story 3 | import "./patches/preload.js"; 4 | 5 | options.preload_tags.audio.push("play", "pause", "resume", "stop"); 6 | options.preload_tags.image.push("frame"); 7 | 8 | // always attempt to break to a new line in a way that 9 | // preserves a minimum number of words per line 10 | import "./patches/minwordsperline.js"; 11 | // click and drag to scroll the page 12 | import "./patches/dragtoscroll.js"; 13 | // convert markdown to HTML tags 14 | import "./patches/markdowntohtml.js" 15 | // ----------------------------------- 16 | // enable saving and loading 17 | import "./patches/memorycard.js" 18 | options.memorycard_applymostrecenttag.push("play", "resume", "pause", "stop"); 19 | // ----------------------------------- 20 | // preserve the player's position if they reload 21 | import "./patches/autosave.js" 22 | // ----------------------------------- 23 | // import media tag stuff for this game 24 | import "./patches/parallaxframes.js" 25 | import "./patches/audioplayer.js" 26 | // ----------------------------------- 27 | // import helper patch for binding shortcuts to choices 28 | import choices from "./patches/shortcuts/choices.js"; 29 | 30 | // bind the number keys to choices 31 | for (var i = 0; i < 9; i++) 32 | { 33 | choices.add((i+1).toString(), i, true); 34 | } 35 | 36 | // bind z, x, and c to the first three shortcuts respectively 37 | ["z", "x", "c"].forEach((key, index) => { choices.add(key, index, true) }) 38 | 39 | // bind spacebar to progress the story, 40 | // provided there's only one choice available 41 | choices.add(" ", 0, true, true); 42 | // ----------------------------------- 43 | // bind custom tags that apply CSS styles to a line 44 | import "./patches/shorthandclasstags.js"; 45 | // set which tags (and by extension CSS styles) to use 46 | options.shorthandclasstags_tags = ["red", "winter"]; 47 | // ----------------------------------- 48 | // allow stepping the story forwards and backwards 49 | import "./patches/stepback.js" 50 | 51 | // bind shortcuts to stepBack and stepForwards 52 | // (we're creating an empty patch so we can wait until 53 | // after the story is loaded before running our code) 54 | Patches.add(function() 55 | { 56 | Shortcuts.add("q", this.stepBack); 57 | Shortcuts.add("e", this.stepForwards); 58 | }); 59 | // ----------------------------------- 60 | 61 | // set our options 62 | options.linedelay = 600.0; 63 | options.passagedelay = 200.0; 64 | options.showlength = 1000.0; 65 | options.hidelength = 750.0; 66 | options.suppresschoice = 0.0; 67 | // enable debug mode, which prints a (probably far too detailed) log to the browser console 68 | options.debug = true; 69 | 70 | // create our game 71 | var winter = new Story("winter.ink"); --------------------------------------------------------------------------------