From 22f7ca865abc96505e359efb20e5183d609b8436 Mon Sep 17 00:00:00 2001 From: Alex Warren Date: Sun, 25 Jan 2026 16:25:13 +0000 Subject: [PATCH 01/13] Update coffeeshop.squiffy --- examples/coffeeshop/coffeeshop.squiffy | 28 +++++++++++++------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/examples/coffeeshop/coffeeshop.squiffy b/examples/coffeeshop/coffeeshop.squiffy index 9fcf2df..313840e 100644 --- a/examples/coffeeshop/coffeeshop.squiffy +++ b/examples/coffeeshop/coffeeshop.squiffy @@ -30,13 +30,13 @@ In the corner booth, a woman in her late twenties hunches over a laptop, occasio Who do you approach first? -[The woman with the laptop] +[[The woman with the laptop]] -[The older man by the window] +[[The older man by the window]] -[Wait for someone to come to you] +[[Wait for someone to come to you]] -[The woman with the laptop]: +[[The woman with the laptop]]: @inc maya You approach the corner booth. The woman looks up, startled, then breaks into a sheepish grin. @@ -52,7 +52,7 @@ She grins. "Exactly. You're going to fit in fine here." [[Continue morning]](morning continues) -[The older man by the window]: +[[The older man by the window]]: @inc ellis You approach the window table. The man looks up—early seventies, neatly dressed, with the kind of face that suggests he's forgotten how to smile but remembers that he used to. @@ -68,7 +68,7 @@ You're not sure what to say to that. [[Continue morning]](morning continues) -[Wait for someone to come to you]: +[[Wait for someone to come to you]]: You busy yourself wiping down the already-clean counter. After a few minutes, the older man by the window raises his empty cup, catching your eye. You head over. He's early seventies, neatly dressed. @@ -100,11 +100,11 @@ The Daily Grind transforms at night. The overhead lights dim, fairy lights come You spot a new face: early twenties, headphones around their neck, dark circles under their eyes. They're sketching something in a worn notebook, barely looking at the page. -[Bring them a menu] +[[Bring them a menu]] -[Wait for them to come up] +[[Wait for them to come up]] -[Bring them a menu]: +[[Bring them a menu]]: @inc sam You grab a menu and head over. They look up, startled—then pull out an earbud. @@ -126,7 +126,7 @@ Sam immediately flips the notebook closed. "It's nothing. Just... keeps my hands [[End of day one]] -[Wait for them to come up]: +[[Wait for them to come up]]: Eventually they shuffle to the counter, headphones still half-on. "Most caffeinated thing you have. Please." @@ -218,11 +218,11 @@ You're wiping down tables when the door opens. Sam shuffles in, looking exhauste {{#if (gt sam 0)}}"{{player_name}}." They manage a tired smile. "Please tell me you have coffee. And maybe a will to live. I'll take either."{{else}}"Coffee. Strongest you've got." They collapse into a chair.{{/if}} -[Check on Sam] +[[Check on Sam]] -[Give them space] +[[Give them space]] -[Check on Sam]: +[[Check on Sam]]: You bring over their usual red-eye and slide into the seat across from them for a moment. "You okay?" @@ -242,7 +242,7 @@ But they don't put their headphones back on, and you take that as a good sign.{{ [[Closing time]] -[Give them space]: +[[Give them space]]: You leave the coffee and step away. Some people need space more than conversation. Sam catches your eye as you walk away and nods once—a small acknowledgment. They understand. From ef267ff8345ffe6ea17733f2ab55d0e4521fe84a Mon Sep 17 00:00:00 2001 From: Alex Warren Date: Sun, 25 Jan 2026 17:00:43 +0000 Subject: [PATCH 02/13] Fix input validation within {{#animate}} --- runtime/src/plugins/animate.ts | 9 ++++++--- runtime/src/squiffy.runtime.ts | 4 ++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/runtime/src/plugins/animate.ts b/runtime/src/plugins/animate.ts index cbe2ab2..07c9eb4 100644 --- a/runtime/src/plugins/animate.ts +++ b/runtime/src/plugins/animate.ts @@ -43,6 +43,11 @@ export function AnimatePlugin(): SquiffyPlugin { continue; } + // Capture content now, before setupInputValidation modifies the links + // with validation-disabled classes. This ensures when we restore the + // innerHTML after animation, users can still interact with the links. + const originalContent = el.innerHTML; + const runAnimation = () => { if (params.loop) { squiffy.animation.runAnimation(params.name, el, params, () => {}, true); @@ -52,14 +57,12 @@ export function AnimatePlugin(): SquiffyPlugin { } squiffy.addTransition(() => { return new Promise((resolve) => { - const currentContent = el.innerHTML; - // Reset opacity so the animation can control visibility el.style.opacity = ""; squiffy.animation.runAnimation(params.name, el, params, () => { el.classList.remove("squiffy-animate"); - el.innerHTML = currentContent; + el.innerHTML = originalContent; resolve(); }, false); }); diff --git a/runtime/src/squiffy.runtime.ts b/runtime/src/squiffy.runtime.ts index 2f66572..ffcf77a 100644 --- a/runtime/src/squiffy.runtime.ts +++ b/runtime/src/squiffy.runtime.ts @@ -184,6 +184,10 @@ export const init = async (options: SquiffyInitOptions): Promise => await run(master, "[[]]"); } await run(currentSection, `[[${sectionName}]]`); + + // Setup validation after transitions complete (animations may have replaced DOM elements) + setupInputValidation(currentSectionElement); + // The JS might have changed which section we're in if (get("_section") == sectionName) { set("_turncount", 0); From dbd45498dd217d2f7d5622fe47e2e45d3f2a7f0d Mon Sep 17 00:00:00 2001 From: Alex Warren Date: Mon, 26 Jan 2026 19:13:15 +0000 Subject: [PATCH 03/13] Some fixes --- examples/gameshow/gameshow.squiffy | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/examples/gameshow/gameshow.squiffy b/examples/gameshow/gameshow.squiffy index e51e9fe..4f9aa1a 100644 --- a/examples/gameshow/gameshow.squiffy +++ b/examples/gameshow/gameshow.squiffy @@ -34,7 +34,6 @@ The audience—three people and a cardboard cutout—applauds uncertainly. {{/animate}} --- -{{#if (not player_name)}}{{set "player_name" "Mystery Contestant"}}{{/if}} @set round = 1 "Welcome, {{player_name}}! Or as I like to call you... {{player_name}}!" @@ -301,30 +300,30 @@ Hank is sweating now. The audience confers. One of them is asleep. The cardboard cutout offers no input. -Finally, a woman in the front row shouts: "MAKE THEM {{random (array "SING" "DANCE" "DO AN IMPRESSION OF A BLENDER" "JUST GIVE THEM POINTS FOR FREE")}}!" +Finally, a woman in the front row shouts: "MAKE THEM {{random (array "SING" "DANCE" "DO AN IMPRESSION OF A BLENDER" "JUST GIVE THEM POINTS FOR FREE") set="make_them"}}!" -{{#if (eq _last_random "SING")}} +{{#if (eq make_them "SING")}} "You heard her! SING, {{player_name}}! Sing like your points depend on it! Because they do!" -[Sing beautifully] -[Sing terribly on purpose] +- [Sing beautifully] +- [Sing terribly on purpose] {{/if}} -{{#if (eq _last_random "DANCE")}} +{{#if (eq make_them "DANCE")}} "DANCE! Show us your moves!" -[Dance with enthusiasm] -[Do the robot] +- [Dance with enthusiasm] +- [Do the robot] {{/if}} -{{#if (eq _last_random "DO AN IMPRESSION OF A BLENDER")}} +{{#if (eq make_them "DO AN IMPRESSION OF A BLENDER")}} "A BLENDER! Do a blender impression! This is normal television!" -[WHIRRRRRR] -[Refuse on grounds of dignity] +- [WHIRRRRRR] +- [Refuse on grounds of dignity] {{/if}} -{{#if (eq _last_random "JUST GIVE THEM POINTS FOR FREE")}} +{{#if (eq make_them "JUST GIVE THEM POINTS FOR FREE")}} "Free points?! That's not how—" Hank checks his cards. "Actually, that IS an option apparently." {{inc "score" 150}} From d142a913d0d626e5c6824173e95551473486700a Mon Sep 17 00:00:00 2001 From: Alex Warren Date: Mon, 26 Jan 2026 19:23:07 +0000 Subject: [PATCH 04/13] More tweaks --- examples/gameshow/gameshow.squiffy | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/examples/gameshow/gameshow.squiffy b/examples/gameshow/gameshow.squiffy index 4f9aa1a..176fcd7 100644 --- a/examples/gameshow/gameshow.squiffy +++ b/examples/gameshow/gameshow.squiffy @@ -134,7 +134,7 @@ Hank backs away nervously. "Professor, we talked about bringing chemicals on set Lab Stability: {{live "stability"}}% Choose your answer: -- [Answer: Au] +- [[Answer: Au]] - [Answer: Go] - [Answer: Gd] - [Drink the beaker] @@ -153,15 +153,6 @@ Professor Boom: "HURRY! The compound is becoming... UNSTABLE!" Small explosions begin occurring around the lab set. -[Answer: Au]: -{{#animate "toast"}}CORRECT!{{/animate}} - -"AU! YES! Gold! Aurum! The element of CHAMPIONS!" Professor Boom throws the beaker in the air and catches it. "You have pleased me, {{player_name}}!" - -{{inc "score" 200}} - -[[Professor Exit]] - [Answer: Go]: "GO?! That's not even— that's just the word GO!" Professor Boom's hair somehow becomes more wild. @@ -176,16 +167,12 @@ A small explosion singes Hank's eyebrows. "MY EYEBROWS! Those were RENTED!" {{/if}} -[[Professor Exit]] - [Answer: Gd]: "Gd is GADOLINIUM! An honest mistake for a FOOL!" {{dec "stability" 20}} {{dec "patience"}} -[[Professor Exit]] - [Drink the beaker]: You grab the beaker and drink it before anyone can stop you. @@ -198,9 +185,15 @@ Professor Boom: "WAIT! That's my lunch!" {{inc "score" 50}} The audience (including the cardboard) applauds your boldness. -[[Professor Exit]] +[[Answer: Au]]: +{{#animate "toast"}}CORRECT!{{/animate}} + +"AU! YES! Gold! Aurum! The element of CHAMPIONS!" Professor Boom throws the beaker in the air and catches it. "You have pleased me, {{player_name}}!" + +{{inc "score" 200}} + ++++Continue... -[[Professor Exit]]: Professor Boom descends back through the trapdoor, cackling. "UNTIL NEXT TIME, QUIZ PARTICIPANT! REMEMBER: SCIENCE IS JUST EXPLOSIONS IN A LAB COAT!" From 87d6169a67b2d260f6114443ca398efa70bd89b7 Mon Sep 17 00:00:00 2001 From: Alex Warren Date: Mon, 26 Jan 2026 19:37:03 +0000 Subject: [PATCH 05/13] Fix set helper to unwrap SafeString values from subexpressions When using helpers like `random` as subexpressions with the `set` helper (e.g., `{{set "x" (random (array "a" "b"))}}`), the value was being stored as a Handlebars SafeString object instead of a plain string, causing `[object Object]` output and broken conditionals. The set helper now converts objects with toString() to strings before storing. Also updates gameshow example to use this pattern for silent random selection, and adds a test for the subexpression scenario. Co-Authored-By: Claude Opus 4.5 --- examples/gameshow/gameshow.squiffy | 20 ++++++++++---------- runtime/src/plugins/plugins.test.ts | 27 +++++++++++++++++++++++++++ runtime/src/textProcessor.ts | 4 ++++ 3 files changed, 41 insertions(+), 10 deletions(-) diff --git a/examples/gameshow/gameshow.squiffy b/examples/gameshow/gameshow.squiffy index 176fcd7..98875f2 100644 --- a/examples/gameshow/gameshow.squiffy +++ b/examples/gameshow/gameshow.squiffy @@ -58,7 +58,7 @@ A rusty wheel descends from the ceiling, wobbling dangerously. It has several qu [[Spin the wheel!]]: The wheel creaks to life, spinning with a sound like a shopping cart hitting a pothole. -It lands on... {{#animate "toast"}}{{sequence (array "GENERAL KNOWLEDGE" "SCIENCE with PROFESSOR BOOM" "MYSTERY BOX" "THE AUDIENCE DECIDES") set="category"}}!{{/animate}} +It lands on... {{#animate "toast"}}{{rotate (array "GENERAL KNOWLEDGE" "SCIENCE with PROFESSOR BOOM" "MYSTERY BOX" "THE AUDIENCE DECIDES") set="category"}}!{{/animate}} {{live "category" section="showCategory"}} @@ -211,14 +211,14 @@ A golden box descends from the ceiling on a frayed rope. The box rattles ominously. -[Open the box carefully] -[Shake the box first] -[Refuse to open it] +- [[Open the box carefully]] +- [[Shake the box first]] +- [[Refuse to open it]] -[Open the box carefully]: +[[Open the box carefully]]: You lift the lid slowly... -{{random (array "prize" "bees" "another_box" "philosophy") set="box_contents"}} +{{set "box_contents" (random (array "prize" "bees" "another_box" "philosophy"))}} {{#if (eq box_contents "prize")}} {{#animate "toast"}}JACKPOT!{{/animate}} @@ -268,23 +268,23 @@ Hank wipes away a single tear. "Beautiful." [[Next Round]] -[Shake the box first]: +[[Shake the box first]]: You shake the box vigorously. Something inside goes "CLANG" and then "moo?" {{dec "patience"}} "Please don't shake the— you know what, fine. Open it." -[Open the box carefully] +[[Open the box carefully]] -[Refuse to open it]: +[[Refuse to open it]]: {{dec "patience"}}{{dec "patience"}} "You HAVE to open it! It's the MYSTERY BOX! The mystery is legally required to be resolved!" Hank is sweating now. -[Open the box carefully] +[[Open the box carefully]] [[Audience Round]]: "THE AUDIENCE DECIDES!" Hank gestures grandly at the three people and cardboard cutout. diff --git a/runtime/src/plugins/plugins.test.ts b/runtime/src/plugins/plugins.test.ts index b528ba6..e8447f5 100644 --- a/runtime/src/plugins/plugins.test.ts +++ b/runtime/src/plugins/plugins.test.ts @@ -115,6 +115,33 @@ test("Random plugin: set attribute", async () => { expect(["apple", "banana", "orange"]).toContain(fruit); }); +test("Random plugin: use as subexpression with set helper", async () => { + const script = ` +{{set "fruit" (random (array "apple" "banana" "orange"))}} + +Fruit is: {{fruit}} + +{{#if (eq fruit "apple")}}It's an apple!{{/if}} +{{#if (eq fruit "banana")}}It's a banana!{{/if}} +{{#if (eq fruit "orange")}}It's an orange!{{/if}} +`; + + const { squiffyApi, element } = await initScript(script); + const fruit = squiffyApi.get("fruit"); + const text = element.textContent || ""; + + // The attribute should be set to one of the random values (as a string, not an object) + expect(["apple", "banana", "orange"]).toContain(fruit); + expect(typeof fruit).toBe("string"); + + // The output should show the fruit value, not [object Object] + expect(text).toMatch(/Fruit is: (apple|banana|orange)/); + expect(text).not.toContain("[object Object]"); + + // One of the conditionals should have matched + expect(text).toMatch(/It's an? (apple|banana|orange)!/); +}); + // ===== ReplaceLabel Tests ===== test("Label plugin: create labeled span", async () => { diff --git a/runtime/src/textProcessor.ts b/runtime/src/textProcessor.ts index 246f6c6..e99b5ef 100644 --- a/runtime/src/textProcessor.ts +++ b/runtime/src/textProcessor.ts @@ -39,6 +39,10 @@ export class TextProcessor { // State modification helpers (side effects, no output) // These execute at render time, so they respect {{#if}} conditions this.handlebars.registerHelper("set", (attribute: string, value: any) => { + // Unwrap SafeString if needed (e.g., from subexpressions like {{set "x" (random ...)}}) + if (value && typeof value === "object" && typeof value.toString === "function") { + value = value.toString(); + } this.state.set(attribute, value); return ""; }); From da257375fccbeeaf37f41b4ef1b54bf9dacab71d Mon Sep 17 00:00:00 2001 From: Alex Warren Date: Mon, 26 Jan 2026 19:53:45 +0000 Subject: [PATCH 06/13] Add array management helpers (append, prepend, pop, remove, contains, length) Implements feature request from issue #166. These helpers allow managing arrays stored in attributes - useful for tracking items, available options, or game state like rounds that shouldn't repeat. - append: Add element(s) to end of array - prepend: Add element(s) to start of array - pop: Remove and return first element - remove: Remove first matching value - contains: Check if array contains value - length: Get number of elements Also refactored set helper to use shared unwrapValue function, and updated the gameshow example to use these helpers for non-repeating rounds. Co-Authored-By: Claude Opus 4.5 --- .../src/__snapshots__/compiler.test.ts.snap | 48 ++-- examples/gameshow/gameshow.squiffy | 8 +- runtime/src/squiffy.runtime.test.ts | 215 ++++++++++++++++++ runtime/src/textProcessor.ts | 95 +++++++- site/src/content/docs/helpers-reference.mdx | 72 ++++++ 5 files changed, 406 insertions(+), 32 deletions(-) diff --git a/compiler/src/__snapshots__/compiler.test.ts.snap b/compiler/src/__snapshots__/compiler.test.ts.snap index 289b8f0..4dd5ae0 100644 --- a/compiler/src/__snapshots__/compiler.test.ts.snap +++ b/compiler/src/__snapshots__/compiler.test.ts.snap @@ -243,7 +243,7 @@ story.sections = { "text": "{{#if (gte patience 3)}}_Cheerful_{{else}}{{#if (gte patience 1)}}_Strained_{{else}}_Murderous_{{/if}}{{/if}}\\n" }, "intro": { - "text": "**{{#animate \\"typewriter\\" interval=200}}WELCOME TO...{{/animate}}**\\n\\n{{#animate \\"toast\\" interval=200}}THE WHEEL OF _DUBIOUS_ FORTUNE!{{/animate}}\\n\\n{{#animate \\"fadeIn\\" duration=2000}}\\nThe studio lights flicker ominously. A man in a sequined jacket stumbles onto the stage, holding cue cards upside down.\\n\\n\\"Hello and welcome! I'm your host, Chuck Wheelsworth!\\" He squints at the cards. \\"No wait, that's not right. I'm Hank Spinneroni. Definitely Hank.\\"\\n\\nThe audience—three people and a cardboard cutout—applauds uncertainly.\\n\\n\\"And YOU are our lucky contestant! What's your name?\\"\\n\\n\\n\\n{{section \\"_continue1\\" text=\\"Let's play!\\"}}\\n{{/animate}}\\n", + "text": "{{set \\"availableRounds\\" (array \\"GENERAL KNOWLEDGE\\" \\"SCIENCE with PROFESSOR BOOM\\" \\"MYSTERY BOX\\" \\"THE AUDIENCE DECIDES\\")}}\\n\\n**{{#animate \\"typewriter\\" interval=200}}WELCOME TO...{{/animate}}**\\n\\n{{#animate \\"toast\\" interval=200}}THE WHEEL OF _DUBIOUS_ FORTUNE!{{/animate}}\\n\\n{{#animate \\"fadeIn\\" duration=2000}}\\nThe studio lights flicker ominously. A man in a sequined jacket stumbles onto the stage, holding cue cards upside down.\\n\\n\\"Hello and welcome! I'm your host, Chuck Wheelsworth!\\" He squints at the cards. \\"No wait, that's not right. I'm Hank Spinneroni. Definitely Hank.\\"\\n\\nThe audience—three people and a cardboard cutout—applauds uncertainly.\\n\\n\\"And YOU are our lucky contestant! What's your name?\\"\\n\\n\\n\\n{{section \\"_continue1\\" text=\\"Let's play!\\"}}\\n{{/animate}}\\n", "attributes": [ "score = 0", "round = 0", @@ -251,7 +251,7 @@ story.sections = { ] }, "_continue1": { - "text": "{{#if (not player_name)}}{{set \\"player_name\\" \\"Mystery Contestant\\"}}{{/if}}\\n\\n\\"Welcome, {{player_name}}! Or as I like to call you... {{player_name}}!\\"\\n\\nHank pauses, waiting for laughter. None comes.\\n\\n{{embed \\"catchphrase\\"}}\\n\\n\\"Now then! {{#animate \\"continue\\" style=\\"wave\\"}}Let's spin...{{/animate}}\\"\\n\\n{{#animate \\"typewriter\\"}}THE WHEEL OF DUBIOUS FORTUNE!{{/animate}}\\n\\n{{#animate \\"fadeIn\\"}}\\nA rusty wheel descends from the ceiling, wobbling dangerously. It has several questionable categories painted on it.\\n\\n{{section \\"Spin the wheel!\\" text=\\"Spin the wheel!\\"}}\\n{{/animate}}\\n", + "text": "\\"Welcome, {{player_name}}! Or as I like to call you... {{player_name}}!\\"\\n\\nHank pauses, waiting for laughter. None comes.\\n\\n{{embed \\"catchphrase\\"}}\\n\\n\\"Now then! {{#animate \\"continue\\" style=\\"wave\\"}}Let's spin...{{/animate}}\\"\\n\\n{{#animate \\"typewriter\\"}}THE WHEEL OF DUBIOUS FORTUNE!{{/animate}}\\n\\n{{#animate \\"fadeIn\\"}}\\nA rusty wheel descends from the ceiling, wobbling dangerously. It has several questionable categories painted on it.\\n\\n{{section \\"Spin the wheel!\\" text=\\"Spin the wheel!\\"}}\\n{{/animate}}\\n", "attributes": [ "round = 1" ], @@ -262,7 +262,7 @@ story.sections = { } }, "Spin the wheel!": { - "text": "The wheel creaks to life, spinning with a sound like a shopping cart hitting a pothole.\\n\\nIt lands on... {{#animate \\"toast\\"}}{{sequence (array \\"GENERAL KNOWLEDGE\\" \\"SCIENCE with PROFESSOR BOOM\\" \\"MYSTERY BOX\\" \\"THE AUDIENCE DECIDES\\") set=\\"category\\"}}!{{/animate}}\\n\\n{{live \\"category\\" section=\\"showCategory\\"}}\\n" + "text": "The wheel creaks to life, spinning with a sound like a shopping cart hitting a pothole.\\n\\n{{set \\"category\\" (random availableRounds)}}\\n{{remove \\"availableRounds\\" category}}\\n\\nIt lands on... {{#animate \\"toast\\"}}{{category}}!{{/animate}}\\n\\n{{live \\"category\\" section=\\"showCategory\\"}}\\n" }, "showCategory": { "text": "{{#if (eq category \\"GENERAL KNOWLEDGE\\")}}{{section \\"General Knowledge Round\\"}}{{/if}}\\n{{#if (eq category \\"SCIENCE with PROFESSOR BOOM\\")}}{{section \\"Science Round\\"}}{{/if}}\\n{{#if (eq category \\"MYSTERY BOX\\")}}{{section \\"Mystery Box Round\\"}}{{/if}}\\n{{#if (eq category \\"THE AUDIENCE DECIDES\\")}}{{section \\"Audience Round\\"}}{{/if}}\\n" @@ -276,7 +276,7 @@ story.sections = { } }, "Science Round": { - "text": "The lights flicker and a trapdoor opens in the floor. Smoke billows out.\\n\\n{{#animate \\"typewriter\\"}}PROFESSOR BOOM has entered the chat.{{/animate}}\\n\\nA wild-haired scientist emerges, goggles askew, lab coat singed.\\n\\n\\"GREETINGS, QUIZ PARTICIPANT! I am Professor Boom, and THIS—\\" he holds up a beaker of bubbling green liquid \\"—is SCIENCE!\\"\\n\\nHank backs away nervously. \\"Professor, we talked about bringing chemicals on set...\\"\\n\\n\\"SILENCE, TELEVISION MAN! The contestant must answer... {{#animate \\"toast\\"}}A SCIENCE QUESTION!{{/animate}}\\"\\n\\n**What is the chemical symbol for Gold?**\\n\\nLab Stability: {{live \\"stability\\"}}%\\n\\nChoose your answer:\\n- {{passage \\"Answer: Au\\"}}\\n- {{passage \\"Answer: Go\\"}}\\n- {{passage \\"Answer: Gd\\"}}\\n- {{passage \\"Drink the beaker\\"}}\\n", + "text": "The lights flicker and a trapdoor opens in the floor. Smoke billows out.\\n\\n{{#animate \\"typewriter\\"}}PROFESSOR BOOM has entered the chat.{{/animate}}\\n\\nA wild-haired scientist emerges, goggles askew, lab coat singed.\\n\\n\\"GREETINGS, QUIZ PARTICIPANT! I am Professor Boom, and THIS—\\" he holds up a beaker of bubbling green liquid \\"—is SCIENCE!\\"\\n\\nHank backs away nervously. \\"Professor, we talked about bringing chemicals on set...\\"\\n\\n\\"SILENCE, TELEVISION MAN! The contestant must answer... {{#animate \\"toast\\"}}A SCIENCE QUESTION!{{/animate}}\\"\\n\\n**What is the chemical symbol for Gold?**\\n\\nLab Stability: {{live \\"stability\\"}}%\\n\\nChoose your answer:\\n- {{section \\"Answer: Au\\"}}\\n- {{passage \\"Answer: Go\\"}}\\n- {{passage \\"Answer: Gd\\"}}\\n- {{passage \\"Drink the beaker\\"}}\\n", "attributes": [ "stability = 100" ], @@ -290,39 +290,37 @@ story.sections = { "@3": { "text": "{{dec \\"stability\\" 25}}\\n{{#animate \\"toast\\"}}WARNING: CRITICAL INSTABILITY{{/animate}}\\n\\nSmall explosions begin occurring around the lab set.\\n" }, - "Answer: Au": { - "text": "{{#animate \\"toast\\"}}CORRECT!{{/animate}}\\n\\n\\"AU! YES! Gold! Aurum! The element of CHAMPIONS!\\" Professor Boom throws the beaker in the air and catches it. \\"You have pleased me, {{player_name}}!\\"\\n\\n{{inc \\"score\\" 200}}\\n\\n{{section \\"Professor Exit\\"}}\\n" - }, "Answer: Go": { - "text": "\\"GO?! That's not even— that's just the word GO!\\" Professor Boom's hair somehow becomes more wild.\\n\\n{{dec \\"stability\\" 30}}\\n{{dec \\"patience\\"}}\\n\\n{{#if (lte stability 30)}}\\n{{#animate \\"toast\\"}}BOOM!{{/animate}}\\n\\nA small explosion singes Hank's eyebrows.\\n\\n\\"MY EYEBROWS! Those were RENTED!\\"\\n{{/if}}\\n\\n{{section \\"Professor Exit\\"}}\\n" + "text": "\\"GO?! That's not even— that's just the word GO!\\" Professor Boom's hair somehow becomes more wild.\\n\\n{{dec \\"stability\\" 30}}\\n{{dec \\"patience\\"}}\\n\\n{{#if (lte stability 30)}}\\n{{#animate \\"toast\\"}}BOOM!{{/animate}}\\n\\nA small explosion singes Hank's eyebrows.\\n\\n\\"MY EYEBROWS! Those were RENTED!\\"\\n{{/if}}\\n" }, "Answer: Gd": { - "text": "\\"Gd is GADOLINIUM! An honest mistake for a FOOL!\\"\\n\\n{{dec \\"stability\\" 20}}\\n{{dec \\"patience\\"}}\\n\\n{{section \\"Professor Exit\\"}}\\n" + "text": "\\"Gd is GADOLINIUM! An honest mistake for a FOOL!\\"\\n\\n{{dec \\"stability\\" 20}}\\n{{dec \\"patience\\"}}\\n" }, "Drink the beaker": { - "text": "You grab the beaker and drink it before anyone can stop you.\\n\\nProfessor Boom: \\"WAIT! That's my lunch!\\"\\n\\n{{#animate \\"toast\\"}}It tastes like... chicken soup?{{/animate}}\\n\\n\\"I was heating up my soup with SCIENCE. Now I have no lunch AND no science demonstration.\\"\\n\\n{{inc \\"score\\" 50}}\\nThe audience (including the cardboard) applauds your boldness.\\n\\n{{section \\"Professor Exit\\"}}\\n" + "text": "You grab the beaker and drink it before anyone can stop you.\\n\\nProfessor Boom: \\"WAIT! That's my lunch!\\"\\n\\n{{#animate \\"toast\\"}}It tastes like... chicken soup?{{/animate}}\\n\\n\\"I was heating up my soup with SCIENCE. Now I have no lunch AND no science demonstration.\\"\\n\\n{{inc \\"score\\" 50}}\\nThe audience (including the cardboard) applauds your boldness.\\n" } } }, - "Professor Exit": { + "Answer: Au": { + "text": "{{#animate \\"toast\\"}}CORRECT!{{/animate}}\\n\\n\\"AU! YES! Gold! Aurum! The element of CHAMPIONS!\\" Professor Boom throws the beaker in the air and catches it. \\"You have pleased me, {{player_name}}!\\"\\n\\n{{inc \\"score\\" 200}}\\n\\n{{section \\"_continue2\\" text=\\"Continue...\\"}}" + }, + "_continue2": { "text": "Professor Boom descends back through the trapdoor, cackling.\\n\\n\\"UNTIL NEXT TIME, QUIZ PARTICIPANT! REMEMBER: SCIENCE IS JUST EXPLOSIONS IN A LAB COAT!\\"\\n\\nThe trapdoor slams shut. Hank pats out a small fire on his jacket.\\n\\n\\"Right. Well. That happened.\\"\\n\\n{{section \\"Next Round\\"}}\\n" }, "Mystery Box Round": { - "text": "A golden box descends from the ceiling on a frayed rope.\\n\\n\\"Ooh, the Mystery Box!\\" Hank claps his hands. \\"This could contain ANYTHING! A prize! A question! A live animal! We genuinely don't know—the intern who loaded it quit this morning!\\"\\n\\nThe box rattles ominously.\\n\\n{{passage \\"Open the box carefully\\"}}\\n{{passage \\"Shake the box first\\"}}\\n{{passage \\"Refuse to open it\\"}}\\n", - "passages": { - "Open the box carefully": { - "text": "You lift the lid slowly...\\n\\n{{random (array \\"prize\\" \\"bees\\" \\"another_box\\" \\"philosophy\\") set=\\"box_contents\\"}}\\n\\n{{#if (eq box_contents \\"prize\\")}}\\n{{#animate \\"toast\\"}}JACKPOT!{{/animate}}\\n\\nInside is a solid gold trophy shaped like a wheel! It's spray-painted gold, but still!\\n\\n{{inc \\"score\\" 300}}\\n\\n\\"Congratulations! You've won our grand prize: a trophy AND the satisfaction of not being attacked by whatever else could have been in there!\\"\\n{{/if}}\\n\\n{{#if (eq box_contents \\"bees\\")}}\\n{{#animate \\"toast\\"}}BEES!{{/animate}}\\n\\nThe box is full of bees. They seem confused but not aggressive.\\n\\n\\"Oh no, NOT AGAIN!\\" Hank runs in circles. \\"WHO KEEPS PUTTING BEES IN THE MYSTERY BOX?!\\"\\n\\n{{dec \\"patience\\"}}\\n\\nA bee lands on your nose, considers its life choices, and flies away. You feel oddly respected.\\n\\n{{inc \\"score\\" 50}}\\n{{/if}}\\n\\n{{#if (eq box_contents \\"another_box\\")}}\\nInside the box is... another, smaller box.\\n\\n\\"Ah, the recursive box!\\" Hank nods sagely. \\"Open it!\\"\\n\\nInside THAT box is a note that says: \\"{{player_name}} wins 100 points!\\"\\n\\n{{inc \\"score\\" 100}}\\n\\n{{#animate \\"toast\\"}}Congratulations, somehow!{{/animate}}\\n{{/if}}\\n\\n{{#if (eq box_contents \\"philosophy\\")}}\\nInside is a small scroll. It reads:\\n\\n_\\"What IS a prize, really? Is not the true prize the friends we made along the way? Also here's 75 points.\\"_\\n\\n{{inc \\"score\\" 75}}\\n\\nHank wipes away a single tear. \\"Beautiful.\\"\\n{{/if}}\\n\\n{{section \\"Next Round\\"}}\\n" - }, - "Shake the box first": { - "text": "You shake the box vigorously. Something inside goes \\"CLANG\\" and then \\"moo?\\"\\n\\n{{dec \\"patience\\"}}\\n\\n\\"Please don't shake the— you know what, fine. Open it.\\"\\n\\n{{passage \\"Open the box carefully\\"}}\\n" - }, - "Refuse to open it": { - "text": "{{dec \\"patience\\"}}{{dec \\"patience\\"}}\\n\\n\\"You HAVE to open it! It's the MYSTERY BOX! The mystery is legally required to be resolved!\\"\\n\\nHank is sweating now.\\n\\n{{passage \\"Open the box carefully\\"}}\\n" - } - } + "text": "A golden box descends from the ceiling on a frayed rope.\\n\\n\\"Ooh, the Mystery Box!\\" Hank claps his hands. \\"This could contain ANYTHING! A prize! A question! A live animal! We genuinely don't know—the intern who loaded it quit this morning!\\"\\n\\nThe box rattles ominously.\\n\\n- {{section \\"Open the box carefully\\"}}\\n- {{section \\"Shake the box first\\"}}\\n- {{section \\"Refuse to open it\\"}}\\n" + }, + "Open the box carefully": { + "text": "You lift the lid slowly...\\n\\n{{set \\"box_contents\\" (random (array \\"prize\\" \\"bees\\" \\"another_box\\" \\"philosophy\\"))}}\\n\\n{{#if (eq box_contents \\"prize\\")}}\\n{{#animate \\"toast\\"}}JACKPOT!{{/animate}}\\n\\nInside is a solid gold trophy shaped like a wheel! It's spray-painted gold, but still!\\n\\n{{inc \\"score\\" 300}}\\n\\n\\"Congratulations! You've won our grand prize: a trophy AND the satisfaction of not being attacked by whatever else could have been in there!\\"\\n{{/if}}\\n\\n{{#if (eq box_contents \\"bees\\")}}\\n{{#animate \\"toast\\"}}BEES!{{/animate}}\\n\\nThe box is full of bees. They seem confused but not aggressive.\\n\\n\\"Oh no, NOT AGAIN!\\" Hank runs in circles. \\"WHO KEEPS PUTTING BEES IN THE MYSTERY BOX?!\\"\\n\\n{{dec \\"patience\\"}}\\n\\nA bee lands on your nose, considers its life choices, and flies away. You feel oddly respected.\\n\\n{{inc \\"score\\" 50}}\\n{{/if}}\\n\\n{{#if (eq box_contents \\"another_box\\")}}\\nInside the box is... another, smaller box.\\n\\n\\"Ah, the recursive box!\\" Hank nods sagely. \\"Open it!\\"\\n\\nInside THAT box is a note that says: \\"{{player_name}} wins 100 points!\\"\\n\\n{{inc \\"score\\" 100}}\\n\\n{{#animate \\"toast\\"}}Congratulations, somehow!{{/animate}}\\n{{/if}}\\n\\n{{#if (eq box_contents \\"philosophy\\")}}\\nInside is a small scroll. It reads:\\n\\n_\\"What IS a prize, really? Is not the true prize the friends we made along the way? Also here's 75 points.\\"_\\n\\n{{inc \\"score\\" 75}}\\n\\nHank wipes away a single tear. \\"Beautiful.\\"\\n{{/if}}\\n\\n{{section \\"Next Round\\"}}\\n" + }, + "Shake the box first": { + "text": "You shake the box vigorously. Something inside goes \\"CLANG\\" and then \\"moo?\\"\\n\\n{{dec \\"patience\\"}}\\n\\n\\"Please don't shake the— you know what, fine. Open it.\\"\\n\\n{{section \\"Open the box carefully\\"}}\\n" + }, + "Refuse to open it": { + "text": "{{dec \\"patience\\"}}{{dec \\"patience\\"}}\\n\\n\\"You HAVE to open it! It's the MYSTERY BOX! The mystery is legally required to be resolved!\\"\\n\\nHank is sweating now.\\n\\n{{section \\"Open the box carefully\\"}}\\n" }, "Audience Round": { - "text": "\\"THE AUDIENCE DECIDES!\\" Hank gestures grandly at the three people and cardboard cutout.\\n\\n\\"Audience members, what challenge should {{player_name}} face?\\"\\n\\nThe audience confers. One of them is asleep. The cardboard cutout offers no input.\\n\\nFinally, a woman in the front row shouts: \\"MAKE THEM {{random (array \\"SING\\" \\"DANCE\\" \\"DO AN IMPRESSION OF A BLENDER\\" \\"JUST GIVE THEM POINTS FOR FREE\\")}}!\\"\\n\\n{{#if (eq _last_random \\"SING\\")}}\\n\\"You heard her! SING, {{player_name}}! Sing like your points depend on it! Because they do!\\"\\n\\n{{passage \\"Sing beautifully\\"}}\\n{{passage \\"Sing terribly on purpose\\"}}\\n{{/if}}\\n\\n{{#if (eq _last_random \\"DANCE\\")}}\\n\\"DANCE! Show us your moves!\\"\\n\\n{{passage \\"Dance with enthusiasm\\"}}\\n{{passage \\"Do the robot\\"}}\\n{{/if}}\\n\\n{{#if (eq _last_random \\"DO AN IMPRESSION OF A BLENDER\\")}}\\n\\"A BLENDER! Do a blender impression! This is normal television!\\"\\n\\n{{passage \\"WHIRRRRRR\\"}}\\n{{passage \\"Refuse on grounds of dignity\\"}}\\n{{/if}}\\n\\n{{#if (eq _last_random \\"JUST GIVE THEM POINTS FOR FREE\\")}}\\n\\"Free points?! That's not how—\\" Hank checks his cards. \\"Actually, that IS an option apparently.\\"\\n\\n{{inc \\"score\\" 150}}\\n{{#animate \\"toast\\"}}FREE POINTS!{{/animate}}\\n\\n\\"Well! That was anticlimactic! Moving on!\\"\\n\\n{{section \\"Next Round\\"}}\\n{{/if}}\\n", + "text": "\\"THE AUDIENCE DECIDES!\\" Hank gestures grandly at the three people and cardboard cutout.\\n\\n\\"Audience members, what challenge should {{player_name}} face?\\"\\n\\nThe audience confers. One of them is asleep. The cardboard cutout offers no input.\\n\\nFinally, a woman in the front row shouts: \\"MAKE THEM {{random (array \\"SING\\" \\"DANCE\\" \\"DO AN IMPRESSION OF A BLENDER\\" \\"JUST GIVE THEM POINTS FOR FREE\\") set=\\"make_them\\"}}!\\"\\n\\n{{#if (eq make_them \\"SING\\")}}\\n\\"You heard her! SING, {{player_name}}! Sing like your points depend on it! Because they do!\\"\\n\\n- {{passage \\"Sing beautifully\\"}}\\n- {{passage \\"Sing terribly on purpose\\"}}\\n{{/if}}\\n\\n{{#if (eq make_them \\"DANCE\\")}}\\n\\"DANCE! Show us your moves!\\"\\n\\n- {{passage \\"Dance with enthusiasm\\"}}\\n- {{passage \\"Do the robot\\"}}\\n{{/if}}\\n\\n{{#if (eq make_them \\"DO AN IMPRESSION OF A BLENDER\\")}}\\n\\"A BLENDER! Do a blender impression! This is normal television!\\"\\n\\n- {{passage \\"WHIRRRRRR\\"}}\\n- {{passage \\"Refuse on grounds of dignity\\"}}\\n{{/if}}\\n\\n{{#if (eq make_them \\"JUST GIVE THEM POINTS FOR FREE\\")}}\\n\\"Free points?! That's not how—\\" Hank checks his cards. \\"Actually, that IS an option apparently.\\"\\n\\n{{inc \\"score\\" 150}}\\n{{#animate \\"toast\\"}}FREE POINTS!{{/animate}}\\n\\n\\"Well! That was anticlimactic! Moving on!\\"\\n\\n{{section \\"Next Round\\"}}\\n{{/if}}\\n", "passages": { "Sing beautifully": { "text": "You belt out an operatic rendition of the show's theme song (which you're making up as you go).\\n\\nThe audience is moved. One of them wakes up specifically to applaud.\\n\\n{{inc \\"score\\" 150}}\\n{{#animate \\"toast\\"}}STANDING OVATION!{{/animate}}\\n\\n{{section \\"Next Round\\"}}\\n" @@ -345,7 +343,7 @@ story.sections = { } }, "Next Round": { - "text": "{{#if (gte round 3)}}\\n{{section \\"Final Round\\"}}\\n{{else}}\\nHank shuffles his cards. \\"Alright, {{player_name}}, ready for round {{round}}?\\"\\n\\nThe wheel looms above you, creaking expectantly.\\n\\n{{section \\"Spin the wheel!\\"}}\\n{{/if}}\\n", + "text": "{{#if (eq (length \\"availableRounds\\") 0)}}\\n{{section \\"Final Round\\"}}\\n{{else}}\\nHank shuffles his cards. \\"Alright, {{player_name}}, ready for round {{round}}?\\"\\n\\nThe wheel looms above you, creaking expectantly.\\n\\n{{section \\"Spin the wheel!\\"}}\\n{{/if}}\\n", "attributes": [ "round+=1" ] diff --git a/examples/gameshow/gameshow.squiffy b/examples/gameshow/gameshow.squiffy index 98875f2..6ac0b54 100644 --- a/examples/gameshow/gameshow.squiffy +++ b/examples/gameshow/gameshow.squiffy @@ -14,6 +14,7 @@ @set score = 0 @set round = 0 @set patience = 3 +{{set "availableRounds" (array "GENERAL KNOWLEDGE" "SCIENCE with PROFESSOR BOOM" "MYSTERY BOX" "THE AUDIENCE DECIDES")}} **{{#animate "typewriter" interval=200}}WELCOME TO...{{/animate}}** @@ -58,7 +59,10 @@ A rusty wheel descends from the ceiling, wobbling dangerously. It has several qu [[Spin the wheel!]]: The wheel creaks to life, spinning with a sound like a shopping cart hitting a pothole. -It lands on... {{#animate "toast"}}{{rotate (array "GENERAL KNOWLEDGE" "SCIENCE with PROFESSOR BOOM" "MYSTERY BOX" "THE AUDIENCE DECIDES") set="category"}}!{{/animate}} +{{set "category" (random availableRounds)}} +{{remove "availableRounds" category}} + +It lands on... {{#animate "toast"}}{{category}}!{{/animate}} {{live "category" section="showCategory"}} @@ -385,7 +389,7 @@ Hank: "Fair enough. Moving on with your dignity intact but your score unchanged. [[Next Round]]: @inc round -{{#if (gte round 3)}} +{{#if (eq (length "availableRounds") 0)}} [[Final Round]] {{else}} Hank shuffles his cards. "Alright, {{player_name}}, ready for round {{round}}?" diff --git a/runtime/src/squiffy.runtime.test.ts b/runtime/src/squiffy.runtime.test.ts index 99bfad1..a79ec75 100644 --- a/runtime/src/squiffy.runtime.test.ts +++ b/runtime/src/squiffy.runtime.test.ts @@ -2008,4 +2008,219 @@ Level: