narrat 1.3.2 → 2.0.0-rc3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/CHANGELOG.md +175 -0
  2. package/README.md +10 -0
  3. package/lib/app.vue.d.ts +27 -52
  4. package/lib/components/debug/debug-menu.vue.d.ts +1 -1
  5. package/lib/config.d.ts +7 -0
  6. package/lib/exports/plugins.d.ts +4 -4
  7. package/lib/index.d.ts +1 -4
  8. package/lib/index.esm.js +1553 -931
  9. package/lib/index.js +1553 -931
  10. package/lib/plugins/NarratPlugin.d.ts +1 -1
  11. package/lib/stores/hud-stats-store.d.ts +2 -0
  12. package/lib/stores/inventory-store.d.ts +2 -1
  13. package/lib/stores/main-store.d.ts +92 -110
  14. package/lib/stores/quest-log.d.ts +4 -1
  15. package/lib/stores/skills.d.ts +7 -4
  16. package/lib/stores/vm-store.d.ts +99 -104
  17. package/lib/types/parser.d.ts +34 -18
  18. package/lib/utils/audio-loader.d.ts +1 -0
  19. package/lib/utils/data-helpers.d.ts +5 -0
  20. package/lib/utils/logger.d.ts +1 -2
  21. package/lib/utils/skillchecks.d.ts +0 -3
  22. package/lib/utils/string-helpers.d.ts +2 -0
  23. package/lib/vm/commands/arithmetic-commands.d.ts +17 -0
  24. package/lib/vm/commands/audio-commands.d.ts +8 -0
  25. package/lib/vm/commands/choice.d.ts +36 -4
  26. package/lib/vm/commands/clear_dialog.d.ts +2 -3
  27. package/lib/vm/commands/command-helpers.d.ts +2 -0
  28. package/lib/vm/commands/command-plugin.d.ts +17 -8
  29. package/lib/vm/commands/flow-commands.d.ts +14 -0
  30. package/lib/vm/commands/if.d.ts +9 -1
  31. package/lib/vm/commands/inventory-commands.d.ts +14 -4
  32. package/lib/vm/commands/logic-command.d.ts +42 -0
  33. package/lib/vm/commands/notify.d.ts +3 -1
  34. package/lib/vm/commands/quest-commands.d.ts +29 -4
  35. package/lib/vm/commands/screen-commands.d.ts +8 -0
  36. package/lib/vm/commands/set.d.ts +5 -1
  37. package/lib/vm/commands/skill-commands.d.ts +33 -0
  38. package/lib/vm/commands/stats-commands.d.ts +12 -0
  39. package/lib/vm/commands/string-commands.d.ts +9 -0
  40. package/lib/vm/commands/text.d.ts +13 -2
  41. package/lib/vm/commands/wait.d.ts +3 -1
  42. package/lib/vm/vm-helpers.d.ts +6 -6
  43. package/lib/vm/vm-parser.d.ts +10 -1
  44. package/lib/vm/vm.d.ts +8 -6
  45. package/package.json +11 -7
package/lib/index.esm.js CHANGED
@@ -1,4 +1,4 @@
1
- // Version: 1.3.2 - June 26, 2022 18:33:37
1
+ // Version: 2.0.0-rc3 - June 28, 2022 22:38:40
2
2
  import 'es6-promise/auto';
3
3
  import { ref, reactive, readonly, defineComponent, openBlock, createElementBlock, normalizeStyle, createElementVNode, createCommentVNode, Fragment, renderList, normalizeClass, createBlock, Transition, withCtx, renderSlot, createTextVNode, computed, resolveComponent, withDirectives, vModelText, toDisplayString, TransitionGroup, createVNode, createApp } from 'vue';
4
4
  import { defineStore, mapState, createPinia } from 'pinia';
@@ -30,7 +30,7 @@ function styleInject(css, ref) {
30
30
  }
31
31
  }
32
32
 
33
- var css_248z = "/*! @import */\n\n/*! modern-normalize v1.0.0 | MIT License | https://github.com/sindresorhus/modern-normalize */\n\n/*\nDocument\n========\n*/\n\n/**\nUse a better box model (opinionated).\n*/\n\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\n/**\nUse a more readable tab size (opinionated).\n*/\n\n:root {\n -moz-tab-size: 4;\n -o-tab-size: 4;\n tab-size: 4;\n}\n\n/**\n1. Correct the line height in all browsers.\n2. Prevent adjustments of font size after orientation changes in iOS.\n*/\n\nhtml {\n line-height: 1.15; /* 1 */\n -webkit-text-size-adjust: 100%; /* 2 */\n}\n\n/*\nSections\n========\n*/\n\n/**\nRemove the margin in all browsers.\n*/\n\nbody {\n margin: 0;\n}\n\n/**\nImprove consistency of default fonts in all browsers. (https://github.com/sindresorhus/modern-normalize/issues/3)\n*/\n\nbody {\n font-family:\n\t\tsystem-ui,\n\t\t-apple-system, /* Firefox supports this but not yet `system-ui` */\n\t\t'Segoe UI',\n\t\tRoboto,\n\t\tHelvetica,\n\t\tArial,\n\t\tsans-serif,\n\t\t'Apple Color Emoji',\n\t\t'Segoe UI Emoji';\n}\n\n/*\nGrouping content\n================\n*/\n\n/**\n1. Add the correct height in Firefox.\n2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)\n*/\n\nhr {\n height: 0; /* 1 */\n color: inherit; /* 2 */\n}\n\n/*\nText-level semantics\n====================\n*/\n\n/**\nAdd the correct text decoration in Chrome, Edge, and Safari.\n*/\n\nabbr[title] {\n -webkit-text-decoration: underline dotted;\n text-decoration: underline dotted;\n}\n\n/**\nAdd the correct font weight in Edge and Safari.\n*/\n\nb,\nstrong {\n font-weight: bolder;\n}\n\n/**\n1. Improve consistency of default fonts in all browsers. (https://github.com/sindresorhus/modern-normalize/issues/3)\n2. Correct the odd 'em' font sizing in all browsers.\n*/\n\ncode,\nkbd,\nsamp,\npre {\n font-family:\n\t\tui-monospace,\n\t\tSFMono-Regular,\n\t\tConsolas,\n\t\t'Liberation Mono',\n\t\tMenlo,\n\t\tmonospace; /* 1 */\n font-size: 1em; /* 2 */\n}\n\n/**\nAdd the correct font size in all browsers.\n*/\n\nsmall {\n font-size: 80%;\n}\n\n/**\nPrevent 'sub' and 'sup' elements from affecting the line height in all browsers.\n*/\n\nsub,\nsup {\n font-size: 75%;\n line-height: 0;\n position: relative;\n vertical-align: baseline;\n}\n\nsub {\n bottom: -0.25em;\n}\n\nsup {\n top: -0.5em;\n}\n\n/*\nTabular data\n============\n*/\n\n/**\n1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)\n2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)\n*/\n\ntable {\n text-indent: 0; /* 1 */\n border-color: inherit; /* 2 */\n}\n\n/*\nForms\n=====\n*/\n\n/**\n1. Change the font styles in all browsers.\n2. Remove the margin in Firefox and Safari.\n*/\n\nbutton,\ninput,\noptgroup,\nselect,\ntextarea {\n font-family: inherit; /* 1 */\n font-size: 100%; /* 1 */\n line-height: 1.15; /* 1 */\n margin: 0; /* 2 */\n}\n\n/**\nRemove the inheritance of text transform in Edge and Firefox.\n1. Remove the inheritance of text transform in Firefox.\n*/\n\nbutton,\nselect { /* 1 */\n text-transform: none;\n}\n\n/**\nCorrect the inability to style clickable types in iOS and Safari.\n*/\n\nbutton,\n[type='button'],\n[type='reset'] {\n -webkit-appearance: button;\n}\n\n/**\nRemove the inner border and padding in Firefox.\n*/\n\n/**\nRestore the focus styles unset by the previous rule.\n*/\n\n/**\nRemove the additional ':invalid' styles in Firefox.\nSee: https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737\n*/\n\n/**\nRemove the padding so developers are not caught out when they zero out 'fieldset' elements in all browsers.\n*/\n\nlegend {\n padding: 0;\n}\n\n/**\nAdd the correct vertical alignment in Chrome and Firefox.\n*/\n\nprogress {\n vertical-align: baseline;\n}\n\n/**\nCorrect the cursor style of increment and decrement buttons in Safari.\n*/\n\n/**\n1. Correct the odd appearance in Chrome and Safari.\n2. Correct the outline style in Safari.\n*/\n\n[type='search'] {\n -webkit-appearance: textfield; /* 1 */\n outline-offset: -2px; /* 2 */\n}\n\n/**\nRemove the inner padding in Chrome and Safari on macOS.\n*/\n\n/**\n1. Correct the inability to style clickable types in iOS and Safari.\n2. Change font properties to 'inherit' in Safari.\n*/\n\n/*\nInteractive\n===========\n*/\n\n/*\nAdd the correct display in Chrome and Safari.\n*/\n\nsummary {\n display: list-item;\n}\n\n/**\n * Manually forked from SUIT CSS Base: https://github.com/suitcss/base\n * A thin layer on top of normalize.css that provides a starting point more\n * suitable for web applications.\n */\n\n/**\n * Removes the default spacing and border for appropriate elements.\n */\n\nblockquote,\ndl,\ndd,\nh1,\nh2,\nh3,\nh4,\nh5,\nh6,\nhr,\nfigure,\np,\npre {\n margin: 0;\n}\n\nbutton {\n background-color: transparent;\n background-image: none;\n}\n\n/**\n * Work around a Firefox/IE bug where the transparent `button` background\n * results in a loss of the default `button` focus styles.\n */\n\nbutton:focus {\n outline: 1px dotted;\n outline: 5px auto -webkit-focus-ring-color;\n}\n\nfieldset {\n margin: 0;\n padding: 0;\n}\n\nol,\nul {\n list-style: none;\n margin: 0;\n padding: 0;\n}\n\n/**\n * Tailwind custom reset styles\n */\n\n/**\n * 1. Use the user's configured `sans` font-family (with Tailwind's default\n * sans-serif font stack as a fallback) as a sane default.\n * 2. Use Tailwind's default \"normal\" line-height so the user isn't forced\n * to override it to ensure consistency even when using the default theme.\n */\n\nhtml {\n font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\"; /* 1 */\n line-height: 1.5; /* 2 */\n}\n\n/**\n * Inherit font-family and line-height from `html` so users can set them as\n * a class directly on the `html` element.\n */\n\nbody {\n font-family: inherit;\n line-height: inherit;\n}\n\n/**\n * 1. Prevent padding and border from affecting element width.\n *\n * We used to set this in the html element and inherit from\n * the parent element for everything else. This caused issues\n * in shadow-dom-enhanced elements like <details> where the content\n * is wrapped by a div with box-sizing set to `content-box`.\n *\n * https://github.com/mozdevs/cssremedy/issues/4\n *\n *\n * 2. Allow adding a border to an element by just adding a border-width.\n *\n * By default, the way the browser specifies that an element should have no\n * border is by setting it's border-style to `none` in the user-agent\n * stylesheet.\n *\n * In order to easily add borders to elements by just setting the `border-width`\n * property, we change the default border-style for all elements to `solid`, and\n * use border-width to hide them instead. This way our `border` utilities only\n * need to set the `border-width` property instead of the entire `border`\n * shorthand, making our border utilities much more straightforward to compose.\n *\n * https://github.com/tailwindcss/tailwindcss/pull/116\n */\n\n*,\n::before,\n::after {\n box-sizing: border-box; /* 1 */\n border-width: 0; /* 2 */\n border-style: solid; /* 2 */\n border-color: #e5e7eb; /* 2 */\n}\n\n/*\n * Ensure horizontal rules are visible by default\n */\n\nhr {\n border-top-width: 1px;\n}\n\n/**\n * Undo the `border-style: none` reset that Normalize applies to images so that\n * our `border-{width}` utilities have the expected effect.\n *\n * The Normalize reset is unnecessary for us since we default the border-width\n * to 0 on all elements.\n *\n * https://github.com/tailwindcss/tailwindcss/issues/362\n */\n\nimg {\n border-style: solid;\n}\n\ntextarea {\n resize: vertical;\n}\n\ninput::-moz-placeholder, textarea::-moz-placeholder {\n color: #9ca3af;\n}\n\ninput:-ms-input-placeholder, textarea:-ms-input-placeholder {\n color: #9ca3af;\n}\n\ninput::placeholder,\ntextarea::placeholder {\n color: #9ca3af;\n}\n\nbutton {\n cursor: pointer;\n}\n\ntable {\n border-collapse: collapse;\n}\n\nh1,\nh2,\nh3,\nh4,\nh5,\nh6 {\n font-size: inherit;\n font-weight: inherit;\n}\n\n/**\n * Reset links to optimize for opt-in styling instead of\n * opt-out.\n */\n\na {\n color: inherit;\n text-decoration: inherit;\n}\n\n/**\n * Reset form element properties that are easy to forget to\n * style explicitly so you don't inadvertently introduce\n * styles that deviate from your design system. These styles\n * supplement a partial reset that is already applied by\n * normalize.css.\n */\n\nbutton,\ninput,\noptgroup,\nselect,\ntextarea {\n padding: 0;\n line-height: inherit;\n color: inherit;\n}\n\n/**\n * Use the configured 'mono' font family for elements that\n * are expected to be rendered with a monospace font, falling\n * back to the system monospace stack if there is no configured\n * 'mono' font family.\n */\n\npre,\ncode,\nkbd,\nsamp {\n font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;\n}\n\n/**\n * Make replaced elements `display: block` by default as that's\n * the behavior you want almost all of the time. Inspired by\n * CSS Remedy, with `svg` added as well.\n *\n * https://github.com/mozdevs/cssremedy/issues/14\n */\n\nimg,\nsvg,\nvideo,\ncanvas,\naudio,\niframe,\nembed,\nobject {\n display: block;\n vertical-align: middle;\n}\n\n/**\n * Constrain images and videos to the parent width and preserve\n * their instrinsic aspect ratio.\n *\n * https://github.com/mozdevs/cssremedy/issues/14\n */\n\nimg,\nvideo {\n max-width: 100%;\n height: auto;\n}\n\n.container {\n width: 100%;\n}\n\n@media (min-width: 640px) {\n .container {\n max-width: 640px;\n }\n}\n\n@media (min-width: 768px) {\n .container {\n max-width: 768px;\n }\n}\n\n@media (min-width: 1024px) {\n .container {\n max-width: 1024px;\n }\n}\n\n@media (min-width: 1280px) {\n .container {\n max-width: 1280px;\n }\n}\n\n@media (min-width: 1536px) {\n .container {\n max-width: 1536px;\n }\n}\n\n.bg-gray-800 {\n --tw-bg-opacity: 1;\n background-color: rgba(31, 41, 55, var(--tw-bg-opacity));\n}\n\n.border {\n border-width: 1px;\n}\n\n.flex {\n display: flex;\n}\n\n.table {\n display: table;\n}\n\n.grid {\n display: grid;\n}\n\n.hidden {\n display: none;\n}\n\n.flex-row {\n flex-direction: row;\n}\n\n.flex-col {\n flex-direction: column;\n}\n\n.flex-grow {\n flex-grow: 1;\n}\n\n.flex-shrink {\n flex-shrink: 1;\n}\n\n.list-disc {\n list-style-type: disc;\n}\n\n.absolute {\n position: absolute;\n}\n\n.resize {\n resize: both;\n}\n\n* {\n --tw-shadow: 0 0 #0000;\n}\n\n* {\n --tw-ring-inset: var(--tw-empty,/*!*/ /*!*/);\n --tw-ring-offset-width: 0px;\n --tw-ring-offset-color: #fff;\n --tw-ring-color: rgba(59, 130, 246, 0.5);\n --tw-ring-offset-shadow: 0 0 #0000;\n --tw-ring-shadow: 0 0 #0000;\n}\n\n.table-auto {\n table-layout: auto;\n}\n\n.line-through {\n text-decoration: line-through;\n}\n\n.w-full {\n width: 100%;\n}\n\n.gap-4 {\n gap: 1rem;\n}\n\n.grid-cols-3 {\n grid-template-columns: repeat(3, minmax(0, 1fr));\n}\n\n.transform {\n --tw-translate-x: 0;\n --tw-translate-y: 0;\n --tw-rotate: 0;\n --tw-skew-x: 0;\n --tw-skew-y: 0;\n --tw-scale-x: 1;\n --tw-scale-y: 1;\n transform: translateX(var(--tw-translate-x)) translateY(var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n\n.transition {\n transition-property: background-color, border-color, color, fill, stroke, opacity, box-shadow, transform;\n transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n transition-duration: 150ms;\n}\n\n@-webkit-keyframes spin {\n to {\n transform: rotate(360deg);\n }\n}\n\n@keyframes spin {\n to {\n transform: rotate(360deg);\n }\n}\n\n@-webkit-keyframes ping {\n 75%, 100% {\n transform: scale(2);\n opacity: 0;\n }\n}\n\n@keyframes ping {\n 75%, 100% {\n transform: scale(2);\n opacity: 0;\n }\n}\n\n@-webkit-keyframes pulse {\n 50% {\n opacity: .5;\n }\n}\n\n@keyframes pulse {\n 50% {\n opacity: .5;\n }\n}\n\n@-webkit-keyframes bounce {\n 0%, 100% {\n transform: translateY(-25%);\n -webkit-animation-timing-function: cubic-bezier(0.8,0,1,1);\n animation-timing-function: cubic-bezier(0.8,0,1,1);\n }\n\n 50% {\n transform: none;\n -webkit-animation-timing-function: cubic-bezier(0,0,0.2,1);\n animation-timing-function: cubic-bezier(0,0,0.2,1);\n }\n}\n\n@keyframes bounce {\n 0%, 100% {\n transform: translateY(-25%);\n -webkit-animation-timing-function: cubic-bezier(0.8,0,1,1);\n animation-timing-function: cubic-bezier(0.8,0,1,1);\n }\n\n 50% {\n transform: none;\n -webkit-animation-timing-function: cubic-bezier(0,0,0.2,1);\n animation-timing-function: cubic-bezier(0,0,0.2,1);\n }\n}\n\n:root {\n --bg-color: #131720;\n --text-color: #d9e1f2;\n --primary: hsl(255, 30%, 55%);\n --focus: hsl(210, 90%, 50%);\n --secondary: #42b983;\n --border-color: hsla(0, 0%, 100%, 0.2);\n --light-1: hsl(210, 30%, 40%);\n --light-2: hsl(255, 30%, 50%);\n --light-background: linear-gradient(to right, var(--light-1), var(--light-2));\n --shadow-1: hsla(236, 50%, 50%, 0.3);\n --shadow-2: hsla(236, 50%, 50%, 0.4);\n --hud-background: rgba(0, 0, 0, 0.4);\n --hud-text-color: var(--text-color);\n --notifications-bg: darkslateblue;\n --skills-text-background: rgba(0, 0, 0, 0.5);\n --skills-text-color: var(--text-color);\n --skills-level-background: rgba(0, 0, 0, 0.5);\n --skills-level-color: orange;\n --skills-xp-bar-height: 40px;\n --skill-check-name-color: orange;\n --skill-check-difficulty: orange;\n --skill-check-success: green;\n --skill-check-failed: red;\n --skill-check-color: orange;\n --dialog-choice-color: orange;\n --dialog-choice-hover-color: var(--text-color);\n --inventory-text-background: rgba(0, 0, 0, 0.5);\n --inventory-text-color: var(--text-color);\n --inventory-amount-background: rgba(0, 0, 0, 0.5);\n --inventory-amount-color: orange;\n --quest-title-color: yellow;\n --completed-quest-title-color: grey;\n --objective-in-progress-color: white;\n --objective-completed-color: grey;\n}\n\n.list-item {\n display: inline-block;\n margin-right: 10px;\n}\n\n.list-enter-active,\n.list-leave-active {\n transition: all 0.3s ease;\n}\n\n/* .list-move {\n transition: transform 0.3s ease;\n} */\n\n.list-enter-from,\n.list-leave-to {\n opacity: 0;\n transform: translateX(300px);\n}\n\n.notification-item {\n display: inline-block;\n margin-right: 10px;\n}\n\n.notification-enter-active,\n.notification-leave-active {\n transition: all 0.3s ease;\n}\n\n/* .notification-move {\n transition: transform 0.3s ease;\n} */\n\n.notification-enter-from,\n.notification-leave-to {\n opacity: 0;\n transform: translateY(-300px);\n}\n\n.fade-enter-active,\n.fade-leave-active {\n transition: opacity 0.3s ease;\n}\n\n.fade-enter-from,\n.fade-leave-to {\n opacity: 0;\n}\n\n.fade-in-enter-active {\n transition: opacity 0.1s ease;\n}\n\n.fade-in-enter-from {\n opacity: 0;\n}\n\nbody {\n padding: 0;\n margin: 0;\n font-family: Arial, sans-serif;\n background-color: black;\n}\n\n.select {\n background: var(--light-background);\n padding: 10px;\n}\n\n.option {\n background-color: var(--light-2);\n padding: 5px;\n color: var(--text-color);\n}\n\n.button {\n background: var(--light-background);\n color: var(--text-color);\n box-shadow: 0.4rem 0.4rem 2.4rem 0.2rem var(--shadow-1);\n border-radius: 100px;\n padding: 10px;\n font-weight: 800;\n font-size: 16px;\n margin: 10px;\n transition: 0.2s;\n}\n\n.button:focus,\n.button:hover {\n transform: translateY(-0.2rem);\n box-shadow: 0 0 2.4rem 0.2rem var(--shadow-2);\n}\n\n.disabled {\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n user-select: none;\n pointer-events: none;\n}\n\n.button.disabled {\n box-shadow: unset;\n color: grey;\n background: var(--border-color);\n}\n\n.input {\n background: var(--light-background);\n color: var(--text-color);\n box-shadow: 0.4rem 0.4rem 2.4rem 0.2rem var(--shadow-1);\n border-radius: 100px;\n padding: 10px;\n font-weight: 800;\n font-size: 16px;\n margin: 10px;\n transition: 0.2s;\n}\n\n.input:focus,\n.input:hover {\n transform: translateY(-0.2rem);\n box-shadow: 0 0 2.4rem 0.2rem var(--shadow-2);\n}\n\na {\n color: pink;\n text-decoration: underline;\n}\n\nth,\ntd {\n padding: 4px;\n border: 1px solid var(--text-color);\n text-align: center;\n}\n\n#game-holder {\n width: 100vw;\n height: 100vh;\n padding: 0;\n margin: 0;\n top: 0;\n left: 0;\n background-color: black;\n display: flex;\n align-items: center;\n justify-content: center;\n min-height: -webkit-fill-available;\n}\n\n.title {\n font-size: 30px;\n font-weight: 700;\n text-align: center;\n}\n\n.container {\n padding: 20px;\n}\n\nh1,\nh2,\nh3,\nh4 {\n font-weight: 700;\n}\n\nh1 {\n font-size: 30px;\n}\n\nh2 {\n font-size: 26px;\n}\n\nh3 {\n font-size: 24px;\n}\n\nhr.solid {\n border: 1px solid var(--text-color);\n margin-top: 30px;\n margin-bottom: 30px;\n}\n\n.card-1 {\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);\n transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);\n}\n\n.card-1:hover {\n box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);\n}\n\n.card-2 {\n box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23);\n}\n\n.card-3 {\n box-shadow: 0 10px 20px rgba(0, 0, 0, 0.19), 0 6px 6px rgba(0, 0, 0, 0.23);\n}\n\n.card-4 {\n box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);\n}\n\n.card-5 {\n box-shadow: 0 19px 38px rgba(0, 0, 0, 0.3), 0 15px 12px rgba(0, 0, 0, 0.22);\n}\n\n.dialog-choice {\n transition: 0.2s;\n}\n\n.dialog-choice:hover {\n transform: scale(1.05, 1.05);\n transform-origin: center;\n}\n\n/* Somewhat arcane CSS to force the hover color to override the child style.\nOtherwise hovering choices doesn't change the color of skill check prompts. */\n\n.dialog-choice:not(:hover) > .skill-check-name,\n.passive-skill-check > .skill-check-name {\n color: var(--skill-check-name-color);\n}\n\n.dialog-choice:not(:hover) > .skill-check-difficulty,\n.passive-skill-check > .skill-check-difficulty {\n color: var(--skill-check-difficulty);\n}\n\n.skill-check-difficulty {\n font-weight: 700;\n}\n\n.dialog-choice:not(:hover) > .skill-check-success,\n.passive-skill-check > .skill-check-success {\n color: var(--skill-check-success);\n}\n\n.dialog-choice:not(:hover) > .skill-check-failed,\n.passive-skill-check > .skill-check-failed {\n color: var(--skill-check-failed);\n}\n\n.dialog-choice:not(:hover) > .skill-check,\n.passive-skill-check.skill-check {\n color: var(--skill-check-color);\n}\n\n.narrat-canvas {\n position: absolute;\n height: 100%;\n top: 0;\n left: 0;\n}\n\n/* Looks ugly */\n\n/* @keyframes strike-anim {\n 0% {\n width: 0;\n }\n 100% {\n width: 100%;\n }\n}\n.strike-anim {\n position: relative;\n}\n.strike-anim::after {\n content: ' ';\n position: absolute;\n top: 50%;\n left: 0;\n width: 100%;\n height: 1px;\n background: var(--text-color);\n animation-name: strike-anim;\n animation-duration: 0.5s;\n animation-timing-function: linear;\n animation-iteration-count: 1;\n animation-fill-mode: forwards;\n} */\n\n#touchTrigger {\n pointer-events: none;\n}\n\n@media (min-width: 640px) {\n}\n\n@media (min-width: 768px) {\n}\n\n@media (min-width: 1024px) {\n}\n\n@media (min-width: 1280px) {\n}\n\n@media (min-width: 1536px) {\n}\n";
33
+ var css_248z = "/*! @import */\n\n/*! modern-normalize v1.0.0 | MIT License | https://github.com/sindresorhus/modern-normalize */\n\n/*\nDocument\n========\n*/\n\n/**\nUse a better box model (opinionated).\n*/\n\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\n/**\nUse a more readable tab size (opinionated).\n*/\n\n:root {\n -moz-tab-size: 4;\n -o-tab-size: 4;\n tab-size: 4;\n}\n\n/**\n1. Correct the line height in all browsers.\n2. Prevent adjustments of font size after orientation changes in iOS.\n*/\n\nhtml {\n line-height: 1.15; /* 1 */\n -webkit-text-size-adjust: 100%; /* 2 */\n}\n\n/*\nSections\n========\n*/\n\n/**\nRemove the margin in all browsers.\n*/\n\nbody {\n margin: 0;\n}\n\n/**\nImprove consistency of default fonts in all browsers. (https://github.com/sindresorhus/modern-normalize/issues/3)\n*/\n\nbody {\n font-family:\n\t\tsystem-ui,\n\t\t-apple-system, /* Firefox supports this but not yet `system-ui` */\n\t\t'Segoe UI',\n\t\tRoboto,\n\t\tHelvetica,\n\t\tArial,\n\t\tsans-serif,\n\t\t'Apple Color Emoji',\n\t\t'Segoe UI Emoji';\n}\n\n/*\nGrouping content\n================\n*/\n\n/**\n1. Add the correct height in Firefox.\n2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)\n*/\n\nhr {\n height: 0; /* 1 */\n color: inherit; /* 2 */\n}\n\n/*\nText-level semantics\n====================\n*/\n\n/**\nAdd the correct text decoration in Chrome, Edge, and Safari.\n*/\n\nabbr[title] {\n -webkit-text-decoration: underline dotted;\n text-decoration: underline dotted;\n}\n\n/**\nAdd the correct font weight in Edge and Safari.\n*/\n\nb,\nstrong {\n font-weight: bolder;\n}\n\n/**\n1. Improve consistency of default fonts in all browsers. (https://github.com/sindresorhus/modern-normalize/issues/3)\n2. Correct the odd 'em' font sizing in all browsers.\n*/\n\ncode,\nkbd,\nsamp,\npre {\n font-family:\n\t\tui-monospace,\n\t\tSFMono-Regular,\n\t\tConsolas,\n\t\t'Liberation Mono',\n\t\tMenlo,\n\t\tmonospace; /* 1 */\n font-size: 1em; /* 2 */\n}\n\n/**\nAdd the correct font size in all browsers.\n*/\n\nsmall {\n font-size: 80%;\n}\n\n/**\nPrevent 'sub' and 'sup' elements from affecting the line height in all browsers.\n*/\n\nsub,\nsup {\n font-size: 75%;\n line-height: 0;\n position: relative;\n vertical-align: baseline;\n}\n\nsub {\n bottom: -0.25em;\n}\n\nsup {\n top: -0.5em;\n}\n\n/*\nTabular data\n============\n*/\n\n/**\n1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)\n2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)\n*/\n\ntable {\n text-indent: 0; /* 1 */\n border-color: inherit; /* 2 */\n}\n\n/*\nForms\n=====\n*/\n\n/**\n1. Change the font styles in all browsers.\n2. Remove the margin in Firefox and Safari.\n*/\n\nbutton,\ninput,\noptgroup,\nselect,\ntextarea {\n font-family: inherit; /* 1 */\n font-size: 100%; /* 1 */\n line-height: 1.15; /* 1 */\n margin: 0; /* 2 */\n}\n\n/**\nRemove the inheritance of text transform in Edge and Firefox.\n1. Remove the inheritance of text transform in Firefox.\n*/\n\nbutton,\nselect { /* 1 */\n text-transform: none;\n}\n\n/**\nCorrect the inability to style clickable types in iOS and Safari.\n*/\n\nbutton,\n[type='button'],\n[type='reset'] {\n -webkit-appearance: button;\n}\n\n/**\nRemove the inner border and padding in Firefox.\n*/\n\n/**\nRestore the focus styles unset by the previous rule.\n*/\n\n/**\nRemove the additional ':invalid' styles in Firefox.\nSee: https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737\n*/\n\n/**\nRemove the padding so developers are not caught out when they zero out 'fieldset' elements in all browsers.\n*/\n\nlegend {\n padding: 0;\n}\n\n/**\nAdd the correct vertical alignment in Chrome and Firefox.\n*/\n\nprogress {\n vertical-align: baseline;\n}\n\n/**\nCorrect the cursor style of increment and decrement buttons in Safari.\n*/\n\n/**\n1. Correct the odd appearance in Chrome and Safari.\n2. Correct the outline style in Safari.\n*/\n\n[type='search'] {\n -webkit-appearance: textfield; /* 1 */\n outline-offset: -2px; /* 2 */\n}\n\n/**\nRemove the inner padding in Chrome and Safari on macOS.\n*/\n\n/**\n1. Correct the inability to style clickable types in iOS and Safari.\n2. Change font properties to 'inherit' in Safari.\n*/\n\n/*\nInteractive\n===========\n*/\n\n/*\nAdd the correct display in Chrome and Safari.\n*/\n\nsummary {\n display: list-item;\n}\n\n/**\n * Manually forked from SUIT CSS Base: https://github.com/suitcss/base\n * A thin layer on top of normalize.css that provides a starting point more\n * suitable for web applications.\n */\n\n/**\n * Removes the default spacing and border for appropriate elements.\n */\n\nblockquote,\ndl,\ndd,\nh1,\nh2,\nh3,\nh4,\nh5,\nh6,\nhr,\nfigure,\np,\npre {\n margin: 0;\n}\n\nbutton {\n background-color: transparent;\n background-image: none;\n}\n\n/**\n * Work around a Firefox/IE bug where the transparent `button` background\n * results in a loss of the default `button` focus styles.\n */\n\nbutton:focus {\n outline: 1px dotted;\n outline: 5px auto -webkit-focus-ring-color;\n}\n\nfieldset {\n margin: 0;\n padding: 0;\n}\n\nol,\nul {\n list-style: none;\n margin: 0;\n padding: 0;\n}\n\n/**\n * Tailwind custom reset styles\n */\n\n/**\n * 1. Use the user's configured `sans` font-family (with Tailwind's default\n * sans-serif font stack as a fallback) as a sane default.\n * 2. Use Tailwind's default \"normal\" line-height so the user isn't forced\n * to override it to ensure consistency even when using the default theme.\n */\n\nhtml {\n font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\"; /* 1 */\n line-height: 1.5; /* 2 */\n}\n\n/**\n * Inherit font-family and line-height from `html` so users can set them as\n * a class directly on the `html` element.\n */\n\nbody {\n font-family: inherit;\n line-height: inherit;\n}\n\n/**\n * 1. Prevent padding and border from affecting element width.\n *\n * We used to set this in the html element and inherit from\n * the parent element for everything else. This caused issues\n * in shadow-dom-enhanced elements like <details> where the content\n * is wrapped by a div with box-sizing set to `content-box`.\n *\n * https://github.com/mozdevs/cssremedy/issues/4\n *\n *\n * 2. Allow adding a border to an element by just adding a border-width.\n *\n * By default, the way the browser specifies that an element should have no\n * border is by setting it's border-style to `none` in the user-agent\n * stylesheet.\n *\n * In order to easily add borders to elements by just setting the `border-width`\n * property, we change the default border-style for all elements to `solid`, and\n * use border-width to hide them instead. This way our `border` utilities only\n * need to set the `border-width` property instead of the entire `border`\n * shorthand, making our border utilities much more straightforward to compose.\n *\n * https://github.com/tailwindcss/tailwindcss/pull/116\n */\n\n*,\n::before,\n::after {\n box-sizing: border-box; /* 1 */\n border-width: 0; /* 2 */\n border-style: solid; /* 2 */\n border-color: #e5e7eb; /* 2 */\n}\n\n/*\n * Ensure horizontal rules are visible by default\n */\n\nhr {\n border-top-width: 1px;\n}\n\n/**\n * Undo the `border-style: none` reset that Normalize applies to images so that\n * our `border-{width}` utilities have the expected effect.\n *\n * The Normalize reset is unnecessary for us since we default the border-width\n * to 0 on all elements.\n *\n * https://github.com/tailwindcss/tailwindcss/issues/362\n */\n\nimg {\n border-style: solid;\n}\n\ntextarea {\n resize: vertical;\n}\n\ninput::-moz-placeholder, textarea::-moz-placeholder {\n color: #9ca3af;\n}\n\ninput::placeholder,\ntextarea::placeholder {\n color: #9ca3af;\n}\n\nbutton {\n cursor: pointer;\n}\n\ntable {\n border-collapse: collapse;\n}\n\nh1,\nh2,\nh3,\nh4,\nh5,\nh6 {\n font-size: inherit;\n font-weight: inherit;\n}\n\n/**\n * Reset links to optimize for opt-in styling instead of\n * opt-out.\n */\n\na {\n color: inherit;\n text-decoration: inherit;\n}\n\n/**\n * Reset form element properties that are easy to forget to\n * style explicitly so you don't inadvertently introduce\n * styles that deviate from your design system. These styles\n * supplement a partial reset that is already applied by\n * normalize.css.\n */\n\nbutton,\ninput,\noptgroup,\nselect,\ntextarea {\n padding: 0;\n line-height: inherit;\n color: inherit;\n}\n\n/**\n * Use the configured 'mono' font family for elements that\n * are expected to be rendered with a monospace font, falling\n * back to the system monospace stack if there is no configured\n * 'mono' font family.\n */\n\npre,\ncode,\nkbd,\nsamp {\n font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;\n}\n\n/**\n * Make replaced elements `display: block` by default as that's\n * the behavior you want almost all of the time. Inspired by\n * CSS Remedy, with `svg` added as well.\n *\n * https://github.com/mozdevs/cssremedy/issues/14\n */\n\nimg,\nsvg,\nvideo,\ncanvas,\naudio,\niframe,\nembed,\nobject {\n display: block;\n vertical-align: middle;\n}\n\n/**\n * Constrain images and videos to the parent width and preserve\n * their instrinsic aspect ratio.\n *\n * https://github.com/mozdevs/cssremedy/issues/14\n */\n\nimg,\nvideo {\n max-width: 100%;\n height: auto;\n}\n\n.container {\n width: 100%;\n}\n\n@media (min-width: 640px) {\n .container {\n max-width: 640px;\n }\n}\n\n@media (min-width: 768px) {\n .container {\n max-width: 768px;\n }\n}\n\n@media (min-width: 1024px) {\n .container {\n max-width: 1024px;\n }\n}\n\n@media (min-width: 1280px) {\n .container {\n max-width: 1280px;\n }\n}\n\n@media (min-width: 1536px) {\n .container {\n max-width: 1536px;\n }\n}\n\n.bg-gray-800 {\n --tw-bg-opacity: 1;\n background-color: rgba(31, 41, 55, var(--tw-bg-opacity));\n}\n\n.border {\n border-width: 1px;\n}\n\n.flex {\n display: flex;\n}\n\n.table {\n display: table;\n}\n\n.grid {\n display: grid;\n}\n\n.hidden {\n display: none;\n}\n\n.flex-row {\n flex-direction: row;\n}\n\n.flex-col {\n flex-direction: column;\n}\n\n.flex-grow {\n flex-grow: 1;\n}\n\n.flex-shrink {\n flex-shrink: 1;\n}\n\n.list-disc {\n list-style-type: disc;\n}\n\n.absolute {\n position: absolute;\n}\n\n.resize {\n resize: both;\n}\n\n* {\n --tw-shadow: 0 0 #0000;\n}\n\n* {\n --tw-ring-inset: var(--tw-empty,/*!*/ /*!*/);\n --tw-ring-offset-width: 0px;\n --tw-ring-offset-color: #fff;\n --tw-ring-color: rgba(59, 130, 246, 0.5);\n --tw-ring-offset-shadow: 0 0 #0000;\n --tw-ring-shadow: 0 0 #0000;\n}\n\n.table-auto {\n table-layout: auto;\n}\n\n.line-through {\n text-decoration: line-through;\n}\n\n.w-full {\n width: 100%;\n}\n\n.gap-4 {\n gap: 1rem;\n}\n\n.grid-cols-3 {\n grid-template-columns: repeat(3, minmax(0, 1fr));\n}\n\n.transform {\n --tw-translate-x: 0;\n --tw-translate-y: 0;\n --tw-rotate: 0;\n --tw-skew-x: 0;\n --tw-skew-y: 0;\n --tw-scale-x: 1;\n --tw-scale-y: 1;\n transform: translateX(var(--tw-translate-x)) translateY(var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));\n}\n\n.transition {\n transition-property: background-color, border-color, color, fill, stroke, opacity, box-shadow, transform;\n transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n transition-duration: 150ms;\n}\n\n@-webkit-keyframes spin {\n to {\n transform: rotate(360deg);\n }\n}\n\n@keyframes spin {\n to {\n transform: rotate(360deg);\n }\n}\n\n@-webkit-keyframes ping {\n 75%, 100% {\n transform: scale(2);\n opacity: 0;\n }\n}\n\n@keyframes ping {\n 75%, 100% {\n transform: scale(2);\n opacity: 0;\n }\n}\n\n@-webkit-keyframes pulse {\n 50% {\n opacity: .5;\n }\n}\n\n@keyframes pulse {\n 50% {\n opacity: .5;\n }\n}\n\n@-webkit-keyframes bounce {\n 0%, 100% {\n transform: translateY(-25%);\n -webkit-animation-timing-function: cubic-bezier(0.8,0,1,1);\n animation-timing-function: cubic-bezier(0.8,0,1,1);\n }\n\n 50% {\n transform: none;\n -webkit-animation-timing-function: cubic-bezier(0,0,0.2,1);\n animation-timing-function: cubic-bezier(0,0,0.2,1);\n }\n}\n\n@keyframes bounce {\n 0%, 100% {\n transform: translateY(-25%);\n -webkit-animation-timing-function: cubic-bezier(0.8,0,1,1);\n animation-timing-function: cubic-bezier(0.8,0,1,1);\n }\n\n 50% {\n transform: none;\n -webkit-animation-timing-function: cubic-bezier(0,0,0.2,1);\n animation-timing-function: cubic-bezier(0,0,0.2,1);\n }\n}\n\n@font-face {\n font-family: 'OpenDyslexic';\n\n src: url('fonts/OpenDyslexic.ttf.woff') format('woff'),\n url('fonts/OpenDyslexic.ttf.svg#OpenDyslexic') format('svg'),\n url('fonts/OpenDyslexic.ttf.eot'),\n url('fonts/OpenDyslexic.ttf.eot?#iefix') format('embedded-opentype');\n\n font-weight: normal;\n\n font-style: normal;\n}\n\n:root {\n --font-family: 'OpenDyslexic';\n --bg-color: #131720;\n --text-color: #d9e1f2;\n --primary: hsl(255, 30%, 55%);\n --focus: hsl(210, 90%, 50%);\n --secondary: #42b983;\n --border-color: hsla(0, 0%, 100%, 0.2);\n --light-1: hsl(210, 30%, 40%);\n --light-2: hsl(255, 30%, 50%);\n --light-background: linear-gradient(to right, var(--light-1), var(--light-2));\n --shadow-1: hsla(236, 50%, 50%, 0.3);\n --shadow-2: hsla(236, 50%, 50%, 0.4);\n --hud-background: rgba(0, 0, 0, 0.4);\n --hud-text-color: var(--text-color);\n --notifications-bg: darkslateblue;\n --skills-text-background: rgba(0, 0, 0, 0.5);\n --skills-text-color: var(--text-color);\n --skills-level-background: rgba(0, 0, 0, 0.5);\n --skills-level-color: orange;\n --skills-xp-bar-height: 40px;\n --skill-check-name-color: orange;\n --skill-check-difficulty: orange;\n --skill-check-success: green;\n --skill-check-failed: red;\n --skill-check-color: orange;\n --dialog-choice-color: orange;\n --dialog-choice-hover-color: var(--text-color);\n --inventory-text-background: rgba(0, 0, 0, 0.5);\n --inventory-text-color: var(--text-color);\n --inventory-amount-background: rgba(0, 0, 0, 0.5);\n --inventory-amount-color: orange;\n --quest-title-color: yellow;\n --completed-quest-title-color: grey;\n --objective-in-progress-color: white;\n --objective-completed-color: grey;\n}\n\nbody {\n font-family: var(--font-family);\n}\n\n* {\n font-family: var(--font-family);\n text-shadow: 1px 1px #000a;\n}\n\n.list-item {\n display: inline-block;\n margin-right: 10px;\n}\n\n.list-enter-active,\n.list-leave-active {\n transition: all 0.3s ease;\n}\n\n#app {\n background-image: url('img/h01.gif');\n background-size: cover;\n}\n\n/* .list-move {\n transition: transform 0.3s ease;\n} */\n\n.list-enter-from,\n.list-leave-to {\n opacity: 0;\n transform: translateX(300px);\n}\n\n.notification-item {\n display: inline-block;\n margin-right: 10px;\n}\n\n.notification-enter-active,\n.notification-leave-active {\n transition: all 0.3s ease;\n}\n\n/* .notification-move {\n transition: transform 0.3s ease;\n} */\n\n.notification-enter-from,\n.notification-leave-to {\n opacity: 0;\n transform: translateY(-300px);\n}\n\n.fade-enter-active,\n.fade-leave-active {\n transition: opacity 0.3s ease;\n}\n\n.fade-enter-from,\n.fade-leave-to {\n opacity: 0;\n}\n\n.fade-in-enter-active {\n transition: opacity 0.1s ease;\n}\n\n.fade-in-enter-from {\n opacity: 0;\n}\n\nbody {\n padding: 0;\n margin: 0;\n font-family: Arial, sans-serif;\n background-color: black;\n}\n\n.select {\n background: var(--light-background);\n padding: 10px;\n}\n\n.option {\n background-color: var(--light-2);\n padding: 5px;\n color: var(--text-color);\n}\n\n.button {\n background: var(--light-background);\n color: var(--text-color);\n box-shadow: 0.4rem 0.5rem 2.4rem 0.2rem var(--shadow-1), inset -2px -4px #0006,\n inset 2px 2px #fff7;\n border-radius: 10px;\n padding: 10px;\n font-weight: 800;\n font-size: 16px;\n margin: 10px;\n transition: 0.2s;\n box-shadow: inset -2px -4px #0006, inset 2px 2px #fff7;\n}\n\n.button.large {\n border-radius: 20px;\n}\n\n.menu-button {\n border-radius: 5px;\n}\n\n.button.main-menu-button {\n border-radius: 20px;\n}\n\n.button:focus,\n.button:hover {\n transform: translateY(-0.2rem);\n box-shadow: 0 0 1rem 0.2rem var(--shadow-2), inset -2px -4px #0006,\n inset 2px 2px #fff7;\n}\n\n.disabled {\n -webkit-user-select: none;\n -moz-user-select: none;\n user-select: none;\n pointer-events: none;\n}\n\n.button.disabled {\n box-shadow: unset;\n color: grey;\n background: var(--border-color);\n}\n\n.input {\n background: var(--light-background);\n color: var(--text-color);\n box-shadow: 0.4rem 0.4rem 2.4rem 0.2rem var(--shadow-1);\n border-radius: 100px;\n padding: 10px;\n font-weight: 800;\n font-size: 16px;\n margin: 10px;\n transition: 0.2s;\n}\n\n.input:focus,\n.input:hover {\n transform: translateY(-0.2rem);\n box-shadow: 0 0 2.4rem 0.2rem var(--shadow-2);\n}\n\na {\n color: pink;\n text-decoration: underline;\n}\n\nth,\ntd {\n padding: 4px;\n border: 1px solid var(--text-color);\n text-align: center;\n}\n\n#game-holder {\n width: 100vw;\n height: 100vh;\n padding: 0;\n margin: 0;\n top: 0;\n left: 0;\n background-color: black;\n display: flex;\n align-items: center;\n justify-content: center;\n min-height: -webkit-fill-available;\n}\n\n.title {\n font-size: 30px;\n font-weight: 700;\n text-align: center;\n}\n\n.container {\n padding: 20px;\n}\n\nh1,\nh2,\nh3,\nh4 {\n font-weight: 700;\n}\n\nh1 {\n font-size: 30px;\n}\n\nh2 {\n font-size: 26px;\n}\n\nh3 {\n font-size: 24px;\n}\n\nhr.solid {\n border: 1px solid var(--text-color);\n margin-top: 30px;\n margin-bottom: 30px;\n}\n\n.card-1 {\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);\n transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);\n}\n\n.card-1:hover {\n box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);\n}\n\n.card-2 {\n box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23);\n}\n\n.card-3 {\n box-shadow: 0 10px 20px rgba(0, 0, 0, 0.19), 0 6px 6px rgba(0, 0, 0, 0.23);\n}\n\n.card-4 {\n box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);\n}\n\n.card-5 {\n box-shadow: 0 19px 38px rgba(0, 0, 0, 0.3), 0 15px 12px rgba(0, 0, 0, 0.22);\n}\n\n.dialog-choice {\n transition: 0.2s;\n}\n\n.dialog-choice:hover {\n transform: scale(1.05, 1.05);\n transform-origin: center;\n}\n\n/* Somewhat arcane CSS to force the hover color to override the child style.\nOtherwise hovering choices doesn't change the color of skill check prompts. */\n\n.dialog-choice:not(:hover) > .skill-check-name,\n.passive-skill-check > .skill-check-name {\n color: var(--skill-check-name-color);\n}\n\n.dialog-choice:not(:hover) > .skill-check-difficulty,\n.passive-skill-check > .skill-check-difficulty {\n color: var(--skill-check-difficulty);\n}\n\n.skill-check-difficulty {\n font-weight: 700;\n}\n\n.dialog-choice:not(:hover) > .skill-check-success,\n.passive-skill-check > .skill-check-success {\n color: var(--skill-check-success);\n}\n\n.dialog-choice:not(:hover) > .skill-check-failed,\n.passive-skill-check > .skill-check-failed {\n color: var(--skill-check-failed);\n}\n\n.dialog-choice:not(:hover) > .skill-check,\n.passive-skill-check.skill-check {\n color: var(--skill-check-color);\n}\n\n.narrat-canvas {\n position: absolute;\n height: 100%;\n top: 0;\n left: 0;\n}\n\n/* Looks ugly */\n\n/* @keyframes strike-anim {\n 0% {\n width: 0;\n }\n 100% {\n width: 100%;\n }\n}\n.strike-anim {\n position: relative;\n}\n.strike-anim::after {\n content: ' ';\n position: absolute;\n top: 50%;\n left: 0;\n width: 100%;\n height: 1px;\n background: var(--text-color);\n animation-name: strike-anim;\n animation-duration: 0.5s;\n animation-timing-function: linear;\n animation-iteration-count: 1;\n animation-fill-mode: forwards;\n} */\n\n#touchTrigger {\n pointer-events: none;\n}\n\n@media (min-width: 640px) {\n}\n\n@media (min-width: 768px) {\n}\n\n@media (min-width: 1024px) {\n}\n\n@media (min-width: 1280px) {\n}\n\n@media (min-width: 1536px) {\n}\n";
34
34
  styleInject(css_248z);
35
35
 
36
36
  const f=ref([]),v=ref(null),m=ref(null),g=ref(null),h=reactive({current:""}),y=[],b=ref(!1),k=readonly(f),w=readonly(v),M=readonly(m),q=readonly(g),x=readonly(h),$=(e=w.value)=>{h.current=e;const t=k.value.findIndex((t=>t.name===e)),n=k.value.map((e=>e.name));for(let a=0;a<n.length;a++){if(a>0&&a<n.length-1){const e=n[a]+"Minus",r=n[a]+"Plus";h[e]=t<=a,h[r]=t>=a;}h[n[a]]=n[a]===e;}},V=(e=M.value)=>{h.orientation=e,h.isLandscape="landscape"===e,h.isPortrait="portrait"===e;},O=(e=q.value||"light")=>{h.theme=e,h.isDark="dark"===e,h.isLight="light"===e;};function j(e,t){if("undefined"==typeof window||!window.matchMedia)return !1;if("undefined"!=typeof window&&!window.matchMedia)return console.error("Vue3 Mq: No MatchMedia support detected in this browser. Responsive breakpoints not available."),!1;{b.value=!0;const n=window.matchMedia(e),a=({matches:e})=>{e&&t();};y.push({mql:n,cb:a});n.addEventListener&&"function"==typeof n.addEventListener?n.addEventListener("change",a):n.addListener(a),a(n);}}var L=Object.freeze({__proto__:null,[Symbol.toStringTag]:"Module",bootstrap5:{xs:0,sm:576,md:768,lg:992,xl:1200,xxl:1400},bootstrap4:{xs:0,sm:576,md:768,lg:992,xl:1200},bootstrap3:{xs:0,sm:768,md:992,lg:1200},vuetify:{xs:0,sm:600,md:960,lg:1264,xl:1904},tailwind:{xs:0,sm:640,md:768,lg:1024,xl:1280,xxl:1536},devices:{phone:0,tablet:768,laptop:1370,desktop:1906}});const T=e=>{if(!e||"object"!=typeof e)return !1;const t=[];for(let n in e){const a=parseFloat(e[n]);n&&"string"==typeof n?/^[^a-z]/i.test(n)||/[^a-zA-Z0-9_]/.test(n)?console.warn(`Vue3 Mq: "${n}" is an invalid breakpoint key. Breakpoint keys must start with a letter and contain only alphanumeric characters and underscores. Skipping.`):!a&&0!==a||isNaN(a)||a<0?console.warn(`Vue3 Mq: "${n}: ${e[n]}" is not a valid breakpoint. Breakpoints should be a number of zero or above. Skipping.`):t.push({name:n,min:a}):console.warn(`Vue3 Mq: Invalid or missing breakpoint key (${JSON.stringify(n)}). Skipping.`);}t.some((e=>0===e.min))||console.warn("Vue3 Mq: You have not declared a breakpoint with a minimum value of 0. There may be screen sizes to which Vue3Mq does not respond.");return new Set(t.map((e=>e.min))).size<t.length&&console.warn("Vue3 Mq: Your breakpoint configuration contains duplicate values. Behaviour may be unpredictable."),0!==t.length&&t.sort(((e,t)=>e.min-t.min))};function R({breakpoints:e,preset:t}){const n=(e=>{if("string"==typeof e&&L[e])return L[e];{const t=Object.keys(L);return console.error(`Vue3 Mq: "${e}" is not a valid preset. Available options are: ${t.join(", ")}`),!1}})(t),a=T(e);if(!1===n&&!a)throw new TypeError("Vue3 Mq: You must provide a valid preset, or valid breakpoint settings.");var r;r=a||T(n),f.value=r,function(){for(;y.length>0;){const e=y.shift();if(e&&"object"==typeof e){const{mql:t,cb:n}=e;t.addEventListener&&"function"==typeof t.addEventListener?t.removeEventListener("change",n):t.removeListener(n);}}}(),(()=>{const e=Object.keys(h);for(let t of e)delete h[t];$(),V(),O();})();const o=k.value.reduce(((e,t,n,a)=>{const r=`(min-width: ${t.min}px)`,o=n<a.length-1?`(max-width: ${a[n+1].min-1}px)`:null,i=r+(o?" and "+o:"");return Object.assign(e,{[t.name]:i})}),{});for(const i in o){j(o[i],(()=>{$(i);}));}["portrait","landscape"].forEach((e=>{j(`(orientation: ${e})`,(()=>{V(e);}));})),["light","dark"].forEach((e=>{j(`(prefers-color-scheme: ${e})`,(()=>{O(e);}));}));}var N={install:(e,{preset:t="bootstrap5",breakpoints:n,defaultBreakpoint:a,defaultOrientation:r="landscape",defaultTheme:o}={})=>{try{const l=!1===["landscape","portrait"].includes(s=r)?(console.error(`Vue3 Mq: "${s}" is not a valid default orientation. Reverting to unset value.`),null):s,u=((e=null)=>!1===["dark","light"].includes(e)&&null!==e?(console.error(`Vue3 Mq: "${e}" is not a valid default theme. Reverting to unset value.`),null):e)(o);i=a,v.value=i,(e=>{m.value=e;})(l),(e=>{g.value=e;})(u),e.provide("mq",x),e.provide("updateBreakpoints",R),R({breakpoints:n,preset:t});}catch(l){console.error(l);}var i,s;}};
@@ -104,9 +104,11 @@ const defaultConfig = {
104
104
  },
105
105
  },
106
106
  quests: {},
107
+ audioTriggers: {},
107
108
  };
108
109
 
109
110
  function parserError(ctx, line, text) {
111
+ console.error(`Parser error: ${ctx.fileName}:${ctx.currentLine}`, text);
110
112
  const errorText = `[Parser Error] in <span class="error-filename">${ctx.fileName}:${line + 1}</span> - <b>${text}</b>`;
111
113
  error(errorText);
112
114
  }
@@ -3445,6 +3447,7 @@ const useAudio = defineStore('audio', {
3445
3447
 
3446
3448
  const audio = {};
3447
3449
  howler.Howler.volume(0.5);
3450
+ let audioTriggers = {};
3448
3451
  async function loadAudioAssets(config) {
3449
3452
  logger.log(`Loading audio`);
3450
3453
  const loadingPromises = [];
@@ -3473,8 +3476,16 @@ async function loadAudioAssets(config) {
3473
3476
  }
3474
3477
  loadingPromises.push(loadAudio(key, config.audio[key]));
3475
3478
  }
3479
+ if (config.audioTriggers) {
3480
+ audioTriggers = config.audioTriggers;
3481
+ }
3476
3482
  return Promise.all(loadingPromises);
3477
3483
  }
3484
+ function audioEvent(event) {
3485
+ if (audioTriggers[event]) {
3486
+ playAudio(audioTriggers[event]);
3487
+ }
3488
+ }
3478
3489
  async function loadAudio(key, config) {
3479
3490
  return new Promise((resolve, reject) => {
3480
3491
  logger.log(`Loading audio ${config.src}`);
@@ -3565,228 +3576,6 @@ function randomId() {
3565
3576
  return `${Date.now() - Math.floor(Math.random() * 99999999)}`;
3566
3577
  }
3567
3578
 
3568
- // Generate a pinia store named notifications with a state using the type NotificationsState, and save type NotificationsSave, with actions:
3569
- // addNotification(text: string): Adds a new notification to the state, and deletes it after a timeout
3570
- // deleteNotification(id: string): Deletes a notification from the state
3571
- const useNotifications = defineStore('notifications', {
3572
- state: () => ({ notifications: {} }),
3573
- actions: {
3574
- async addNotification(text) {
3575
- const id = `${Date.now()}-${Math.random() * 10000}`;
3576
- this.notifications[id] = {
3577
- text,
3578
- };
3579
- if (getConfig().notifications.alsoPrintInDialogue) {
3580
- writeText(`[NOTIFICATION] ${text}`);
3581
- }
3582
- await timeout(getConfig().notifications.timeOnScreen * 1000);
3583
- this.deleteNotification(id);
3584
- },
3585
- deleteNotification(id) {
3586
- delete this.notifications[id];
3587
- },
3588
- },
3589
- });
3590
-
3591
- // create a pinia store named inventory with a state containing items and actions to add and delete items
3592
- // create a pinia store named inventory with a state of type ItemState and actions to add and delete items. Adding items should increase the amount of any existing item that matches the id.
3593
- const useInventory = defineStore('inventory', {
3594
- state: () => ({
3595
- items: {},
3596
- interactionTags: {},
3597
- }),
3598
- actions: {
3599
- generateSaveData() {
3600
- return {
3601
- items: this.items,
3602
- interactionTags: this.interactionTags,
3603
- };
3604
- },
3605
- loadSaveData(save) {
3606
- this.items = { ...this.items, ...save.items };
3607
- this.interactionTags = { ...save.interactionTags };
3608
- },
3609
- setupItems(items) {
3610
- Object.keys(items).forEach((key) => {
3611
- this.items[key] = {
3612
- amount: 0,
3613
- id: key,
3614
- };
3615
- });
3616
- },
3617
- getExistingItem(id) {
3618
- return this.items[id];
3619
- },
3620
- getItemAmount(id) {
3621
- return this.getExistingItem(id)?.amount || 0;
3622
- },
3623
- add(item) {
3624
- const existingItem = this.getExistingItem(item.id);
3625
- if (existingItem) {
3626
- existingItem.amount += item.amount;
3627
- }
3628
- else {
3629
- this.items[item.id] = { ...item };
3630
- }
3631
- useNotifications().addNotification(`Received item: ${getItemConfig(item.id).name} x ${item.amount}`);
3632
- },
3633
- enableInteraction(tag) {
3634
- if (!tag) {
3635
- tag = 'default';
3636
- }
3637
- this.interactionTags[tag] = {
3638
- blockedInteraction: false,
3639
- };
3640
- },
3641
- disableInteraction(tag) {
3642
- if (!tag) {
3643
- tag = 'default';
3644
- }
3645
- this.interactionTags[tag] = {
3646
- blockedInteraction: true,
3647
- };
3648
- },
3649
- onScriptStart() {
3650
- const tags = getConfig().interactionTags;
3651
- Object.keys(tags).forEach((tag) => {
3652
- const conf = tags[tag];
3653
- if (conf.onlyInteractOutsideOfScripts) {
3654
- this.disableInteraction(tag);
3655
- }
3656
- });
3657
- },
3658
- onScriptEnd() {
3659
- const tags = getConfig().interactionTags;
3660
- Object.keys(tags).forEach((tag) => {
3661
- const conf = tags[tag];
3662
- if (conf.onlyInteractOutsideOfScripts) {
3663
- this.enableInteraction(tag);
3664
- }
3665
- });
3666
- },
3667
- isInteractionTagBlocked(tag) {
3668
- if (!tag) {
3669
- tag = 'default';
3670
- }
3671
- if (this.interactionTags[tag]) {
3672
- return this.interactionTags[tag].blockedInteraction;
3673
- }
3674
- return false;
3675
- },
3676
- remove(item) {
3677
- const existingItem = this.getExistingItem(item.id);
3678
- if (existingItem) {
3679
- existingItem.amount -= item.amount;
3680
- useNotifications().addNotification(`Lost item: ${getItemConfig(item.id).name} x ${item.amount}`);
3681
- if (existingItem.amount <= 0) {
3682
- this.deleteItem(item.id);
3683
- }
3684
- }
3685
- },
3686
- deleteItem(id) {
3687
- const existingItem = this.getExistingItem(id);
3688
- if (existingItem) {
3689
- this.items[id].amount = 0;
3690
- }
3691
- },
3692
- },
3693
- });
3694
-
3695
- const everyObject = (object, predicate) => {
3696
- for (const key in object) {
3697
- if (!predicate(object[key])) {
3698
- return false;
3699
- }
3700
- }
3701
- return true;
3702
- };
3703
- const filterObject = (object, predicate) => {
3704
- const result = {};
3705
- for (const key in object) {
3706
- if (predicate(object[key])) {
3707
- result[key] = object[key];
3708
- }
3709
- }
3710
- return result;
3711
- };
3712
-
3713
- const useQuests = defineStore('quests', {
3714
- state: () => ({
3715
- quests: {},
3716
- }),
3717
- actions: {
3718
- getQuest(questId) {
3719
- const quest = this.quests[questId];
3720
- if (quest) {
3721
- return quest;
3722
- }
3723
- error(`Quest ${questId} doesn't exist!`);
3724
- },
3725
- getObjective(quest, objectiveId) {
3726
- const questObjective = this.getQuest(quest).objectives[objectiveId];
3727
- if (questObjective) {
3728
- return questObjective;
3729
- }
3730
- error(`Objective ${objectiveId} doesn't exist in quest ${quest}!`);
3731
- },
3732
- setupQuests(quests) {
3733
- // iterate through quests to generate quest states to add to this.quests object
3734
- for (const key of Object.keys(quests)) {
3735
- const data = quests[key];
3736
- this.quests[key] = {
3737
- id: key,
3738
- state: 'hidden',
3739
- objectives: {},
3740
- };
3741
- // iterate through data.objectives to populate the objectives array of this.quests[key]
3742
- for (const objectiveKey of Object.keys(data.objectives)) {
3743
- const objective = data.objectives[objectiveKey];
3744
- this.quests[key].objectives[objectiveKey] = {
3745
- id: objectiveKey,
3746
- state: objective.hidden ? 'hidden' : 'unlocked',
3747
- };
3748
- }
3749
- }
3750
- },
3751
- startQuest(questId) {
3752
- this.getQuest(questId).state = 'unlocked';
3753
- useNotifications().addNotification(`Started quest: ${getQuestConfig(questId).title}`);
3754
- },
3755
- startObjective(questId, objectiveId) {
3756
- this.getObjective(questId, objectiveId).state = 'unlocked';
3757
- useNotifications().addNotification(`New quest objective: ${getObjectiveConfig(questId, objectiveId).description}`);
3758
- },
3759
- completeObjective(questId, objectiveId) {
3760
- this.getObjective(questId, objectiveId).state = 'completed';
3761
- useNotifications().addNotification(`Objective completed!`);
3762
- },
3763
- completeQuest(questId, ending) {
3764
- this.getQuest(questId).state = 'completed';
3765
- if (ending) {
3766
- this.getQuest(questId).ending = ending;
3767
- }
3768
- useNotifications().addNotification(`Quest completed: ${getQuestConfig(questId).title}`);
3769
- },
3770
- isQuestCompleted(questId) {
3771
- const quest = this.getQuest(questId);
3772
- return everyObject(quest.objectives, (objective) => objective.state === 'completed');
3773
- },
3774
- removeQuest(id) {
3775
- delete this.quests[id];
3776
- },
3777
- generateSaveData() {
3778
- return {
3779
- quests: {
3780
- ...this.quests,
3781
- },
3782
- };
3783
- },
3784
- loadSaveData(data) {
3785
- this.quests = data.quests;
3786
- },
3787
- },
3788
- });
3789
-
3790
3579
  var isMergeableObject = function isMergeableObject(value) {
3791
3580
  return isNonNullObject(value)
3792
3581
  && !isSpecial(value)
@@ -3919,6 +3708,29 @@ var deepmerge_1 = deepmerge;
3919
3708
 
3920
3709
  var cjs = deepmerge_1;
3921
3710
 
3711
+ // Generate a pinia store named notifications with a state using the type NotificationsState, and save type NotificationsSave, with actions:
3712
+ // addNotification(text: string): Adds a new notification to the state, and deletes it after a timeout
3713
+ // deleteNotification(id: string): Deletes a notification from the state
3714
+ const useNotifications = defineStore('notifications', {
3715
+ state: () => ({ notifications: {} }),
3716
+ actions: {
3717
+ async addNotification(text) {
3718
+ const id = `${Date.now()}-${Math.random() * 10000}`;
3719
+ this.notifications[id] = {
3720
+ text,
3721
+ };
3722
+ if (getConfig().notifications.alsoPrintInDialogue) {
3723
+ writeText(`[NOTIFICATION] ${text}`);
3724
+ }
3725
+ await timeout(getConfig().notifications.timeOnScreen * 1000);
3726
+ this.deleteNotification(id);
3727
+ },
3728
+ deleteNotification(id) {
3729
+ delete this.notifications[id];
3730
+ },
3731
+ },
3732
+ });
3733
+
3922
3734
  // Create a pinia store named skills with a state using the type Skills, with actions:
3923
3735
  // setupSkillCheck(skillCheck: SkillCheckState, id: string)
3924
3736
  // passSkillCheck(skillCheckId: string)
@@ -3937,13 +3749,19 @@ const useSkills = defineStore('skills', {
3937
3749
  setupSkillCheck(skillCheck, id) {
3938
3750
  this.skillChecks[id] = skillCheck;
3939
3751
  },
3940
- passSkillCheck(skillCheckId) {
3941
- this.skillChecks[skillCheckId].passed = true;
3942
- this.skillChecks[skillCheckId].available = false;
3752
+ passSkillCheck(skillCheckId, hide) {
3753
+ this.skillChecks[skillCheckId].happened = true;
3754
+ this.skillChecks[skillCheckId].succeeded = true;
3755
+ if (hide) {
3756
+ this.skillChecks[skillCheckId].hidden = true;
3757
+ }
3943
3758
  },
3944
- failSkillCheck(skillCheckId) {
3945
- this.skillChecks[skillCheckId].passed = false;
3946
- this.skillChecks[skillCheckId].available = false;
3759
+ failSkillCheck(skillCheckId, hide) {
3760
+ this.skillChecks[skillCheckId].happened = true;
3761
+ this.skillChecks[skillCheckId].succeeded = false;
3762
+ if (hide) {
3763
+ this.skillChecks[skillCheckId].hidden = true;
3764
+ }
3947
3765
  },
3948
3766
  generateSaveData() {
3949
3767
  return {
@@ -3951,6 +3769,20 @@ const useSkills = defineStore('skills', {
3951
3769
  skills: this.skills,
3952
3770
  };
3953
3771
  },
3772
+ getSkillCheck(id) {
3773
+ if (!this.skillChecks[id]) {
3774
+ this.setupSkillCheck(this.createSkillCheckState(), id);
3775
+ }
3776
+ return this.skillChecks[id];
3777
+ },
3778
+ createSkillCheckState() {
3779
+ const skillCheck = {
3780
+ happened: false,
3781
+ succeeded: false,
3782
+ hidden: false,
3783
+ };
3784
+ return skillCheck;
3785
+ },
3954
3786
  loadSaveData(data) {
3955
3787
  this.skillChecks = cjs(this.skillChecks, data.skillChecks);
3956
3788
  this.skills = cjs(this.skills, data.skills);
@@ -4009,8 +3841,266 @@ function getFile(url) {
4009
3841
  });
4010
3842
  }
4011
3843
 
4012
- // Create a pinia store named screens with a state using the type ScreenState, with actions:
4013
- // setScreen(screen: string): Sets the current screen to the given screen
3844
+ // Create a pinia store named hud with a state using the type HudState, and save type HudSave, with actions:
3845
+ // setupHudStats(stats: { [key: string]: HudStatConfig }): Iterates the stats argument to add new stats to the state
3846
+ // setStat(stat: string, value: number): Sets the value of a stat
3847
+ // addStat(stat: string, value: number): Adds a value to a stat
3848
+ // generateSaveData(): Function that generates a HudSave object from the data in the state
3849
+ // loadSaveData(data: HudSave): Function that loads the data into the state
3850
+ const useHud = defineStore('hud', {
3851
+ state: () => ({
3852
+ hudStats: {},
3853
+ }),
3854
+ actions: {
3855
+ setupHudStats(stats) {
3856
+ for (const stat in stats) {
3857
+ this.hudStats[stat] = {
3858
+ value: stats[stat].startingValue,
3859
+ };
3860
+ }
3861
+ },
3862
+ setStat(stat, value) {
3863
+ this.hudStats[stat].value = value;
3864
+ },
3865
+ addStat(stat, value) {
3866
+ this.hudStats[stat].value += value;
3867
+ },
3868
+ getStat(stat) {
3869
+ return this.hudStats[stat];
3870
+ },
3871
+ getStatValue(stat) {
3872
+ return this.hudStats[stat].value;
3873
+ },
3874
+ generateSaveData() {
3875
+ return {
3876
+ hudStats: this.hudStats,
3877
+ };
3878
+ },
3879
+ loadSaveData(data) {
3880
+ this.hudStats = cjs(this.hudStats, data.hudStats);
3881
+ },
3882
+ },
3883
+ });
3884
+
3885
+ // create a pinia store named inventory with a state containing items and actions to add and delete items
3886
+ // create a pinia store named inventory with a state of type ItemState and actions to add and delete items. Adding items should increase the amount of any existing item that matches the id.
3887
+ const useInventory = defineStore('inventory', {
3888
+ state: () => ({
3889
+ items: {},
3890
+ interactionTags: {},
3891
+ }),
3892
+ actions: {
3893
+ generateSaveData() {
3894
+ return {
3895
+ items: this.items,
3896
+ interactionTags: this.interactionTags,
3897
+ };
3898
+ },
3899
+ loadSaveData(save) {
3900
+ this.items = { ...this.items, ...save.items };
3901
+ this.interactionTags = { ...save.interactionTags };
3902
+ },
3903
+ setupItems(items) {
3904
+ Object.keys(items).forEach((key) => {
3905
+ this.items[key] = {
3906
+ amount: 0,
3907
+ id: key,
3908
+ };
3909
+ });
3910
+ },
3911
+ hasItem(itemId, amount) {
3912
+ if (!amount) {
3913
+ amount = 1;
3914
+ }
3915
+ return this.items[itemId]?.amount >= amount;
3916
+ },
3917
+ getExistingItem(id) {
3918
+ return this.items[id];
3919
+ },
3920
+ getItemAmount(id) {
3921
+ return this.getExistingItem(id)?.amount || 0;
3922
+ },
3923
+ add(item) {
3924
+ const existingItem = this.getExistingItem(item.id);
3925
+ if (existingItem) {
3926
+ existingItem.amount += item.amount;
3927
+ }
3928
+ else {
3929
+ this.items[item.id] = { ...item };
3930
+ }
3931
+ useNotifications().addNotification(`Received item: ${getItemConfig(item.id).name} x ${item.amount}`);
3932
+ },
3933
+ enableInteraction(tag) {
3934
+ if (!tag) {
3935
+ tag = 'default';
3936
+ }
3937
+ this.interactionTags[tag] = {
3938
+ blockedInteraction: false,
3939
+ };
3940
+ },
3941
+ disableInteraction(tag) {
3942
+ if (!tag) {
3943
+ tag = 'default';
3944
+ }
3945
+ this.interactionTags[tag] = {
3946
+ blockedInteraction: true,
3947
+ };
3948
+ },
3949
+ onScriptStart() {
3950
+ const tags = getConfig().interactionTags;
3951
+ Object.keys(tags).forEach((tag) => {
3952
+ const conf = tags[tag];
3953
+ if (conf.onlyInteractOutsideOfScripts) {
3954
+ this.disableInteraction(tag);
3955
+ }
3956
+ });
3957
+ },
3958
+ onScriptEnd() {
3959
+ const tags = getConfig().interactionTags;
3960
+ Object.keys(tags).forEach((tag) => {
3961
+ const conf = tags[tag];
3962
+ if (conf.onlyInteractOutsideOfScripts) {
3963
+ this.enableInteraction(tag);
3964
+ }
3965
+ });
3966
+ },
3967
+ isInteractionTagBlocked(tag) {
3968
+ if (!tag) {
3969
+ tag = 'default';
3970
+ }
3971
+ if (this.interactionTags[tag]) {
3972
+ return this.interactionTags[tag].blockedInteraction;
3973
+ }
3974
+ return false;
3975
+ },
3976
+ remove(item) {
3977
+ const existingItem = this.getExistingItem(item.id);
3978
+ if (existingItem) {
3979
+ existingItem.amount -= item.amount;
3980
+ useNotifications().addNotification(`Lost item: ${getItemConfig(item.id).name} x ${item.amount}`);
3981
+ if (existingItem.amount <= 0) {
3982
+ this.deleteItem(item.id);
3983
+ }
3984
+ }
3985
+ },
3986
+ deleteItem(id) {
3987
+ const existingItem = this.getExistingItem(id);
3988
+ if (existingItem) {
3989
+ this.items[id].amount = 0;
3990
+ }
3991
+ },
3992
+ },
3993
+ });
3994
+
3995
+ const everyObject = (object, predicate) => {
3996
+ for (const key in object) {
3997
+ if (!predicate(object[key])) {
3998
+ return false;
3999
+ }
4000
+ }
4001
+ return true;
4002
+ };
4003
+ const filterObject = (object, predicate) => {
4004
+ const result = {};
4005
+ for (const key in object) {
4006
+ if (predicate(object[key])) {
4007
+ result[key] = object[key];
4008
+ }
4009
+ }
4010
+ return result;
4011
+ };
4012
+
4013
+ const useQuests = defineStore('quests', {
4014
+ state: () => ({
4015
+ quests: {},
4016
+ }),
4017
+ actions: {
4018
+ getQuest(questId) {
4019
+ const quest = this.quests[questId];
4020
+ if (quest) {
4021
+ return quest;
4022
+ }
4023
+ error(`Quest ${questId} doesn't exist!`);
4024
+ },
4025
+ getObjective(quest, objectiveId) {
4026
+ const questObjective = this.getQuest(quest).objectives[objectiveId];
4027
+ if (questObjective) {
4028
+ return questObjective;
4029
+ }
4030
+ error(`Objective ${objectiveId} doesn't exist in quest ${quest}!`);
4031
+ },
4032
+ setupQuests(quests) {
4033
+ // iterate through quests to generate quest states to add to this.quests object
4034
+ for (const key of Object.keys(quests)) {
4035
+ const data = quests[key];
4036
+ this.quests[key] = {
4037
+ id: key,
4038
+ state: 'hidden',
4039
+ objectives: {},
4040
+ };
4041
+ // iterate through data.objectives to populate the objectives array of this.quests[key]
4042
+ for (const objectiveKey of Object.keys(data.objectives)) {
4043
+ const objective = data.objectives[objectiveKey];
4044
+ this.quests[key].objectives[objectiveKey] = {
4045
+ id: objectiveKey,
4046
+ state: objective.hidden ? 'hidden' : 'unlocked',
4047
+ };
4048
+ }
4049
+ }
4050
+ },
4051
+ startQuest(questId) {
4052
+ this.getQuest(questId).state = 'unlocked';
4053
+ useNotifications().addNotification(`Started quest: ${getQuestConfig(questId).title}`);
4054
+ },
4055
+ startObjective(questId, objectiveId) {
4056
+ this.getObjective(questId, objectiveId).state = 'unlocked';
4057
+ useNotifications().addNotification(`New quest objective: ${getObjectiveConfig(questId, objectiveId).description}`);
4058
+ },
4059
+ completeObjective(questId, objectiveId) {
4060
+ this.getObjective(questId, objectiveId).state = 'completed';
4061
+ useNotifications().addNotification(`Objective completed!`);
4062
+ },
4063
+ completeQuest(questId, ending) {
4064
+ this.getQuest(questId).state = 'completed';
4065
+ if (ending) {
4066
+ this.getQuest(questId).ending = ending;
4067
+ }
4068
+ useNotifications().addNotification(`Quest completed: ${getQuestConfig(questId).title}`);
4069
+ },
4070
+ isQuestCompleted(questId) {
4071
+ const quest = this.getQuest(questId);
4072
+ return everyObject(quest.objectives, (objective) => objective.state === 'completed');
4073
+ },
4074
+ isObjectiveCompleted(questId, objectiveId) {
4075
+ const objective = this.getObjective(questId, objectiveId);
4076
+ return objective.state === 'completed';
4077
+ },
4078
+ isQuestStarted(questId) {
4079
+ const quest = this.getQuest(questId);
4080
+ return quest.state === 'unlocked';
4081
+ },
4082
+ isObjectiveStarted(questId, objectiveId) {
4083
+ const objective = this.getObjective(questId, objectiveId);
4084
+ return objective.state === 'unlocked';
4085
+ },
4086
+ removeQuest(id) {
4087
+ delete this.quests[id];
4088
+ },
4089
+ generateSaveData() {
4090
+ return {
4091
+ quests: {
4092
+ ...this.quests,
4093
+ },
4094
+ };
4095
+ },
4096
+ loadSaveData(data) {
4097
+ this.quests = data.quests;
4098
+ },
4099
+ },
4100
+ });
4101
+
4102
+ // Create a pinia store named screens with a state using the type ScreenState, with actions:
4103
+ // setScreen(screen: string): Sets the current screen to the given screen
4014
4104
  // setButtons(buttons: { [key: string]: ButtonConfig }): Adds buttons to the buttons state by using the values in the buttons config object
4015
4105
  // changeButton(button: string, newValue: boolean): Changes the value of a button in the buttons state
4016
4106
  // generateSaveData(): Function that generates a ScreenState object from the data in the state
@@ -4063,6 +4153,25 @@ function findDataHelper(sourceObj, path) {
4063
4153
  key = keys[i];
4064
4154
  return [obj, key];
4065
4155
  }
4156
+ function findDataHelperWithoutAutoCreate(sourceObj, path) {
4157
+ const keys = path.split('.');
4158
+ let obj = sourceObj;
4159
+ const end = keys.length - 1;
4160
+ let key = keys[0];
4161
+ let i = 0;
4162
+ for (i = 0; i < end; i++) {
4163
+ key = keys[i];
4164
+ if (!obj[key]) {
4165
+ obj[key] = {};
4166
+ }
4167
+ obj = obj[key];
4168
+ }
4169
+ key = keys[i];
4170
+ if (typeof obj[key] === 'undefined') {
4171
+ obj[key] = null;
4172
+ }
4173
+ return [obj, key];
4174
+ }
4066
4175
  function setDataHelper(sourceObj, path, value) {
4067
4176
  const [obj, key] = findDataHelper(sourceObj, path);
4068
4177
  obj[key] = value;
@@ -4077,44 +4186,78 @@ function getModifiableDataPinia() {
4077
4186
  const skills = useSkills();
4078
4187
  const screens = useScreens();
4079
4188
  const inventory = useInventory();
4080
- return {
4189
+ const scope = useVM().currentScope;
4190
+ const state = {
4081
4191
  data: vm.data,
4082
4192
  skills: skills.skills,
4083
4193
  buttons: screens.buttons,
4084
4194
  items: inventory.items,
4085
4195
  quests: useQuests().quests,
4196
+ stats: useHud().hudStats,
4197
+ scope,
4086
4198
  };
4199
+ const proxy = new Proxy(state, {
4200
+ get: (target, prop, receiver) => {
4201
+ const scope = useVM().currentScope;
4202
+ if (typeof scope[prop] !== 'undefined') {
4203
+ return scope[prop];
4204
+ }
4205
+ return Reflect.get(target, prop, receiver);
4206
+ },
4207
+ set: (target, prop, value, receiver) => {
4208
+ const scope = useVM().currentScope;
4209
+ if (typeof scope[prop] !== 'undefined') {
4210
+ scope[prop] = value;
4211
+ return scope[prop];
4212
+ }
4213
+ return Reflect.set(target, prop, value, receiver);
4214
+ },
4215
+ });
4216
+ return proxy;
4087
4217
  }
4088
4218
 
4219
+ const loggerManager = new LogManager();
4220
+ loggerManager.setupDebugger(false);
4221
+ const logger$1 = loggerManager.logger;
4089
4222
  function parseScript(errorHandler, code, fileName) {
4090
4223
  const ctx = {
4091
4224
  fileName,
4225
+ currentLine: 0,
4092
4226
  error: (line, text) => errorHandler(ctx, line, text),
4093
4227
  processCommandsFunction: processCommands,
4094
4228
  indentSize: 0, // Will be overriden soon
4095
4229
  };
4096
4230
  ctx.indentSize = detectIndentation(ctx, code);
4097
4231
  const lines = findLines(ctx, code);
4098
- logger.log(lines);
4232
+ ctx.currentLine = 0;
4233
+ logger$1.log(lines);
4099
4234
  const script = {};
4100
4235
  for (const line of lines) {
4101
4236
  if (line.code.search(':') === -1) {
4102
4237
  ctx.error(line.line, `First indentation level should only be used to specify labels`);
4103
4238
  }
4104
- const labelName = line.code.replace(':', '');
4239
+ const labelString = line.code.replace(':', '');
4240
+ const labelWords = labelString.split(/ +/g);
4241
+ const labelName = labelWords[0];
4242
+ const labelArgs = labelWords.slice(1);
4105
4243
  if (!line.branch) {
4106
4244
  ctx.error(line.line, `This line should have a branch but doesn't`);
4107
4245
  }
4108
- script[labelName] = processCommands(ctx, line.branch, undefined);
4246
+ script[labelName] = {
4247
+ branch: processCommands(ctx, line.branch, undefined),
4248
+ args: labelArgs,
4249
+ };
4109
4250
  }
4110
4251
  return script;
4111
4252
  }
4112
4253
  function processCommands(ctx, lines, parentLine) {
4254
+ const startLine = ctx.currentLine;
4113
4255
  const branchContext = {
4114
4256
  processCommandsFunction: processCommands,
4115
4257
  parserContext: ctx,
4116
4258
  lines,
4117
4259
  currentLine: 0,
4260
+ line: lines[0],
4118
4261
  };
4119
4262
  const branch = [];
4120
4263
  if (!lines) {
@@ -4127,29 +4270,58 @@ function processCommands(ctx, lines, parentLine) {
4127
4270
  }
4128
4271
  while (branchContext.currentLine < lines.length) {
4129
4272
  const line = lines[branchContext.currentLine];
4130
- const operator = line.operator;
4131
- const args = line.args;
4132
- const command = {
4133
- code: line.code,
4134
- operator,
4135
- args,
4136
- line: line.line,
4137
- fileName: ctx.fileName,
4138
- };
4139
4273
  branchContext.line = line;
4140
- branchContext.command = command;
4141
- const commandPlugin = vm.commands[operator];
4274
+ const parsed = parseExpression(ctx, line, line.expression);
4275
+ const commandPlugin = vm.commands[parsed.command.operator];
4142
4276
  let parseFunction = commandPlugin?.parser;
4143
4277
  if (!parseFunction) {
4144
4278
  // default to text function
4145
4279
  parseFunction = vm.commands.text.parser;
4146
4280
  }
4147
- parseFunction(branchContext);
4148
- branch.push(command);
4281
+ logger$1.log(vm.commands.text);
4282
+ const { newLine } = parseFunction(branchContext, parsed);
4283
+ branchContext.currentLine = newLine;
4284
+ ctx.currentLine = startLine + newLine;
4285
+ branch.push(parsed);
4149
4286
  }
4150
4287
  return branch;
4151
4288
  }
4152
- function parseValue(value) {
4289
+ function parseExpression(ctx, line, expression) {
4290
+ logger$1.log(expression);
4291
+ if (typeof expression[0] !== 'string') {
4292
+ ctx.error(line.line, `Expression operator should be a string`);
4293
+ }
4294
+ const parsed = {
4295
+ code: line.code,
4296
+ fileName: ctx.fileName,
4297
+ line: line.line,
4298
+ command: {
4299
+ commandType: expression[0],
4300
+ operator: expression[0],
4301
+ args: expression.slice(1).map((arg) => parseArgument(ctx, line, arg)),
4302
+ options: {},
4303
+ },
4304
+ };
4305
+ const firstElement = expression[0];
4306
+ const command = vm.commands[firstElement];
4307
+ if (!command) {
4308
+ const otherKeywords = ['else', 'success', 'failure'];
4309
+ if (!isParsedTokenString(firstElement) &&
4310
+ !otherKeywords.includes(firstElement)) {
4311
+ ctx.error(line.line, `Unknown command ${firstElement}`);
4312
+ }
4313
+ }
4314
+ return parsed;
4315
+ }
4316
+ function parseArgument(ctx, line, argument) {
4317
+ if (Array.isArray(argument)) {
4318
+ return parseExpression(ctx, line, argument);
4319
+ }
4320
+ else {
4321
+ return argument;
4322
+ }
4323
+ }
4324
+ function parseTokenToPrimitive(value) {
4153
4325
  if (value === 'true') {
4154
4326
  return true;
4155
4327
  }
@@ -4159,26 +4331,18 @@ function parseValue(value) {
4159
4331
  else if (!isNaN(Number(value))) {
4160
4332
  return Number(value);
4161
4333
  }
4162
- else if (value.charAt(0) === '"') {
4163
- return value.substr(1, value.length - 2);
4164
- }
4165
- else {
4166
- return value;
4167
- }
4334
+ return value;
4168
4335
  }
4169
- function parseCodeLine(codeToProcess) {
4336
+ function parseCodeLine(ctx, codeToProcess) {
4170
4337
  if (codeToProcess.charAt(codeToProcess.length - 1) === ':') {
4171
4338
  codeToProcess = codeToProcess.substr(0, codeToProcess.length - 1);
4172
4339
  }
4173
- let code = codeToProcess;
4174
- let ifWords = [];
4175
- const ifIndex = codeToProcess.search(/\$if/g);
4176
- // If we find a code block, everything after is code and won't be looked it
4177
- // TODO: Generalise for other code operators
4178
- if (ifIndex !== -1) {
4179
- code = codeToProcess.substr(0, ifIndex);
4180
- ifWords = ['if', codeToProcess.substr(ifIndex + 3)];
4181
- }
4340
+ const tokens = parseCodeLineIntoTokens(codeToProcess);
4341
+ const [expression] = tokensToExpression(ctx, tokens);
4342
+ return expression;
4343
+ }
4344
+ function parseCodeLineIntoTokens(code) {
4345
+ // Finds strings and processes the code around them to get all tokens
4182
4346
  const regex = /(["'])(?:\\\1|.)*?\1/g;
4183
4347
  const matches = [];
4184
4348
  let match;
@@ -4186,26 +4350,92 @@ function parseCodeLine(codeToProcess) {
4186
4350
  matches.push(match);
4187
4351
  }
4188
4352
  let currentIndex = 0;
4189
- let words = [];
4353
+ let tokens = [];
4190
4354
  for (const match of matches) {
4355
+ // For each string match, process the code before it to add all tokens
4191
4356
  const index = match.index;
4192
4357
  if (index > currentIndex) {
4193
4358
  const inBetween = code.substr(currentIndex, index - currentIndex);
4194
- const newWords = inBetween.split(' ').filter((word) => word);
4195
- words = [...words, ...newWords];
4359
+ const newTokens = splitIntoTokens(inBetween);
4360
+ tokens = [...tokens, ...newTokens];
4196
4361
  }
4197
4362
  // Remove backticks for escaped quotes
4198
4363
  const finalMatch = match[0].replace(/\\/g, '');
4199
- words.push(finalMatch);
4364
+ tokens.push(`$$"${finalMatch.substring(1, finalMatch.length - 1)}`);
4200
4365
  currentIndex = index + match[0].length;
4201
4366
  }
4202
- const newWords = code
4203
- .substr(currentIndex)
4204
- .split(' ')
4205
- .filter((code) => code);
4206
- words = [...words, ...newWords, ...ifWords];
4207
- words.forEach((word, index) => (words[index] = parseValue(word)));
4208
- return words;
4367
+ // Process the last tokens from after the last string
4368
+ tokens = [...tokens, ...splitIntoTokens(code.substr(currentIndex))];
4369
+ // Adding a closing parenthesis at the end of the line so users don't have to
4370
+ tokens.push(')');
4371
+ return tokens;
4372
+ }
4373
+ // Splits a chunk of code (without strings) into tokens
4374
+ function splitIntoTokens(code) {
4375
+ code = code.replace(/: *$/g, '');
4376
+ let result = code.split(' ').filter((el) => el);
4377
+ result = result.reduce((total, curr) => [...total, ...curr.split(/(\(|\))/g)].filter((el) => el && el), []);
4378
+ return result.map((token) => parseTokenToPrimitive(token));
4379
+ }
4380
+ function tokensToExpression(ctx, tokens) {
4381
+ logger$1.log('===============');
4382
+ let expression = [];
4383
+ let cursor = 0;
4384
+ // Find sub expressions inside this expression
4385
+ let parenthesisIndex = findExpressionStart(tokens) + cursor;
4386
+ let parenthesisEndIndex = findExpressionEnd(tokens) + cursor;
4387
+ logger$1.log(`Parsing expression: ${tokens}`);
4388
+ logger$1.log(`Parenthesis start index: ${parenthesisIndex} - end: ${parenthesisEndIndex}`);
4389
+ while (parenthesisIndex !== -1 && parenthesisEndIndex > parenthesisIndex) {
4390
+ // Add everything before the sub expression
4391
+ expression = [...expression, ...tokens.slice(cursor, parenthesisIndex)];
4392
+ const subExpressionString = tokens.slice(parenthesisIndex + 1);
4393
+ logger$1.log(`Found a sub expression. Before: ${expression} - After: ${subExpressionString}`);
4394
+ cursor = parenthesisIndex;
4395
+ // Process the sub expression
4396
+ const [subExpression, subExpressionLength] = tokensToExpression(ctx, subExpressionString);
4397
+ const subExpressionEndIndex = cursor + subExpressionLength;
4398
+ expression.push(subExpression);
4399
+ cursor = subExpressionEndIndex + 1;
4400
+ const restOfString = tokens.slice(cursor);
4401
+ logger$1.log(`Sub expression came back: ${subExpression} - rest of string: ${restOfString}`);
4402
+ // Find the next sub expression, if any
4403
+ parenthesisIndex = findExpressionStart(restOfString);
4404
+ if (parenthesisIndex !== -1) {
4405
+ parenthesisIndex += cursor;
4406
+ }
4407
+ parenthesisEndIndex = findExpressionEnd(restOfString);
4408
+ if (parenthesisEndIndex !== -1) {
4409
+ parenthesisEndIndex += cursor;
4410
+ }
4411
+ }
4412
+ if (parenthesisEndIndex !== -1) {
4413
+ logger$1.log(`Found parenthesis end before new opening parenthesis, close this expression`);
4414
+ }
4415
+ // Find the end of the current expression
4416
+ const endIndex = findExpressionEnd(tokens.slice(cursor)) + cursor;
4417
+ if (endIndex === -1) {
4418
+ ctx.error(ctx.currentLine, `Expression is not closed (missing ")" closing parenthesis)`);
4419
+ return [expression, endIndex];
4420
+ }
4421
+ const restOfString = tokens.slice(cursor, endIndex);
4422
+ logger$1.log(`End of expression: ${endIndex} - ${restOfString}`);
4423
+ logger$1.log('===================');
4424
+ // Add the remaining tokens to the expression
4425
+ expression = [...expression, ...restOfString];
4426
+ validateExpression(ctx, expression);
4427
+ return [expression, endIndex + 1];
4428
+ }
4429
+ function validateExpression(ctx, expression) {
4430
+ if (expression.length < 1) {
4431
+ ctx.error(ctx.currentLine, `Expression is empty`);
4432
+ }
4433
+ }
4434
+ function findExpressionStart(tokens) {
4435
+ return tokens.findIndex((token) => token === '(');
4436
+ }
4437
+ function findExpressionEnd(tokens) {
4438
+ return tokens.findIndex((token) => token === ')');
4209
4439
  }
4210
4440
  function findLines(ctx, data) {
4211
4441
  const code = data.split(/\r?\n|$/).map((line) => {
@@ -4228,11 +4458,12 @@ function findBranches(ctx, code, startLine, indentLevel) {
4228
4458
  }
4229
4459
  let lineText = code[currentLine];
4230
4460
  if (lineText.search(/^\s*$/) !== -1) {
4461
+ // Ignore empty lines
4231
4462
  currentLine++;
4232
4463
  }
4233
4464
  else {
4234
4465
  const lineIndent = getIndentLevel(ctx, lineText);
4235
- lineText = lineText.substr(lineIndent * ctx.indentSize);
4466
+ lineText = lineText.substring(lineIndent * ctx.indentSize);
4236
4467
  validateIndent(ctx, lineIndent, currentLine);
4237
4468
  if (lineIndent < indentLevel) {
4238
4469
  stillInBranch = false;
@@ -4246,16 +4477,16 @@ function findBranches(ctx, code, startLine, indentLevel) {
4246
4477
  currentLine = branchLines.endLine;
4247
4478
  }
4248
4479
  else {
4249
- const words = parseCodeLine(lineText);
4480
+ const expression = parseCodeLine(ctx, lineText);
4250
4481
  const line = {
4251
4482
  code: lineText,
4252
4483
  indentation: lineIndent,
4253
4484
  line: currentLine,
4254
- operator: words[0],
4255
- args: words.slice(1),
4485
+ expression,
4256
4486
  };
4257
4487
  lines.push(line);
4258
4488
  currentLine++;
4489
+ ctx.currentLine = currentLine;
4259
4490
  }
4260
4491
  }
4261
4492
  }
@@ -4278,7 +4509,7 @@ function detectIndentation(ctx, script) {
4278
4509
  if (result.length < 2) {
4279
4510
  ctx.error(0, `Can't detect indentation level. Make sure you indent with at least 2 spaces and consistently`);
4280
4511
  }
4281
- logger.log(result);
4512
+ logger$1.log(result);
4282
4513
  return result[1].length;
4283
4514
  }
4284
4515
 
@@ -4301,6 +4532,7 @@ const useVM = defineStore('vm', {
4301
4532
  lastLabel: 'main',
4302
4533
  script: {},
4303
4534
  labelStack: ['main'],
4535
+ currentScope: {},
4304
4536
  }),
4305
4537
  actions: {
4306
4538
  generateSaveData() {
@@ -4313,6 +4545,26 @@ const useVM = defineStore('vm', {
4313
4545
  this.lastLabel = data.lastLabel;
4314
4546
  this.data = data.data;
4315
4547
  },
4548
+ resetScope() {
4549
+ this.currentScope = {};
4550
+ },
4551
+ setReturnValue(value) {
4552
+ this.currentStack.returnValue = value;
4553
+ },
4554
+ removeFromScope(vars) {
4555
+ for (const varName of vars) {
4556
+ delete this.currentScope[varName];
4557
+ }
4558
+ },
4559
+ addScopedVariable(key, value) {
4560
+ this.currentScope[key] = value;
4561
+ if (this.currentStack) {
4562
+ this.currentStack.scope[key] = value;
4563
+ }
4564
+ },
4565
+ extendScope(scope) {
4566
+ Object.assign(this.currentScope, scope);
4567
+ },
4316
4568
  async loadScripts(scriptPaths) {
4317
4569
  const filePromises = [];
4318
4570
  for (const path of scriptPaths) {
@@ -4333,13 +4585,20 @@ const useVM = defineStore('vm', {
4333
4585
  this.setScript(scripts);
4334
4586
  },
4335
4587
  start() {
4336
- this.stack = [
4337
- {
4338
- currentIndex: 0,
4339
- branch: this.script.main,
4340
- label: 'main',
4588
+ this.setStack({
4589
+ currentIndex: 0,
4590
+ branchData: {
4591
+ branch: this.script.main.branch,
4592
+ },
4593
+ label: 'main',
4594
+ });
4595
+ this.setStack({
4596
+ currentIndex: 0,
4597
+ branchData: {
4598
+ branch: this.script.main.branch,
4341
4599
  },
4342
- ];
4600
+ label: 'main',
4601
+ });
4343
4602
  },
4344
4603
  setLastLabel(label) {
4345
4604
  this.lastLabel = label;
@@ -4347,9 +4606,11 @@ const useVM = defineStore('vm', {
4347
4606
  reset() {
4348
4607
  this.stack = [];
4349
4608
  this.data = {};
4350
- this.stack.push({
4609
+ this.setStack({
4351
4610
  currentIndex: 0,
4352
- branch: this.script.main,
4611
+ branchData: {
4612
+ branch: this.script.main.branch,
4613
+ },
4353
4614
  label: 'main',
4354
4615
  });
4355
4616
  },
@@ -4360,9 +4621,30 @@ const useVM = defineStore('vm', {
4360
4621
  this.data = data;
4361
4622
  },
4362
4623
  setStack(stack) {
4624
+ this.resetScope();
4363
4625
  this.stack = [];
4626
+ const newStack = this.stackOptionsToStack(stack);
4364
4627
  this.lastLabel = stack.label;
4365
- this.stack.push(stack);
4628
+ this.stack.push(newStack);
4629
+ },
4630
+ stackOptionsToStack(stack) {
4631
+ const newStack = {
4632
+ ...stack,
4633
+ scope: {},
4634
+ returnValue: null,
4635
+ };
4636
+ if (stack.scope) {
4637
+ newStack.scope = stack.scope;
4638
+ }
4639
+ if (stack.args && stack.branchData.args) {
4640
+ for (const [index, argName] of stack.branchData.args.entries()) {
4641
+ if (stack.args.length > index) {
4642
+ newStack.scope[argName] = stack.args[index];
4643
+ }
4644
+ }
4645
+ }
4646
+ this.extendScope(newStack.scope || {});
4647
+ return newStack;
4366
4648
  },
4367
4649
  setData(path, value) {
4368
4650
  const dataToModify = getModifiableDataPinia();
@@ -4372,10 +4654,11 @@ const useVM = defineStore('vm', {
4372
4654
  const dataToModify = getModifiableDataPinia();
4373
4655
  addDataHelper(dataToModify, path, value);
4374
4656
  },
4375
- addStack(newStack) {
4376
- if (!newStack.label) {
4377
- newStack.label = this.currentStack.label;
4657
+ addStack(newStackOptions) {
4658
+ if (!newStackOptions.label) {
4659
+ newStackOptions.label = this.currentStack.label;
4378
4660
  }
4661
+ const newStack = this.stackOptionsToStack(newStackOptions);
4379
4662
  this.stack.push(newStack);
4380
4663
  return this.runLine();
4381
4664
  },
@@ -4385,13 +4668,12 @@ const useVM = defineStore('vm', {
4385
4668
  return;
4386
4669
  }
4387
4670
  if (this.currentStack.currentIndex <
4388
- this.currentStack.branch.length - 1) {
4671
+ this.currentStack.branchData.branch.length - 1) {
4389
4672
  this.currentStack.currentIndex++;
4390
4673
  }
4391
4674
  else {
4392
- this.previousStack();
4675
+ return this.previousStack();
4393
4676
  // This branch is finished, go back to previous stack
4394
- return this.nextLine();
4395
4677
  }
4396
4678
  if (this.stack.length === 0) {
4397
4679
  this.finishGame();
@@ -4401,8 +4683,17 @@ const useVM = defineStore('vm', {
4401
4683
  }
4402
4684
  this.currentStack.currentIndex++;
4403
4685
  },
4404
- previousStack() {
4686
+ async previousStack() {
4687
+ const previousScope = this.currentStack.scope;
4688
+ this.removeFromScope(Object.keys(previousScope));
4689
+ const { returnValue, onComplete } = this.currentStack;
4405
4690
  this.stack.splice(this.stack.length - 1, 1);
4691
+ if (onComplete) {
4692
+ onComplete(returnValue);
4693
+ }
4694
+ else {
4695
+ return this.nextLine();
4696
+ }
4406
4697
  },
4407
4698
  finishGame() {
4408
4699
  useInventory().onScriptEnd();
@@ -4416,38 +4707,41 @@ const useVM = defineStore('vm', {
4416
4707
  }
4417
4708
  },
4418
4709
  async runLine() {
4419
- const cmd = this.currentLine;
4710
+ const expression = this.currentLine;
4420
4711
  useInventory().onScriptStart();
4421
- await runCommand(cmd);
4712
+ await runCommand(expression);
4422
4713
  },
4423
- async runLabelFunction(label, comeBackToSameLine) {
4424
- if (comeBackToSameLine) {
4425
- this.currentStack.currentIndex--;
4426
- }
4427
- const branch = this.script[label];
4428
- if (!branch) {
4429
- console.error(`Label ${branch} doesn't exist`);
4430
- }
4431
- const stack = {
4432
- currentIndex: 0,
4433
- branch: branch,
4434
- label,
4435
- };
4436
- await this.addStack(stack);
4714
+ runLabelFunction(label, ...args) {
4715
+ return new Promise((resolve, reject) => {
4716
+ const branchData = this.script[label];
4717
+ if (!branchData) {
4718
+ console.error(`Label ${label} doesn't exist`);
4719
+ }
4720
+ // TODO: Inject args
4721
+ const stack = {
4722
+ currentIndex: 0,
4723
+ branchData: branchData,
4724
+ label,
4725
+ args,
4726
+ onComplete: (returnValue) => {
4727
+ resolve(returnValue);
4728
+ },
4729
+ };
4730
+ this.addStack(stack);
4731
+ });
4437
4732
  },
4438
- runLabel(label) {
4439
- const branch = this.script[label];
4440
- if (!branch) {
4441
- console.error(`Label ${branch} doesn't exist`);
4733
+ runLabel(label, ...args) {
4734
+ const branchData = this.script[label];
4735
+ if (!branchData) {
4736
+ console.error(`Label ${branchData} doesn't exist`);
4442
4737
  }
4443
4738
  this.setLastLabel(label);
4444
- this.stack = [
4445
- {
4446
- currentIndex: 0,
4447
- branch,
4448
- label,
4449
- },
4450
- ];
4739
+ this.setStack({
4740
+ currentIndex: 0,
4741
+ branchData,
4742
+ args,
4743
+ label,
4744
+ });
4451
4745
  this.runLine();
4452
4746
  },
4453
4747
  },
@@ -4458,7 +4752,7 @@ const useVM = defineStore('vm', {
4458
4752
  },
4459
4753
  currentLine() {
4460
4754
  const currentStack = this.currentStack;
4461
- return currentStack.branch[currentStack.currentIndex];
4755
+ return currentStack.branchData.branch[currentStack.currentIndex];
4462
4756
  },
4463
4757
  },
4464
4758
  });
@@ -4468,16 +4762,20 @@ function processText(text) {
4468
4762
  const skillStore = useSkills();
4469
4763
  return text.replace(/%{[^}]*}/g, (match) => {
4470
4764
  const key = match.substr(2, match.length - 3);
4471
- const searchableState = {
4472
- data: vmStore.data,
4473
- skills: skillStore.skills,
4474
- items: useInventory().items,
4475
- quests: useQuests().quests,
4476
- };
4765
+ const searchableState = getModifiableDataPinia();
4477
4766
  const [obj, newKey] = findDataHelper(searchableState, key);
4478
4767
  return obj[newKey];
4479
4768
  });
4480
- }
4769
+ }
4770
+ const stringRegex = /\$\$"/;
4771
+ const isParsedTokenString = (arg) => {
4772
+ if (typeof arg === 'string') {
4773
+ if (arg.search(stringRegex) === 0) {
4774
+ return true;
4775
+ }
4776
+ }
4777
+ return false;
4778
+ };
4481
4779
 
4482
4780
  // Create a pinia store named dialog with a state using the type DialogState, with actions addDialog and clearDialog
4483
4781
  const useDialogStore = defineStore('dialog', {
@@ -4515,57 +4813,6 @@ const useDialogStore = defineStore('dialog', {
4515
4813
  },
4516
4814
  });
4517
4815
 
4518
- // Create a pinia store named hud with a state using the type HudState, and save type HudSave, with actions:
4519
- // setupHudStats(stats: { [key: string]: HudStatConfig }): Iterates the stats argument to add new stats to the state
4520
- // setStat(stat: string, value: number): Sets the value of a stat
4521
- // addStat(stat: string, value: number): Adds a value to a stat
4522
- // generateSaveData(): Function that generates a HudSave object from the data in the state
4523
- // loadSaveData(data: HudSave): Function that loads the data into the state
4524
- const useHud = defineStore('hud', {
4525
- state: () => ({
4526
- hudStats: {},
4527
- }),
4528
- actions: {
4529
- setupHudStats(stats) {
4530
- for (const stat in stats) {
4531
- this.hudStats[stat] = {
4532
- value: stats[stat].startingValue,
4533
- };
4534
- }
4535
- },
4536
- setStat(stat, value) {
4537
- this.hudStats[stat].value = value;
4538
- },
4539
- addStat(stat, value) {
4540
- this.hudStats[stat].value += value;
4541
- },
4542
- generateSaveData() {
4543
- return {
4544
- hudStats: this.hudStats,
4545
- };
4546
- },
4547
- loadSaveData(data) {
4548
- this.hudStats = cjs(this.hudStats, data.hudStats);
4549
- },
4550
- },
4551
- });
4552
-
4553
- function createSkillCheckState() {
4554
- const skillCheck = {
4555
- passed: false,
4556
- available: true,
4557
- };
4558
- return skillCheck;
4559
- }
4560
- function getSkillCheckState(skillCheckId) {
4561
- const skillStore = useSkills();
4562
- let skillCheck = skillStore.skillChecks[skillCheckId];
4563
- if (!skillCheck) {
4564
- skillCheck = createSkillCheckState();
4565
- skillStore.setupSkillCheck(skillCheck, skillCheckId);
4566
- }
4567
- return skillCheck;
4568
- }
4569
4816
  function getSkillCheckDifficultyScore(value, level) {
4570
4817
  return value - level * getConfig().skillChecks.skillMultiplier;
4571
4818
  }
@@ -4593,16 +4840,16 @@ function getSkillCheckDifficultyText(value, level) {
4593
4840
  }
4594
4841
  function getSkillCheckText({ skill, skillCheckId, value, }) {
4595
4842
  const skillStore = useSkills();
4596
- const skillCheckState = getSkillCheckState(skillCheckId);
4843
+ const skillCheckState = skillStore.getSkillCheck(skillCheckId);
4597
4844
  const skillConfig = getSkillConfig(skill);
4598
4845
  const level = skillStore.skills[skill].level;
4599
4846
  const difficultyText = getSkillCheckDifficultyText(value, level);
4600
4847
  let allowed = true;
4601
4848
  let text = `<span class='skill-check'>[<span class='skill-check-name'>${skillConfig.name}</span> - `;
4602
- if (skillCheckState.available) {
4849
+ if (!skillCheckState.happened) {
4603
4850
  text += ` <span class='skill-check-difficulty'>${difficultyText}</span>]</span>`;
4604
4851
  }
4605
- else if (skillCheckState.passed) {
4852
+ else if (skillCheckState.succeeded) {
4606
4853
  text = '';
4607
4854
  }
4608
4855
  else {
@@ -4646,41 +4893,44 @@ function resolveSkillCheck(params) {
4646
4893
  success = false;
4647
4894
  }
4648
4895
  logger.log(`[SKILL CHECK ${skill.name}]: ${success ? '✅' : '❌'}`, `(${params.id}) - ${roll}/${params.value}`);
4896
+ success
4897
+ ? audioEvent('onSkillCheckSuccess')
4898
+ : audioEvent('onSkillCheckFailure');
4649
4899
  return success;
4650
4900
  }
4651
4901
 
4652
- function processSkillCheck(skillcheck) {
4653
- return runSkillCheck({
4654
- skill: skillcheck.skill,
4655
- value: skillcheck.value,
4656
- id: skillcheck.id,
4657
- success: skillcheck.success.text,
4658
- failure: skillcheck.failure.text,
4659
- });
4660
- }
4661
4902
  function runSkillCheck(params) {
4662
- const success = resolveSkillCheck(params);
4663
4903
  const skillStore = useSkills();
4904
+ const result = skillStore.getSkillCheck(params.id);
4905
+ if (result && result.happened) {
4906
+ return result;
4907
+ }
4908
+ const success = resolveSkillCheck(params);
4664
4909
  writeText(getPassiveSkillCheckText(success, params));
4665
4910
  if (success) {
4666
- skillStore.passSkillCheck(params.id);
4667
- return true;
4911
+ skillStore.passSkillCheck(params.id, params.hideAfterRoll);
4668
4912
  }
4669
- skillStore.failSkillCheck(params.id);
4670
- return false;
4913
+ else {
4914
+ skillStore.failSkillCheck(params.id, params.hideAfterRoll);
4915
+ }
4916
+ return skillStore.getSkillCheck(params.id);
4671
4917
  }
4672
4918
  function runConditionCommand(command) {
4673
4919
  const options = command.options;
4674
- const result = runCondition(options.condition);
4920
+ const staticOptions = command.staticOptions;
4921
+ const result = !!options.condition;
4675
4922
  logger.log(result);
4676
4923
  if (result) {
4677
- return options.success;
4924
+ return staticOptions.success;
4678
4925
  }
4679
- if (!result && options.failure) {
4680
- return options.failure;
4926
+ if (!result && staticOptions.failure) {
4927
+ return staticOptions.failure;
4681
4928
  }
4682
4929
  return undefined;
4683
4930
  }
4931
+ function isExpression(arg) {
4932
+ return typeof arg === 'object';
4933
+ }
4684
4934
  function writeText(text) {
4685
4935
  const dialog = {
4686
4936
  speaker: 'game',
@@ -4690,48 +4940,6 @@ function writeText(text) {
4690
4940
  const dialogStore = useDialogStore();
4691
4941
  dialogStore.addDialog(dialog);
4692
4942
  }
4693
- function runCondition(condition) {
4694
- return conditionFunction(condition);
4695
- }
4696
- function conditionFunction(condition) {
4697
- const skillStore = useSkills();
4698
- const vm = useVM();
4699
- const inventory = useInventory();
4700
- const context = {
4701
- data: vm.data,
4702
- skills: skillStore.skills,
4703
- skillChecks: skillStore.skillChecks,
4704
- stats: useHud().hudStats,
4705
- items: inventory.items,
4706
- quests: useQuests().quests,
4707
- itemAmount: (id) => {
4708
- return useInventory().getItemAmount(id);
4709
- },
4710
- roll: (checkId, skill, value) => {
4711
- const skillCheckState = getSkillCheckState(checkId);
4712
- if (skillCheckState) {
4713
- if (skillCheckState.passed) {
4714
- return true;
4715
- }
4716
- if (!skillCheckState.available) {
4717
- return false;
4718
- }
4719
- }
4720
- return runSkillCheck({
4721
- skill,
4722
- value,
4723
- id: checkId,
4724
- });
4725
- },
4726
- };
4727
- return evalInContext(`(${condition})`, context);
4728
- }
4729
- function evalInContext(script, context) {
4730
- // # Return the results of the in-line anonymous function we .call with the passed context
4731
- return function () {
4732
- return eval(script);
4733
- }.call(context);
4734
- }
4735
4943
  async function textCommand(dialog) {
4736
4944
  const dialogStore = useDialogStore();
4737
4945
  dialogStore.addDialog(dialog);
@@ -4765,73 +4973,112 @@ class VM {
4765
4973
  }
4766
4974
  }
4767
4975
  const vm = new VM();
4768
- async function runCommand(cmd, choices) {
4976
+ async function runCommand(expression, choices) {
4769
4977
  const vmStore = useVM();
4770
4978
  try {
4771
- const commandPlugin = vm.commands[cmd.commandType];
4772
- if (commandPlugin) {
4773
- commandPlugin.runner(cmd, choices);
4774
- }
4979
+ const result = await runExpression(expression, choices);
4980
+ return result;
4775
4981
  }
4776
4982
  catch (err) {
4777
- logger.log(`Error at: `, vmStore.currentStack.label);
4778
4983
  console.error(err);
4779
- error(`Narrat script runtime error at <span class="error-filename">${cmd.fileName}:${cmd.line + 1}</span>
4984
+ console.error(expression);
4985
+ error(`Narrat script runtime error at <span class="error-filename">${expression.fileName}:${expression.line + 1}</span>
4780
4986
  <b>${err}</b>
4781
- Script: ${cmd.code}
4987
+ Script: ${expression.code}
4782
4988
  Label: ${vmStore.currentStack.label}`);
4783
4989
  }
4784
4990
  }
4785
- async function playerAnswered(choiceIndex) {
4786
- const vmStore = useVM();
4787
- const cmd = vmStore.currentLine;
4788
- const dialogStore = useDialogStore();
4789
- switch (cmd.commandType) {
4790
- case 'choice':
4791
- const options = cmd.options;
4792
- const choice = options.choices[choiceIndex];
4793
- let playerText = choice.choice;
4794
- let newBranch;
4795
- const skillcheck = choice.skillCheck;
4796
- if (skillcheck) {
4797
- const skillCheckState = getSkillCheckState(choice.skillCheck.id);
4798
- if (skillCheckState.passed) {
4799
- newBranch = skillcheck.success.branch;
4800
- playerText = skillcheck.success.text;
4991
+ async function generateCommand(expr, choices) {
4992
+ const command = expr.command;
4993
+ const commandPlugin = vm.commands[command.commandType];
4994
+ if (commandPlugin) {
4995
+ const generatedCommand = {
4996
+ args: [],
4997
+ options: {},
4998
+ operator: command.operator,
4999
+ staticOptions: command.staticOptions,
5000
+ code: expr.code,
5001
+ commandType: command.commandType,
5002
+ fileName: expr.fileName,
5003
+ line: expr.line,
5004
+ };
5005
+ // Evaluate arguments
5006
+ const argTypes = commandPlugin.argTypes;
5007
+ generatedCommand.options = {};
5008
+ for (const [i, arg] of command.args.entries()) {
5009
+ let finalArg;
5010
+ if (isExpression(arg)) {
5011
+ // Arg is an expression, run it
5012
+ finalArg = await runExpression(arg, choices);
5013
+ }
5014
+ else if (typeof arg === 'string') {
5015
+ if (arg.search(stringRegex) === 0) {
5016
+ // This is an actual string
5017
+ finalArg = arg.substring(3);
5018
+ }
5019
+ else if (arg.search(/\$/) === 0) {
5020
+ // This is potentially a variable token
5021
+ const modifiable = getModifiableDataPinia();
5022
+ const result = findDataHelperWithoutAutoCreate(modifiable, arg.substring(1));
5023
+ if (result) {
5024
+ const [target, key] = result;
5025
+ finalArg = target[key];
5026
+ }
5027
+ else {
5028
+ finalArg = arg;
5029
+ }
4801
5030
  }
4802
5031
  else {
4803
- const result = processSkillCheck(skillcheck);
4804
- const winner = result ? skillcheck.success : skillcheck.failure;
4805
- newBranch = winner.branch;
4806
- playerText = undefined;
5032
+ finalArg = arg;
4807
5033
  }
4808
5034
  }
4809
5035
  else {
4810
- newBranch = choice.branch;
4811
- }
4812
- if (playerText) {
4813
- // If the choice involves printing a player dialog, show it
4814
- const dialog = {
4815
- speaker: 'player',
4816
- text: playerText,
4817
- interactive: false,
4818
- };
4819
- dialogStore.addDialog(dialog);
5036
+ finalArg = arg;
4820
5037
  }
4821
- if (newBranch) {
4822
- const newStack = {
4823
- currentIndex: 0,
4824
- branch: newBranch,
4825
- };
4826
- const vmStore = useVM();
4827
- return vmStore.addStack(newStack);
5038
+ generatedCommand.args.push(finalArg);
5039
+ if (Array.isArray(argTypes) && argTypes.length > i) {
5040
+ const argType = argTypes[i];
5041
+ if (finalArg !== null) {
5042
+ generatedCommand.options[argType.name] = finalArg;
5043
+ }
4828
5044
  }
4829
- else {
4830
- vmStore.nextLine();
5045
+ }
5046
+ return generatedCommand;
5047
+ }
5048
+ else {
5049
+ throw new Error(`${command.commandType} is not a valid command`);
5050
+ }
5051
+ }
5052
+ async function runExpression(expr, choices) {
5053
+ const command = await generateCommand(expr, choices);
5054
+ const commandPlugin = vm.commands[command.commandType];
5055
+ if (commandPlugin) {
5056
+ const result = await commandPlugin.runner(command, choices);
5057
+ return result;
5058
+ }
5059
+ else {
5060
+ throw new Error(`${command.commandType} is not a valid command`);
5061
+ }
5062
+ }
5063
+ async function playerAnswered(choiceIndex) {
5064
+ audioEvent('onPlayerAnswered');
5065
+ const vmStore = useVM();
5066
+ // For some super weird reason, vmStore.currentCommand has a broken type?
5067
+ const command = vmStore.lastChoiceCommand;
5068
+ vmStore.lastChoiceCommand = undefined;
5069
+ const currentLine = vmStore.currentLine;
5070
+ try {
5071
+ if (command) {
5072
+ const commandPlugin = vm.commands[command.commandType];
5073
+ if (commandPlugin && commandPlugin.onPlayerAnswered) {
5074
+ return await commandPlugin.onPlayerAnswered(command, choiceIndex);
4831
5075
  }
4832
- break;
4833
- default:
4834
- vmStore.nextLine();
5076
+ }
5077
+ return await vmStore.nextLine();
5078
+ }
5079
+ catch (err) {
5080
+ console.error(err);
5081
+ error(`Error after player answer at ${currentLine.fileName}:${currentLine.line + 1} (${currentLine.code}<br /> - Error: ${err}`);
4835
5082
  }
4836
5083
  }
4837
5084
 
@@ -5287,7 +5534,7 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
5287
5534
  ], 4))
5288
5535
  }
5289
5536
 
5290
- var css_248z$1 = ".dialog-title {\n font-size: 20px;\n font-weight: bold;\n}\n\n.dialog-text {\n font-size: 16px;\n}\n\n.dialog-box {\n /* border-radius: 10px; */\n /* border: 1px solid #a8a8a8; */\n color: var(--text-color);\n /* background-color: #2e2e2e; */\n padding: 10px;\n padding-left: 2em;\n margin-bottom: 10px;\n}\n\n.dialog-choice {\n color: var(--dialog-choice-color);\n}\n\n.dialog-choice:hover {\n color: var(--dialog-choice-hover-color);\n cursor: pointer;\n}\n\n.buttons-container {\n width: 100%;\n padding: 10px;\n display: flex;\n justify-content: space-evenly;\n align-items: stretch;\n box-sizing: border-box;\n}\n\n.interact-button {\n cursor: pointer;\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n user-select: none;\n height: 50px;\n color: var(--text-color);\n border: 1px solid black;\n font-weight: bold;\n font-size: 24px;\n text-align: center;\n flex-grow: 2;\n display: flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n}\n\n.interact-button:not(:last-child) {\n margin-right: 10px;\n}\n";
5537
+ var css_248z$1 = ".dialog-title {\n font-size: 20px;\n font-weight: bold;\n}\n\n.dialog-text {\n font-size: 16px;\n}\n\n.dialog-box {\n /* border-radius: 10px; */\n /* border: 1px solid #a8a8a8; */\n color: var(--text-color);\n /* background-color: #2e2e2e; */\n padding: 10px;\n padding-left: 2em;\n margin-bottom: 10px;\n}\n\n.dialog-choice {\n color: var(--dialog-choice-color);\n}\n\n.dialog-choice:hover {\n color: var(--dialog-choice-hover-color);\n cursor: pointer;\n}\n\n.buttons-container {\n width: 100%;\n padding: 10px;\n display: flex;\n justify-content: space-evenly;\n align-items: stretch;\n box-sizing: border-box;\n}\n\n.interact-button {\n cursor: pointer;\n -webkit-user-select: none;\n -moz-user-select: none;\n user-select: none;\n height: 50px;\n color: var(--text-color);\n border: 1px solid black;\n font-weight: bold;\n font-size: 24px;\n text-align: center;\n flex-grow: 2;\n display: flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n}\n\n.interact-button:not(:last-child) {\n margin-right: 10px;\n}\n";
5291
5538
  styleInject(css_248z$1);
5292
5539
 
5293
5540
  script.render = render;
@@ -6738,13 +6985,13 @@ const KeyType = {
6738
6985
  PATTERN: '$val'
6739
6986
  };
6740
6987
 
6741
- const isExpression = (query) =>
6988
+ const isExpression$1 = (query) =>
6742
6989
  !!(query[LogicalOperator.AND] || query[LogicalOperator.OR]);
6743
6990
 
6744
6991
  const isPath = (query) => !!query[KeyType.PATH];
6745
6992
 
6746
6993
  const isLeaf = (query) =>
6747
- !isArray(query) && isObject(query) && !isExpression(query);
6994
+ !isArray(query) && isObject(query) && !isExpression$1(query);
6748
6995
 
6749
6996
  const convertToExplicit = (query) => ({
6750
6997
  [LogicalOperator.AND]: Object.keys(query).map((key) => ({
@@ -6760,7 +7007,7 @@ function parse(query, options, { auto = true } = {}) {
6760
7007
 
6761
7008
  const isQueryPath = isPath(query);
6762
7009
 
6763
- if (!isQueryPath && keys.length > 1 && !isExpression(query)) {
7010
+ if (!isQueryPath && keys.length > 1 && !isExpression$1(query)) {
6764
7011
  return next(convertToExplicit(query))
6765
7012
  }
6766
7013
 
@@ -6803,7 +7050,7 @@ function parse(query, options, { auto = true } = {}) {
6803
7050
  return node
6804
7051
  };
6805
7052
 
6806
- if (!isExpression(query)) {
7053
+ if (!isExpression$1(query)) {
6807
7054
  query = convertToExplicit(query);
6808
7055
  }
6809
7056
 
@@ -7312,7 +7559,7 @@ var script$3 = defineComponent({
7312
7559
  items: inventoryStore.items,
7313
7560
  skills: skillsStore.skills,
7314
7561
  skillChecks: skillsStore.skillChecks,
7315
- }
7562
+ },
7316
7563
  },
7317
7564
  onChange: (updatedContent) => {
7318
7565
  vmStore.overrideData(updatedContent.json.data);
@@ -7390,28 +7637,30 @@ var script$3 = defineComponent({
7390
7637
  const scripts = Object.values(this.script);
7391
7638
  const count = scripts.reduce((count, script) => {
7392
7639
  logger.log(count);
7393
- return count + this.countWordsInScriptBranch(script);
7640
+ return count + this.countWordsInScriptBranch(script.branch);
7394
7641
  }, 0);
7395
7642
  alert(`You have ${count} words`);
7396
7643
  },
7397
7644
  countWordsInScriptLine(scriptLine) {
7398
- if (scriptLine.commandType === 'talk') {
7399
- return this.countWordsInString(scriptLine.args[2]);
7645
+ if (scriptLine.command.commandType === 'talk') {
7646
+ if (typeof scriptLine.command.args[2] === 'string') {
7647
+ return this.countWordsInString(scriptLine.command.args[2]);
7648
+ }
7400
7649
  }
7401
- if (scriptLine.commandType === 'text') {
7402
- return this.countWordsInString(scriptLine.options.text);
7650
+ if (scriptLine.command.commandType === 'text') {
7651
+ return this.countWordsInString(scriptLine.code);
7403
7652
  }
7404
- if (scriptLine.commandType === 'choice') {
7405
- const opt = scriptLine.options;
7653
+ if (scriptLine.command.commandType === 'choice') {
7654
+ const opt = scriptLine.command.staticOptions;
7406
7655
  let count = this.countWordsInScriptLine(opt.prompt);
7407
- count += opt.choices.reduce((count, choice) => count + this.countWordsInString(choice.choice), 0);
7656
+ count += opt.choices.reduce((count, choice) => count + this.countWordsInString(choice.prompt.code), 0);
7408
7657
  const choices = opt.choices;
7409
7658
  return choices.reduce((count, choice) => {
7410
7659
  return count + this.countWordsInScriptBranch(choice.branch);
7411
7660
  }, count);
7412
7661
  }
7413
- if (scriptLine.commandType === 'if') {
7414
- const opt = scriptLine.options;
7662
+ if (scriptLine.command.commandType === 'if') {
7663
+ const opt = scriptLine.command.staticOptions;
7415
7664
  const branches = [opt.success, opt.failure];
7416
7665
  return branches.reduce((count, choice) => {
7417
7666
  if (choice) {
@@ -7628,8 +7877,8 @@ function render$3(_ctx, _cache, $props, $setup, $data, $options) {
7628
7877
  (openBlock(true), createElementBlock(Fragment, null, renderList(_ctx.skillChecks, (check, key) => {
7629
7878
  return (openBlock(), createElementBlock("tr", { key: key }, [
7630
7879
  createElementVNode("td", null, toDisplayString(key), 1),
7631
- createElementVNode("td", null, toDisplayString(!check.available ? '✅' : '❌'), 1),
7632
- createElementVNode("td", null, toDisplayString(check.available ? 'NA' : check.passed ? '✅' : '❌'), 1)
7880
+ createElementVNode("td", null, toDisplayString(check.happened ? '✅' : '❌'), 1),
7881
+ createElementVNode("td", null, toDisplayString(!check.happened ? 'NA' : check.succeeded ? '✅' : '❌'), 1)
7633
7882
  ]))
7634
7883
  }), 128))
7635
7884
  ])
@@ -8666,9 +8915,6 @@ var script$d = defineComponent({
8666
8915
  }
8667
8916
  return undefined;
8668
8917
  },
8669
- command() {
8670
- return this.currentLine;
8671
- },
8672
8918
  picture() {
8673
8919
  if (this.lastDialog) {
8674
8920
  return getCharacterPictureUrl(this.lastDialog.speaker, this.lastDialog.pose);
@@ -8809,10 +9055,12 @@ var script$d = defineComponent({
8809
9055
  return localStorage.getItem(SAVE_FILE);
8810
9056
  },
8811
9057
  async startGame() {
9058
+ audioEvent('onPressStart');
8812
9059
  const main = useMain();
8813
9060
  await main.startGame();
8814
9061
  },
8815
9062
  async loadGame() {
9063
+ audioEvent('onPressStart');
8816
9064
  useMain().loadGame(this.getSaveFile());
8817
9065
  },
8818
9066
  isDialogActive(i) {
@@ -8860,13 +9108,14 @@ const _hoisted_1$c = ["width", "height"];
8860
9108
  const _hoisted_2$a = /*#__PURE__*/createElementVNode("div", { class: "anchor" }, null, -1);
8861
9109
  const _hoisted_3$7 = {
8862
9110
  key: 2,
8863
- class: "flex flex-col",
9111
+ id: "game-menu",
8864
9112
  style: {"height":"100%","padding":"20px"}
8865
9113
  };
8866
- const _hoisted_4$6 = { id: "game-title-container" };
8867
- const _hoisted_5$6 = { id: "game-title-text" };
8868
- const _hoisted_6$6 = { class: "flex flex-col" };
8869
- const _hoisted_7$5 = { key: 3 };
9114
+ const _hoisted_4$6 = { id: "game-header" };
9115
+ const _hoisted_5$6 = { id: "game-title-container" };
9116
+ const _hoisted_6$6 = { id: "game-title-text" };
9117
+ const _hoisted_7$5 = { class: "flex flex-col" };
9118
+ const _hoisted_8$4 = { key: 3 };
8870
9119
 
8871
9120
  function render$d(_ctx, _cache, $props, $setup, $data, $options) {
8872
9121
  const _component_Hud = resolveComponent("Hud");
@@ -8975,23 +9224,25 @@ function render$d(_ctx, _cache, $props, $setup, $data, $options) {
8975
9224
  : (_ctx.gameLoaded)
8976
9225
  ? (openBlock(), createElementBlock("div", _hoisted_3$7, [
8977
9226
  createElementVNode("div", _hoisted_4$6, [
8978
- createElementVNode("h1", _hoisted_5$6, toDisplayString(_ctx.gameTitle), 1)
8979
- ]),
8980
- createElementVNode("div", _hoisted_6$6, [
8981
- createElementVNode("button", {
8982
- class: "button menu-button start-button override",
8983
- onClick: _cache[0] || (_cache[0] = (...args) => (_ctx.startGame && _ctx.startGame(...args)))
8984
- }, " Start Game "),
8985
- (_ctx.hasSave)
8986
- ? (openBlock(), createElementBlock("button", {
8987
- key: 0,
8988
- class: "button menu-button continue-button override",
8989
- onClick: _cache[1] || (_cache[1] = (...args) => (_ctx.loadGame && _ctx.loadGame(...args)))
8990
- }, " Continue Game "))
8991
- : createCommentVNode("", true)
9227
+ createElementVNode("div", _hoisted_5$6, [
9228
+ createElementVNode("h1", _hoisted_6$6, toDisplayString(_ctx.gameTitle), 1)
9229
+ ]),
9230
+ createElementVNode("div", _hoisted_7$5, [
9231
+ createElementVNode("button", {
9232
+ class: "button menu-button main-menu-button larg start-button override",
9233
+ onClick: _cache[0] || (_cache[0] = (...args) => (_ctx.startGame && _ctx.startGame(...args)))
9234
+ }, " Start Game "),
9235
+ (_ctx.hasSave)
9236
+ ? (openBlock(), createElementBlock("button", {
9237
+ key: 0,
9238
+ class: "button menu-button main-menu-button large continue-button override",
9239
+ onClick: _cache[1] || (_cache[1] = (...args) => (_ctx.loadGame && _ctx.loadGame(...args)))
9240
+ }, " Continue Game "))
9241
+ : createCommentVNode("", true)
9242
+ ])
8992
9243
  ])
8993
9244
  ]))
8994
- : (openBlock(), createElementBlock("div", _hoisted_7$5, [
9245
+ : (openBlock(), createElementBlock("div", _hoisted_8$4, [
8995
9246
  createVNode(_component_LoadingBar, {
8996
9247
  percentage: _ctx.loadingPercentage,
8997
9248
  step: _ctx.loadingStep
@@ -9004,7 +9255,7 @@ function render$d(_ctx, _cache, $props, $setup, $data, $options) {
9004
9255
  ], 4))
9005
9256
  }
9006
9257
 
9007
- var css_248z$e = "#app {\n background-color: var(--bg-color);\n width: 100%;\n height: 100%;\n position: absolute;\n display: flex;\n flex-direction: row;\n align-items: center;\n justify-content: center;\n color: var(--text-color);\n box-sizing: border-box;\n overflow: hidden;\n transform-origin: center center;\n}\n\n.game {\n background-color: var(--bg-color);\n position: relative;\n display: flex;\n flex-direction: row;\n align-items: center;\n justify-content: space-between;\n width: 100%;\n height: 100%;\n box-sizing: border-box;\n overflow: hidden;\n}\n\n.interact-button {\n height: 50px;\n border: 1px solid black;\n font-weight: bold;\n font-size: 20px;\n text-align: center;\n flex-grow: 2;\n display: flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n}\n\n.interact-button:not(:last-child) {\n margin-right: 10px;\n}\n\n.dialog-container {\n flex-shrink: 2;\n /* padding: 20px; */\n min-height: 100%;\n width: 100%;\n background-color: (var(--bg-color));\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n justify-content: flex-end;\n align-items: center;\n overflow-x: hidden;\n}\n\n.dialog {\n overflow-y: auto;\n overflow-x: hidden;\n position: relative;\n -ms-overflow-style: none; /* IE and Edge */\n scrollbar-width: none; /* Firefox */\n}\n\n.dialog::-webkit-scrollbar {\n display: none; /* webkit */\n}\n\n.dialog * {\n overflow-anchor: none;\n}\n\n.background {\n margin: 0;\n}\n\n#background-canvas {\n height: 100%;\n}\n\n.anchor {\n overflow-anchor: auto;\n height: 1px;\n}\n\n.menu-toggle {\n position: fixed;\n bottom: 0px;\n right: 15%;\n z-index: 2;\n}\n\n#game-title-container {\n margin-bottom: 50px;\n}\n\n#game-title-text {\n text-align: center;\n font-size: 50px;\n}\n\n.menu-button {\n font-size: 25px;\n}\n";
9258
+ var css_248z$e = "#app {\n background-color: var(--bg-color);\n width: 100%;\n height: 100%;\n position: absolute;\n display: flex;\n flex-direction: row;\n align-items: center;\n justify-content: center;\n color: var(--text-color);\n box-sizing: border-box;\n overflow: hidden;\n transform-origin: center center;\n}\n\n.game {\n background-color: var(--bg-color);\n position: relative;\n display: flex;\n flex-direction: row;\n align-items: center;\n justify-content: space-between;\n width: 100%;\n height: 100%;\n box-sizing: border-box;\n overflow: hidden;\n}\n\n.interact-button {\n height: 50px;\n border: 1px solid black;\n font-weight: bold;\n font-size: 20px;\n text-align: center;\n flex-grow: 2;\n display: flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n}\n\n.interact-button:not(:last-child) {\n margin-right: 10px;\n}\n\n.dialog-container {\n flex-shrink: 2;\n /* padding: 20px; */\n min-height: 100%;\n width: 100%;\n background-color: (var(--bg-color));\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n justify-content: flex-end;\n align-items: center;\n overflow-x: hidden;\n}\n\n.dialog {\n overflow-y: auto;\n overflow-x: hidden;\n position: relative;\n -ms-overflow-style: none; /* IE and Edge */\n scrollbar-width: none; /* Firefox */\n}\n\n.dialog::-webkit-scrollbar {\n display: none; /* webkit */\n}\n\n.dialog * {\n overflow-anchor: none;\n}\n\n.background {\n margin: 0;\n}\n\n#background-canvas {\n height: 100%;\n}\n\n.anchor {\n overflow-anchor: auto;\n height: 1px;\n}\n\n.menu-toggle {\n position: fixed;\n bottom: 0px;\n right: 15%;\n z-index: 2;\n}\n\n#game-title-container {\n margin-bottom: 50px;\n}\n\n#game-title-text {\n text-align: center;\n font-size: 50px;\n}\n\n.menu-button {\n font-size: 25px;\n}\n\n#game-header {\n position: relative;\n top: 100px;\n}\n";
9008
9259
  styleInject(css_248z$e);
9009
9260
 
9010
9261
  script$d.render = render$d;
@@ -9090,7 +9341,7 @@ function mouseclick(e) {
9090
9341
  // clicked button
9091
9342
  const scriptToRun = button.action;
9092
9343
  const newStack = {
9093
- branch: vmStore.script[scriptToRun],
9344
+ branchData: vmStore.script[scriptToRun],
9094
9345
  currentIndex: 0,
9095
9346
  label: scriptToRun,
9096
9347
  };
@@ -9102,264 +9353,203 @@ function mouseclick(e) {
9102
9353
  }
9103
9354
  }
9104
9355
 
9356
+ function commandRuntimeError(cmd, errorText) {
9357
+ console.error(`Runtime error =========================`);
9358
+ console.error('Args: ', cmd.args);
9359
+ console.error('Options: ', cmd.options);
9360
+ error(`Runtime error at ${cmd.fileName}:${cmd.line} (${cmd.commandType}) ${cmd.code}. -
9361
+ <br />
9362
+ Error: ${errorText}`);
9363
+ console.error('============================');
9364
+ }
9365
+
9105
9366
  class CommandPlugin {
9106
9367
  keyword;
9107
9368
  runner;
9369
+ argTypes;
9108
9370
  parser;
9109
- constructor(keyword, runner, parser) {
9371
+ onPlayerAnswered;
9372
+ constructor(keyword, argTypes, runner, parser) {
9110
9373
  this.keyword = keyword;
9111
9374
  this.runner = runner;
9112
- this.parser = parser;
9375
+ this.argTypes = argTypes;
9376
+ if (!parser) {
9377
+ this.parser = generateParser(this.keyword, this.argTypes);
9378
+ }
9379
+ else {
9380
+ this.parser = parser;
9381
+ }
9113
9382
  }
9114
9383
  }
9115
- function generateParser(keyword) {
9116
- return (ctx) => {
9117
- ctx.command.commandType = keyword;
9118
- ctx.currentLine++;
9119
- };
9120
- }
9121
-
9122
- const add = async (cmd) => {
9123
- const addKey = cmd.args[0];
9124
- const addValue = cmd.args[1];
9125
- const vm = useVM();
9126
- vm.addInstruction(addKey, addValue);
9127
- return vm.nextLine();
9128
- };
9129
- const addPlugin = new CommandPlugin('add', add, generateParser('add'));
9130
-
9131
- const addLevelRunner = async (cmd) => {
9132
- const [skillKey, levelToAdd] = cmd.args;
9133
- if (!skillKey || !levelToAdd) {
9134
- error(`add_level command needs a skill id and a value as parameters`);
9135
- }
9136
- useSkills().incrementSkill(skillKey, levelToAdd);
9137
- return useVM().nextLine();
9138
- };
9139
- const addLevelParser = (ctx) => {
9140
- ctx.command.commandType = 'add_level';
9141
- ctx.currentLine++;
9142
- };
9143
- const addLevelPlugin = new CommandPlugin('add_level', addLevelRunner, addLevelParser);
9144
-
9145
- const addStatRunner = async (cmd) => {
9146
- const statKey = cmd.args[0];
9147
- const amountToAdd = cmd.args[1];
9148
- if (!statKey || !amountToAdd) {
9149
- error(`add_stat command needs a stat id and a value as parameters`);
9150
- }
9151
- const hud = useHud();
9152
- hud.addStat(statKey, amountToAdd);
9153
- return useVM().nextLine();
9154
- };
9155
- const addStatParser = (ctx) => {
9156
- ctx.command.commandType = 'add_stat';
9157
- ctx.currentLine++;
9158
- };
9159
- const addStatPlugin = new CommandPlugin('add_stat', addStatRunner, addStatParser);
9160
-
9161
- const addXp = async (cmd) => {
9162
- const xpKey = cmd.args[0];
9163
- const xpToAdd = cmd.args[1];
9164
- if (!xpKey || !xpToAdd) {
9165
- error(`add_xp command needs a skill id and a value as parameters`);
9166
- }
9167
- useSkills().addXp(xpKey, xpToAdd);
9168
- return useVM().nextLine();
9169
- };
9170
- const addXpPlugin = new CommandPlugin('add_xp', addXp, generateParser('add_xp'));
9171
-
9172
- const choice = async (cmd) => {
9173
- await runChoice(cmd);
9174
- };
9175
- async function runChoice(cmd) {
9176
- const options = cmd.options;
9177
- const prompt = options.prompt;
9178
- const choices = options.choices
9179
- .filter((choice) => {
9180
- // Delete the choice if it fails a condition
9181
- if (choice.condition) {
9182
- return runCondition(choice.condition);
9384
+ function generateParser(keyword, argTypes) {
9385
+ let expectedArgCount = [];
9386
+ if (argTypes !== 'any') {
9387
+ expectedArgCount = [argTypes.length];
9388
+ const optionalArgs = argTypes.reduce((total, argType) => (argType.optional ? total + 1 : total), 0);
9389
+ if (optionalArgs > 1) {
9390
+ throw new Error(`Only one optional argument is allowed for commands but ${keyword} has multiple ones`);
9391
+ }
9392
+ if (optionalArgs === 1) {
9393
+ expectedArgCount.push(argTypes.length - 1);
9183
9394
  }
9184
- if (choice.skillCheck) {
9185
- if (!getSkillCheckState(choice.skillCheck.id).available &&
9186
- choice.skillCheck.hideAfterRoll) {
9187
- return false;
9395
+ }
9396
+ return (ctx, parsed) => {
9397
+ const returnValue = {
9398
+ newLine: ctx.currentLine + 1
9399
+ };
9400
+ const args = parsed.command.args;
9401
+ if (argTypes !== 'any') {
9402
+ if (!expectedArgCount.includes(args.length)) {
9403
+ console.log("Error details");
9404
+ console.log(parsed.command);
9405
+ console.log(args);
9406
+ ctx.parserContext.error(ctx.line.line, `Command ${keyword}: Expected ${expectedArgCount.join(' or ')} arguments but got ${args.length}`);
9407
+ return returnValue;
9188
9408
  }
9189
9409
  }
9190
- return true;
9191
- })
9192
- .map((choice) => {
9193
- let text = choice.choice;
9194
- let choiceAllowed = true;
9195
- if (choice.skillCheck) {
9196
- const check = choice.skillCheck;
9197
- const { difficultyText, allowed } = getSkillCheckText({
9198
- skill: check.skill,
9199
- skillCheckId: check.id,
9200
- value: check.value,
9410
+ parsed.command.commandType = keyword;
9411
+ // todo: check types of args
9412
+ if (argTypes !== 'any') {
9413
+ parsed.command.args.forEach((arg, index) => {
9414
+ const argType = argTypes[index];
9415
+ if (!isExpression(arg)) {
9416
+ // Only run this if the arg isn't an expression, as expressions aren't currently typed
9417
+ // eslint-disable-next-line valid-typeof
9418
+ const isValid = argType.type === 'any' || typeof arg === argType.type;
9419
+ if (!isValid) {
9420
+ ctx.parserContext.error(ctx.line.line, `Command ${keyword}: Argument #${index + 1} (${argType.name}) should be a ${argType.type}, but got type ${typeof arg}: ${JSON.stringify(arg)}`);
9421
+ }
9422
+ }
9201
9423
  });
9202
- text = `${difficultyText} ${text}`;
9203
- choiceAllowed = allowed;
9204
9424
  }
9205
- const result = {
9206
- choice: text,
9207
- originalIndex: choice.index,
9208
- allowed: choiceAllowed,
9425
+ return {
9426
+ newLine: ctx.currentLine + 1,
9209
9427
  };
9210
- return result;
9211
- });
9212
- runCommand(prompt, choices);
9213
- }
9214
- const choiceParser = (ctx) => {
9215
- const { line, command } = ctx;
9216
- if (!line.branch || line.branch.length < 2) {
9217
- ctx.parserContext.error(line.line, `Choice menu needs to have at least one option`);
9218
- }
9219
- const prompt = line.branch[0];
9220
- const choices = line.branch.slice(1);
9221
- const prompts = choices.map((choice, index) => {
9222
- if (!choice.branch) {
9223
- ctx.parserContext.error(choice.line, `Choice option doesn't have any branch to go to (${choice.code})`);
9224
- }
9225
- return parseChoiceOption(ctx, choice, index);
9226
- });
9227
- command.options = {
9228
- prompt: ctx.processCommandsFunction(ctx.parserContext, [prompt], line)[0],
9229
- choices: prompts,
9230
9428
  };
9231
- command.commandType = 'choice';
9232
- ctx.currentLine++;
9233
- };
9234
- function parseChoiceOption(ctx, choice, index) {
9235
- let choiceText = choice.operator;
9236
- let condition;
9237
- let skillCheck;
9238
- if (choice.operator === 'roll') {
9239
- if (choice.args.length < 4) {
9240
- ctx.parserContext.error(choice.line, `Skillchecks need 4 arguments!`);
9241
- }
9242
- choiceText = choice.args[3];
9243
- const hideAfterRoll = choice.args.length >= 5;
9244
- const successBranch = choice.branch[0];
9245
- const failureBranch = choice.branch[1];
9246
- const success = {
9247
- text: successBranch.args[0],
9248
- branch: ctx.processCommandsFunction(ctx.parserContext, successBranch.branch, choice),
9249
- };
9250
- let failedBranch;
9251
- if (failureBranch.branch) {
9252
- failedBranch = ctx.processCommandsFunction(ctx.parserContext, failureBranch.branch, choice);
9253
- }
9254
- const failure = {
9255
- text: failureBranch.args[0],
9256
- branch: failedBranch,
9257
- };
9258
- skillCheck = {
9259
- id: choice.args[0],
9260
- skill: choice.args[1],
9261
- value: choice.args[2],
9262
- hideAfterRoll,
9263
- success,
9264
- failure,
9265
- };
9429
+ }
9430
+
9431
+ const addLevelPlugin = new CommandPlugin('add_level', [
9432
+ {
9433
+ name: 'skillKey',
9434
+ type: 'string',
9435
+ },
9436
+ {
9437
+ name: 'amount',
9438
+ type: 'number',
9439
+ },
9440
+ ], async (cmd) => {
9441
+ const { skillKey, amount } = cmd.options;
9442
+ if (!skillKey || !amount) {
9443
+ commandRuntimeError(cmd, `add_level command needs a skill id and a value as parameters`);
9266
9444
  }
9267
- if (choice.args[0] === 'if') {
9268
- condition = choice.args[1];
9445
+ useSkills().incrementSkill(skillKey, amount);
9446
+ return useVM().nextLine();
9447
+ });
9448
+ const addXpPlugin = new CommandPlugin('add_xp', [
9449
+ { name: 'xpKey', type: 'string' },
9450
+ { name: 'xpToAdd', type: 'number' },
9451
+ ], async (cmd) => {
9452
+ const { xpKey, xpToAdd } = cmd.options;
9453
+ if (!xpKey || !xpToAdd) {
9454
+ commandRuntimeError(cmd, `add_xp command needs a skill id and a value as parameters`);
9269
9455
  }
9270
- return {
9271
- choice: choiceText,
9272
- condition,
9273
- skillCheck,
9274
- branch: ctx.processCommandsFunction(ctx.parserContext, choice.branch, choice),
9275
- index,
9276
- };
9277
- }
9278
- const choicePlugin = new CommandPlugin('choice', choice, choiceParser);
9279
-
9280
- const clearDialog = async (cmd) => {
9281
- useDialogStore().clearDialog();
9456
+ useSkills().addXp(xpKey, xpToAdd);
9282
9457
  return useVM().nextLine();
9283
- };
9284
- const clearDialogPlugin = new CommandPlugin('clear_dialog', clearDialog, generateParser('clear_dialog'));
9458
+ });
9459
+ const rollPlugin = new CommandPlugin('roll', [
9460
+ { name: 'id', type: 'string' },
9461
+ { name: 'skill', type: 'string' },
9462
+ { name: 'value', type: 'number' },
9463
+ { name: 'hideAfterRoll', type: 'boolean', optional: true },
9464
+ ], async (cmd) => {
9465
+ const { id, skill, value, hideAfterRoll } = cmd.options;
9466
+ const skillCheck = {
9467
+ id,
9468
+ skill,
9469
+ value,
9470
+ hideAfterRoll,
9471
+ };
9472
+ const result = runSkillCheck(skillCheck);
9473
+ return result;
9474
+ });
9285
9475
 
9286
- const ifFunction = async (cmd) => {
9287
- const newBranch = runConditionCommand(cmd);
9288
- const vmStore = useVM();
9289
- if (newBranch) {
9290
- const newStack = {
9291
- branch: newBranch,
9292
- currentIndex: 0,
9293
- };
9294
- return vmStore.addStack(newStack);
9476
+ const addStatPlugin = new CommandPlugin('add_stat', [
9477
+ { name: 'statKey', type: 'string' },
9478
+ { name: 'amountToAdd', type: 'number' },
9479
+ ], async (cmd) => {
9480
+ const { statKey, amountToAdd } = cmd.options;
9481
+ if (!statKey || !amountToAdd) {
9482
+ commandRuntimeError(cmd, `add_stat command needs a stat id and a value as parameters`);
9483
+ }
9484
+ const hud = useHud();
9485
+ hud.addStat(statKey, amountToAdd);
9486
+ return useVM().nextLine();
9487
+ });
9488
+ const setStatPlugin = new CommandPlugin('set_stat', [
9489
+ { name: 'statKey', type: 'string' },
9490
+ { name: 'value', type: 'number' },
9491
+ ], async (cmd) => {
9492
+ const { statKey, value } = cmd.options;
9493
+ if (!statKey || !value) {
9494
+ commandRuntimeError(cmd, `set_stat command needs a stat id and a value as parameters`);
9295
9495
  }
9296
- return vmStore.nextLine();
9297
- };
9298
- const ifParser = (ctx) => {
9299
- const { command, lines, currentLine, line } = ctx;
9300
- command.commandType = 'if';
9301
- let failure;
9302
- const nextLine = getLine(lines, currentLine + 1);
9303
- if (nextLine && nextLine.operator === 'else') {
9304
- failure = ctx.processCommandsFunction(ctx.parserContext, nextLine.branch, line);
9305
- ctx.currentLine++;
9496
+ const hud = useHud();
9497
+ hud.setStat(statKey, value);
9498
+ return useVM().nextLine();
9499
+ });
9500
+ const getStatPlugin = new CommandPlugin('get_stat_value', [{ name: 'statKey', type: 'string' }], async (cmd) => {
9501
+ const { statKey } = cmd.options;
9502
+ if (!statKey) {
9503
+ commandRuntimeError(cmd, `get_stat_value command needs a stat id as parameter`);
9306
9504
  }
9307
- command.options = {
9308
- condition: command.args[0],
9309
- success: ctx.processCommandsFunction(ctx.parserContext, line.branch, line),
9310
- failure,
9311
- };
9312
- ctx.currentLine++;
9313
- };
9314
- const ifCommand = new CommandPlugin('if', ifFunction, ifParser);
9505
+ const hud = useHud();
9506
+ return hud.getStatValue(statKey);
9507
+ });
9315
9508
 
9316
- const jump = async (cmd, choices) => {
9317
- const branch = cmd.args[0];
9509
+ const jumpCommand = new CommandPlugin('jump', 'any', async (cmd) => {
9510
+ if (cmd.args.length < 1 || typeof cmd.args[0] !== 'string') {
9511
+ commandRuntimeError(cmd, `requires a label argument`);
9512
+ }
9513
+ const label = cmd.args[0];
9318
9514
  const vm = useVM();
9319
9515
  const newStack = {
9320
- branch: vm.script[branch],
9321
- label: branch,
9516
+ branchData: vm.script[label],
9517
+ label: label,
9518
+ args: cmd.args.splice(1),
9322
9519
  currentIndex: 0,
9323
9520
  };
9324
9521
  vm.setStack(newStack);
9325
9522
  await useMain().saveGame();
9326
9523
  await vm.runLine();
9327
- };
9328
- const jumpCommand = new CommandPlugin('jump', jump, generateParser('jump'));
9329
-
9330
- const notify = async (cmd) => {
9331
- const text = cmd.args[0];
9332
- useNotifications().addNotification(text);
9333
- return useVM().nextLine();
9334
- };
9335
- const notifyPlugin = new CommandPlugin('notify', notify, generateParser('notify'));
9336
-
9337
- const pause = async (cmd) => {
9338
- const pauseOptions = cmd.options;
9339
- if (pauseOptions.mode === 'music') {
9340
- const audioStore = useAudio();
9341
- pauseAudio(audioStore.currentMusic);
9342
- }
9343
- else if (pauseOptions.mode === 'sound' && pauseOptions.audio) {
9344
- pauseAudio(pauseOptions.audio);
9345
- }
9346
- else {
9347
- error(`pause first option needs to either be in music mode, or if stopping a sound needs to have the sound name supplied as second argument.`);
9524
+ });
9525
+ // Write a CommandPlugin for running a label using the runLabelFunction of the useVM store
9526
+ const runLabelPlugin = new CommandPlugin('run_label', 'any', async (cmd) => {
9527
+ if (cmd.args.length < 1 || typeof cmd.args[0] !== 'string') {
9528
+ commandRuntimeError(cmd, `run_label command needs a label to argument run`);
9348
9529
  }
9530
+ const label = cmd.args[0];
9531
+ const res = await useVM().runLabelFunction(label, ...cmd.args.slice(1));
9532
+ return res;
9533
+ });
9534
+ const defineVariablePlugin = new CommandPlugin('var', [
9535
+ { name: 'name', type: 'string' },
9536
+ { name: 'value', type: 'any' },
9537
+ ], async (cmd) => {
9538
+ const { name, value } = cmd.options;
9539
+ useVM().addScopedVariable(name, value);
9349
9540
  return useVM().nextLine();
9350
- };
9351
- const pauseParser = (ctx) => {
9352
- const { command } = ctx;
9353
- command.commandType = 'pause';
9354
- command.options = {
9355
- mode: command.args[0],
9356
- audio: command.args[1],
9357
- };
9358
- ctx.currentLine++;
9359
- };
9360
- const pauseCommand = new CommandPlugin('pause', pause, pauseParser);
9541
+ });
9542
+ const returnPlugin = new CommandPlugin('return', [{ name: 'value', type: 'any' }], async (cmd) => {
9543
+ const { value } = cmd.options;
9544
+ useVM().setReturnValue(value);
9545
+ return useVM().nextLine();
9546
+ });
9361
9547
 
9362
- const play = async (cmd) => {
9548
+ const playCommandArgs = [
9549
+ { name: 'mode', type: 'string' },
9550
+ { name: 'audio', type: 'string', optional: true },
9551
+ ];
9552
+ const playCommand = new CommandPlugin('play', playCommandArgs, async (cmd) => {
9363
9553
  const playOptions = cmd.options;
9364
9554
  if (playOptions.mode === 'music') {
9365
9555
  changeMusic(playOptions.audio);
@@ -9368,69 +9558,22 @@ const play = async (cmd) => {
9368
9558
  playAudio(playOptions.audio);
9369
9559
  }
9370
9560
  useVM().nextLine();
9371
- };
9372
- const playParser = (ctx) => {
9373
- const { command } = ctx;
9374
- command.commandType = 'play';
9375
- command.options = {
9376
- mode: command.args[0],
9377
- audio: command.args[1],
9378
- };
9379
- ctx.currentLine++;
9380
- };
9381
- const playCommand = new CommandPlugin('play', play, playParser);
9382
-
9383
- const set = async (cmd, choices) => {
9384
- const key = cmd.args[0];
9385
- const value = cmd.args[1];
9386
- const vm = useVM();
9387
- vm.setData(key, value);
9388
- return vm.nextLine();
9389
- };
9390
- const setCommand = new CommandPlugin('set', set, generateParser('set'));
9391
-
9392
- const setButton = async (cmd) => {
9393
- const screens = useScreens();
9394
- screens.changeButton(cmd.args[0], cmd.args[1]);
9395
- return useVM().nextLine();
9396
- };
9397
- const setButtonParser = (ctx) => {
9398
- const { command, line } = ctx;
9399
- command.commandType = 'set_button';
9400
- if (command.args.length !== 2) {
9401
- ctx.parserContext.error(line.line, `set_button command should have 2 arguments`);
9561
+ });
9562
+ const pauseCommand = new CommandPlugin('pause', playCommandArgs, async (cmd) => {
9563
+ const pauseOptions = cmd.options;
9564
+ if (pauseOptions.mode === 'music') {
9565
+ const audioStore = useAudio();
9566
+ pauseAudio(audioStore.currentMusic);
9402
9567
  }
9403
- ctx.currentLine++;
9404
- };
9405
- const setButtonCommand = new CommandPlugin('set_button', setButton, setButtonParser);
9406
-
9407
- const setScreen = async (cmd) => {
9408
- const screens = useScreens();
9409
- screens.setScreen(cmd.options.screen);
9410
- return useVM().nextLine();
9411
- };
9412
- const setScreenParser = (ctx) => {
9413
- const { command } = ctx;
9414
- command.commandType = 'set_screen';
9415
- command.options = {
9416
- screen: command.args[0],
9417
- };
9418
- ctx.currentLine++;
9419
- };
9420
- const setScreenCommand = new CommandPlugin('set_screen', setScreen, setScreenParser);
9421
-
9422
- const setStat = async (cmd) => {
9423
- const setStatKey = cmd.args[0];
9424
- const setAmount = cmd.args[1];
9425
- if (!setStatKey || !setAmount) {
9426
- error(`set_stat command needs a stat id and a value as parameters`);
9568
+ else if (pauseOptions.mode === 'sound' && pauseOptions.audio) {
9569
+ pauseAudio(pauseOptions.audio);
9570
+ }
9571
+ else {
9572
+ commandRuntimeError(cmd, `pause first option needs to either be in music mode, or if stopping a sound needs to have the sound name supplied as second argument.`);
9427
9573
  }
9428
- useHud().setStat(setStatKey, setAmount);
9429
9574
  return useVM().nextLine();
9430
- };
9431
- const setStatCommand = new CommandPlugin('set_stat', setStat, generateParser('set_stat'));
9432
-
9433
- const stop = async (cmd) => {
9575
+ });
9576
+ const stopCommand = new CommandPlugin('stop', playCommandArgs, async (cmd) => {
9434
9577
  const stopOptions = cmd.options;
9435
9578
  if (stopOptions.mode === 'music') {
9436
9579
  const audioStore = useAudio();
@@ -9440,186 +9583,665 @@ const stop = async (cmd) => {
9440
9583
  stopAudio(stopOptions.audio);
9441
9584
  }
9442
9585
  else {
9443
- error(`stop option needs to either be in music mode, or if stopping a sound needs to have the sound name supplied as second argument.`);
9586
+ commandRuntimeError(cmd, `stop option needs to either be in music mode, or if stopping a sound needs to have the sound name supplied as second argument.`);
9444
9587
  }
9445
9588
  return useVM().nextLine();
9446
- };
9447
- const stopParser = (ctx) => {
9448
- const { command } = ctx;
9449
- command.commandType = 'stop';
9450
- command.options = {
9451
- mode: command.args[0],
9452
- audio: command.args[1],
9453
- };
9454
- ctx.currentLine++;
9455
- };
9456
- const stopCommand = new CommandPlugin('stop', stop, stopParser);
9589
+ });
9590
+
9591
+ const setCommand = new CommandPlugin('set', [
9592
+ { name: 'key', type: 'string' },
9593
+ { name: 'value', type: 'any' },
9594
+ ], async (cmd) => {
9595
+ const state = getModifiableDataPinia();
9596
+ setDataHelper(state, cmd.options.key, cmd.options.value);
9597
+ return useVM().nextLine();
9598
+ });
9599
+ const addPlugin = new CommandPlugin('add', [
9600
+ { name: 'key', type: 'string' },
9601
+ { name: 'value', type: 'any' },
9602
+ ], async (cmd) => {
9603
+ const state = getModifiableDataPinia();
9604
+ addDataHelper(state, cmd.options.key, cmd.options.value);
9605
+ return useVM().nextLine();
9606
+ });
9607
+
9608
+ const setScreenCommand = new CommandPlugin('set_screen', [{ name: 'screen', type: 'string' }], (cmd) => {
9609
+ const screens = useScreens();
9610
+ screens.setScreen(cmd.options.screen);
9611
+ return useVM().nextLine();
9612
+ });
9613
+ const setButtonCommand = new CommandPlugin('set_button', [
9614
+ { name: 'buttonId', type: 'string' },
9615
+ { name: 'enabled', type: 'boolean' },
9616
+ ], async (cmd) => {
9617
+ const { buttonId, enabled } = cmd.options;
9618
+ const screens = useScreens();
9619
+ screens.changeButton(buttonId, enabled);
9620
+ return useVM().nextLine();
9621
+ });
9457
9622
 
9458
- const talk = async (cmd, choices) => {
9623
+ const talkCommand = new CommandPlugin('talk', [
9624
+ {
9625
+ name: 'speaker',
9626
+ type: 'string',
9627
+ },
9628
+ {
9629
+ name: 'pose',
9630
+ type: 'string',
9631
+ },
9632
+ {
9633
+ name: 'text',
9634
+ type: 'string',
9635
+ },
9636
+ ], async (cmd, choices) => {
9459
9637
  await textCommand({
9460
- speaker: cmd.args[0],
9461
- pose: cmd.args[1],
9462
- text: `"${cmd.args[2]}"`,
9638
+ speaker: cmd.options.speaker,
9639
+ pose: cmd.options.pose,
9640
+ text: `"${cmd.options.text}"`,
9463
9641
  choices,
9464
9642
  interactive: true,
9465
9643
  });
9644
+ });
9645
+ const textParser = () => {
9646
+ const parser = generateParser('text', []);
9647
+ return (ctx, parsed) => {
9648
+ const result = parser(ctx, parsed);
9649
+ parsed.command.staticOptions = {
9650
+ text: parsed.code,
9651
+ };
9652
+ return result;
9653
+ };
9466
9654
  };
9467
- const talkParser = (ctx) => {
9468
- const { command, line } = ctx;
9469
- command.commandType = 'talk';
9470
- if (command.args.length < 3) {
9471
- ctx.parserContext.error(line.line, `Talk command needs 3 arguments!`);
9472
- }
9473
- ctx.currentLine++;
9474
- };
9475
- const talkCommand = new CommandPlugin('talk', talk, talkParser);
9476
-
9477
- const text = async (cmd, choices) => {
9655
+ const textCommandPlugin = new CommandPlugin('text', [], async (cmd, choices) => {
9478
9656
  await textCommand({
9479
9657
  speaker: 'game',
9480
- text: cmd.options.text,
9658
+ text: cmd.staticOptions.text,
9481
9659
  choices,
9482
9660
  interactive: true,
9483
9661
  });
9484
- };
9485
- const textParser = (ctx) => {
9486
- const { command, line } = ctx;
9487
- command.commandType = 'text';
9488
- command.options = {
9489
- text: line.operator,
9490
- };
9491
- ctx.currentLine++;
9492
- };
9493
- const textCommandPlugin = new CommandPlugin('text', text, textParser);
9494
-
9495
- const wait = async (cmd) => {
9496
- await timeout(cmd.options.duration);
9497
- return useVM().nextLine();
9498
- };
9499
- const waitParser = (ctx) => {
9500
- const { command } = ctx;
9501
- command.commandType = 'wait';
9502
- command.options = {
9503
- duration: parseInt(command.args[0], 10),
9504
- };
9505
- ctx.currentLine++;
9506
- };
9507
- const waitCommand = new CommandPlugin('wait', wait, waitParser);
9662
+ }, textParser());
9663
+ console.log(textCommandPlugin);
9508
9664
 
9509
9665
  // Write a CommandPlugin for adding items to the inventory store
9510
- const addItemPlugin = new CommandPlugin('add_item', async (cmd) => {
9511
- const id = cmd.args[0];
9512
- const amount = cmd.args[1];
9666
+ const addItemPlugin = new CommandPlugin('add_item', [
9667
+ { name: 'id', type: 'string' },
9668
+ { name: 'amount', type: 'number' },
9669
+ ], async (cmd) => {
9670
+ const { id, amount } = cmd.options;
9513
9671
  const inventory = useInventory();
9514
9672
  inventory.add({
9515
9673
  id,
9516
9674
  amount,
9517
9675
  });
9518
9676
  return useVM().nextLine();
9519
- }, generateParser('add_item'));
9677
+ });
9520
9678
  // Write a CommandPlugin for removeing items to the inventory store
9521
- const removeItemPlugin = new CommandPlugin('remove_item', async (cmd) => {
9522
- const id = cmd.args[0];
9523
- const amount = cmd.args[1];
9679
+ const removeItemPlugin = new CommandPlugin('remove_item', [
9680
+ { name: 'id', type: 'string' },
9681
+ { name: 'amount', type: 'number' },
9682
+ ], async (cmd) => {
9683
+ const { id, amount } = cmd.options;
9524
9684
  const inventory = useInventory();
9525
9685
  inventory.remove({
9526
9686
  id,
9527
9687
  amount,
9528
9688
  });
9529
9689
  return useVM().nextLine();
9530
- }, generateParser('remove_item'));
9690
+ });
9691
+ // Write a CommandPlugin for command named 'has_item?' taking options itemId and optional parameter 'amount', which returns true if the inventory has the item, using the inventory store hasItem function and passing itemId and amount
9692
+ const hasItemPlugin = new CommandPlugin('has_item?', [
9693
+ { name: 'id', type: 'string' },
9694
+ { name: 'amount', type: 'number', optional: true },
9695
+ ], async (cmd) => {
9696
+ const { id, amount } = cmd.options;
9697
+ const inventory = useInventory();
9698
+ return inventory.hasItem(id, amount);
9699
+ });
9700
+ // Write a CommandPlugin named 'item_amount?' taking option itemId and returning how many of itemId the player has using the inventory store getItemAmount function and passing itemId
9701
+ const itemAmountPlugin = new CommandPlugin('item_amount?', [{ name: 'id', type: 'string' }], async (cmd) => {
9702
+ const { id } = cmd.options;
9703
+ const inventory = useInventory();
9704
+ return inventory.getItemAmount(id);
9705
+ });
9531
9706
  // Write a CommandPlugin for enabling an interactiongTag in the inventory store
9532
- const enableInteractionPlugin = new CommandPlugin('enable_interaction', async (cmd) => {
9533
- const tag = cmd.args[0];
9707
+ const enableInteractionPlugin = new CommandPlugin('enable_interaction', [{ name: 'tag', type: 'string' }], async (cmd) => {
9708
+ const tag = cmd.options.tag;
9534
9709
  const inventory = useInventory();
9535
9710
  inventory.enableInteraction(tag);
9536
9711
  return useVM().nextLine();
9537
- }, generateParser('enable_interaction'));
9712
+ });
9538
9713
  // Write a CommandPlugin for disabling an interactiongTag in the inventory store
9539
- const disableInteractionPlugin = new CommandPlugin('disable_interaction', async (cmd) => {
9540
- const tag = cmd.args[0];
9714
+ const disableInteractionPlugin = new CommandPlugin('disable_interaction', [{ name: 'tag', type: 'string' }], async (cmd) => {
9715
+ const tag = cmd.options.tag;
9541
9716
  const inventory = useInventory();
9542
9717
  inventory.disableInteraction(tag);
9543
9718
  return useVM().nextLine();
9544
- }, generateParser('disable_interaction'));
9719
+ });
9545
9720
 
9546
9721
  // Write a CommandPlugin for starting a quest with the useQuests quests store
9547
- const startQuestPlugin = new CommandPlugin('start_quest', async (cmd) => {
9548
- const questId = cmd.args[0];
9722
+ const startQuestPlugin = new CommandPlugin('start_quest', [{ name: 'questId', type: 'string' }], async (cmd) => {
9723
+ const questId = cmd.options.questId;
9549
9724
  const quests = useQuests();
9550
9725
  quests.startQuest(questId);
9551
9726
  return useVM().nextLine();
9552
- }, generateParser('start_quest'));
9727
+ });
9553
9728
  // Write a CommandPlugin for starting a quest objective with the cmd having arguments questId and objectiveId, using the useQuests store's startObjective method
9554
- const startObjectivePlugin = new CommandPlugin('start_objective', async (cmd) => {
9555
- const questId = cmd.args[0];
9556
- const objectiveId = cmd.args[1];
9729
+ const startObjectivePlugin = new CommandPlugin('start_objective', [
9730
+ { name: 'questId', type: 'string' },
9731
+ { name: 'objectiveId', type: 'string' },
9732
+ ], async (cmd) => {
9733
+ const { questId, objectiveId } = cmd.options;
9557
9734
  const quests = useQuests();
9558
9735
  quests.startObjective(questId, objectiveId);
9559
9736
  return useVM().nextLine();
9560
- }, generateParser('start_objective'));
9737
+ });
9561
9738
  // Write a CommandPlugin for completing a quest objective with the cmd having arguments questId and objectiveId, using the useQuests store's completeObjective method
9562
- const completeObjectivePlugin = new CommandPlugin('complete_objective', async (cmd) => {
9563
- const questId = cmd.args[0];
9564
- const objectiveId = cmd.args[1];
9739
+ const completeObjectivePlugin = new CommandPlugin('complete_objective', [
9740
+ { name: 'questId', type: 'string' },
9741
+ { name: 'objectiveId', type: 'string' },
9742
+ ], async (cmd) => {
9743
+ const { questId, objectiveId } = cmd.options;
9565
9744
  const quests = useQuests();
9566
9745
  quests.completeObjective(questId, objectiveId);
9567
9746
  return useVM().nextLine();
9568
- }, generateParser('complete_objective'));
9747
+ });
9569
9748
  // Write a CommandPlugin for completing a quest with the cmd having arguments questId, using the useQuests store's completeQuest method
9570
- const completeQuestPlugin = new CommandPlugin('complete_quest', async (cmd) => {
9571
- const questId = cmd.args[0];
9572
- let ending;
9573
- if (cmd.args.length > 1) {
9574
- ending = cmd.args[1];
9575
- }
9749
+ const completeQuestPlugin = new CommandPlugin('complete_quest', [
9750
+ { name: 'questId', type: 'string' },
9751
+ { name: 'ending', type: 'string', optional: true },
9752
+ ], async (cmd) => {
9753
+ const { questId, ending } = cmd.options;
9576
9754
  const quests = useQuests();
9577
9755
  quests.completeQuest(questId, ending);
9578
9756
  return useVM().nextLine();
9579
- }, generateParser('complete_quest'));
9757
+ });
9758
+ // Write a CommandPlugin for a command named 'quest_completed?' that returns true if the quest with the given questId has status value completed, false otherwise, using the useQuests store's isQuestCompleted method
9759
+ const questCompletedPlugin = new CommandPlugin('quest_completed?', [{ name: 'questId', type: 'string' }], async (cmd) => {
9760
+ const { questId } = cmd.options;
9761
+ const quests = useQuests();
9762
+ return quests.isQuestCompleted(questId);
9763
+ });
9764
+ // Write a CommandPlugin for a command named 'objective_completed?' that returns true if the quest with the given questId has the given objectiveId completed, false otherwise, using the useQuests store's isObjectiveCompleted method
9765
+ const objectiveCompletedPlugin = new CommandPlugin('objective_completed?', [
9766
+ { name: 'questId', type: 'string' },
9767
+ { name: 'objectiveId', type: 'string' },
9768
+ ], async (cmd) => {
9769
+ const { questId, objectiveId } = cmd.options;
9770
+ const quests = useQuests();
9771
+ return quests.isObjectiveCompleted(questId, objectiveId);
9772
+ });
9773
+ // Write a CommandPlugin for a command named 'quest_started?' that returns true if the quest with the given questId has status value started, false otherwise, using the useQuests store's isQuestStarted method
9774
+ const questStartedPlugin = new CommandPlugin('quest_started?', [{ name: 'questId', type: 'string' }], async (cmd) => {
9775
+ const { questId } = cmd.options;
9776
+ const quests = useQuests();
9777
+ return quests.isQuestStarted(questId);
9778
+ });
9779
+ // Write a CommandPlugin for a command named 'objective_started?' that returns true if the quest with the given questId has the given objectiveId started, false otherwise, using the useQuests store's isObjectiveStarted method
9780
+ const objectiveStartedPlugin = new CommandPlugin('objective_started?', [
9781
+ { name: 'questId', type: 'string' },
9782
+ { name: 'objectiveId', type: 'string' },
9783
+ ], async (cmd) => {
9784
+ const { questId, objectiveId } = cmd.options;
9785
+ const quests = useQuests();
9786
+ return quests.isObjectiveStarted(questId, objectiveId);
9787
+ });
9580
9788
 
9581
- // Write a CommandPlugin for running a label using the runLabelFunction of the useVM store
9582
- const runLabelPlugin = new CommandPlugin('run_label', async (cmd) => {
9583
- const label = cmd.args[0];
9584
- if (!label) {
9585
- error(`run_label command needs a label to argument run`);
9789
+ const waitCommand = new CommandPlugin('wait', [{ name: 'duration', type: 'number' }], async (cmd) => {
9790
+ await timeout(cmd.options.duration);
9791
+ return useVM().nextLine();
9792
+ });
9793
+
9794
+ const notifyPlugin = new CommandPlugin('notify', [{ name: 'text', type: 'string' }], async (cmd) => {
9795
+ const { text } = cmd.options;
9796
+ useNotifications().addNotification(text);
9797
+ return useVM().nextLine();
9798
+ });
9799
+
9800
+ const clearDialogPlugin = new CommandPlugin('clear_dialog', [], async (cmd) => {
9801
+ useDialogStore().clearDialog();
9802
+ return useVM().nextLine();
9803
+ });
9804
+
9805
+ const ifCommand = new CommandPlugin('if', [{ name: 'condition', type: 'boolean' }], async (cmd) => {
9806
+ const newBranch = runConditionCommand(cmd);
9807
+ const vmStore = useVM();
9808
+ if (newBranch) {
9809
+ const newStack = {
9810
+ branchData: {
9811
+ branch: newBranch,
9812
+ },
9813
+ currentIndex: 0,
9814
+ };
9815
+ return vmStore.addStack(newStack);
9816
+ }
9817
+ return vmStore.nextLine();
9818
+ }, (ctx, parsed) => {
9819
+ let newLine = ctx.currentLine;
9820
+ const parser = generateParser('if', [
9821
+ { name: 'condition', type: 'string' },
9822
+ ]);
9823
+ parser(ctx, parsed);
9824
+ const { lines, currentLine, line } = ctx;
9825
+ const command = parsed.command;
9826
+ let failure;
9827
+ const nextLine = getLine(lines, currentLine + 1);
9828
+ if (nextLine && nextLine.code === 'else:') {
9829
+ failure = ctx.processCommandsFunction(ctx.parserContext, nextLine.branch, line);
9830
+ newLine++;
9831
+ }
9832
+ command.staticOptions = {
9833
+ success: ctx.processCommandsFunction(ctx.parserContext, line.branch, line),
9834
+ failure,
9835
+ };
9836
+ newLine++;
9837
+ return {
9838
+ newLine,
9839
+ };
9840
+ });
9841
+
9842
+ /* eslint-disable eqeqeq */
9843
+ // export const add: CommandRunner = async (cmd) => {
9844
+ // const addKey = cmd.args[0];
9845
+ // const addValue = cmd.args[1];
9846
+ // const vm = useVM();
9847
+ // vm.addInstruction(addKey, addValue as any);
9848
+ // return vm.nextLine();
9849
+ // };
9850
+ const equalPlugin = new CommandPlugin('==', 'any', async (cmd) => {
9851
+ if (cmd.args.length < 2) {
9852
+ commandRuntimeError(cmd, `requires at least two arguments`);
9853
+ }
9854
+ let previousValue = cmd.args[0];
9855
+ let allEqual = true;
9856
+ for (const arg of cmd.args) {
9857
+ if (arg != previousValue) {
9858
+ allEqual = false;
9859
+ break;
9860
+ }
9861
+ else {
9862
+ previousValue = arg;
9863
+ }
9864
+ }
9865
+ return allEqual;
9866
+ });
9867
+ const greaterThanPlugin = new CommandPlugin('>', [
9868
+ { name: 'a', type: 'any' },
9869
+ { name: 'b', type: 'any' },
9870
+ ], async (cmd) => {
9871
+ const { a, b } = cmd.options;
9872
+ return a > b;
9873
+ });
9874
+ // Write a lesserThanPlugin, greaterOrEqualPlugin, lesserOrEqualPlugin, notEqualPlugin
9875
+ const lesserThanPlugin = new CommandPlugin('<', [
9876
+ { name: 'a', type: 'any' },
9877
+ { name: 'b', type: 'any' },
9878
+ ], async (cmd) => {
9879
+ const { a, b } = cmd.options;
9880
+ return a < b;
9881
+ });
9882
+ const greaterOrEqualPlugin = new CommandPlugin('>=', [
9883
+ { name: 'a', type: 'any' },
9884
+ { name: 'b', type: 'any' },
9885
+ ], async (cmd) => {
9886
+ const { a, b } = cmd.options;
9887
+ return a >= b;
9888
+ });
9889
+ const lesserOrEqualPlugin = new CommandPlugin('<=', [
9890
+ { name: 'a', type: 'any' },
9891
+ { name: 'b', type: 'any' },
9892
+ ], async (cmd) => {
9893
+ const { a, b } = cmd.options;
9894
+ return a <= b;
9895
+ });
9896
+ const notEqualPlugin = new CommandPlugin('!=', [
9897
+ { name: 'a', type: 'any' },
9898
+ { name: 'b', type: 'any' },
9899
+ ], async (cmd) => {
9900
+ const { a, b } = cmd.options;
9901
+ return a != b;
9902
+ });
9903
+ const notPlugin = new CommandPlugin('!', [{ name: 'a', type: 'any' }], async (cmd) => {
9904
+ const { a } = cmd.options;
9905
+ return !a;
9906
+ });
9907
+ // Create an andPlugin and an orPlugin
9908
+ const andPlugin = new CommandPlugin('&&', 'any', async (cmd) => {
9909
+ const args = cmd.args;
9910
+ if (cmd.args.length < 2) {
9911
+ commandRuntimeError(cmd, 'requires at least two arguments');
9912
+ }
9913
+ return args.reduce((acc, curr) => acc && curr, true);
9914
+ });
9915
+ const orPlugin = new CommandPlugin('||', 'any', async (cmd) => {
9916
+ if (cmd.args.length < 2) {
9917
+ commandRuntimeError(cmd, 'requires at least two arguments');
9918
+ }
9919
+ return cmd.args.reduce((acc, curr) => acc || curr, false);
9920
+ });
9921
+ // Write a ternaryPlugin
9922
+ const ternaryPlugin = new CommandPlugin('?', [
9923
+ { name: 'a', type: 'any' },
9924
+ { name: 'b', type: 'any' },
9925
+ { name: 'c', type: 'any' },
9926
+ ], async (cmd) => {
9927
+ const { a, b, c } = cmd.options;
9928
+ return a ? b : c;
9929
+ });
9930
+
9931
+ // export const add: CommandRunner = async (cmd) => {
9932
+ // const addKey = cmd.args[0];
9933
+ // const addValue = cmd.args[1];
9934
+ // const vm = useVM();
9935
+ // vm.addInstruction(addKey, addValue as any);
9936
+ // return vm.nextLine();
9937
+ // };
9938
+ const additionPlugin = new CommandPlugin('+', 'any', async (cmd) => {
9939
+ if (cmd.args.length < 2) {
9940
+ commandRuntimeError(cmd, `requires at least two arguments`);
9941
+ }
9942
+ return cmd.args.reduce((acc, curr) => {
9943
+ if (typeof curr === 'number') {
9944
+ return acc + curr;
9945
+ }
9946
+ commandRuntimeError(cmd, `requires all arguments to be numbers`);
9947
+ return acc;
9948
+ }, 0);
9949
+ });
9950
+ const substractionPlugin = new CommandPlugin('-', 'any', async (cmd) => {
9951
+ if (cmd.args.length < 2) {
9952
+ commandRuntimeError(cmd, `requires at least two arguments`);
9953
+ }
9954
+ return cmd.args.reduce((acc, curr) => {
9955
+ if (typeof curr === 'number') {
9956
+ return acc - curr;
9957
+ }
9958
+ commandRuntimeError(cmd, `requires all arguments to be numbers`);
9959
+ return acc;
9960
+ }, 0);
9961
+ });
9962
+ // Write multiplicationPlugin and divisionPlugin
9963
+ const multiplicationPlugin = new CommandPlugin('*', 'any', async (cmd) => {
9964
+ if (cmd.args.length < 2) {
9965
+ commandRuntimeError(cmd, `requires at least two arguments`);
9966
+ }
9967
+ return cmd.args.reduce((acc, curr) => {
9968
+ if (typeof curr === 'number') {
9969
+ return acc * curr;
9970
+ }
9971
+ commandRuntimeError(cmd, `requires all arguments to be numbers`);
9972
+ return acc;
9973
+ }, 0);
9974
+ });
9975
+ const divisionPlugin = new CommandPlugin('/', 'any', async (cmd) => {
9976
+ if (cmd.args.length < 2) {
9977
+ commandRuntimeError(cmd, `requires at least two arguments`);
9978
+ }
9979
+ return cmd.args.reduce((acc, curr) => {
9980
+ if (typeof curr === 'number') {
9981
+ return acc / curr;
9982
+ }
9983
+ commandRuntimeError(cmd, `requires all arguments to be numbers`);
9984
+ return acc;
9985
+ }, 0);
9986
+ });
9987
+
9988
+ const runChoice = async (cmd) => {
9989
+ // Hack for us to be able to find that command again on playerAnswered
9990
+ useVM().lastChoiceCommand = cmd;
9991
+ const { prompt, choices } = cmd.staticOptions;
9992
+ // For each possible choice, we run the choice prompt expression which will return us the info we need to generate the choice options
9993
+ const choiceResults = [];
9994
+ for (const [index, choice] of choices.entries()) {
9995
+ const choiceResult = await runExpression(choice.prompt);
9996
+ choiceResults.push(choiceResult);
9997
+ }
9998
+ // Lazy hack to store those results for after the player answers.
9999
+ cmd.options.choiceResults = choiceResults;
10000
+ // Convert the results into dialog options
10001
+ const dialogChoices = choiceResults
10002
+ .map((res, index) => {
10003
+ let allowed = true;
10004
+ if (res.skillCheck) {
10005
+ allowed = res.skillCheck?.allowed ?? false;
10006
+ }
10007
+ const result = {
10008
+ choice: res.text,
10009
+ originalIndex: index,
10010
+ allowed,
10011
+ };
10012
+ return result;
10013
+ })
10014
+ // Choices that have a null text are options that didn't pass conditions, or hidden skill checks
10015
+ .filter((el) => el.choice);
10016
+ runCommand(prompt, dialogChoices);
10017
+ };
10018
+ function parseChoiceOption(ctx, choice, index) {
10019
+ const command = choice.expression;
10020
+ if (!choice.branch) {
10021
+ ctx.parserContext.error(choice.line, `Choice option doesn't have any branch to go to (${choice.code} - ${choice.line})`);
10022
+ }
10023
+ let skillBranches;
10024
+ let mainBranch;
10025
+ if (choice.expression[1] === 'roll') {
10026
+ skillBranches = {
10027
+ success: ctx.processCommandsFunction(ctx.parserContext, choice.branch[0].branch, choice),
10028
+ failure: ctx.processCommandsFunction(ctx.parserContext, choice.branch[1].branch, choice),
10029
+ };
10030
+ }
10031
+ else {
10032
+ mainBranch = ctx.processCommandsFunction(ctx.parserContext, choice.branch, choice);
10033
+ }
10034
+ const choiceInfo = {
10035
+ prompt: ctx.processCommandsFunction(ctx.parserContext, [choice], choice)[0],
10036
+ branch: mainBranch,
10037
+ skillBranches,
10038
+ };
10039
+ return choiceInfo;
10040
+ }
10041
+ const choiceParser = (ctx, parsed) => {
10042
+ let newLine = ctx.currentLine;
10043
+ const parser = generateParser('choice', []);
10044
+ const { line } = ctx;
10045
+ const command = parsed.command;
10046
+ if (!line.branch || line.branch.length < 2) {
10047
+ ctx.parserContext.error(line.line, `Choice menu needs to have at least one option`);
10048
+ }
10049
+ const prompt = line.branch[0];
10050
+ if (!prompt) {
10051
+ ctx.parserContext.error(ctx.line.line, `Choice prompt is missing `);
10052
+ }
10053
+ const choices = line.branch.slice(1);
10054
+ const prompts = choices.map((choice, index) => {
10055
+ if (!choice.branch) {
10056
+ ctx.parserContext.error(choice.line, `Choice option doesn't have any branch to go to (${choice.code})`);
10057
+ }
10058
+ choice = getChoiceOptionLineFromChoicePrompt(ctx.parserContext, choice);
10059
+ return parseChoiceOption(ctx, choice);
10060
+ });
10061
+ command.staticOptions = {
10062
+ prompt: ctx.processCommandsFunction(ctx.parserContext, [prompt], line)[0],
10063
+ choices: prompts,
10064
+ };
10065
+ newLine++;
10066
+ return {
10067
+ newLine,
10068
+ };
10069
+ };
10070
+ const choicePlugin = new CommandPlugin('choice', [], runChoice, choiceParser);
10071
+ choicePlugin.onPlayerAnswered = async (command, choiceIndex) => {
10072
+ // The player has answered, we now need to go to the right branch and perform any needed skill checks
10073
+ const vmStore = useVM();
10074
+ const { choices } = command.staticOptions;
10075
+ const choice = choices[choiceIndex];
10076
+ const choicePromptResult = command.options.choiceResults[choiceIndex];
10077
+ let newBranch;
10078
+ if (choicePromptResult.skillCheck) {
10079
+ const result = runSkillCheck({
10080
+ skill: choicePromptResult.skillCheck.skillId,
10081
+ value: choicePromptResult.skillCheck.difficulty,
10082
+ id: choicePromptResult.skillCheck.skillCheckId,
10083
+ hideAfterRoll: choicePromptResult.skillCheck.hideAfterRoll,
10084
+ });
10085
+ if (result.succeeded) {
10086
+ newBranch = choice.skillBranches.success;
10087
+ }
10088
+ else {
10089
+ newBranch = choice.skillBranches.failure;
10090
+ }
10091
+ }
10092
+ else {
10093
+ newBranch = choice.branch;
10094
+ }
10095
+ if (newBranch) {
10096
+ const newStack = {
10097
+ currentIndex: 0,
10098
+ branchData: {
10099
+ branch: newBranch,
10100
+ },
10101
+ };
10102
+ return vmStore.addStack(newStack);
10103
+ }
10104
+ else {
10105
+ vmStore.nextLine();
10106
+ }
10107
+ };
10108
+ // Turns a choice prompt (Which is normally just text + an optional condition) into a proper choice prompt command so it can be parsed properly
10109
+ function getChoiceOptionLineFromChoicePrompt(ctx, choice) {
10110
+ const generatedCode = `choicePrompt ${choice.code}`;
10111
+ const newLine = {
10112
+ code: generatedCode,
10113
+ indentation: choice.indentation,
10114
+ line: choice.line,
10115
+ branch: choice.branch,
10116
+ expression: ['choicePrompt', ...choice.expression],
10117
+ };
10118
+ return newLine;
10119
+ }
10120
+ /** Custom "fake" instruction generated on choice prompts to process their optional conditions */
10121
+ const choicePromptCommandPlugin = new CommandPlugin('choicePrompt', 'any',
10122
+ // Will return null if the choice prompt should not be used (failed condition or hidden skillcheck). Otherwise, returns the info needed to display the prompt + later run the skillcheck
10123
+ async (cmd) => {
10124
+ const args = cmd.args;
10125
+ if (args[0] === 'roll') {
10126
+ const skillCheckId = args[1];
10127
+ const skillId = args[2];
10128
+ const difficulty = args[3];
10129
+ const skillText = args[4];
10130
+ const hideAfterRoll = args.length > 5 ? args[5] : false;
10131
+ const state = useSkills().getSkillCheck(skillCheckId);
10132
+ if (state.hidden) {
10133
+ return {
10134
+ text: null,
10135
+ };
10136
+ }
10137
+ const skillCheckFailed = state.happened && !state.succeeded;
10138
+ const { difficultyText, allowed } = getSkillCheckText({
10139
+ skill: skillId,
10140
+ skillCheckId: skillCheckId,
10141
+ value: difficulty,
10142
+ });
10143
+ const text = `${difficultyText} ${skillText}`;
10144
+ return {
10145
+ text,
10146
+ skillCheck: {
10147
+ allowed: !skillCheckFailed,
10148
+ skillId,
10149
+ skillCheckId,
10150
+ difficulty,
10151
+ hideAfterRoll,
10152
+ },
10153
+ };
9586
10154
  }
9587
- await useVM().runLabelFunction(label);
9588
- }, generateParser('run_label'));
10155
+ else if (args.length > 1 && args[1] === 'if') {
10156
+ const text = args[0];
10157
+ let condition = true;
10158
+ if (args.length > 2) {
10159
+ condition = args[2];
10160
+ }
10161
+ if (condition) {
10162
+ return {
10163
+ text,
10164
+ };
10165
+ }
10166
+ else {
10167
+ return {
10168
+ text: null,
10169
+ };
10170
+ }
10171
+ }
10172
+ else {
10173
+ return {
10174
+ text: args[0],
10175
+ };
10176
+ }
10177
+ });
9589
10178
 
9590
10179
  function registerBaseCommands(vm) {
9591
- vm.addCommand(addLevelPlugin);
9592
- vm.addCommand(addStatPlugin);
9593
- vm.addCommand(addXpPlugin);
9594
- vm.addCommand(addPlugin);
10180
+ // Choices
9595
10181
  vm.addCommand(choicePlugin);
9596
- vm.addCommand(clearDialogPlugin);
10182
+ vm.addCommand(choicePromptCommandPlugin);
9597
10183
  vm.addCommand(ifCommand);
9598
- vm.addCommand(jumpCommand);
10184
+ // Stats
10185
+ vm.addCommand(addStatPlugin);
10186
+ vm.addCommand(setStatPlugin);
10187
+ vm.addCommand(getStatPlugin);
10188
+ vm.addCommand(clearDialogPlugin);
9599
10189
  vm.addCommand(notifyPlugin);
10190
+ // Audio
9600
10191
  vm.addCommand(pauseCommand);
9601
10192
  vm.addCommand(playCommand);
10193
+ vm.addCommand(stopCommand);
10194
+ // Screens
9602
10195
  vm.addCommand(setButtonCommand);
9603
10196
  vm.addCommand(setScreenCommand);
9604
- vm.addCommand(setStatCommand);
10197
+ vm.addCommand(waitCommand);
10198
+ // Logic operations
10199
+ vm.addCommand(equalPlugin);
10200
+ vm.addCommand(greaterThanPlugin);
10201
+ vm.addCommand(lesserThanPlugin);
10202
+ vm.addCommand(greaterOrEqualPlugin);
10203
+ vm.addCommand(lesserOrEqualPlugin);
10204
+ vm.addCommand(notEqualPlugin);
10205
+ vm.addCommand(notPlugin);
10206
+ vm.addCommand(andPlugin);
10207
+ vm.addCommand(orPlugin);
10208
+ vm.addCommand(ternaryPlugin);
10209
+ // Arithmetic operations
10210
+ vm.addCommand(additionPlugin);
10211
+ vm.addCommand(substractionPlugin);
10212
+ vm.addCommand(multiplicationPlugin);
10213
+ vm.addCommand(divisionPlugin);
10214
+ // Setting variables
10215
+ vm.addCommand(addPlugin);
9605
10216
  vm.addCommand(setCommand);
9606
- vm.addCommand(stopCommand);
9607
- vm.addCommand(talkCommand);
10217
+ // Text display
9608
10218
  vm.addCommand(textCommandPlugin);
9609
- vm.addCommand(waitCommand);
9610
- // functions and labels
10219
+ vm.addCommand(talkCommand);
10220
+ // // functions and labels
9611
10221
  vm.addCommand(jumpCommand);
9612
10222
  vm.addCommand(runLabelPlugin);
9613
- // Quests
10223
+ vm.addCommand(defineVariablePlugin);
10224
+ vm.addCommand(returnPlugin);
10225
+ // // Quests
9614
10226
  vm.addCommand(startQuestPlugin);
9615
10227
  vm.addCommand(startObjectivePlugin);
9616
10228
  vm.addCommand(completeObjectivePlugin);
9617
10229
  vm.addCommand(completeQuestPlugin);
9618
- // Inventory
10230
+ vm.addCommand(questStartedPlugin);
10231
+ vm.addCommand(objectiveStartedPlugin);
10232
+ vm.addCommand(questCompletedPlugin);
10233
+ vm.addCommand(objectiveCompletedPlugin);
10234
+ // // Inventory
9619
10235
  vm.addCommand(addItemPlugin);
9620
10236
  vm.addCommand(removeItemPlugin);
9621
10237
  vm.addCommand(enableInteractionPlugin);
9622
10238
  vm.addCommand(disableInteractionPlugin);
10239
+ vm.addCommand(hasItemPlugin);
10240
+ vm.addCommand(itemAmountPlugin);
10241
+ // Skills
10242
+ vm.addCommand(addLevelPlugin);
10243
+ vm.addCommand(addXpPlugin);
10244
+ vm.addCommand(rollPlugin);
9623
10245
  }
9624
10246
 
9625
10247
  class NarratPlugin {
@@ -9662,7 +10284,7 @@ async function startApp(config, options) {
9662
10284
  });
9663
10285
  registerBaseCommands(vm);
9664
10286
  logManager.setupDebugger(options.debug);
9665
- console.log('%c Narrat game engine – 1.3.2 - June 26, 2022 18:33:37', 'background: #222; color: #bada55');
10287
+ console.log('%c Narrat game engine – 2.0.0-rc3 - June 28, 2022 22:38:40', 'background: #222; color: #bada55');
9666
10288
  vm.callHook('onNarratSetup');
9667
10289
  app.mount('#game-holder');
9668
10290
  if (options.debug) {