/*\nsqCharacter HTML5 Character Macro Suite\nCopyright 2014 Tory Hoke\n\nProgram URI: http://www.sub-q.com/plugins/sqTwineSound/\nDescription: Sound macros for Twine creations, including controls for volume, fade interval, and playing multiple tracks at once.\nVersion: 0.8.0\nAuthor: Tory Hoke\nAuthor URI: http://www.toryhoke.com\nLicense: GNU General Public License\nLicense URI: http://www.opensource.org/licenses/gpl-license.php\nRepository: https://github.com/AteYourLembas/sqTwineSound\nFAQ / Q & A: http://sub-q.com/questions (password: ThinkVast)\nBug Reports/Feature Requests: http://sub-q.com/forums/topic/what-would-you-like-to-see-sqtwinesound-do-that-its-not-doing/ (password: ThinkVast)\n\n sub-Q.com is password-protected while it's in beta (until January 2015.)\n Please kick the tires and report any issues with the website\n via the sub-Q.com Contact form.\n\n\nThis program based on:\nTwine: HTML5 sound macros by Leon Arnott of Glorious Trainwrecks\nthe source and influence of which appear under a Creative Commons CC0 1.0 Universal License\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n \nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\nGNU General Public License for more details.\n\n*/\n\n(function () {\n version.extensions.characterMacros = {\n major: 0,\n minor: 8,\n revision: 0\n };\n\n // All known globals at this point (used to load from saved state)\n // Current defaults\n //\n state.active.variables.theme = "day"; \n state.active.variables.metric = true;\n state.active.variables.shoulderChip = "looks";\n state.active.variables.disturbing = false;\n state.active.variables.erotic = false;\n state.active.variables.puzzleScore = 0;\n state.active.variables.chrs = {};\n state.active.variables.locksPicked = []; \n\n state.active.variables.limitHate = 0;\n state.active.variables.limitDistrust = 25;\n state.active.variables.limitTrust = 50;\n state.active.variables.limitAdore = 75;\n\n state.active.variables.genders = {"M": new gender("M", "he", "him", "his", "himself", "brother", "son", "man", "boy", "handsome"),\n "F": new gender("F", "she", "her", "her", "herself", "sister", "daughter", "woman", "girl", "pretty"),\n "A": new gender("A", "ze", "zir", "zir", "zirself", "sibling", "child", "person", "child", "cute")}; \n\n state.active.variables.mainName = "Sachi";\n state.active.variables.siblingName = "Healani";\n state.active.variables.chrs["Sachi"] = new character(state.active.variables.mainName, "Silva", "F", ["M","F","A"], 50);\n state.active.variables.chrs["Healani"] = new character(state.active.variables.siblingName, "Silva", "F", ["M","F","A"], 50);\n\n\n function capitalizeFirstLetter(string) { return string.charAt(0).toUpperCase() + string.slice(1); }\n\n function error(message) { return throwError(message); }\n\n //------------- gender -----------------\n //--------------------------------------\n function gender(genderType, pronounSub, pronounObj, pronounPos, pronounReflex, sibRel, kidRel, socRel, youngTerm, looksCompliment) {\n\n this.genderType = genderType;\n this.pronounSub = pronounSub;\n this.pronounObj = pronounObj;\n this.pronounPos = pronounPos;\n this.pronounReflex = pronounReflex;\n\n this.sibRel = sibRel;\n this.kidRel = kidRel;\n this.socRel = socRel;\n this.youngTerm = youngTerm;\n this.looksCompliment = looksCompliment;\n\n }\n //------------- /gender ----------------\n //--------------------------------------\n\n //------------- character ---------\n //--------------------------------------\n function character(givenName, surname, genderType, preferredGenderTypes, romanceableLimit) {\n\n this.givenName = givenName;\n this.surname = "";\n this.trustMap = {};\n this.gender = state.active.variables.genders[genderType];\n this.preferredGenderTypes = preferredGenderTypes;\n this.romanceableLimit = romanceableLimit;\n\n this.isRomanceable = function(givenChar) {\n if (!this.trustMap.hasOwnProperty(givenChar.givenName)) { return error("Could not determine " + givenName + "'s trust level of " + givenChar.givenName + ". Please check your save state."); } \n return this.preferredGenderTypes.indexOf(givenChar.gender.genderType) > -1 && this.trustMap[givenChar.givenName] > this.romanceableLimit; \n };\n\n this.report = function() { var reporting = this.givenName + " " + this.surname + ", " + this.gender.genderType + ", prefers " + this.preferredGenderTypes + ", has romanceableLimit " + this.romanceableLimit; return reporting; };\n\n this.updateTrust = function(givenChar, value) { \n if (!this.trustMap.hasOwnProperty(givenChar.givenName)) { return error("Could not determine " + givenName + "'s trust level of " + givenChar.givenName + ". Please check your save state."); } \n this.trustMap[givenChar.givenName] += value;\n };\n\n }\n //------------- /character --------\n //--------------------------------------\n\n\n\n /***********************************************************\n /***********************************************************\n * MACROS\n /***********************************************************\n /***********************************************************\n */\n\n /* createChar */\n macros.add("createChar", {\n handler: function () {\n\n var expectedParams = 5;\n\n var params = this.args;\n if (this.args === undefined) { return throwError("No parameters specified."); } \n\n params = params.toString().split(",");\n\n if (params.length < expectedParams) { return throwError("Expected " + expectedParams + " params but got " + params.length); } \n\n var preferredGenderTypes = params[3].split(";");\n var romanceableLimit = parseFloat(params[4]);\n\n state.active.variables.chrs[params[0]] = new character(params[0], params[1], params[2], preferredGenderTypes, romanceableLimit);\n\n }\n });\n\n\n /* pickLock */\n macros.add("pickLock", {\n handler: function () {\n\n var expectedParams = 1;\n\n var params = this.args;\n if (this.args === undefined) { return throwError("No parameters specified."); } \n params = params.toString().split(",");\n if (params.length < expectedParams) { return throwError("Expected " + expectedParams + " params but got " + params.length); } \n\n if (state.active.variables.locksPicked.indexOf(params[0]) < 0) {\n state.active.variables.locksPicked.push(params[0]);\n }\n\n }\n });\n\n\n /***********************************************************\n * END MACROS\n /***********************************************************/\n\n\n\n}());\n
<<set $frame to 0>>\s\n<<set $sound = "no">>\s\n<<set $theme = "day">>\s\n<<set $continueText = "...">>\s\n<<set $puzzleAttempt to false>>\s\n<<set $puzzleOutcome = "no">>\s\n<<set $puzzleRightAnswerHash = "-1875794667">>\s\n<<set $respec to false>>\n<<display "Style Changer">><<day>>\s\n<<display "Style Changer">><<visibleBar>>\s\n\nLet's get a few things straight.\n\nI never said I was a hero. I'm just a good ol' <<cyclinglink "$genderDesc" "girl" "boy" "cousin">> from Palekaiko. Nobody suspected I'd even survive.\n\nI mean, look at me. I'm <<cyclinglink "$metricDesc" "150 cm" "five foot nothing" "five foot four and 120 lbs" "137 cm and 54 kilo" "137 cm and not even nine stone" "five foot eight and 140 lbs" "173 cm and 65 kilo" "173 cm and 10 stone">>. Who's gonna let me lead a charge?\n\nI'm no beauty, either. Be serious. Who's gonna paint my picture for free?\n\nMy older <<cyclinglink "$siblingDesc" "sister" "brother">>, <<textbox "$siblingName" "Healani">> , now--there's where all the <<cyclinglink "$shoulderChip" "looks" "muscles">> went. That's where to start, if you want me to start.\n\nDon't worry. I'll leave <<cyclinglink "$disturbingDesc" "in" "out">> all the gory parts.\n\nAnd I'll leave <<cyclinglink "$eroticDesc" "in" "out">> the sex. Don't act so surprised. I can be charming if I put a mind to it.\n\nSay, could you <<cyclinglink "$themeDesc" "leave the lights up" "lower the lights" >>? It's easier on my eyes.\n\nMy family name is Silva, from Palekaiko Town. That's on the Mainland. If you want to know my name, well... You can call me <<textbox "$mainName" "Sachi">> .\n\n<div class="dayForm"><<button [[You want to see a picture of my Ma?|Prologue01]]>><</button>></div>
<center>¤</center>\s
\nThat's as good a place to stop as any.\n\nDo me a kindness and save our place. Might even want to stash a note about it. Next time, we'll pick up where we left off.\n\nUntil then, tell 'em this big nobody said 'hey.'
macros.hash = {\n handler: function(a, b, s, destination){\n\tvar s = s.toString();\n\tvar hashResult= s.split("").reduce(function(a,b){a=((a<<5)-a)+b.charCodeAt(0);return a&a},0); \n\n\tstate.active.variables[destination.rawArgs().split(" ")[1]] = hashResult; \n\n}\n};
<<set $mainCharGenderType to "F">>\s\n<<if $genderDesc eq "boy">><<set $mainCharGenderType to "M">>\s\n<<elseif $genderDesc eq "cousin">><<set $mainCharGenderType to "A">>\s\n<<endif>>\s\n<<set $metric to true>>\s\n<<if $metricDesc eq "five foot nothing" or $metricDesc eq "five foot four and 120 lbs" or $metricDesc eq "five foot eight and 140 lbs" or $altMetricDesc eq "mile">><<set $metric to false>><<endif>>\s\n<<set $siblingGenderType to "F">>\s\n<<if $siblingDesc eq "brother">><<set $siblingGenderType to "M">><<endif>>\s\n<<set $disturbing to true>>\s\n<<if $disturbingDesc eq "out">><<set $disturbing to false>><<endif>>\s\n<<set $erotic to true>>\s\n<<if $eroticDesc eq "out">><<set $erotic to false>><<endif>>\s\n<<set $theme to "day">>\s\n<<if $themeDesc eq "lower the lights">><<set $theme to "night">><<display "Style Changer">><<night>><<else>><<set $theme to "day">><<display "Style Changer">><<day>><<endif>>\s
[[Recap]]\n\n[[About]]\n\n\n\n
version.extensions.cyclinglinkMacro = {\n major: 3,\n minor: 3,\n revision: 0\n};\nmacros.cyclinglink = {\n handler: function (a, b, c) {\n var rl = "cyclingLink";\n\n function toggleText(w) {\n w.classList.remove("cyclingLinkInit");\n w.classList.toggle(rl + "Enabled");\n w.classList.toggle(rl + "Disabled");\n w.style.display = ((w.style.display == "none") ? "inline" : "none")\n }\n switch (c[c.length - 1]) {\n case "end":\n var end = true;\n c.pop();\n break;\n case "oot":\n var oot = true;\n c.pop();\n break\n }\n var v = "";\n if (c.length && c[0][0] == "$") {\n v = c[0].slice(1);\n c.shift()\n }\n var h = state.active.variables;\n if (oot && h[v] === "") {\n return\n }\n var l = document.createElement("a");\n a.appendChild(l);\n l.className = "internalLink cyclingLink";\n l.setAttribute("data-cycle", 0);\n for (var i = 0; i < c.length; i++) {\n var on = (i == Math.max(c.indexOf(h[v]), 0));\n var d = insertElement(null, "span", null, "cyclingLinkInit cyclingLink" + ((on) ? "En" : "Dis") + "abled");\n if (on) {\n h[v] = c[i];\n l.setAttribute("data-cycle", i)\n } else {\n d.style.display = "none"\n }\n insertText(d, c[i]);\n if (on && end && i == c.length - 1) {\n l.parentNode.replaceChild(d, l)\n } else {\n l.appendChild(d)\n }\n }\n l.onclick = function () {\n var t = this.childNodes;\n var u = this.getAttribute("data-cycle") - 0;\n var m = t.length;\n toggleText(t[u]);\n u = (u + 1);\n if (!(oot && u == m)) {\n u %= m;\n if (v) {\n h[v] = c[u]\n }\n } else {\n h[v] = ""\n } if ((end || oot) && u == m - (end ? 1 : 0)) {\n if (end) {\n var n = this.removeChild(t[u]);\n n.className = rl + "End";\n n.style.display = "inline";\n this.parentNode.replaceChild(n, this)\n } else {\n this.parentNode.removeChild(this);\n return\n }\n return\n }\n toggleText(t[u]);\n this.setAttribute("data-cycle", u)\n }\n }\n};
<<set $altMetricDesc to "klick">><<if $metric eq false>><<set $altMetricDesc to "mile">><<endif>>\s\n\nMy name's <<textbox "$mainName" $mainName>> Silva. I'm just a <<cyclinglink "$genderDesc" $chrs[$siblingName].gender.youngTerm "girl" "boy" "cousin">> from Palekaiko, Lahui'i.\n\nMy older <<cyclinglink "$siblingDesc" "sister" "brother">>, <<textbox "$siblingName" $siblingName>> got the <<cyclinglink "$shoulderChip" $shoulderChip "looks" "muscles">> in the family. I'm not so big or so pretty, but I can run a <<cyclinglink "$metricDesc" $altMetricDesc "mile" "klick">> and throw a knife. Not that any of that did us any good when the Norg barbarians broke their treaty and seized our island. \n\nI'll tell you what happened, but I'm gonna leave <<cyclinglink "$disturbingDesc" $disturbingDesc "in" "out">> all the gory parts. And I'll leave <<cyclinglink "$eroticDesc" $eroticDesc "in" "out">> the sex.\n\nIt's easier on my eyes if you <<cyclinglink "$themeDesc" "leave the lights up" "lower the lights">>.\n\n<<if $locksPicked.length >= 1>>I've picked <<print $locksPicked.length>> lock(s) so far.<<endif>>\n\n<div class="dayForm"><<button [[Remember this.|SaveAndRecap]]>><</button>></div>\n<div class="dayForm"><<button [[Nevermind.|Start]]>><</button>></div>\n\n
<<silently>>\n<<set $StyleChanger = \nfunction()\n{\n\n\tfunction changeLinkClass(newClass)\n\t{\n\t\tvar links = document.getElementsByTagName("a");\n\t\tfor(var i=0;i<links.length;i++)\n\t\t{\n\t\t\tlinks[i].className = newClass;\n\t\t\talert("setting className " + links[i] + " " + newClass);\n\t\t} \n\t}\n\n\tmacros['day'] =\n\t{\n\t\thandler: function(){\n\t\t\tdocument.getElementsByTagName("body")[0].style.backgroundColor="#f1e9d2";\n\t\t\tdocument.getElementById("passages").className = "day";\n\t\t}\n\t\n\t};\n\n\tmacros['hiddenBar'] =\n\t{\n\t\thandler: function(){\n\t\t\tdocument.getElementById("ui-bar").style.display="none";\n\t\t\tdocument.getElementById("passages").style.border=0;\n\t\t}\n\t};\n\n\tmacros['visibleBar'] =\n\t{\n\t\thandler: function(){\n\t\t\tdocument.getElementById("ui-bar").style.display=true;\n\t\t}\n\t};\n\n\tmacros['night'] =\n\t{\n\t\thandler: function(){\n\t\t\tdocument.getElementsByTagName("body")[0].style.backgroundColor="#003768";\n\t\t\tdocument.getElementById("passages").className = "night";\n\t\t}\n\t\n\t};\n\n}>>\n<<print $StyleChanger()>>\n<<endsilently>>
<<display "Respec">>\s\n<<createChar $mainName "Silva" $mainCharGenderType "M;F;A" 50>>\s\n<<createChar $siblingName "Silva" $siblingGenderType "M;F;A" 50>>\s \n\n<p class="ind">Every kid thinks their ma's the most beautiful woman in the world, but in my case it was true.</p>\s\n<p class="ind">Ma was real Lahui, bronze with dark eyes, an egret neck and long hair as thick as her waist. Pa would say she looked good enough to eat, and Ma would say she’d like to see him try it, and then he’d nibble on her neck until me and <<print $chrs[$siblingName].givenName>> got annoyed and left.</p>\s\n<p class="ind">She had a long body and walked like a queen. One time coming back from market we passed a cotton seller who craned his neck so far he drove into a ditch. Our town was only half a day's walk from the palace, so our market had a lot of pretty people, but Ma was the only one anyone ditched themselves to see.</p>\s\n<<if $shoulderChip eq "looks">>\s\n<p class="ind">My <<print $chrs[$siblingName].gender.sibRel>>, <<print $chrs[$siblingName].givenName>>, was nice-looking like that, too, so people expected nice acting, and <<print $chrs[$siblingName].gender.pronounSub>> didn't let 'em down. When <<print $chrs[$siblingName].gender.pronounSub>> said "yes mam" and "no mam" all Ma's friends smiled and sighed like <<print $chrs[$siblingName].gender.pronounSub>> twirled fire or something.</p>\s\n<<else>>\s\n<p class="ind">My <<print $chrs[$siblingName].gender.sibRel>>, <<print $chrs[$siblingName].givenName>>, grew up tall and strong like that, too, so people expected strong character, and <<print $chrs[$siblingName].gender.pronounSub>> didn't let 'em down. When <<print $chrs[$siblingName].gender.pronounSub>> fetched water four buckets at a time, all Pa's friends smiled and nodded like <<print $chrs[$siblingName].gender.pronounSub>> twirled fire or something.</p>\s\n<<endif>>\s\n<p class="ind">Well, I took after Pa, and he came from the continent down east, big-nosed and wiry. I looked like mischief so people treated me like mischief, but if anyone sneered at me I sneered right back. I got bloodied but I got faster and then I didn't get bloodied anymore.</p>\s\n<p class="ind">Like Pa I had a head full of curly black hair that only does right in braids. It got loose. It was irritating. <<print $chrs[$siblingName].givenName>> teased me about it when we were little-little, but when <<print $chrs[$siblingName].gender.pronounSub>> turned nine Ma gave <<print $chrs[$siblingName].gender.pronounObj>> a talk real quiet and <<print $chrs[$siblingName].gender.pronounSub>> stopped, and that was worse somehow. Morning and night we both said the prayer Ma taught us, but it seemed like God listened to <<print $chrs[$siblingName].givenName>> better.</p>\s\n<p class="ind">Every kid in Palekaiko came around for <<print $chrs[$siblingName].givenName>>. Older kids, younger kids, nevermind. "Where's <<print $chrs[$siblingName].givenName>>?" Like I had the hogpox. "Where's <<print $chrs[$siblingName].givenName>>?"</p>\s\n<p class="ind">"Why's it always you they want?" I asked <<print $chrs[$siblingName].gender.pronounObj>>.</p>\s\n<p class="ind">"<<if $chrs[$siblingName].gender.genderType eq "F">>Oh<<else>>Come on<<endif>>, <<print $chrs[$mainName].givenName>>. It's not always me."</p>\s\n<p class="ind">"Says you."</p>\s\n\s\n<<display "Divider">>\n\s\n<p class="ind">One day I was out back shelling peanuts, sweating, hair getting loose and falling in my eyes, when kids came around asking "where's <<print $chrs[$siblingName].givenName>>?" I got so mad I told 'em off and ran in the house. I grabbed Ma's knife, and I spread that loose hair on the kitchen table and chopped it off. Then I just kept chopping till it was all sticking straight up like a broken hedge. When Ma came in she looked at my head and the knife and the table and turned purple.</p>\s\n<p class="ind">What an earful I got! But I didn't even care anymore, and she knew it, cos when Pa came home she handed me off. He picked a switch and dragged me behind the pigpen. Not my first trip. We had a trail beat so deep it was muddy.</p>\s\n<p class="ind">Pa said, "Turn around," but I wasn't crying or hanging my head about it, so I could look at him. I saw he wouldn't look me in the eye.</p>\s\n<p class="ind">"I won't," I said.</p>\s\n<p class="ind">"Do it, <<print $chrs[$mainName].gender.youngTerm>>."</p>\s\n<p class="ind">"No. You wanna switch me, you gotta look me in the eye."</p>\s\n<p class="ind">"You made a mess and ruined your mother's table. You've got to learn."</p>\s\n<p class="ind">"You know by now a beating ain't gonna learn me."</p>\s\n<p class="ind">Pa took a long time to make up his mind, but then he said, "You're sharp, <<print $chrs[$mainName].givenName>>, and that's a mixed blessing. You ever heard of a 'mixed blessing'?"\n<p class="ind">"It means when a thing's a good thing and a bad thing."</p>\s\n<p class="ind">"That's right. Use the good part of sharp. Your Ma works as hard as I do. Make her life easier and I'll do the same for you. Understand?"</p>\s\n<p class="ind">"Yes, sir."</p>\s\n<p class="ind">"Now holler while I whip this tree, 'cause she's listening."</p>\s\n<p class="ind">The next day Pa got me a scarf to wear on my head. Ma was mad, but Pa said he felt bad about the whipping. I liked the scarf so much and I thought I looked so <<if $shoulderChip eq "looks">>fine<<else>>tough<<endif>> until I passed a mirror and I saw I still looked like me.</p>\s\n<<if $shoulderChip eq "looks">>\s\n<p class="ind">So after dinner, when <<print $chrs[$siblingName].givenName>> went up to read and Pa went out to count and Ma sat down to spin, I sat with her. She wasn't mad anymore. I asked her, "Am I <<print $chrs[$mainName].gender.looksCompliment>>?"</p>\s\n<p class="ind">"All little <<if $chrs[$mainName].gender.youngTerm eq "child">>children<<else>><<print $chrs[$mainName].gender.youngTerm>>s<<endif>> are <<print $chrs[$mainName].gender.looksCompliment>>," she said.</p>\s\n<p class="ind">"That’s not true. Gert Suzuki's my age and <<print $chrs[$mainName].gender.pronounSub>>'s ugly as a wet cat."</p>\s\n<p class="ind">"It’s cruel to speak that way."</p>\s\n<p class="ind">"It’s cruel to lie to your <<print $chrs[$mainName].gender.kidRel>>."</p>\s\n<p class="ind">"All right. If that's how it is." She looked me over from head to toe. "You have good bones. Your teeth are coming in nice. You have pretty eyes and that’s as far as most people look. You’ll turn your share of heads, but not more."</p>\s\n<<else>>\s\n<p class="ind">So after dinner, when <<print $chrs[$siblingName].givenName>> went up to read and Pa went out to count and Ma sat down to spin, I sat with her. She wasn't mad anymore. I asked her, "Will I get big and strong when I grow up?"\n<p class="ind">"You'll be as strong as a <<print $chrs[$mainName].gender.youngTerm>> needs to be," she said.</p>\s\n<p class="ind">"What if I need to carry four buckets?"</p>\s\n<p class="ind">"Why would you need to carry four buckets?"</p>\s\n<p class="ind">"<<print $chrs[$siblingName].givenName>> can."</p>\s\n<p class="ind">"All right. If that's how it is." She looked me over from head to toe. "Your bones don't break easy, I've seen that much. You could run fast, if you practice. Your joints won't give you trouble. But, no, you won't be as strong as the strongest there is. It's not likely you'll even be tall."</p>\s\n<<endif>>\s\n<p class="ind">I didn't mean to show how much it hurt. Ma scooped me up in her arms. "My sweet <<print $chrs[$mainName].gender.kidRel>>. I know how much you want to be <<if $shoulderChip eq "looks">>beautiful<<else>>big and strong<<endif>>. But you don't need it. You don't." She kissed me on the head. "That brain in here is <<if $shoulderChip eq "looks">>beautiful<<else>>big and strong<<endif>>. I've seen you working things out ever since you were born. You found a way onto the roof when you were seven. Unlocked the back door to do it. Do you remember?"</p>\s\n<p class="ind">I did. That lock was an Journeyman Three-Stage and the pins were soft.</p>\s\n\n<p class="ind">- [[I can still picture it.|ProloguePickLock][$puzzleAttempt = true]]</p>\s\n<p class="ind">- [[Nevermind about that.|Prologue02][$puzzleAttempt = false]]</p>\s
<<display "Respec">>\s\n<<createChar $mainName "Silva" $mainCharGenderType "M;F;A" 50>>\s\n<<createChar $siblingName "Silva" $siblingGenderType "M;F;A" 50>>\s\nExcuse me. What I meant to say is...\n<<display "Recap">>\s
<u>Big Nobody</u> is a serialized interactive novel. Chapters are released Thursdays at 9:00 AM EST. There are a total of 25 chapters.\n\nTo save your choices from each chapter, click "Save" in the upper right hand corner. Each save slot you see listed is stored in a cookie on your browser. To avoid losing saved states when you delete cookies or clear your browsing history, or to load a saved state on a different computer, use "Save to Disk…" and "Load from Disk…"\n\nSaved states are contained completely on your computer browser. Your choices are not saved on any other computer. You can download the chapter's HTML and run it from your file system, if you prefer.\n\nSubmit any questions, bugs, or other thoughts to the author's <a href="http://www.toryhoke.com/contact" target="_blank">contact form</a>.\n\n<<display "Credits and How-Tos">>
/*\nsqTwineSound HTML5 Sound Macro Suite\nCopyright 2014 Tory Hoke\n\nProgram URI: http://www.sub-q.com/plugins/sqTwineSound/\nDescription: Sound macros for Twine creations, including controls for volume, fade interval, and playing multiple tracks at once.\nVersion: 0.8.0\nAuthor: Tory Hoke\nAuthor URI: http://www.toryhoke.com\nLicense: GNU General Public License\nLicense URI: http://www.opensource.org/licenses/gpl-license.php\nRepository: https://github.com/AteYourLembas/sqTwineSound\nFAQ / Q & A: http://sub-q.com/questions (password: ThinkVast)\nBug Reports/Feature Requests: http://sub-q.com/forums/topic/what-would-you-like-to-see-sqtwinesound-do-that-its-not-doing/ (password: ThinkVast)\n\n sub-Q.com is password-protected while it's in beta (until January 2015.)\n Please kick the tires and report any issues with the website\n via the sub-Q.com Contact form.\n\n\nThis program based on:\nTwine: HTML5 sound macros by Leon Arnott of Glorious Trainwrecks\nthe source and influence of which appear under a Creative Commons CC0 1.0 Universal License\n\nThis program uses\n\n easeInOutSine()\n Copyright © 2001 Robert Penner\n All rights reserved.\n \n As distributed by Kirupa\n http://www.kirupa.com/forum/showthread.php?378287-Robert-Penner-s-Easing-Equations-in-Pure-JS-(no-jQuery)\n \n Open source under the BSD License. \n \n \n Redistribution and use in source and binary forms, with or without modification, \n are permitted provided that the following conditions are met:\n \n Redistributions of source code must retain the above copyright notice, this list of \n conditions and the following disclaimer.\n Redistributions in binary form must reproduce the above copyright notice, this list \n of conditions and the following disclaimer in the documentation and/or other materials \n provided with the distribution.\n \n Neither the name of the author nor the names of contributors may be used to endorse \n or promote products derived from this software without specific prior written permission.\n \n THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY \n EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF\n MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE\n COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,\n EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE\n GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED \n AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING\n NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED \n OF THE POSSIBILITY OF SUCH DAMAGE. \n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n \nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\nGNU General Public License for more details.\n\n*/\n\n(function () {\n version.extensions.soundMacros = {\n major: 0,\n minor: 8,\n revision: 0\n };\n\n var globalVolume = 1.0;\n var updateInterval = 10; //Update sound volume, etc. once every 10 ms\n var defaultOverlap = 1000; //Default track overlap is 1000 ms\n var minVolume = 0.01; // Minimum possible volume -- 0 is mute, so we want somethings slightly above that\n var soundInterval = 0.1; // Creates an interval of 1/10 creates ten stages of loudness. Used by quieter/louder. Feel free to tweak\n var fileExtensions = ["ogg", "mp3", "wav", "webm"]; // Acceptable file extensions for audio\n var clips = {};\n\n // Convenience vars\n var clipNameLabel = "Clip Name";\n var overlapLabel = "Overlap";\n var volumeProportionLabel = "Volume Proportion";\n var loopLabel = "Loop?";\n\n\n //------------ Robert Penner via Kirupa math methods ----------\n //-------------------------------------------------------------\n\n function easeInOutSine(currentIteration, startValue, changeInValue, totalIterations) {\n return changeInValue / 2 * (1 - Math.cos(Math.PI * currentIteration / totalIterations)) + startValue;\n }\n\n\n //------------ End Math methods -------------------------------\n //-------------------------------------------------------------\n\n //------------- pausableTimeout ---------\n //--------------------------------------\n function pausableTimeout(func, params) {\n\n this.funcToRun = func;\n this.waitStartTime = -1;\n this.waitEndTime = -1;\n this.waitDuration = -1;\n\n this.activate = function(waitDuration) {\n\n if (this.pausedAt !== undefined) { this.waitDuration = this.timeRemaining(); }\n else if (waitDuration !== undefined) this.waitDuration = waitDuration;\n else if (this.waitDuration > -1 ) { console.log("Warning: No wait duration given to pausableTimeout. Using last specified one."); }\n else return; // Don't bother to start a loop with no wait duration\n\n this.waitStartTime = new Date().getTime();\n this.waitEndTime = new Date().getTime() + this.waitDuration;\n this.timeout = setTimeout(this.funcToRun, this.waitDuration, params);\n };\n\n this.deactivate = function() {\n this.pausedAt = this.timeElapsed();\n if (this.timeout !== undefined) clearTimeout(this.timeout);\n };\n\n this.stopAndClear = function() {\n if (this.pausedAt !== undefined) delete this.pausedAt;\n if (this.timeout !== undefined) { clearTimeout(this.timeout); delete this.timeout; }\n };\n\n this.timeElapsed = function() {\n return new Date().getTime() - this.waitStartTime;\n };\n\n this.timeRemaining = function() {\n if (this.pausedAt !== undefined) return this.waitDuration - this.pausedAt;\n return this.waitEndTime - new Date().getTime();\n };\n }\n //------------- /pausableTimeout --------\n //--------------------------------------\n\n\n //------------- sqAudio ----------------\n //--------------------------------------\n function sqAudio(fullPath, clipName, fileExt) {\n\n this.fullPath = fullPath;\n this.clipName = clipName; // Let a clip know its own name\n this.fileExt = fileExt;\n\n // Defaults\n this.volumeProportion = 1.0; // By default, full volume\n this.overlap = defaultOverlap; // By default, defaultOverlap ms\n this.isPlayable = false; // Assume audio is not playable\n this.looping = false; // Assume audio not looping\n this.alternate = false;\n this.mainAudio = new Audio();\n this.partnerAudio = new Audio();\n\n this.mainAudio.setAttribute("src", this.fullPath);\n if (this.mainAudio.canPlayType) {\n for (var i = -1; i < fileExtensions.length; i += 1) {\n if (i >= 0) fileExt = fileExtensions[i];\n if (this.mainAudio.canPlayType("audio/" + fileExt)) break;\n }\n if (i < fileExtensions.length) {\n this.mainAudio.interval = null;\n this.partnerAudio.setAttribute("src", this.fullPath);\n this.partnerAudio.interval = null;\n this.isPlayable = true;\n\n } else {\n console.log("Browser can't play '" + this.clipName + "'");\n }\n } \n\n // Convenience method for getting duration\n // TODO : protect this against audio not being loaded yet\n //\n this.getDuration = function () {\n\n return this.mainAudio.duration;\n };\n\n // Get what we consider the current audio track\n //\n this._getActiveAudio = function() {\n return (this.alternate) ? this.partnerAudio : this.mainAudio;\n };\n\n // Get what we consider the idle audio track\n //\n this._getIdleAudio = function() {\n return (this.alternate) ? this.mainAudio : this.partnerAudio;\n };\n\n\n\n // Perform fade on specified audio\n // Use ease\n //\n this.__fadeSound = function(audioObj, fadeIn) {\n\n var startVolume = fadeIn ? 0 : globalVolume * this.volumeProportion;\n var deltaVolume = globalVolume * this.volumeProportion * (fadeIn ? 1 : -1);\n\n //alert("__fadeSound! fadeIn " + fadeIn + ", globalVolume " + globalVolume + ", volProp " + this.volumeProportion + " startVol " + startVolume + " deltaVolume " + deltaVolume);\n\n // Handy vars for easing\n var totalIterations = this.overlap/updateInterval;\n var currentIteration = 1;\n\n audioObj.interval = setInterval(function() {\n\n //Use easing to prevent sound popping in or out\n //\n var desiredVolume = easeInOutSine(currentIteration, startVolume, deltaVolume, totalIterations);\n \n //alert("Well desiredVol is " + desiredVolume + " cos currIter " + currentIteration + " startVol " + startVolume + " delta vol " + deltaVolume + " total iter " + totalIterations);\n //This should never happen, but if it does, skip the fade\n if (isNaN(desiredVolume)) {\n audioObj.volume = startVolume + deltaVolume;\n console.log("There was a problem with the fade. Possibly overlap " + this.overlap + " is shorter than updateInterval " + updateInterval + "? ");\n } else {\n audioObj.volume = desiredVolume;\n }\n currentIteration += 1;\n \n if (audioObj.volume === (startVolume + deltaVolume)) { \n //alert("Grats! You reached your destination of " + audioObj.volume); \n clearInterval(audioObj.interval); \n }\n\n //This effectively stops the loop and poises the volume to be played again\n //That way the clip isn't needlessly looping when no one can hear it.\n if (audioObj.volume === 0) {\n audioObj.pause();\n audioObj.currentTime = 0;\n }\n }, updateInterval);\n\n };\n\n\n // Manages starting one loop before the last play has ended\n // and cross-fading the ends\n //\n this._crossfadeLoop = function(params) {\n\n var sqAudioObj = params[0];\n var currAudioObj = params[1];\n\n // Let loop expire if no longer looping\n //\n if (!sqAudioObj.looping) { return; }\n\n var nextAudioObj = sqAudioObj.alternate ? sqAudioObj.mainAudio : sqAudioObj.partnerAudio;\n sqAudioObj.alternate = !sqAudioObj.alternate;\n\n // Don't even bother with crossfade if there's no overlap\n if (sqAudioObj.overlap !== undefined && sqAudioObj.overlap > 1) {\n\n // fade out current sound\n //\n sqAudioObj._fadeSound(currAudioObj, false);\n\n // And fade in our partner\n //\n //nextAudioObj.volume = 0; \n //if (nextAudioObj.currentTime > 0) nextAudioObj.currentTime = 0;\n //nextAudioObj.play();\n sqAudioObj._fadeSound(nextAudioObj, true);\n\n }\n else {\n sqAudioObj.updateVolume(); \n nextAudioObj.currentTime = 0;\n nextAudioObj.play();\n }\n\n // Kick off the next timer to crossfade\n // Might as well garbage collect the old crossfadeTimeout, too.\n //\n //if (sqAudioObj.crossfadeTimeout !== undefined) { sqAudioObj.crossfadeTimeout.stopAndClear(); delete sqAudioObj.crossfadeTimeout; }\n //if (isNaN(sqAudioObj.getDuration())) { throwError("Can't loop because duration is not known (audio not loaded, probably not found.)"); return; }\n //sqAudioObj.crossfadeTimeout = new pausableTimeout(sqAudioObj._crossfadeLoop, [sqAudioObj, nextAudioObj]); \n //sqAudioObj.crossfadeTimeout.activate(sqAudioObj.getDuration()*1000-sqAudioObj.overlap);\n\n };\n\n\n this._fadeSound = function(activeAudioObj, fadeIn) {\n\n // Set the goal volume as a proportion of the global volume\n // (e.g. if global volume is 0.4, and volume proportion is 0.25, overall the goal volume is 0.1)\n //\n var goalVolume = globalVolume * this.volumeProportion;\n if (activeAudioObj.interval) clearInterval(activeAudioObj.interval);\n if (fadeIn) {\n if (activeAudioObj.currentTime > 0) activeAudioObj.currentTime = 0;\n activeAudioObj.volume = 0; \n this.loop();\n\n } else {\n\n if (!activeAudioObj.currentTime) return;\n activeAudioObj.volume = goalVolume;\n activeAudioObj.play();\n }\n this.__fadeSound(activeAudioObj, fadeIn);\n\n };\n\n\n // Fade sound on whatever the active audio is\n //\n this.fadeSound = function(fadeIn) {\n if (fadeIn) {\n this.stopAndClear();\n this.looping = true;\n }\n else this.looping = false;\n this._fadeSound(this._getActiveAudio(), fadeIn);\n };\n\n // Update volume proportion and volume of both audio clips\n //\n this.setVolumeProportion = function(volumeProportion) {\n this.volumeProportion = volumeProportion;\n };\n\n // Update volume of active audio clips (assumes vol proportion and global vol already set)\n //\n this.updateVolume = function() {\n\n //alert("about to set vol to " + globalVolume + " x " + this.volumeProportion);\n this._getActiveAudio().volume = globalVolume * this.volumeProportion;\n };\n\n // Play the current audio object and reactivate any paused timer\n //\n this.play = function(loop) {\n\n //If it's a loop we want, just loop and don't make a big deal out of it\n if (loop) this.loop();\n\n else {\n\n var activeAudioObj = this._getActiveAudio();\n if (activeAudioObj) { \n activeAudioObj.play();\n }\n }\n };\n\n // Pause whatever audio is currently playing and pause the timer, too\n //\n this.pause = function() {\n if (this.crossfadeTimeout !== undefined) this.crossfadeTimeout.deactivate();\n this._getActiveAudio().pause();\n };\n\n // Stop whatever audio is currently playing and dump the timer\n //\n this.stopAndClear = function() {\n var activeAudioObj = this._getActiveAudio();\n activeAudioObj.pause();\n if (activeAudioObj.currentTime > 0) activeAudioObj.currentTime = 0;\n if (this.crossfadeTimeout !== undefined) { this.crossfadeTimeout.stopAndClear(); delete this.crossfadeTimeout; }\n };\n\n\n // Loop the track\n //\n this.loop = function() {\n\n this.looping = true;\n var activeAudioObj = this._getActiveAudio();\n\n // Create new timeout if one does not already exist; otherwise just reuse the existing one\n //\n this.crossfadeTimeout = (this.crossfadeTimeout === undefined) ? new pausableTimeout(this._crossfadeLoop, [this, activeAudioObj]) : this.crossfadeTimeout; \n if (isNaN(this.getDuration())) { return throwError("Can't loop because duration is not known (audio not loaded, probably not found.)"); }\n this.crossfadeTimeout.activate((this.getDuration()*1000)-this.overlap);\n activeAudioObj.play();\n };\n\n\n }\n //------------ /sqAudio ----------------\n //--------------------------------------\n\n\n\n /***********************************************************\n * MAIN METHOD\n /***********************************************************\n /\n / Here be monsters. Proceed with caution.\n /\n */\n\n // Verify that the audio can be played in browser\n //\n function parseAudio(c) {\n\n var d = c.exec(div.innerHTML); // returns list of form ["url/to/audio.fileType",/to/audio,fileType]\n\n while(d) {\n if (d) {\n if (!clips.hasOwnProperty(d[1])) {\n\n var parser = document.createElement('a');\n parser.href = d[1].toString();\n var pathnameSubstrings = parser.pathname.split("/");\n var clipName = pathnameSubstrings[pathnameSubstrings.length-1];\n var sqAudioObj = new sqAudio(parser.href + "." + d[2].toString(), clipName, d[2].toString());\n if (sqAudioObj.isPlayable) { clips[clipName] = sqAudioObj;}\n }\n }\n d = c.exec(div.innerHTML); // yes, we could just do a do/while, but some envs don't like that\n }\n }\n\n // Parse all used audio file names\n // Use whatever store area element is available in the story format\n //\n var storeElement = (document.getElementById("store-area") ? document.getElementById("store-area") : document.getElementById("storeArea"));\n var div = storeElement.firstChild;\n while (div) {\n var b = String.fromCharCode(92);\n var q = '"';\n var re = "['" + q + "]([^" + q + "']*?)" + b + ".(" + fileExtensions.join("|") + ")['" + q + "]";\n parseAudio(new RegExp(re, "gi"));\n div = div.nextSibling;\n }\n /***********************************************************\n * END MAIN METHOD\n /***********************************************************/\n\n\n\n /***********************************************************\n * SUPPORTING FUNCTIONS FOR THE MACROS\n /***********************************************************\n /\n / Here be monsters.\n /\n */\n\n // Given the clipName, get the active soundtrack\n //\n function getSoundTrack(clipName) {\n clipName = cleanClipName(clipName.toString());\n if (!clips.hasOwnProperty(clipName)) { return throwError("Given clipName " + clipName + " does not exist in this project. Please check your variable names."); }\n return clips[clipName];\n\n }\n\n\n // Centralized function for sound fading\n //\n function fadeSound(clipName, fadeIn) {\n\n var soundtrack = getSoundTrack(clipName);\n if (soundtrack === "undefined") { return throwError("audio clip " + clipName + " not found"); } \n soundtrack.fadeSound(fadeIn);\n \n }\n\n\n // Adjust the volume of ALL audio in the page\n //\n function adjustVolume(direction) {\n\n // Note soundInterval and minVolume are declared globally (at top of the script)\n var maxVolume = 1.0; // This is native to JavaScript. Changing will cause unexpected behavior\n globalVolume = Math.max(minVolume, Math.min(maxVolume, globalVolume + (soundInterval * direction)));\n for (var soundIndex in clips) {\n if (clips.hasOwnProperty(soundIndex)) {\n clips[soundIndex].updateVolume();\n }\n }\n }\n\n // Common argument management\n // Because of the total expected arguments (one string, one float, one int, one boolean)\n // This method attempts to be forgiving of sequence. \n // Be advised if there were even one more argument, it probably couldn't be so forgiving anymore!\n //\n function manageCommonArgs(func, requiredArgs) {\n\n // Look at the list of available arguments, clean them up, and take the first one of each desired type:\n // Recreate the arguments as a list in required sequence [clipName, volumeProportion, overlap, loop]\n\n var clipName;\n var volumeProportion;\n var overlap;\n var loop;\n\n for (var i = 0; i < func.args.length; i++) {\n switch (typeof func.args[i]) {\n case "string" :\n if (clipName === undefined) clipName = func.args[i].toString();\n break;\n case "number" :\n var tempNum = parseFloat(func.args[i]);\n if (volumeProportion === undefined && tempNum <= 1.0) volumeProportion = tempNum;\n else if (overlap === undefined && tempNum >=updateInterval) overlap = tempNum; \n break;\n case "boolean" :\n if (loop === undefined) loop = func.args[i];\n break;\n }\n }\n\n for (var requiredArg in requiredArgs) {\n if (requiredArgs.hasOwnProperty(requiredArg)) {\n switch (requiredArg) {\n case clipNameLabel :\n if (clipName === undefined) { return throwError("No audio clip name specified."); } \n break;\n case volumeProportionLabel :\n if (volumeProportion === undefined || volumeProportion > 1.0 || volumeProportion < 0.0) { return throwError("No volume proportion specified (must be a decimal number no smaller than 0.0 and no bigger than 1.0.)"); }\n break;\n case overlapLabel :\n if (overlap === undefined) { return throwError("No fade duration specified (must be a number in milliseconds greater than + " + updateInterval + " ms.)"); }\n break;\n case loopLabel :\n if (loop === undefined) { return throwError("No loop flag provided (must be a boolean, aka true or false.)"); }\n break;\n }\n }\n }\n return [clipName, volumeProportion, overlap, loop];\n }\n\n // Get the clipName up to the . if a . exists, otherwise do no harm\n //\n function cleanClipName(clipName) {\n\n var parser = document.createElement('a');\n parser.href = clipName.toString();\n var pathnameSubstrings = parser.pathname.split("/");\n clipName = pathnameSubstrings[pathnameSubstrings.length-1];\n return clipName.lastIndexOf(".") > -1 ? clipName.slice(0, clipName.lastIndexOf(".")) : clipName;\n }\n\n\n /***********************************************************\n * END SUPPORTING FUNCTIONS FOR THE MACROS\n /***********************************************************/\n\n\n\n /***********************************************************\n /***********************************************************\n * MACROS\n /***********************************************************\n /***********************************************************\n */\n\n /* updatevolume\n \n Given a decimal between 0.0 and 1.0, \n updates the clip's volume proportion and the clip's actual volume\n \n */\n macros.add("updatevolume", {\n handler: function () {\n \n var args = manageCommonArgs(this, [clipNameLabel, volumeProportionLabel]);\n var soundtrack = getSoundTrack(this.args[0]);\n soundtrack.setVolumeProportion(args[1]);\n soundtrack.updateVolume();\n }\n });\n\n /** playsound \n\n This version of the macro lets you do a little bit of sound mixing.\n \n Parameters:\n\n REQUIRED: clipName \n OPTIONAL: decimal proportion of volume (0.0 being minimum/mute, and 1.0 being maximum/default)\n OPTIONAL: number of milliseconds to overlap/crossfade the loop (0 ms by default)\n OPTIONAL: true if you'd like to loop, false if no\n \n \n */\n macros.add("playsound", {\n handler : function () {\n\n var args = manageCommonArgs(this, [clipNameLabel]);\n\n var soundtrack = getSoundTrack(this.args[0]);\n var volumeProportion = args[1] !== undefined ? args[1] : soundtrack.volumeProportion;\n soundtrack.overlap = args[2] !== undefined ? args[2] : defaultOverlap;\n var loop = args[3] !== undefined ? args[3] : false;\n soundtrack.setVolumeProportion(volumeProportion);\n soundtrack.updateVolume();\n soundtrack.play(loop); \n }\n });\n\n\n /* playsounds\n \n Play multiple sounds at once (picking up where we left off)\n If you give it no sounds to play, it quietly ignores the command.\n\n Parameters:\n\n OPTIONAL: clipName\n OPTIONAL: decimal proportion of volume (0.0 being minimum/mute, and 1.0 being maximum/default)\n OPTIONAL: number of milliseconds to overlap/crossfade (0 ms by default)\n OPTIONAL: true if you'd like to loop, false if no\n \n /\n */\n macros.add("playsounds", {\n handler: function () {\n\n var clipNameString = this.args[0];\n if (this.args[0] === undefined || this.args[0] == "") return;\n clipNameString = this.args[0].toString();\n if (clipNameString == "[]") return;\n var clipNames = clipNameString.split(",");\n if (clipNames.length < 1) return;\n var args = manageCommonArgs(this);\n for (var index = 0; index < clipNames.length; index++) {\n var soundtrack = getSoundTrack(cleanClipName(clipNames[index]));\n var volumeProportion = args[1] !== undefined ? args[1] : soundtrack.volumeProportion;\n soundtrack.overlap = args[2] !== undefined ? args[2] : defaultOverlap;\n var loop = args[3] !== undefined ? args[3] : false;\n soundtrack.setVolumeProportion(volumeProportion);\n soundtrack.updateVolume();\n soundtrack.play(loop); \n }\n }\n });\n\n\n\n /* pausesound\n \n Pauses clip at its current location. \n Use playsound to resume it.\n\n Parameters:\n\n REQUIRED: clipName\n\n */ \n macros.add("pausesound", {\n handler: function() {\n var args = manageCommonArgs(this, [clipNameLabel]); \n getSoundTrack(this.args[0]).pause();\n }\n });\n\n\n /* <<pauseallsound>> \n \n Pauses all sounds at their current location. \n \n If you'd like the option to start multiple sounds,\n take a look at <<playsounds>> and <<fadeinsounds>>\n */ \n macros.add("pauseallsound", {\n handler: function () {\n for (var clipName in clips) {\n if (clips.hasOwnProperty(clipName)) {\n getSoundTrack(clipName).pause();\n }\n }\n }\n });\n\n /* stopsound\n \n Stop the given sound immediately\n If the sound is played again, it will play from the beginning\n \n Parameters:\n\n REQUIRED: clipName \n */ \n macros.add("stopsound", {\n handler: function() {\n var args = manageCommonArgs(this, [clipNameLabel]); \n getSoundTrack(this.args[0]).stopAndClear();\n }\n });\n\n\n /* <<stopallsound>>\n \n Stops all sounds immediately.\n If any stopped sound is played again, it will play from the beginning\n \n If you'd like the option to start multiple sounds,\n take a look at <<playsounds>> and <<fadeinsounds>>\n */ \n macros.add("stopallsound", {\n handler: function () {\n for (var clipName in clips) {\n if (clips.hasOwnProperty(clipName)) {\n if (clips[clipName] !== undefined) clips[clipName].stopAndClear();\n }\n }\n }\n });\n\n /* loopsound\n \n Starts playing the given clip on repeat.\n Note that browsers will not necessarily play looping audio seamlessly.\n For seamless audio, use a fade duration/overlap (third parameter) greater than 1 millisecond\n (Well, you probably want something more perceptibe than 1 millisecond!)\n \n Parameters:\n\n REQUIRED: clipName \n OPTIONAL: decimal proportion of volume (0.0 being minimum/mute, and 1.0 being maximum/default)\n OPTIONAL: number of milliseconds to overlap/crossfade the loop (0 ms by default)\n */ \n macros.add("loopsound", {\n handler: function () {\n \n var args = manageCommonArgs(this, [clipNameLabel]);\n var soundtrack = getSoundTrack(this.args[0]);\n var volumeProportion = args[1] !== undefined ? args[1] : soundtrack.volumeProportion;\n soundtrack.overlap = args[2] !== undefined ? args[2] : defaultOverlap;\n soundtrack.setVolumeProportion(volumeProportion);\n soundtrack.updateVolume();\n soundtrack.loop();\n }\n });\n\n\n /* unloopsound \n \n Let the given sound stop when it finishes its current loop\n (so the sound no longer repeats.)\n\n Parameters:\n\n REQUIRED: clipName \n\n */ \n macros.add("unloopsound", {\n handler: function () {\n var args = manageCommonArgs(this, [clipNameLabel]); \n getSoundTrack(this.args[0]).looping = false;\n }\n });\n\n\n /* fadeinsound\n \n Identical to loopsound, but fades in the sound over 2 seconds.\n\n Parameters:\n\n REQUIRED: clipName\n OPTIONAL: decimal proportion of volume (0.0 being minimum/mute, and 1.0 being maximum/default)\n OPTIONAL: number of milliseconds to overlap/crossfade the loop (defaults to clip's last set overlap)\n\n */\n macros.add("fadeinsound", {\n handler: function () {\n\n var args = manageCommonArgs(this, [clipNameLabel]);\n \n var soundtrack = getSoundTrack(this.args[0]);\n var volumeProportion = args[1] !== undefined ? args[1] : soundtrack.volumeProportion; \n soundtrack.overlap = args[2] !== undefined ? args[2] : soundtrack.overlap;\n soundtrack.volumeProportion=volumeProportion;\n soundtrack.fadeSound(true);\n }\n });\n\n /* fadeinsounds\n\n Fade in multiple sounds at once.\n \n Parameters:\n\n REQUIRED: clipNames as list \n OPTIONAL: decimal proportion of volume (0.0 being minimum/mute, and 1.0 being maximum/default)\n OPTIONAL: number of milliseconds to overlap/crossfade the loop (defaults to clip's last set overlap)\n \n */\n macros.add("fadeinsounds", {\n handler: function () {\n\n var clipNameString = this.args[0];\n if (this.args[0] === undefined || this.args[0] == "") return;\n clipNameString = this.args[0].toString();\n if (clipNameString == "[]") return;\n var clipNames = clipNameString.split(",");\n if (clipNames.length < 1) return;\n\n var args = manageCommonArgs(this);\n\n for (var index = 0; index < clipNames.length; index++) {\n var soundtrack = getSoundTrack(this.args[0]);\n var volumeProportion = args[1] !== undefined ? args[1] : soundtrack.volumeProportion; \n soundtrack.overlap = args[2] !== undefined ? args[2] : soundtrack.overlap;\n soundtrack.volumeProportion=volumeProportion;\n soundtrack.fadeSound(true); \n }\n }\n });\n\n /* fadeoutsound\n \n Identical to stopsound, but fades out the sound over the stored fade duration (overlap).\n \n Parameters:\n\n REQUIRED: clipName \n\n */\n macros.add("fadeoutsound", {\n handler: function () {\n var args = manageCommonArgs(this, [clipNameLabel]); \n fadeSound(this.args[0].toString(), false);\n }\n });\n\n\n /* fadeoutsounds\n \n Fade out multiple sounds at once.\n If you give it no sounds to play, it quietly ignores the command.\n\n Parameters:\n\n REQUIRED: clipNames as list \n \n */\n macros.add("fadeoutsounds", {\n handler: function () {\n\n var clipNameString = this.args[0];\n if (this.args[0] === undefined) return;\n clipNameString = this.args[0].toString();\n if (clipNameString == "[]") return;\n var clipNames = clipNameString.split(",");\n if (clipNames.length < 1) return;\n\n for (var index = 0; index < clipNames.length; index++) {\n fadeSound(cleanClipName(clipNames[index]), false);\n }\n }\n });\n\n\n /* <<quieter>>\n \n Reduces the story's globalVolume by 1/10th of the reader's system volume.\n Thus creates a 10-unit volume range for the story\n \n */\n macros.add("quieter", {\n handler: function () {\n adjustVolume(-1);\n }\n });\n\n /* <<louder>>\n \n Increases the story's globalVolume by 1/10th of the reader's system volume.\n Thus creates a 10-unit volume range for the story\n \n */\n macros.add("louder", {\n handler: function () {\n adjustVolume(1);\n }\n });\n\n\n /* jumpscare\n \n Play the clip at maximum story volume\n Don't affect any stored volume options\n PLEASE GIVE THE READER A STARTLE WARNING BEFORE USING THIS.\n \n */\n macros.add("jumpscare", {\n handler: function () {\n var args = manageCommonArgs(this, [clipNameLabel]);\n var soundtrack = getSoundTrack(this.args[0]);\n soundtrack.setVolumeProportion(1.0);\n soundtrack.updateVolume();\n soundtrack.play();\n }\n });\n\n /***********************************************************\n * END MACROS\n /***********************************************************/\n\n\n\n}());\n\n// You read the whole thing! THAT'S PRETTY RAD. Keep up the good work, and happy Twining.\n\n
<div class="audioButton" align="right"><<button "[]">><<stopallsound>><<set $sound = "no">><</button>> \s\n<<button "<)">><<quieter>><</button>> \s\n<<button "<)))">><<louder>><</button>> \s\n<<button "|>">><<fadeinsounds $currentLoops>><<set $sound = "yes">><</button>></div>\s
\n<iframe src="lockPickPrologue.html" width="650px" height="400px">\n </iframe>\n\nThe first time I popped it, I said:\n<p class="ind">- [[That was messy.|Prologue02]]</p>\s\n<p class="ind">- [[That was glorious.|Prologue02]]</p>\s\n<p class="ind"><div class="dayForm">- <<textbox "$puzzleOutcome" "">> <<button [[Submit|Prologue02]]>><<hash $puzzleOutcome puzzleOutcomeHash>><</button>></div></p>\s
<h2><center>Big Nobody</center></h2><h4><center>Credits and How-Tos</center></h4>\n(All links open in new tabs.)\n\n<h3>Text</h3>\s\nText by Tory Hoke. More work appears at her <a href="http://www.toryhoke.com" target="_blank">blog</a>. Follow her on Twitter <a href="http://www.twitter.com/toryhoke" target="_blank">@toryhoke</a>.\n\n<h3>Art</h3>\s\nArt by Tory Hoke. View her <a href="http://www.toryhoke.com/portfolio" target="_blank">portfolio</a>.\n\n<h3>Sound</h3>\s\n\nText and images appear under a Creative Commons Attribution-NonCommercial-ShareAlike License.\nSound acquired from <a href="http://www.audiojungle.net" target="_blank">AudioJungle</a> for single-use license only. Please do not reuse without a license.\n<ul><li><a href="" target="_blank"></a> by </li>\n</ul>\s\n\n<h3>Interactivity</h3>\s\nInteractivity by Tory Hoke on the following foundation:\n<ul>\n<li>Twine can be downloaded at <a href="http://twinery.org/" target="_blank">Twinery.org</a></li>\n<li><a href="http://www.motoslave.net/sugarcube/">Sugarcube story format v 1.0.3</a> by Thomas Michael Edwards</li>\n<li><a href="http://www.puzzlescript.net/">PuzzleScript</a> (engine for "Pick the Lock") by Stephen Lavell</li>\n<li>Sound macros by <a href="http://sub-q.com/plugins/sqtwinesound/" target="_blank">sub-Q</a> (while site is in beta, use password: ThinkVast).</li>\n<li>Timed replace macro and CSS by <a href="http://www.glorioustrainwrecks.com/blog/584">Leon Arnott</a>.</li>\n<li>Style Changer macro (changing background colors) learned from the work of <a href="http://www.lifeinneon.com/mygames/" target="_blank">Lydia Neon</a>.</li>\n<li>Styling "submit" button learned from <a href="http://twinery.org/forum/index.php/topic,707.msg959.html#msg959" target="_blank">this post by L</a> (CSS selector is ".passage button")</li>\n<li>CSS for a pretty "submit" button acquired via <a href="http://www.bestcssbuttongenerator.com/" target="_blank">Button X</a>.</li></ul>\s\n\nFeel free to import this HTML file into Twine for more details, and happy Twining.
/* Your story will use the CSS in this passage to style the page.\nGive this passage more tags, and it will only affect passages with those tags.\nExample selectors: */\n\n@import url(http://fonts.googleapis.com/css?family=Lato|Vollkorn|Cabin);\n\nbody {\n\t/* This affects the entire page */\n\tbackground-color: #003768;\n\tcolor: #CCC;\n\tmargin: 0;\n\tfont-family: 'Vollkorn', serif;\n}\n\na {\tcolor: #CC6666; }\na:hover { color: #FF6633; }\na:hover, a:visited, a:link, a:active\n{ text-decoration: none; }\n\n\n\ntd { vertical-align: top; }\ntd.align-right { text-align: right; }\ntd.align-center { text-align: center; }\n\np.ind { \n\ttext-indent: 2em;\n}\n\ninput, textarea {\n\tcolor: #fff;\n\tbackground-color:#F7F2E4;\n\tborder: 1px solid #fff;\n}\ninput[type=text] {\n\tcolor:#333;\n\tfont-family: 'Vollkorn', serif;\n\tfont-size:16px;\n\tfont-weight:bold;\n\tpadding: 3px 10px;\n\ttext-decoration:none;\n\ttext-shadow:0px 1px 0px #fffff;\n\twidth: 8em;\n\tmin-width: 4em;\n}\ninput:focus, textarea:focus {\n\tborder: 1px solid #FF6633;\n}\ninput:hover, textarea:hover {\n\tborder: 1px solid #CC6666;\n}\n\n#ui-bar {\n\twidth: 125px;\n\tbackground-color: #003768;\n\tposition: relative;\n\ttop: 0;\n\tleft: 0;\n\twidth: auto;\n\theight: auto;\n\tmargin: 0;\n\tpadding: 2.5% 3.5% 0;\n\tborder: none;\n\tborder-bottom: 1px solid #fff;\n\tfont-family: 'Lato', sans-serif;\n\n}\n#ui-body {\n\tmax-width: 100%;\n\tmax-height: 100%;\n}\n\n\t#ui-bar header, #story-caption, #ui-bar footer {\n\t\ttext-align: left;\n\t\twidth: 66%;\n\t}\n\t#menu {\n\t\tposition: absolute;\n\t\ttop: 0;\n\t\tright: 0;\n\t\tmargin: 2.5% 3.5% 0 0;\n\t}\n\t#menu ul {\n\t\tmargin: 0;\n\t}\n\t#menu li {\n\t\tmargin-bottom: 0.5em;\n\t}\n\t#menu-story {\n\t\tmargin-bottom: 0.7em;\n\t\tline-height: 0.85;\n\t}\n\n\n\n#passages {\n\tmargin-left: 0px;\n\theight:100%;\n\tmin-height: 100%;\n\tpadding-bottom: 0;\n\tmargin-bottom: 0;\n\tborder: 0;\n\twidth: auto;\n\tmargin: 1.5em 3.5% 3.5%;\n\tfont-family: 'Vollkorn', serif;\n}\n\n\n#passages.day {\tcolor: #333; }\n#passages.day a {\n\t/* This affects passage links */\n\tcolor: #CC6666;\t\n}\n#passages.day a:hover {\n\t/* This affects links while the cursor is over them */\n\tcolor: #FF6633;\n}\n#passages.day a:hover, a:visited, a:link, a:active\n{\n text-decoration: none;\n}\n\n#passages.night { color: #DDD; }\n#passages.night a {\n\t/* This affects passage links */\n\tcolor: #66CCFF;\n}\n#passages.night a:hover {\n\t/* This affects links while the cursor is over them */\n\tcolor: #D1F0FF;\n}\n#passages.night a:hover, a:visited, a:link, a:active\n{\n text-decoration: none;\n}\n\n\n\n\n.passage {\n\t/* This only affects passages */\n\t/* font */\n\tfont-size: medium;\n\n\t/* transition */\n\ttransition: 2.0s;\n\t-webkit-transition: 2.0s;\n\n}\n\n\n.transition-in {\n\topacity: 0;\n\tposition: absolute;\n}\n.transition-out {\n\topacity: 0 !important;\n\tposition: absolute;\n}\n\n.revision-span-in {\n\topacity: 0;\n}\n.revision-span:not(.revision-span-out) {\n\ttransition: 2s; \n\t-webkit-transition: 2s;\n}\n.revision-span-out {\n\tposition:absolute;\n\topacity: 0;\n}\n\n\n.timedreplacement.replacement-in {\n\topacity: 0;\n}\n.timedreplacement {\n\ttransition: 0s;\n\t-webkit-transition: 0s;\n}\n.timedreplacement.replacement-out {\n\topacity: 0;\n}\n\n\n\n.disabled { display:none; }\n\n\n\n\n\n\n\n.dayForm button {\n\t-moz-box-shadow:inset 0px 1px 0px 0px #ffffff;\n\t-webkit-box-shadow:inset 0px 1px 0px 0px #ffffff;\n\tbox-shadow:inset 0px 1px 0px 0px #ffffff;\n\tbackground:-webkit-gradient(linear, left top, left bottom, color-stop(0.05, #E0DDD4), color-stop(1, #F7F2E4));\n\tbackground:-moz-linear-gradient(top, #E0DDD4 5%, #F7F2E4 100%);\n\tbackground:-webkit-linear-gradient(top, #E0DDD4 5%, #F7F2E4 100%);\n\tbackground:-o-linear-gradient(top, #E0DDD4 5%, #F7F2E4 100%);\n\tbackground:-ms-linear-gradient(top, #E0DDD4 5%, #F7F2E4 100%);\n\tbackground:linear-gradient(to bottom, #E0DDD4 5%, #F7F2E4 100%);\n\tfilter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#E0DDD4', endColorstr='#F7F2E4',GradientType=0);\n\tbackground-color:#F7F2E4;\n\t-moz-border-radius:2px;\n\t-webkit-border-radius:2px;\n\tborder-radius:2px;\n\tborder:1px solid #dcdcdc;\n\tdisplay:inline-block;\n\tcursor:pointer;\n\tcolor:#CC6666;\n\tfont-family: 'Vollkorn', serif;\n\tfont-size:16px;\n\tfont-weight:bold;\n\tpadding:5px 10px;\n\ttext-decoration:none;\n\ttext-shadow:0px 1px 0px #ffffff;\n}\n.dayForm button:hover {\n\tbackground:-webkit-gradient(linear, left top, left bottom, color-stop(0.05, #F7F2E4), color-stop(1, #E0DDD4));\n\tbackground:-moz-linear-gradient(top, #F7F2E4 5%, #E0DDD4 100%);\n\tbackground:-webkit-linear-gradient(top, #F7F2E4 5%, #E0DDD4 100%);\n\tbackground:-o-linear-gradient(top, #F7F2E4 5%, #E0DDD4 100%);\n\tbackground:-ms-linear-gradient(top, #F7F2E4 5%, #E0DDD4 100%);\n\tbackground:linear-gradient(to bottom, #F7F2E4 5%, #E0DDD4 100%);\n\tfilter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#F7F2E4', endColorstr='#E0DDD4',GradientType=0);\n\tbackground-color:#F9F6EC;\n}\n.dayForm button:active {\n\tposition:relative;\n\ttop:1px;\n}\n\n\n\n\n\n\n.audioButton button {\n\t-moz-box-shadow:inset 0px 1px 0px 0px #ffffff;\n\t-webkit-box-shadow:inset 0px 1px 0px 0px #ffffff;\n\tbox-shadow:inset 0px 1px 0px 0px #ffffff;\n\tbackground:-webkit-gradient(linear, left top, left bottom, color-stop(0.05, #E0DDD4), color-stop(1, #F7F2E4));\n\tbackground:-moz-linear-gradient(top, #E0DDD4 5%, #F7F2E4 100%);\n\tbackground:-webkit-linear-gradient(top, #E0DDD4 5%, #F7F2E4 100%);\n\tbackground:-o-linear-gradient(top, #E0DDD4 5%, #F7F2E4 100%);\n\tbackground:-ms-linear-gradient(top, #E0DDD4 5%, #F7F2E4 100%);\n\tbackground:linear-gradient(to bottom, #E0DDD4 5%, #F7F2E4 100%);\n\tfilter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#E0DDD4', endColorstr='#F7F2E4',GradientType=0);\n\tbackground-color:#E0DDD4;\n\t-moz-border-radius:4px;\n\t-webkit-border-radius:4px;\n\tborder-radius:4px;\n\tborder:1px solid #dcdcdc;\n\tdisplay:inline-block;\n\tcursor:pointer;\n\tcolor:#777777;\n\tfont-family:Verdana;\n\tfont-size:10px;\n\tpadding:4px 3px;\n\ttext-decoration:none;\n\ttext-shadow:0px 1px 0px #ffffff;\n}\n.audioButton button:hover {\n\tbackground:-webkit-gradient(linear, left top, left bottom, color-stop(0.05, #F7F2E4), color-stop(1, #E0DDD4));\n\tbackground:-moz-linear-gradient(top, #F7F2E4 5%, #E0DDD4 100%);\n\tbackground:-webkit-linear-gradient(top, #F7F2E4 5%, #E0DDD4 100%);\n\tbackground:-o-linear-gradient(top, #F7F2E4 5%, #E0DDD4 100%);\n\tbackground:-ms-linear-gradient(top, #F7F2E4 5%, #E0DDD4 100%);\n\tbackground:linear-gradient(to bottom, #F7F2E4 5%, #E0DDD4 100%);\n\tfilter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#F7F2E4', endColorstr='#E0DDD4',GradientType=0);\n\tbackground-color:#F7F2E4;\n}\n.audioButton button:active {\n\tposition:relative;\n\ttop:1px;\n}\n\n
Big Nobody
// Replace macros created by Glorious Trainwrecks\n// http://www.glorioustrainwrecks.com/blog/584\n\n\n\n(function() {\n version.extensions.replaceMacrosCombined = {\n major: 1,\n minor: 1,\n revision: 4\n };\n var nullobj = {\n handler: function() {}\n };\n\n function showVer(n, notrans) {\n if (!n) {\n return\n }\n n.innerHTML = "";\n new Wikifier(n, n.tweecode);\n n.setAttribute("data-enabled", "true");\n n.style.display = "inline";\n n.classList.remove("revision-span-out");\n if (!notrans) {\n n.classList.add("revision-span-in");\n if (n.timeout) {\n clearTimeout(n.timeout)\n }\n n.timeout = setTimeout(function() {\n n.classList.remove("revision-span-in");\n n = null\n }, 1)\n }\n }\n\n function hideVer(n, notrans) {\n if (!n) {\n return\n }\n n.setAttribute("data-enabled", "false");\n n.classList.remove("revision-span-in");\n if (n.timeout) {\n clearTimeout(n.timeout)\n }\n if (!notrans) {\n n.classList.add("revision-span-out");\n n.timeout = setTimeout(function() {\n if (n.getAttribute("data-enabled") == "false") {\n n.classList.remove("revision-span-out");\n n.style.display = "none";\n n.innerHTML = ""\n }\n n = null\n }, 1000)\n } else {\n n.style.display = "none";\n n.innerHTML = "";\n n = null\n }\n }\n\n function tagcontents(b, starttags, desttags, endtags, k) {\n var l = 0,\n c = "",\n tg, a, i;\n\n function tagfound(i, e) {\n for (var j = 0; j < e.length; j++) {\n if (a.indexOf("<<" + e[j], i) == i) {\n return e[j]\n }\n }\n }\n a = b.source.slice(k);\n for (i = 0; i < a.length; i++) {\n if (tg = tagfound(i, starttags)) {\n l++\n } else {\n if ((tg = tagfound(i, desttags)) && l == 0) {\n b.nextMatch = k + i + tg.length + 4;\n return [c, tg]\n } else {\n if (tg = tagfound(i, endtags)) {\n l--;\n if (l < 0) {\n return null\n }\n }\n }\n }\n c += a.charAt(i)\n }\n return null\n }\n var begintags = [];\n var endtags = [];\n\n function revisionSpanHandler(g, e, f, b) {\n var k = b.source.indexOf(">>", b.matchStart) + 2,\n vsns = [],\n vtype = e,\n flen = f.length,\n becomes, c, cn, m, h, vsn;\n\n function mkspan(vtype) {\n h = insertElement(m, "span", null, "revision-span " + vtype);\n h.setAttribute("data-enabled", false);\n h.style.display = "none";\n h.tweecode = "";\n return h\n }\n if (this.shorthand && flen) {\n while (f.length > 0) {\n vsns.push([f.shift(), (this.flavour == "insert" ? "gains" : "becomes")])\n }\n } else {\n if (this.flavour == "insert" || (this.flavour == "continue" && this.trigger == "time")) {\n vsns.push(["", "becomes"])\n }\n } if (this.flavour == "continue" && flen) {\n b.nextMatch = k + b.source.slice(k).length;\n vsns.push([b.source.slice(k), vtype])\n } else {\n becomes = ["becomes", "gains"];\n c = tagcontents(b, begintags, becomes.concat(endtags), endtags, k);\n if (c && endtags.indexOf(c[1]) == -1) {\n while (c) {\n vsns.push(c);\n c = tagcontents(b, begintags, becomes, endtags, b.nextMatch)\n }\n c = tagcontents(b, begintags, ["end" + e], endtags, b.nextMatch)\n }\n if (!c) {\n throwError(g, "can't find matching end" + e);\n return\n }\n vsns.push(c);\n if (this.flavour == "continue") {\n k = b.nextMatch;\n b.nextMatch = k + b.source.slice(k).length;\n vsns.push([b.source.slice(k), ""])\n }\n } if (this.flavour == "remove") {\n vsns.push(["", "becomes"])\n }\n cn = 0;\n m = insertElement(g, "span", null, e);\n m.setAttribute("data-flavour", this.flavour);\n h = mkspan("initial");\n vsn = vsns.shift();\n h.tweecode = vsn[0];\n showVer(h, true);\n while (vsns.length > 0) {\n if (vsn) {\n vtype = vsn[1]\n }\n vsn = vsns.shift();\n h = mkspan(vtype);\n h.tweecode = vsn[0]\n }\n if (typeof this.setup == "function") {\n this.setup(m, g, f)\n }\n }\n\n function quantity(m) {\n return (m.children.length - 1) + (m.getAttribute("data-flavour") == "remove")\n }\n\n function revisionSetup(m, g, f) {\n m.className += " " + f[0].replace(" ", "_")\n }\n\n function keySetup(m, g, f) {\n var key = f[0];\n m.setEventListener("keydown", function l(e) {\n var done = !revise("revise", m);\n if (done) {\n m.removeEventListener("keydown", l)\n }\n })\n }\n\n function timeSetup(m, g, f) {\n function cssTimeUnit(s) {\n if (typeof s == "string") {\n if (s.slice(-2).toLowerCase() == "ms") {\n return Number(s.slice(0, -2)) || 0\n } else {\n if (s.slice(-1).toLowerCase() == "s") {\n return Number(s.slice(0, -1)) * 1000 || 0\n }\n }\n }\n throwError(g, s + " isn't a CSS time unit");\n return 0\n }\n var tm = cssTimeUnit(f[0]);\n setTimeout(function timefn() {\n var done = !revise("revise", m);\n if (!done) {\n setTimeout(timefn, tm)\n }\n }, tm)\n }\n\n function hoverSetup(m) {\n var fn, noMouseEnter = (document.head.onmouseenter !== null),\n m1 = m.children[0],\n m2 = m.children[1];\n if (!m1 || !m2) {\n return\n }\n m1.onmouseenter = function(e) {\n if (this.getAttribute("data-enabled") != "false") {\n revise("revise", this.parentNode)\n }\n };\n m2.onmouseleave = function(e) {\n if (this.getAttribute("data-enabled") != "false") {\n revise("revert", this.parentNode)\n }\n };\n if (noMouseEnter) {\n fn = function(n) {\n return function(e) {\n if (!event.relatedTarget || (event.relatedTarget != this && !(this.compareDocumentPosition(event.relatedTarget) & Node.DOCUMENT_POSITION_CONTAINED_BY))) {\n this[n]()\n }\n }\n };\n m1.onmouseover = fn("onmouseenter");\n m2.onmouseout = fn("onmouseleave")\n }\n m = null\n }\n\n function mouseSetup(m) {\n var evt = (document.head.onmouseenter === null ? "onmouseenter" : "onmouseover");\n m[evt] = function() {\n var done = !revise("revise", this);\n if (done) {\n this[evt] = null\n }\n };\n m = null\n }\n\n function linkSetup(m, g, f) {\n var l = Wikifier.createInternalLink(),\n p = m.parentNode;\n l.className = "internalLink replaceLink";\n p.insertBefore(l, m);\n l.insertBefore(m, null);\n l.onclick = function() {\n var p, done = false;\n if (m && m.parentNode == this) {\n done = !revise("revise", m);\n scrollWindowTo(m)\n }\n if (done) {\n this.parentNode.insertBefore(m, this);\n this.parentNode.removeChild(this)\n }\n };\n l = null\n }\n\n function visitedSetup(m, g, f) {\n var i, done, shv = state.history[0].variables,\n os = "once seen",\n d = (m.firstChild && (this.flavour == "insert" ? m.firstChild.nextSibling : m.firstChild).tweecode);\n shv[os] = shv[os] || {};\n if (d && !shv[os].hasOwnProperty(d)) {\n shv[os][d] = 1\n } else {\n for (i = shv[os][d]; i > 0 && !done; i--) {\n done = !revise("revise", m, true)\n }\n if (shv[os].hasOwnProperty(d)) {\n shv[os][d] += 1\n }\n }\n }[{\n name: "insert",\n flavour: "insert",\n trigger: "link",\n setup: linkSetup\n }, {\n name: "timedinsert",\n flavour: "insert",\n trigger: "time",\n setup: timeSetup\n }, {\n name: "insertion",\n flavour: "insert",\n trigger: "revisemacro",\n setup: revisionSetup\n }, {\n name: "later",\n flavour: "insert",\n trigger: "visited",\n setup: visitedSetup\n }, {\n name: "keyinsert",\n flavour: "insert",\n trigger: "key",\n setup: keySetup\n }, {\n name: "replace",\n flavour: "replace",\n trigger: "link",\n setup: linkSetup\n }, {\n name: "timedreplace",\n flavour: "replace",\n trigger: "time",\n setup: timeSetup\n }, {\n name: "mousereplace",\n flavour: "replace",\n trigger: "mouse",\n setup: mouseSetup\n }, {\n name: "hoverreplace",\n flavour: "replace",\n trigger: "hover",\n setup: hoverSetup\n }, {\n name: "revision",\n flavour: "replace",\n trigger: "revisemacro",\n setup: revisionSetup\n }, {\n name: "keyreplace",\n flavour: "replace",\n trigger: "key",\n setup: keySetup\n }, {\n name: "timedremove",\n flavour: "remove",\n trigger: "time",\n setup: timeSetup\n }, {\n name: "mouseremove",\n flavour: "remove",\n trigger: "mouse",\n setup: mouseSetup\n }, {\n name: "hoverremove",\n flavour: "remove",\n trigger: "hover",\n setup: hoverSetup\n }, {\n name: "removal",\n flavour: "remove",\n trigger: "revisemacro",\n setup: revisionSetup\n }, {\n name: "once",\n flavour: "remove",\n trigger: "visited",\n setup: visitedSetup\n }, {\n name: "keyremove",\n flavour: "remove",\n trigger: "key",\n setup: keySetup\n }, {\n name: "continue",\n flavour: "continue",\n trigger: "link",\n setup: linkSetup\n }, {\n name: "timedcontinue",\n flavour: "continue",\n trigger: "time",\n setup: timeSetup\n }, {\n name: "mousecontinue",\n flavour: "continue",\n trigger: "mouse",\n setup: mouseSetup\n }, {\n name: "keycontinue",\n flavour: "continue",\n trigger: "key",\n setup: keySetup\n }, {\n name: "cycle",\n flavour: "cycle",\n trigger: "revisemacro",\n setup: revisionSetup\n }, {\n name: "mousecycle",\n flavour: "cycle",\n trigger: "mouse",\n setup: mouseSetup\n }, {\n name: "timedcycle",\n flavour: "cycle",\n trigger: "time",\n setup: timeSetup\n }, {\n name: "keycycle",\n flavour: "replace",\n trigger: "key",\n setup: keySetup\n }].forEach(function(e) {\n e.handler = revisionSpanHandler;\n e.shorthand = (["link", "mouse", "hover"].indexOf(e.trigger) > -1);\n macros[e.name] = e;\n macros["end" + e.name] = nullobj;\n begintags.push(e.name);\n endtags.push("end" + e.name)\n });\n\n function insideDepartingSpan(elem) {\n var r = elem.parentNode;\n while (!r.classList.contains("passage")) {\n if (r.classList.contains("revision-span-out")) {\n return true\n }\n r = r.parentNode\n }\n }\n\n function reviseAll(rt, rname) {\n var rall = document.querySelectorAll(".passage [data-flavour]." + rname),\n ret = false;\n for (var i = 0; i < rall.length; i++) {\n if (!insideDepartingSpan(rall[i])) {\n ret = revise(rt, rall[i]) || ret\n }\n }\n return ret\n }\n\n function revise(rt, r, notrans) {\n var ind2, curr, next, ind = -1,\n rev = (rt == "revert"),\n rnd = (rt.indexOf("random") > -1),\n fl = r.getAttribute("data-flavour"),\n rc = r.childNodes,\n cyc = (fl == "cycle"),\n rcl = rc.length - 1;\n\n function doToGainerSpans(n, fn) {\n for (var k = n - 1; k >= 0; k--) {\n if (rc[k + 1].classList.contains("gains")) {\n fn(rc[k], notrans)\n } else {\n break\n }\n }\n }\n for (var k = 0; k <= rcl; k++) {\n if (rc[k].getAttribute("data-enabled") == "true") {\n ind = k\n }\n }\n if (rev) {\n ind -= 1\n }\n curr = (ind >= 0 ? rc[ind] : (cyc ? rc[rcl] : null));\n ind2 = ind;\n if (rnd) {\n ind2 = (ind + (Math.floor(Math.random() * rcl))) % rcl\n }\n next = ((ind2 < rcl) ? rc[ind2 + 1] : (cyc ? rc[0] : null));\n var docurr = (rev ? showVer : hideVer);\n var donext = (rev ? hideVer : showVer);\n var currfn = function() {\n if (!(next && next.classList.contains("gains")) || rnd) {\n docurr(curr, notrans);\n doToGainerSpans(ind, docurr, notrans)\n }\n };\n var nextfn = function() {\n donext(next, notrans);\n if (rnd) {\n doToGainerSpans(ind2 + 1, donext, notrans)\n }\n };\n if (!rev) {\n currfn();\n nextfn()\n } else {\n nextfn();\n currfn()\n }\n return (cyc ? true : (rev ? (ind > 0) : (ind2 < rcl - 1)))\n }\n macros.revert = macros.revise = macros.randomise = macros.randomize = {\n handler: function(a, b, c) {\n var l, rev, rname;\n\n function disableLink(l) {\n l.style.display = "none"\n }\n\n function enableLink(l) {\n l.style.display = "inline"\n }\n\n function updateLink(l) {\n if (l.className.indexOf("random") > -1) {\n enableLink(l);\n return\n }\n var rall = document.querySelectorAll(".passage [data-flavour]." + rname),\n cannext, canprev, i, ind, r, fl;\n for (i = 0; i < rall.length; i++) {\n r = rall[i], fl = r.getAttribute("data-flavour");\n if (insideDepartingSpan(r)) {\n continue\n }\n if (fl == "cycle") {\n cannext = canprev = true\n } else {\n if (r.firstChild.getAttribute("data-enabled") == !1 + "") {\n canprev = true\n }\n if (r.lastChild.getAttribute("data-enabled") == !1 + "") {\n cannext = true\n }\n }\n }\n var can = (l.classList.contains("revert") ? canprev : cannext);\n (can ? enableLink : disableLink)(l)\n }\n\n function toggleText(w) {\n w.classList.toggle(rl + "Enabled");\n w.classList.toggle(rl + "Disabled");\n w.style.display = ((w.style.display == "none") ? "inline" : "none")\n }\n var rl = "reviseLink";\n if (c.length < 2) {\n throwError(a, b + " macro needs 2 parameters");\n return\n }\n rname = c.shift().replace(" ", "_");\n l = Wikifier.createInternalLink(a, null);\n l.className = "internalLink " + rl + " " + rl + "_" + rname + " " + b;\n var v = "";\n var end = false;\n var out = false;\n if (c.length > 1 && c[0][0] == "$") {\n v = c[0].slice(1);\n c.shift()\n }\n switch (c[c.length - 1]) {\n case "end":\n end = true;\n c.pop();\n break;\n case "out":\n out = true;\n c.pop();\n break\n }\n var h = state.history[0].variables;\n for (var i = 0; i < c.length; i++) {\n var on = (i == Math.max(c.indexOf(h[v]), 0));\n var d = insertElement(null, "span", null, rl + ((on) ? "En" : "Dis") + "abled");\n if (on) {\n h[v] = c[i];\n l.setAttribute("data-cycle", i)\n } else {\n d.style.display = "none"\n }\n insertText(d, c[i]);\n l.appendChild(d)\n }\n l.onclick = function() {\n reviseAll(b, rname);\n var t = this.childNodes,\n u = this.getAttribute("data-cycle") - 0,\n m = t.length,\n n, lall, i;\n if ((end || out) && u == m - (end ? 2 : 1)) {\n if (end) {\n n = this.removeChild(t[u + 1]);\n n.className = rl + "End";\n n.style.display = "inline";\n this.parentNode.replaceChild(n, this)\n } else {\n this.parentNode.removeChild(this);\n return\n }\n } else {\n toggleText(t[u]);\n u = (u + 1) % m;\n if (v) {\n h[v] = c[u]\n }\n toggleText(t[u]);\n this.setAttribute("data-cycle", u)\n }\n lall = document.getElementsByClassName(rl + "_" + rname);\n for (i = 0; i < lall.length; i++) {\n updateLink(lall[i])\n }\n };\n l = null\n }\n };\n macros.mouserevise = macros.hoverrevise = {\n handler: function(a, b, c, d) {\n var endtags = ["end" + b],\n evt = (window.onmouseenter === null ? "onmouseenter" : "onmouseover"),\n t = tagcontents(d, [b], endtags, endtags, d.source.indexOf(">>", d.matchStart) + 2);\n if (t) {\n var rname = c[0].replace(" ", "_"),\n h = insertElement(a, "span", null, "hoverrevise hoverrevise_" + rname),\n f = function() {\n var done = !reviseAll("revise", rname);\n if (b != "hoverrevise" && done) {\n this[evt] = null\n }\n };\n new Wikifier(h, t[0]);\n if (b == "hoverrevise") {\n h.onmouseover = f;\n h.onmouseout = function() {\n reviseAll("revert", rname)\n }\n } else {\n h[evt] = f\n }\n h = null\n }\n }\n };\n macros.instantrevise = {\n handler: function(a, b, c, d) {\n reviseAll("revise", c[0].replace(" ", "_"))\n }\n };\n macros.endmouserevise = nullobj;\n macros.endhoverrevise = nullobj\n}());
by Tory Hoke\n\nPROLOGUE
\n<<if $puzzleAttempt>><p class="ind"><<if $puzzleOutcomeHash eq $puzzleRightAnswerHash>><<pickLock "prologue">>That's exactly what I said.<<else>>That's not quite it, but you get the picture.<<endif>></p>\s\n\s\n<<display "Divider">>\n<<endif>>\s\n<p class="ind">"Scared the wits out of me," said Ma. "Don't compare yourself to anybody. Use what you have. Learn everything you can learn. Read everything you can read. I tell you, <<print $shoulderChip>> catch little fish. What do we do with little fish?"</p>\s\n<p class="ind">"We throw 'em back."</p>\s\n<p class="ind">"That's right. They're not worth keeping."</p>\s\n<<display "Divider">>\n<p class="ind">I slept like a stone that night, and the next day I was a different <<print $chrs[$mainName].gender.youngTerm>>. I did my chores at double speed and asked Ma for an extra. When that was done I saw how many times I could run up our hill without stopping (not many), and how I could touch my toes (barely), and how far I could throw a knife in a eucalyptus and make it stick (not very). I practiced every day. Kids came for <<print $chrs[$siblingName].givenName>> and I was glad when they left. By the time I turned nine I could hit our fencepost from the back door, and Pa would pay me a copper coin to see it.</p>\s\n\s\n<<display "Divider">>\n\s\n<p class="ind">Later that year, the air in town got hushed and spooky. Men I never saw before showed up on the road, riding short-faced thick-leg horses I never saw before. By the well, I caught sight of a young man with a missing hand. He was giving a nervous talk to five farmers, and the whites of their eyes all flashed. The farmers said things like, "What's the king doing to stop them?" and "we had a deal" and "I knew the Norg would turn on us one day."</p>\s\n\s\n<<display "Divider">>\n\s\n<p class="ind">At dinner I asked Ma, "What's a 'Norg'?" She straightened up and swapped looks with Pa, and she asked where I'd heard it. I told her, and she said it wasn't a word for me to worry about, and I shouldn't spend time by the well any more.</p>\s\n<p class="ind">At night, Ma and Pa had long mumbles after I went to bed, their <i>effs</i> and <i>esses</i> cutting through the wall. I couldn't sort out what they were saying, but it always ended with sniffling and <i>shh shh</i>. I'd look over at <<print $chrs[$siblingName].givenName>>, who'd pretend to be sleeping, and <i>psst</i> at <<print $chrs[$siblingName].gender.pronounObj>> until <<print $chrs[$siblingName].gender.pronounSub>> looked over.</p>\s\n<p class="ind">"You hear that?" I'd ask.</p>\s\n<p class="ind">"Stop being a pest. Ma and Pa know what to do."</p>\s\n\s\n<<display "Divider">>\s\n\s\n<p class="ind">When it happened, we were both working in the back yard; I was slopping the pigs and <<print $chrs[$siblingName].givenName>> was doing laundry. Ma whistled us in real loud and sharp. It spooked me. It was only noon.</p>\s\n<p class="ind">There in the kitchen was Pa with a man in a ikaika lacquer shirt. Most every healthy young man and woman in the town was gathered in the street with horses and spears and wind rifles, if they had them. Some had makini helmets, some had heavy vests, and all the real Lahui showed showed their tattoos.</p>\s\n<p class="ind">The man in the ikaika shirt said something about "many hoonie archers" and "taking back Kahoku palace."</p>\s\n<p class="ind">"The Norg don't know what typhoon's about to hit 'em," he said. "They'll wish they never laid eyes on this island."</p>\s\n<p class="ind">I didn’t see what that had to do with us. Pa was just a farmer. I wish I’d been older. I could have gone with them. I would have cut every Norg throat I saw.</p>\s\n<p class="ind">Pa took <<print $chrs[$siblingName].givenName>> aside and told <<print $chrs[$siblingName].gender.pronounObj>> something, and then he took me aside. He kneeled down and put his big hands on both sides of my head and stared in my eyes like he was trying to find something <<if $metric eq true>>klicks<<else>>miles<<endif>> away. All he said was, "Stay sharp."</p>\s\n<p class="ind">Ma told us to hug Pa, and we did, and then she hugged him long and hard. They said things to each other I couldn’t hear. He nibbled her neck, but that made her weepy. Then Pa left with the man in the ikaika shirt, and that was all.</p>\s\n\s\n<<display "Divider">>\n\s\n<p class="ind">There was extra meat at dinner, but nobody ate it.</p>\s\n\s\n<<display "Divider">>\n\s\n<p class="ind">The next day <<print $chrs[$siblingName].gender.givenName>> and I did our best to be good. We kept the noise down and the house tidy. But if I had to do it over, I would have chased chickens through the parlor to take everybody’s mind off. We could have had one more ordinary day.</p>\s\n<p class="ind">The sun was still up when there came stomping hooves and a banging on the door. It was a boy with bloody knees and red eyes. He wasn't much older than <<print $chrs[$siblingName].givenName>>. "King's dead. Queen's dead. We got the princess out, but they turned all our army to trees."</p>\s\n<p class="ind">"Trees?" Ma went gray in the face. "What does that mean? Are they dead?"</p>\s\n<p class="ind">"No one knows, mam. Norg man named Stag. He did some evil magic and turned them. They're coming, the Norg. They're coming here now. Stag said no survivors."</p>\s\n<p class="ind">Ma didn’t scream or cry or anything. She sent the boy off, and she grabbed me and <<print $chrs[$siblingName].givenName>> and a sack of gear, and she ran us to the well in the center of town. There were others there: children three-to-a-horse, and every able body left in Palekaiko. Every stooped old-timer and one-legged woman lined up with knives and rakes and machetes. Ma brought us to Rosalie Kelima, the innkeeper’s wife, who was big pregnant. They didn’t say a word.</p>\s\n<p class="ind">Ma put <<print $chrs[$siblingName].givenName>> on a horse and me in <<print $chrs[$siblingName].gender.pronounPos>> lap. She put <<print $chrs[$siblingName].gender.givenName>>’s arms tight around me and said, "Don’t look back, and don’t let go. You hear me?"</p>\s\n<p class="ind"><<print $chrs[$siblingName].givenName>> nodded, but I yelled <i>no, no, no</i>.</p>\s\n<p class="ind">"I’m with you," Ma said. "I love you. I'm always with you." She kissed us once and once again and smacked our horse, and we galloped after Rosalie Kelima and every kid in Palekaiko, little-littles and babies all on horses like a kid army. We made for the forest, for the deep valleys, where the Norg wouldn’t find us. Something passed over the sun like a cloud, but darker than a cloud. I looked up and saw it was smoke, a whole gray acre of it, like someone threw a blanket on the sky.</p>\s\n<p class="ind">I looked back for Ma.</p>\s\n<p class="ind">Palekaiko was on fire. Our house was on fire. I could see the Norg riding around on tall horses, and the little shapes of Ma and the rest battling them back and giving us time to run. We should have been beside her. We shouldn't have let her leave us.</p>\s\n<p class="ind">So I kicked and I screamed <i>let me go let me go</i> but <<print $chrs[$siblingName].givenName>> held me tight. I bit <<print $chrs[$siblingName].gender.pronounPos>> arms and bit and bit until I broke <<print $chrs[$siblingName].gender.pronounPos>> skin and <<print $chrs[$siblingName].gender.pronounPos>> blood ran down <<print $chrs[$siblingName].gender.pronounPos>> hands and <<print $chrs[$siblingName].gender.pronounPos>> tears fell in my hair but <<print $chrs[$siblingName].gender.pronounSub>> didn’t let go.</p>\s\n\s\n<<display "Divider">>\n\s\n<p class="ind">The island stayed dark for a long, long time.</p>\s\n<p class="ind">They say Ma held her hand to the sky and asked God to blot the sun. They say the God took pity on us and cupped the sun in his hands. They say the Norg were afraid, but we saw in the dark, and they soaked the earth with Norgen blood.</p>\s\n<p class="ind">I believe part of that happened. I believe Ma scared the Norg. But these days I don't put much stock in God.</p>\s\n<p class="ind">[[$continueText|PrologueEnd]]</p>