diff --git a/compiler/src/__snapshots__/compiler.test.ts.snap b/compiler/src/__snapshots__/compiler.test.ts.snap index 289b8f0..983817d 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,79 +290,73 @@ 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", - "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" - }, - "Sing terribly on purpose": { - "text": "You screech like a cat in a tumble dryer. On purpose. For art.\\n\\n{{dec \\"patience\\"}}\\n\\nHank: \\"That was... that was really something. Moving on quickly.\\"\\n\\n{{inc \\"score\\" 25}}\\n\\n{{section \\"Next Round\\"}}\\n" - }, - "Dance with enthusiasm": { - "text": "You dance like nobody's watching, except everyone is watching, and also there are cameras.\\n\\n{{inc \\"score\\" 125}}\\nThe cardboard cutout falls over again, but in an approving way.\\n\\n{{section \\"Next Round\\"}}\\n" - }, - "Do the robot": { - "text": "You do the robot so convincingly that a stagehand tries to unplug you.\\n\\n{{inc \\"score\\" 100}}\\n{{#animate \\"toast\\"}}BEEP BOOP{{/animate}}\\n\\n{{section \\"Next Round\\"}}\\n" - }, - "WHIRRRRRR": { - "text": "You spin around making blender noises. It's deeply unsettling.\\n\\n{{inc \\"score\\" 175}}\\n\\nProfessor Boom's voice echoes from below: \\"MAGNIFICENT!\\"\\n\\n{{section \\"Next Round\\"}}\\n" - }, - "Refuse on grounds of dignity": { - "text": "\\"I'm not doing that.\\"\\n\\n{{dec \\"patience\\"}}\\n\\nHank: \\"Fair enough. Moving on with your dignity intact but your score unchanged.\\"\\n\\n{{section \\"Next Round\\"}}\\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- {{section \\"Sing beautifully\\"}}\\n- {{section \\"Sing terribly on purpose\\"}}\\n{{/if}}\\n\\n{{#if (eq make_them \\"DANCE\\")}}\\n\\"DANCE! Show us your moves!\\"\\n\\n- {{section \\"Dance with enthusiasm\\"}}\\n- {{section \\"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- {{section \\"WHIRRRRRR\\"}}\\n- {{section \\"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" + }, + "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" + }, + "Sing terribly on purpose": { + "text": "You screech like a cat in a tumble dryer. On purpose. For art.\\n\\n{{dec \\"patience\\"}}\\n\\nHank: \\"That was... that was really something. Moving on quickly.\\"\\n\\n{{inc \\"score\\" 25}}\\n\\n{{section \\"Next Round\\"}}\\n" + }, + "Dance with enthusiasm": { + "text": "You dance like nobody's watching, except everyone is watching, and also there are cameras.\\n\\n{{inc \\"score\\" 125}}\\nThe cardboard cutout falls over again, but in an approving way.\\n\\n{{section \\"Next Round\\"}}\\n" + }, + "Do the robot": { + "text": "You do the robot so convincingly that a stagehand tries to unplug you.\\n\\n{{inc \\"score\\" 100}}\\n{{#animate \\"toast\\"}}BEEP BOOP{{/animate}}\\n\\n{{section \\"Next Round\\"}}\\n" + }, + "WHIRRRRRR": { + "text": "You spin around making blender noises. It's deeply unsettling.\\n\\n{{inc \\"score\\" 175}}\\n\\nProfessor Boom's voice echoes from below: \\"MAGNIFICENT!\\"\\n\\n{{section \\"Next Round\\"}}\\n" + }, + "Refuse on grounds of dignity": { + "text": "\\"I'm not doing that.\\"\\n\\n{{dec \\"patience\\"}}\\n\\nHank: \\"Fair enough. Moving on with your dignity intact but your score unchanged.\\"\\n\\n{{section \\"Next Round\\"}}\\n" }, "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" ] }, "Final Round": { - "text": "The studio lights go RED. Dramatic music plays (someone is humming aggressively in the background).\\n\\n{{#animate \\"typewriter\\"}}THE FINAL ROUND{{/animate}}\\n\\n\\"This is it, {{player_name}}! The FINAL ROUND!\\" Hank is vibrating with excitement or possibly caffeine.\\n\\n\\"You have {{score}} points! But this last question is worth... {{#animate \\"toast\\"}}DOUBLE OR NOTHING!{{/animate}}\\"\\n\\nThe audience gasps. The cardboard cutout gasps (the AC kicked in).\\n\\n\\"Here is your FINAL QUESTION:\\"\\n\\n{{#animate \\"typewriter\\"}}**What is the name of your host?**{{/animate}}\\n\\n{{passage \\"Hank Spinneroni\\"}}\\n{{passage \\"Chuck Wheelsworth\\"}}\\n{{passage \\"I genuinely don't remember\\"}}\\n", - "passages": { - "Hank Spinneroni": { - "text": "\\"CORRECT! I AM HANK SPINNERONI! Probably!\\"\\n\\n{{set \\"score\\" (score * 2)}}\\n{{#animate \\"toast\\"}}SCORE DOUBLED!{{/animate}}\\n\\n{{section \\"Ending\\"}}\\n" - }, - "Chuck Wheelsworth": { - "text": "\\"That was my FIRST name! The WRONG first name! But you remembered SOMETHING!\\"\\n\\n{{inc \\"score\\" 50}}\\n\\n\\"Close enough! I'll allow it because I'm tired!\\"\\n\\n{{section \\"Ending\\"}}\\n" - }, - "I genuinely don't remember": { - "text": "{{#if (lte patience 1)}}\\nHank stares at you. A vein throbs in his forehead.\\n\\n\\"It's... it's HANK. My name is HANK. I said it at the START.\\"\\n\\nHe takes a deep breath.\\n\\n\\"You know what? You still win. I just want to go home.\\"\\n\\n{{inc \\"score\\" 25}}\\n{{else}}\\nHank laughs. \\"Honestly? I respect the honesty!\\"\\n\\n{{inc \\"score\\" 75}}\\n\\n\\"The real prize was the confusion we shared along the way!\\"\\n{{/if}}\\n\\n{{section \\"Ending\\"}}\\n" - } - } + "text": "The studio lights go RED. Dramatic music plays (someone is humming aggressively in the background).\\n\\n{{#animate \\"typewriter\\"}}THE FINAL ROUND{{/animate}}\\n\\n\\"This is it, {{player_name}}! The FINAL ROUND!\\" Hank is vibrating with excitement or possibly caffeine.\\n\\n\\"You have {{score}} points! But this last question is worth... {{#animate \\"toast\\"}}DOUBLE OR NOTHING!{{/animate}}\\"\\n\\nThe audience gasps. The cardboard cutout gasps (the AC kicked in).\\n\\n\\"Here is your FINAL QUESTION:\\"\\n\\n{{#animate \\"typewriter\\"}}**What is the name of your host?**{{/animate}}\\n\\n- {{section \\"Hank Spinneroni\\"}}\\n- {{section \\"Chuck Wheelsworth\\"}}\\n- {{section \\"I genuinely don't remember\\"}}\\n" + }, + "Hank Spinneroni": { + "text": "\\"CORRECT! I AM HANK SPINNERONI! Probably!\\"\\n\\n{{inc \\"score\\" score}}\\n{{#animate \\"toast\\"}}SCORE DOUBLED!{{/animate}}\\n\\n{{section \\"Ending\\"}}\\n" + }, + "Chuck Wheelsworth": { + "text": "\\"That was my FIRST name! The WRONG first name! But you remembered SOMETHING!\\"\\n\\n{{inc \\"score\\" 50}}\\n\\n\\"Close enough! I'll allow it because I'm tired!\\"\\n\\n{{section \\"Ending\\"}}\\n" + }, + "I genuinely don't remember": { + "text": "{{#if (lte patience 1)}}\\nHank stares at you. A vein throbs in his forehead.\\n\\n\\"It's... it's HANK. My name is HANK. I said it at the START.\\"\\n\\nHe takes a deep breath.\\n\\n\\"You know what? You still win. I just want to go home.\\"\\n\\n{{inc \\"score\\" 25}}\\n{{else}}\\nHank laughs. \\"Honestly? I respect the honesty!\\"\\n\\n{{inc \\"score\\" 75}}\\n\\n\\"The real prize was the confusion we shared along the way!\\"\\n{{/if}}\\n\\n{{section \\"Ending\\"}}\\n" }, "Ending": { "text": "{{#if (gte score 500)}}\\n{{#animate \\"toast\\"}}GRAND CHAMPION!{{/animate}}\\n\\nBalloons fall from the ceiling. Confetti explodes. Professor Boom emerges to set off celebratory fireworks (indoors, against regulations).\\n\\n\\"{{player_name}}, you are our GRAND CHAMPION with {{score}} points!\\"\\n\\nHank hands you an oversized novelty check. It bounces.\\n\\n\\"Congratulations! You've won fame, glory, and whatever prizes we can legally give you after the lawyers review the footage!\\"\\n\\nThe audience carries you out on their shoulders. The cardboard cutout leads the parade.\\n\\n**THE END — GRAND CHAMPION**\\n{{else}}\\n{{#if (gte score 200)}}\\n{{#animate \\"toast\\"}}WINNER!{{/animate}}\\n\\n\\"{{player_name}}, you finished with {{score}} points! That's... that's actually pretty good!\\"\\n\\nHank shakes your hand. \\"You've won a lovely parting gift!\\"\\n\\nA stagehand hands you a gift basket containing: one (1) cheese, a coupon for 10% off at a store that closed in 2019, and a sincere thank-you note from Hank.\\n\\n\\"Come back anytime! Assuming we're not cancelled by then!\\"\\n\\n**THE END — WINNER**\\n{{else}}\\n\\"{{player_name}}... you finished with {{score}} points.\\"\\n\\nHank looks at his cards. Looks at you. Looks at the audience.\\n\\n\\"You know what? You showed up. You tried. And in the end, isn't that what matters?\\"\\n\\n{{embed \\"catchphrase\\"}}\\n\\n\\"No, actually, points are what matter. But good effort!\\"\\n\\nHe hands you a single balloon. It's already deflating.\\n\\n**THE END — PARTICIPANT**\\n{{/if}}\\n{{/if}}\\n" 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. diff --git a/examples/gameshow/gameshow.squiffy b/examples/gameshow/gameshow.squiffy index e51e9fe..119add4 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}}** @@ -34,7 +35,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}}!" @@ -59,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"}}{{sequence (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"}} @@ -135,7 +138,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] @@ -154,15 +157,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. @@ -177,16 +171,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. @@ -199,9 +189,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!" @@ -219,14 +215,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}} @@ -276,23 +272,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. @@ -301,30 +297,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}} @@ -335,7 +331,7 @@ Finally, a woman in the front row shouts: "MAKE THEM {{random (array "SING" "DAN [[Next Round]] {{/if}} -[Sing beautifully]: +[[Sing beautifully]]: You belt out an operatic rendition of the show's theme song (which you're making up as you go). The audience is moved. One of them wakes up specifically to applaud. @@ -345,7 +341,7 @@ The audience is moved. One of them wakes up specifically to applaud. [[Next Round]] -[Sing terribly on purpose]: +[[Sing terribly on purpose]]: You screech like a cat in a tumble dryer. On purpose. For art. {{dec "patience"}} @@ -356,7 +352,7 @@ Hank: "That was... that was really something. Moving on quickly." [[Next Round]] -[Dance with enthusiasm]: +[[Dance with enthusiasm]]: You dance like nobody's watching, except everyone is watching, and also there are cameras. {{inc "score" 125}} @@ -364,7 +360,7 @@ The cardboard cutout falls over again, but in an approving way. [[Next Round]] -[Do the robot]: +[[Do the robot]]: You do the robot so convincingly that a stagehand tries to unplug you. {{inc "score" 100}} @@ -372,7 +368,7 @@ You do the robot so convincingly that a stagehand tries to unplug you. [[Next Round]] -[WHIRRRRRR]: +[[WHIRRRRRR]]: You spin around making blender noises. It's deeply unsettling. {{inc "score" 175}} @@ -381,7 +377,7 @@ Professor Boom's voice echoes from below: "MAGNIFICENT!" [[Next Round]] -[Refuse on grounds of dignity]: +[[Refuse on grounds of dignity]]: "I'm not doing that." {{dec "patience"}} @@ -393,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}}?" @@ -418,19 +414,19 @@ The audience gasps. The cardboard cutout gasps (the AC kicked in). {{#animate "typewriter"}}**What is the name of your host?**{{/animate}} -[Hank Spinneroni] -[Chuck Wheelsworth] -[I genuinely don't remember] +- [[Hank Spinneroni]] +- [[Chuck Wheelsworth]] +- [[I genuinely don't remember]] -[Hank Spinneroni]: +[[Hank Spinneroni]]: "CORRECT! I AM HANK SPINNERONI! Probably!" -{{set "score" (score * 2)}} +{{inc "score" score}} {{#animate "toast"}}SCORE DOUBLED!{{/animate}} [[Ending]] -[Chuck Wheelsworth]: +[[Chuck Wheelsworth]]: "That was my FIRST name! The WRONG first name! But you remembered SOMETHING!" {{inc "score" 50}} @@ -439,7 +435,7 @@ The audience gasps. The cardboard cutout gasps (the AC kicked in). [[Ending]] -[I genuinely don't remember]: +[[I genuinely don't remember]]: {{#if (lte patience 1)}} Hank stares at you. A vein throbs in his forehead. 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/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/squiffy.runtime.test.ts b/runtime/src/squiffy.runtime.test.ts index 99bfad1..dfd629f 100644 --- a/runtime/src/squiffy.runtime.test.ts +++ b/runtime/src/squiffy.runtime.test.ts @@ -2008,4 +2008,249 @@ Level: