diff --git a/under_the_neighborhood/.vscode/settings.json b/under_the_neighborhood/.vscode/settings.json
new file mode 100644
index 0000000000..d88db28e20
--- /dev/null
+++ b/under_the_neighborhood/.vscode/settings.json
@@ -0,0 +1,5 @@
+{
+ "editor.autoClosingBrackets": "always",
+ "editor.fontFamily": "Fira Code",
+ "editor.fontLigatures": true
+}
\ No newline at end of file
diff --git a/under_the_neighborhood/README.md b/under_the_neighborhood/README.md
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/under_the_neighborhood/prepros.config b/under_the_neighborhood/prepros.config
new file mode 100644
index 0000000000..4f4e8a67b9
--- /dev/null
+++ b/under_the_neighborhood/prepros.config
@@ -0,0 +1,946 @@
+{
+ "version": "7",
+ "about": "This is a Prepros (https://prepros.io) configuration file. You can commit this file to a git repo to backup and sync project configurations.",
+ "config": {
+ "proxy": {
+ "enable": false,
+ "target": "",
+ "useLocalAssets": false
+ },
+ "reload": {
+ "enable": true,
+ "delay": 0,
+ "animate": true,
+ "afterUpload": false
+ },
+ "sync": {
+ "enable": false,
+ "mouse": true,
+ "keyboard": true,
+ "form": true,
+ "scroll": true
+ },
+ "watcher": {
+ "enable": true,
+ "maxFiles": 2000,
+ "usePolling": true,
+ "pollingInterval": 500,
+ "extensions": [
+ ".html",
+ ".htm",
+ ".php"
+ ],
+ "ignore": {
+ "patterns": [
+ ".*",
+ "wp-admin",
+ "wp-includes",
+ "node_modules",
+ "Prepros Export",
+ "bower_components"
+ ],
+ "exceptions": []
+ }
+ },
+ "exporter": {
+ "ignore": {
+ "patterns": [
+ ".*",
+ "desktop.ini",
+ "prepros.cfg",
+ "node_modules",
+ "Prepros Export",
+ "prepros.config",
+ "prepros-6.config",
+ "*-original.jpg",
+ "*-original.jpeg",
+ "*-original.png",
+ "*-original.svg",
+ "*.scss",
+ "*.sass",
+ "*.less",
+ "*.pug",
+ "*.jade",
+ "*.styl",
+ "*.haml",
+ "*.slim",
+ "*.coffee",
+ "*.kit",
+ "*.turf",
+ "*.ts"
+ ],
+ "exceptions": []
+ }
+ },
+ "uploader": {
+ "remotePath": "",
+ "timeout": 20000,
+ "autoUpload": false,
+ "reuseConnection": true,
+ "connectionType": "ftp",
+ "history": []
+ },
+ "packages": {
+ "createPackageLock": true
+ },
+ "images": {
+ "preserveOriginal": true
+ },
+ "tasks": {
+ "autoprefixer": {
+ "cascade": true,
+ "add": true,
+ "remove": true,
+ "supports": true,
+ "flexbox": true,
+ "grid": "autoplace",
+ "browsers": [
+ "last 2 versions"
+ ],
+ "sourceMap": false
+ },
+ "babel": {
+ "sourceMap": false,
+ "presets": {
+ "@babel/preset-env": {
+ "enable": true,
+ "options": {
+ "targets": [
+ "last 2 versions"
+ ],
+ "preserveImports": false,
+ "polyfills": false
+ }
+ },
+ "@babel/preset-react": true,
+ "@babel/preset-flow": false
+ },
+ "plugins": {
+ "@babel/plugin-proposal-class-static-block": false,
+ "@babel/plugin-proposal-class-properties": false,
+ "@babel/plugin-proposal-decorators": {
+ "enable": false,
+ "options": {
+ "decoratorsBeforeExport": true
+ }
+ },
+ "@babel/plugin-proposal-export-namespace-from": false,
+ "@babel/plugin-proposal-function-sent": false,
+ "@babel/plugin-proposal-logical-assignment-operators": false,
+ "@babel/plugin-proposal-nullish-coalescing-operator": false,
+ "@babel/plugin-proposal-numeric-separator": false,
+ "@babel/plugin-proposal-optional-chaining": false,
+ "@babel/plugin-proposal-private-methods": false,
+ "@babel/plugin-proposal-throw-expressions": false
+ },
+ "customPresets": [],
+ "customPlugins": []
+ },
+ "bundle-js": {
+ "sourceMap": false,
+ "exclude": [
+ "node_modules",
+ "bower_components"
+ ],
+ "devMode": true,
+ "globals": [],
+ "externals": [],
+ "babel": {
+ "enable": true,
+ "options": {
+ "sourceMap": false,
+ "presets": {
+ "@babel/preset-env": {
+ "enable": true,
+ "options": {
+ "targets": [
+ "last 2 versions"
+ ],
+ "preserveImports": false,
+ "polyfills": false
+ }
+ },
+ "@babel/preset-react": true,
+ "@babel/preset-flow": false
+ },
+ "plugins": {
+ "@babel/plugin-proposal-class-static-block": false,
+ "@babel/plugin-proposal-class-properties": false,
+ "@babel/plugin-proposal-decorators": {
+ "enable": false,
+ "options": {
+ "decoratorsBeforeExport": true
+ }
+ },
+ "@babel/plugin-proposal-export-namespace-from": false,
+ "@babel/plugin-proposal-function-sent": false,
+ "@babel/plugin-proposal-logical-assignment-operators": false,
+ "@babel/plugin-proposal-nullish-coalescing-operator": false,
+ "@babel/plugin-proposal-numeric-separator": false,
+ "@babel/plugin-proposal-optional-chaining": false,
+ "@babel/plugin-proposal-private-methods": false,
+ "@babel/plugin-proposal-throw-expressions": false
+ },
+ "customPresets": [],
+ "customPlugins": []
+ }
+ },
+ "css": {
+ "enable": true
+ },
+ "fonts": {
+ "enable": true
+ }
+ },
+ "coffeescript": {
+ "header": false,
+ "bare": false,
+ "sourceMap": false
+ },
+ "command": {
+ "command": "",
+ "rootDir": ""
+ },
+ "concat-js": {
+ "sourceMap": false,
+ "rootDir": ""
+ },
+ "copy": {
+ "sourceMap": false
+ },
+ "dart-sass": {
+ "indentType": "space",
+ "allowWildcardImports": false,
+ "indentWidth": 2,
+ "linefeed": "lf",
+ "sourceMap": false
+ },
+ "haml": {
+ "doubleQuoteAttributes": true
+ },
+ "jpg": {
+ "quality": 90
+ },
+ "less": {
+ "javascriptEnabled": false,
+ "strictImports": false,
+ "insecure": false,
+ "math": "always",
+ "strictUnits": false,
+ "dumpLineNumbers": false,
+ "sourceMap": false
+ },
+ "markdown": {
+ "githubFlavored": true,
+ "wrapWithHtml": false
+ },
+ "minify-css": {
+ "sourceMap": false
+ },
+ "minify-html": {
+ "caseSensitive": false,
+ "collapseBooleanAttributes": true,
+ "collapseInlineTagWhitespace": false,
+ "collapseWhitespace": true,
+ "conservativeCollapse": false,
+ "decodeEntities": false,
+ "html5": true,
+ "includeAutoGeneratedTags": true,
+ "keepClosingSlash": false,
+ "minifyCSS": true,
+ "minifyJS": true,
+ "preserveLineBreaks": false,
+ "preventAttributesEscaping": false,
+ "processConditionalComments": false,
+ "removeAttributeQuotes": false,
+ "removeComments": true,
+ "removeEmptyAttributes": false,
+ "removeEmptyElement": false,
+ "removeOptionalTags": false,
+ "removeRedundantAttributes": false,
+ "removeScriptTypeAttributes": false,
+ "removeStyleLinkTypeAttributes": false,
+ "removeTagWhitespace": false,
+ "sortAttributes": false,
+ "sortClassName": false,
+ "useShortDoctype": true
+ },
+ "minify-js": {
+ "parse": {
+ "bare_returns": false
+ },
+ "compress": {
+ "arrows": true,
+ "arguments": false,
+ "booleans": true,
+ "booleans_as_integers": false,
+ "collapse_vars": true,
+ "comparisons": true,
+ "computed_props": true,
+ "conditionals": true,
+ "dead_code": true,
+ "directives": true,
+ "drop_console": false,
+ "drop_debugger": true,
+ "evaluate": true,
+ "expression": false,
+ "global_defs": [],
+ "hoist_funs": false,
+ "hoist_props": true,
+ "hoist_vars": false,
+ "if_return": true,
+ "inline": 3,
+ "join_vars": true,
+ "keep_fargs": true,
+ "keep_infinity": false,
+ "loops": true,
+ "negate_iife": true,
+ "properties": true,
+ "pure_funcs": [],
+ "pure_getters": false,
+ "reduce_funcs": true,
+ "reduce_vars": true,
+ "sequences": true,
+ "side_effects": true,
+ "switches": true,
+ "top_retain": [],
+ "typeofs": true,
+ "unsafe": false,
+ "unsafe_arrows": false,
+ "unsafe_comps": false,
+ "unsafe_Function": false,
+ "unsafe_math": false,
+ "unsafe_proto": false,
+ "unsafe_regexp": false,
+ "unsafe_undefined": false,
+ "unused": true
+ },
+ "mangle": {
+ "eval": false,
+ "reserved": []
+ },
+ "output": {
+ "ascii_only": false,
+ "braces": false,
+ "comments": "none",
+ "inline_script": true,
+ "keep_numbers": false,
+ "keep_quoted_props": false,
+ "preamble": null,
+ "quote_keys": false,
+ "quote_style": 0,
+ "semicolons": true,
+ "shebang": true,
+ "webkit": false,
+ "wrap_iife": false,
+ "wrap_func_args": true
+ },
+ "sourceMap": false,
+ "toplevel": false,
+ "ie8": false,
+ "keep_classnames": false,
+ "keep_fnames": false,
+ "safari10": false
+ },
+ "node-sass": {
+ "indentType": "space",
+ "allowWildcardImports": false,
+ "indentWidth": 2,
+ "linefeed": "lf",
+ "outputStyle": "expanded",
+ "precision": 10,
+ "sourceMap": false,
+ "sourceComments": false
+ },
+ "png": {
+ "quality": 90
+ },
+ "postcss-import": {
+ "ignoreKeywords": [],
+ "sourceMap": false
+ },
+ "postcss-preset-env": {
+ "stage": 2,
+ "browsers": [
+ "last 2 versions"
+ ],
+ "sourceMap": false
+ },
+ "pug": {
+ "pretty": true
+ },
+ "slim": {
+ "indent": "space",
+ "indentSize": 2,
+ "pretty": true
+ },
+ "stylus": {
+ "useNib": true,
+ "sourceMap": false,
+ "linenos": false
+ },
+ "svg": {
+ "cleanupAttrs": true,
+ "removeDoctype": true,
+ "removeXMLProcInst": true,
+ "removeComments": true,
+ "removeMetadata": true,
+ "removeTitle": true,
+ "removeDesc": true,
+ "removeUselessDefs": true,
+ "removeEditorsNSData": true,
+ "removeEmptyAttrs": true,
+ "removeHiddenElems": true,
+ "removeEmptyText": true,
+ "removeEmptyContainers": true,
+ "removeViewBox": false,
+ "cleanupEnableBackground": true,
+ "convertStyleToAttrs": true,
+ "convertColors": true,
+ "convertPathData": true,
+ "convertTransform": true,
+ "removeUnknownsAndDefaults": true,
+ "removeNonInheritableGroupAttrs": true,
+ "removeUselessStrokeAndFill": true,
+ "removeUnusedNS": true,
+ "cleanupIDs": true,
+ "cleanupNumericValues": true,
+ "moveElemsAttrsToGroup": true,
+ "moveGroupAttrsToElems": true,
+ "collapseGroups": true,
+ "removeRasterImages": false,
+ "mergePaths": true,
+ "convertShapeToPath": true,
+ "sortAttrs": true,
+ "removeDimensions": true
+ },
+ "tailwindcss": {
+ "rootDir": "",
+ "content": [
+ "**/*.html",
+ "**/*.php",
+ "**/*.js",
+ "!wp-admin",
+ "!wp-includes",
+ "!node_modules",
+ "!Prepros Export"
+ ]
+ },
+ "turf": {
+ "rootDir": ""
+ },
+ "typescript": {
+ "allowJs": false,
+ "allowSyntheticDefaultImports": true,
+ "allowUmdGlobalAccess": false,
+ "allowUnreachableCode": false,
+ "allowUnusedLabels": false,
+ "alwaysStrict": false,
+ "charset": "utf8",
+ "checkJs": false,
+ "declaration": false,
+ "disableSizeLimit": false,
+ "downlevelIteration": false,
+ "emitBOM": false,
+ "emitDecoratorMetadata": false,
+ "experimentalDecorators": false,
+ "forceConsistentCasingInFileNames": false,
+ "importHelpers": false,
+ "jsx": "React",
+ "keyofStringsOnly": false,
+ "lib": [],
+ "maxNodeModuleJsDepth": 0,
+ "module": "ES2015",
+ "moduleResolution": "NodeJs",
+ "newLine": "LineFeed",
+ "noFallthroughCasesInSwitch": false,
+ "noImplicitAny": false,
+ "noImplicitReturns": false,
+ "noImplicitThis": false,
+ "noStrictGenericChecks": false,
+ "noUnusedLocals": false,
+ "noUnusedParameters": false,
+ "noImplicitUseStrict": false,
+ "noLib": false,
+ "noResolve": false,
+ "preserveConstEnums": false,
+ "jsxFactory": "React.createElement",
+ "removeComments": false,
+ "skipLibCheck": false,
+ "sourceMap": false,
+ "strict": false,
+ "strictFunctionTypes": false,
+ "strictBindCallApply": false,
+ "strictNullChecks": false,
+ "strictPropertyInitialization": false,
+ "suppressExcessPropertyErrors": false,
+ "suppressImplicitAnyIndexErrors": false,
+ "target": "ES3",
+ "resolveJsonModule": false,
+ "esModuleInterop": false,
+ "useDefineForClassFields": false
+ }
+ },
+ "fileTypes": {
+ "sass": {
+ "extensions": [
+ ".scss",
+ ".sass"
+ ],
+ "autoCompile": true,
+ "sourceMap": false,
+ "tasks": [
+ {
+ "task": "dart-sass",
+ "enable": true
+ },
+ {
+ "task": "autoprefixer",
+ "enable": false
+ },
+ {
+ "task": "minify-css",
+ "enable": false
+ }
+ ],
+ "output": {
+ "extension": ".css",
+ "type": "SOURCE_RELATIVE",
+ "segments": [
+ {
+ "segment": "scss",
+ "replaceWith": "css"
+ },
+ {
+ "segment": "sass",
+ "replaceWith": "css"
+ }
+ ]
+ }
+ },
+ "less": {
+ "extensions": [
+ ".less"
+ ],
+ "autoCompile": true,
+ "sourceMap": false,
+ "tasks": [
+ {
+ "task": "less",
+ "enable": true
+ },
+ {
+ "task": "autoprefixer",
+ "enable": true
+ },
+ {
+ "task": "minify-css",
+ "enable": false
+ }
+ ],
+ "output": {
+ "extension": ".css",
+ "type": "REPLACE_SEGMENTS",
+ "segments": [
+ {
+ "segment": "less",
+ "replaceWith": "css"
+ }
+ ]
+ }
+ },
+ "pug": {
+ "extensions": [
+ ".pug",
+ ".jade"
+ ],
+ "autoCompile": true,
+ "tasks": [
+ {
+ "task": "pug",
+ "enable": true
+ },
+ {
+ "task": "minify-html",
+ "enable": false
+ }
+ ],
+ "output": {
+ "extension": ".html",
+ "type": "SOURCE_RELATIVE",
+ "segments": [
+ {
+ "segment": "pug",
+ "replaceWith": "html"
+ }
+ ]
+ }
+ },
+ "css": {
+ "extensions": [
+ ".css"
+ ],
+ "autoCompile": false,
+ "sourceMap": false,
+ "tasks": [
+ {
+ "task": "copy",
+ "enable": true
+ },
+ {
+ "task": "tailwindcss",
+ "enable": false
+ },
+ {
+ "task": "postcss-import",
+ "enable": false
+ },
+ {
+ "task": "postcss-preset-env",
+ "enable": false
+ },
+ {
+ "task": "autoprefixer",
+ "enable": false
+ },
+ {
+ "task": "minify-css",
+ "enable": false
+ }
+ ],
+ "output": {
+ "extension": ".css",
+ "type": "SOURCE_RELATIVE",
+ "relativePath": "",
+ "suffix": "-dist",
+ "alwaysSuffix": false
+ }
+ },
+ "javascript": {
+ "extensions": [
+ ".js",
+ ".jsx"
+ ],
+ "autoCompile": false,
+ "sourceMap": false,
+ "tasks": [
+ {
+ "task": "copy",
+ "enable": true
+ },
+ {
+ "task": "concat-js",
+ "enable": false
+ },
+ {
+ "task": "babel",
+ "enable": false
+ },
+ {
+ "task": "bundle-js",
+ "enable": false
+ },
+ {
+ "task": "minify-js",
+ "enable": true
+ }
+ ],
+ "output": {
+ "extension": ".js",
+ "type": "SOURCE_RELATIVE",
+ "relativePath": "",
+ "suffix": "-dist",
+ "alwaysSuffix": false
+ }
+ },
+ "stylus": {
+ "extensions": [
+ ".styl"
+ ],
+ "autoCompile": true,
+ "sourceMap": false,
+ "tasks": [
+ {
+ "task": "stylus",
+ "enable": true
+ },
+ {
+ "task": "autoprefixer",
+ "enable": true
+ },
+ {
+ "task": "minify-css",
+ "enable": false
+ }
+ ],
+ "output": {
+ "extension": ".css",
+ "type": "REPLACE_SEGMENTS",
+ "segments": [
+ {
+ "segment": "stylus",
+ "replaceWith": "css"
+ },
+ {
+ "segment": "styl",
+ "replaceWith": "css"
+ }
+ ]
+ }
+ },
+ "markdown": {
+ "extensions": [
+ ".md",
+ ".markdown",
+ ".mkd"
+ ],
+ "autoCompile": false,
+ "tasks": [
+ {
+ "task": "markdown",
+ "enable": true
+ },
+ {
+ "task": "minify-html",
+ "enable": false
+ }
+ ],
+ "output": {
+ "extension": ".html",
+ "type": "REPLACE_SEGMENTS",
+ "segments": [
+ {
+ "segment": "markdown",
+ "replaceWith": "html"
+ }
+ ]
+ }
+ },
+ "haml": {
+ "extensions": [
+ ".haml"
+ ],
+ "autoCompile": true,
+ "tasks": [
+ {
+ "task": "haml",
+ "enable": true
+ },
+ {
+ "task": "minify-html",
+ "enable": false
+ }
+ ],
+ "output": {
+ "extension": ".html",
+ "type": "REPLACE_SEGMENTS",
+ "segments": [
+ {
+ "segment": "haml",
+ "replaceWith": "html"
+ }
+ ]
+ }
+ },
+ "slim": {
+ "extensions": [
+ ".slim"
+ ],
+ "autoCompile": true,
+ "tasks": [
+ {
+ "task": "slim",
+ "enable": true
+ },
+ {
+ "task": "minify-html",
+ "enable": false
+ }
+ ],
+ "output": {
+ "extension": ".html",
+ "type": "REPLACE_SEGMENTS",
+ "segments": [
+ {
+ "segment": "slim",
+ "replaceWith": "html"
+ }
+ ]
+ }
+ },
+ "coffeescript": {
+ "extensions": [
+ ".coffee"
+ ],
+ "autoCompile": true,
+ "sourceMap": false,
+ "tasks": [
+ {
+ "task": "coffeescript",
+ "enable": true
+ },
+ {
+ "task": "babel",
+ "enable": false
+ },
+ {
+ "task": "bundle-js",
+ "enable": false
+ },
+ {
+ "task": "minify-js",
+ "enable": false
+ }
+ ],
+ "output": {
+ "extension": ".js",
+ "type": "REPLACE_SEGMENTS",
+ "segments": [
+ {
+ "segment": "coffee-script",
+ "replaceWith": "js"
+ },
+ {
+ "segment": "coffeescript",
+ "replaceWith": "js"
+ },
+ {
+ "segment": "coffee",
+ "replaceWith": "js"
+ }
+ ]
+ }
+ },
+ "turf": {
+ "extensions": [
+ ".turf",
+ ".kit"
+ ],
+ "autoCompile": true,
+ "tasks": [
+ {
+ "task": "turf",
+ "enable": true
+ },
+ {
+ "task": "minify-html",
+ "enable": false
+ }
+ ],
+ "output": {
+ "extension": ".html",
+ "type": "REPLACE_SEGMENTS",
+ "segments": [
+ {
+ "segment": "turf",
+ "replaceWith": "html"
+ }
+ ]
+ }
+ },
+ "typescript": {
+ "extensions": [
+ ".ts",
+ ".tsx"
+ ],
+ "autoCompile": true,
+ "sourceMap": false,
+ "tasks": [
+ {
+ "task": "typescript",
+ "enable": true
+ },
+ {
+ "task": "babel",
+ "enable": false
+ },
+ {
+ "task": "bundle-js",
+ "enable": false
+ },
+ {
+ "task": "minify-js",
+ "enable": false
+ }
+ ],
+ "output": {
+ "extension": ".js",
+ "type": "REPLACE_SEGMENTS",
+ "segments": [
+ {
+ "segment": "typescript",
+ "replaceWith": "js"
+ },
+ {
+ "segment": "ts",
+ "replaceWith": "js"
+ }
+ ]
+ }
+ },
+ "jpg": {
+ "extensions": [
+ ".jpg",
+ ".jpeg"
+ ],
+ "tasks": [
+ {
+ "task": "jpg",
+ "enable": true
+ }
+ ],
+ "output": {
+ "extension": ".jpg",
+ "type": "SOURCE_RELATIVE",
+ "relativePath": ""
+ }
+ },
+ "png": {
+ "extensions": [
+ ".png"
+ ],
+ "tasks": [
+ {
+ "task": "png",
+ "enable": true
+ }
+ ],
+ "output": {
+ "extension": ".png",
+ "type": "SOURCE_RELATIVE",
+ "relativePath": ""
+ }
+ },
+ "svg": {
+ "extensions": [
+ ".svg"
+ ],
+ "tasks": [
+ {
+ "task": "svg",
+ "enable": true
+ }
+ ],
+ "output": {
+ "extension": ".svg",
+ "type": "SOURCE_RELATIVE",
+ "relativePath": ""
+ }
+ }
+ },
+ "files": [
+ {
+ "file": "source/system.pug",
+ "config": {
+ "customOutput": "system.html"
+ }
+ },
+ {
+ "file": "source/system.scss",
+ "config": {
+ "customOutput": "system.css"
+ }
+ }
+ ]
+ }
+}
diff --git a/under_the_neighborhood/preview.png b/under_the_neighborhood/preview.png
new file mode 100644
index 0000000000..201b0ea172
Binary files /dev/null and b/under_the_neighborhood/preview.png differ
diff --git a/under_the_neighborhood/sheet.json b/under_the_neighborhood/sheet.json
new file mode 100644
index 0000000000..622a3e9bc2
--- /dev/null
+++ b/under_the_neighborhood/sheet.json
@@ -0,0 +1,9 @@
+{
+ "html": "system.html",
+ "css": "system.css",
+ "authors": "Scott Casey",
+ "roll20userid": "459831",
+ "preview": "preview.png",
+ "instructions": "The official sheet for [Under the Neighborhood](https://www.drivethrurpg.com/product/390786/Under-the-Neighborhood?manufacturers_id=15101) by the [Quest Friends crew](https://www.questfriendspodcast.com/).",
+ "legacy": false
+}
\ No newline at end of file
diff --git a/under_the_neighborhood/source/articles/_character.pug b/under_the_neighborhood/source/articles/_character.pug
new file mode 100644
index 0000000000..41a7502884
--- /dev/null
+++ b/under_the_neighborhood/source/articles/_character.pug
@@ -0,0 +1,9 @@
+article#character.character.navigable-section.navigable-section--character.character-type.character-type--character.character-type--mon
+ include sections/_header.pug
+ include sections/_attributes.pug
+ include sections/_skills.pug
+ include sections/_playbook.pug
+ include sections/_compatibility.pug
+ include sections/_meter.pug
+ include sections/_modifiers.pug
+ include sections/_advancement.pug
\ No newline at end of file
diff --git a/under_the_neighborhood/source/articles/_game_screen.pug b/under_the_neighborhood/source/articles/_game_screen.pug
new file mode 100644
index 0000000000..a01209785a
--- /dev/null
+++ b/under_the_neighborhood/source/articles/_game_screen.pug
@@ -0,0 +1,4 @@
+article#gamescreen.gamescreen.navigable-section.navigable-section--gamescreen.character-type.character-type--gamescreen
+ include sections/_header.pug
+ include sections/_lifeslice.pug
+ include sections/_clock.pug
\ No newline at end of file
diff --git a/under_the_neighborhood/source/articles/_settings.pug b/under_the_neighborhood/source/articles/_settings.pug
new file mode 100644
index 0000000000..d9df3ca88b
--- /dev/null
+++ b/under_the_neighborhood/source/articles/_settings.pug
@@ -0,0 +1,53 @@
+article#settings.settings.navigable-section.navigable-section--settings.active
+ h1(data-i18n='settings')
+ +select-label({
+ inputObj:{name:'sheet type',class:'underlined'},
+ spanObj:{'data-i18n':'sheet type',role:'heading','aria-level':4}
+ })
+ +option({value:'character','data-i18n':'character',selected:'',trigger:{triggeredFuncs:['characterDisplay']}})
+ +option({value:'mon','data-i18n':'mon'})
+ +option({value:'gamescreen','data-i18n':'game screen'})
+ +input-label({
+ inputObj:{name:'is weird',type:'checkbox',value:1,trigger:{triggeredFuncs:['sectionDisplay']}},
+ spanObj:{'data-i18n':'is weird',role:'heading','aria-level':4}
+ })
+ +input-label({
+ inputObj:{name:'use initiative',type:'checkbox',value:1,trigger:{triggeredFuncs:['sectionDisplay']}},
+ spanObj:{'data-i18n':'use initiative',role:'heading','aria-level':4}
+ })
+ +input-label({
+ inputObj:{name:'has arc',type:'checkbox',value:1,trigger:{triggeredFuncs:['sectionDisplay']}},
+ spanObj:{'data-i18n':'has arc/flaw',role:'heading','aria-level':4}
+ })
+ +input-label({
+ inputObj:{name:'use roll modifiers',type:'checkbox',value:1,checked:'',trigger:{triggeredFuncs:['modifierDisplay']}},
+ spanObj:{'data-i18n':'use roll modifiers',role:'heading','aria-level':4}
+ })
+ +input-label({
+ inputObj:{name:'use physical dice',type:'checkbox',value:1},
+ spanObj:{'data-i18n':'use physical dice',role:'heading','aria-level':4}
+ })
+ +input-label({
+ inputObj:{name:'skill basic move',type:'checkbox',value:1,checked:''},
+ spanObj:{'data-i18n':'skills use moves',role:'heading','aria-level':4}
+ })
+ +select-label({
+ inputObj:{name:'whisper',class:'underlined'},
+ spanObj:{'data-i18n':'whisper to gm',role:'heading','aria-level':4}
+ })
+ +option({value:'','data-i18n':'never',selected:''})
+ +option({value:'/w gm','data-i18n':'always'})
+ .span-all.move-stats
+ h2(data-i18n='basic move stats').span-all
+ each obj,name in basicMoves
+ +select-label({
+ inputObj:{name:`${name} stat`,class:'underlined'},
+ spanObj:{'data-i18n':name,role:'heading','aria-level':4}
+ })
+ +attributeOptions(obj.stat)
+ +select-label({
+ inputObj:{name:`initiative stat`,class:'underlined'},
+ spanObj:{'data-i18n':'initiative',role:'heading','aria-level':4},
+ divObj:{class:'initiative'}
+ })
+ +attributeOptions('fierce')
\ No newline at end of file
diff --git a/under_the_neighborhood/source/articles/sections/_advancement.pug b/under_the_neighborhood/source/articles/sections/_advancement.pug
new file mode 100644
index 0000000000..69200d4661
--- /dev/null
+++ b/under_the_neighborhood/source/articles/sections/_advancement.pug
@@ -0,0 +1,8 @@
+section#advancement.advancement.span-all.character-type.character-type--character
+ h3(data-i18n='advancements')
+ each section in ['basic','super','ultra']
+ .repeating-container
+ h4(data-i18n=section).header
+ +customControlFieldset({name:`${section}-advancement`})
+ +checkbox({name:'picked',class:'boxed ratio1-1'})
+ +adaptiveTextarea({name:'description',class:'underlined'})
\ No newline at end of file
diff --git a/under_the_neighborhood/source/articles/sections/_attributes.pug b/under_the_neighborhood/source/articles/sections/_attributes.pug
new file mode 100644
index 0000000000..7da694f2ac
--- /dev/null
+++ b/under_the_neighborhood/source/articles/sections/_attributes.pug
@@ -0,0 +1,9 @@
+
+section#attributes.attributes
+ h3(data-i18n='stats').header
+ each name in attributes
+ +roller-label({
+ buttonObj:{name,role:'heading','aria-level':4,'data-i18n':name,trigger:{triggeredFuncs:['rollStat']}},
+ inputObj:{name,type:'number',class:'underlined',value:0,min:-3,max:3,trigger:{affects:[`${name}_mod`]}},
+ divObj:{class:name}
+ })
\ No newline at end of file
diff --git a/under_the_neighborhood/source/articles/sections/_clock.pug b/under_the_neighborhood/source/articles/sections/_clock.pug
new file mode 100644
index 0000000000..33c898dff5
--- /dev/null
+++ b/under_the_neighborhood/source/articles/sections/_clock.pug
@@ -0,0 +1,12 @@
+section(aria-labelledby='clock-header')#clock.clock.span-2--if-wide.repeating-container
+ h3(data-i18n='clocks')#clock-header.header
+ +customControlFieldset({name:'clock'})
+ +hidden({name:'value',value:0,class:'clock-control--current'})
+ +hidden({name:'value max',value:10,class:'clock-control--max'})
+ .clock-progress
+ .clock-label
+ +radio({name:'value',value:0,class:'clear-x'})
+ +number({name:'value',value:0})
+ span.slash /
+ +number({name:'value max',value:10,min:2,max:10})
+ +text({name:'name',class:'underlined',role:'heading','aria-level':5})
\ No newline at end of file
diff --git a/under_the_neighborhood/source/articles/sections/_compatibility.pug b/under_the_neighborhood/source/articles/sections/_compatibility.pug
new file mode 100644
index 0000000000..44f7630738
--- /dev/null
+++ b/under_the_neighborhood/source/articles/sections/_compatibility.pug
@@ -0,0 +1,13 @@
+section#compatibility.compatibility.repeating-container
+ h3(data-i18n='compatibility scores')
+ .repeat-column
+ each header in ['character','score']
+ h4(data-i18n=header class=replaceSpaces(header))
+ +customControlFieldset({name:'compatibility'})
+ .input-label
+ +checkbox({name:'activate',value:1})
+ +text({name:'name',class:'underlined',role:'heading','aria-level':5})
+ +number({name:'score',class:'underlined'})
+ label.input-label
+ +checkbox({name:'interference',value:1})
+ h5(data-i18n='interference').underlined.underlined--invisible
\ No newline at end of file
diff --git a/under_the_neighborhood/source/articles/sections/_header.pug b/under_the_neighborhood/source/articles/sections/_header.pug
new file mode 100644
index 0000000000..81ee44a80b
--- /dev/null
+++ b/under_the_neighborhood/source/articles/sections/_header.pug
@@ -0,0 +1,41 @@
+header#character-header.header.span-all
+ img(src="https://s3.amazonaws.com/files.d20.io/images/277217218/yuDLFwlFjVStADz6M5aNLg/original.png" data-i18n-alt="Under the Neighborhood by Kyle Decker" role='heading' aria-level=1).header-image
+ if headerType === 'character'
+ .sentence.character-type.character-type--character(data-i18n-list='character sentence order')
+ +adaptiveInput({name:'character name',type:'text',class:'underlined',role:'heading','aria-level':2}).character-name
+ .input-label
+ span(data-i18n='sentence the' data-list-item='the')
+ +adaptiveInput({name:'playbook',type:'text',class:'underlined',' data-list-item':'playbook','data-i18n-placeholder':'playbook placeholder'})
+ .input-label
+ span(data-i18n='sentence who' data-list-item='who')
+ +adaptiveInput({name:'descriptor',type:'text',class:'underlined',' data-list-item':'descriptor','data-i18n-placeholder':'descriptor placeholder'})
+ .character-summary.character-type.character-type--character
+ .generic-details.span-all
+ each name, index in ['pronouns','age','appearance','homeworld','want']
+ - const divObj = {};
+ if index > 1
+ - divObj.class='span-2';
+ .input-label&attributes(divObj)
+ +span({role:'heading','aria-level':5,'data-i18n':name})
+ +adaptiveInput({name,type:'text',class:'underlined'})
+ .headed-textarea.arc
+ h5(data-i18n='character arc/flaw')
+ +adaptiveTextarea({name:'arc flaw',class:'underlined'})
+ +input-label({
+ label:'adventure points',
+ inputObj:{name:'ap',type:'number',class:'underlined'},
+ divObj:{class:'stacked ap adventure-points'},
+ spanObj:{role:'heading','aria-level':4}
+ })
+ .character-summary.character-type.character-type--mon
+ +adaptiveInput({name:'character name',type:'text',class:'underlined',role:'heading','aria-level':2}).character-name
+ +select-label({
+ inputObj:{name:'signature stat',class:'underlined'},
+ spanObj:{'data-i18n':'signature stat',role:'heading','aria-level':4}
+ })
+ +option({value:'','data-i18n':'select one'})
+ each name in attributes
+ +option({value:name,'data-i18n':name,class:name})
+ .input-label
+ +span({'data-i18n':'element',role:'heading','aria-level':4})
+ +text({name:'element',class:'underlined'})
\ No newline at end of file
diff --git a/under_the_neighborhood/source/articles/sections/_lifeslice.pug b/under_the_neighborhood/source/articles/sections/_lifeslice.pug
new file mode 100644
index 0000000000..de595e7460
--- /dev/null
+++ b/under_the_neighborhood/source/articles/sections/_lifeslice.pug
@@ -0,0 +1,5 @@
+section(aria-labelledby='lifesliceheader')#lifeslice.lifeslice.span-all.repeating-container
+ h3(data-i18n='slice of life complications')#lifesliceheader
+ +customControlFieldset({name:'complication'})
+ +checkbox({name:'activate',value:1})
+ +adaptiveTextarea({name:'description',class:'underlined'})
\ No newline at end of file
diff --git a/under_the_neighborhood/source/articles/sections/_meter.pug b/under_the_neighborhood/source/articles/sections/_meter.pug
new file mode 100644
index 0000000000..df51ce1822
--- /dev/null
+++ b/under_the_neighborhood/source/articles/sections/_meter.pug
@@ -0,0 +1,23 @@
+section(aria-labelledby='meter-header')#meter.meter.span-2--if-wide.repeating-container
+ h3(data-i18n='meters')#meter-header.header
+ .repeat-column
+ h4(data-i18n='name')
+ each num in meterValues
+ h4 #{num}
+ +customControlFieldset({name:'meter'})
+ +collapse
+ +text({name:'name',class:'underlined',role:'heading','aria-level':5})
+ +hidden({name:'value max',value:10,class:'fill-left__max-control'})
+ +fillLeft({
+ radioObj:{name:'value',class:'square',value:0,trigger:{calculation:'limitMeterValue'}},
+ valueArray:varObjects.meterValues
+ })
+ h5(data-i18n='max').expanded
+ +fillLeft({
+ radioObj:{name:'value max',class:'square',value:10,'data-i18n-title':'set the meter\'s maximum value.',trigger:{affects:['repeating_meter_$X_value']}},
+ valueArray:meterValues,
+ noClear:true
+ }).expanded.no-fill
+ .headed-textarea.description.expanded.span-all
+ +h5({'data-i18n':'description'})
+ +adaptiveTextarea({name:'description',class:'underlined'})
\ No newline at end of file
diff --git a/under_the_neighborhood/source/articles/sections/_modifiers.pug b/under_the_neighborhood/source/articles/sections/_modifiers.pug
new file mode 100644
index 0000000000..9819880353
--- /dev/null
+++ b/under_the_neighborhood/source/articles/sections/_modifiers.pug
@@ -0,0 +1,9 @@
+section(aria-labelledby='modifier-header')#modifiers.repeating-container
+ h3(data-i18n='roll modifiers')#modifier-header
+ +customControlFieldset({name:'modifier'})
+ +collapse
+ +checkbox({name:'activate',value:1})
+ +text({name:'name',class:'underlined'})
+ .headed-textarea.description.expanded.span-all
+ h5(data-i18n='effects')
+ +adaptiveTextarea({name:'effect',class:'underlined'})
\ No newline at end of file
diff --git a/under_the_neighborhood/source/articles/sections/_playbook.pug b/under_the_neighborhood/source/articles/sections/_playbook.pug
new file mode 100644
index 0000000000..cc6e392b4e
--- /dev/null
+++ b/under_the_neighborhood/source/articles/sections/_playbook.pug
@@ -0,0 +1,29 @@
+section#playbook.playbook.repeating-container.span-2--if-wide.character-type.character-type--character
+ h3(data-i18n='character moves').header
+ .repeat-column
+ h4(data-i18n='name')
+ h4(data-i18n='cost')
+ h4(data-i18n='source')
+ +customControlFieldset({name:'move'})
+ +collapse
+ +roller-label({
+ buttonObj:{name:'roll',trigger:{triggeredFuncs:['rollCharacterMove']}},
+ inputObj:{name:'name',type:'text',class:'underlined',role:'heading','aria-level':5}
+ })
+ +text({name:'cost',class:'underlined'})
+ +select({name:'source',class:'underlined'})
+ +option({value:'','data-i18n':'select one'})
+ each value in ['playbook','descriptor','arc']
+ +option({value,'data-i18n':value})
+ .collapse-container.expanded.span-all
+ +checkbox({name:'result collapse',class:'collapse text-collapse__check',value:1,checked:'',hidden:''})
+ label.text-collapse
+ +checkbox({name:'result collapse',hidden:'',value:1,checked:'',class:'text-collapse__check'})
+ h5(data-i18n='customize results').text-collapse__text
+ each result in ['critical success','full success','mixed success','failure','critical failure']
+ .headed-textarea.expanded.span-all
+ h5(data-i18n=result)
+ +adaptiveTextarea({name:result,class:'underlined'})
+ .headed-textarea.description.expanded.span-all
+ +h5({'data-i18n':'description'})
+ +adaptiveTextarea({name:'description',class:'underlined'})
\ No newline at end of file
diff --git a/under_the_neighborhood/source/articles/sections/_skills.pug b/under_the_neighborhood/source/articles/sections/_skills.pug
new file mode 100644
index 0000000000..0873a681f6
--- /dev/null
+++ b/under_the_neighborhood/source/articles/sections/_skills.pug
@@ -0,0 +1,57 @@
+section(aria-labelledby='skill-header').span-2--if-wide
+ #skills.skills.repeating-container
+ h3(data-i18n='skills and inabilities')#skill-header.header
+ .repeat-column
+ h4(data-i18n='skill')
+ h4(data-i18n='inability')
+ +customControlFieldset({name:'skill'})
+ +collapse
+ +roller-label({
+ buttonObj:{name:'roll',trigger:{triggeredFuncs:['rollSkill']}},
+ inputObj:{name:'name',type:'text',class:'underlined'}
+ })
+ +roller-label({
+ buttonObj:{name:'inability',trigger:{triggeredFuncs:['rollSkill']}},
+ inputObj:{name:'inability',type:'text',class:'underlined'}
+ })
+ .expanded
+ +select-label({
+ inputObj:{name:'skill stat',class:'underlined'},
+ spanObj:{'data-i18n':'stat',role:'heading','aria-level':5}
+ })
+ +attributeOptions
+ +input-label({
+ inputObj:{name:'super',type:'checkbox',value:1,class:'boxed ratio1-1',class:'underlined'},
+ spanObj:{'data-i18n':'super',role:'heading','aria-level':5},
+ divObj:{class:'expanded super'}
+ })
+ .expanded
+ +select-label({
+ inputObj:{name:'inability stat',class:'underlined'},
+ spanObj:{'data-i18n':'stat',role:'heading','aria-level':5}
+ })
+ +attributeOptions
+ +input-label({
+ inputObj:{name:'inability super',type:'checkbox',value:1,class:'boxed ratio1-1',class:'underlined'},
+ spanObj:{'data-i18n':'super',role:'heading','aria-level':5},
+ divObj:{class:'expanded super'}
+ })
+ #signature-item.signature-item.repeating-container.character-type.character-type--character
+ h4(data-i18n='signature item')
+ +customControlFieldset({name:'signature'})
+ +collapse
+ +roller-label({
+ inputObj:{name:'name',type:'text',class:'underlined'},
+ buttonObj:{name:'roll',role:'heading',trigger:{triggeredFuncs:['rollSkill']}}
+ })
+ .expanded
+ +select-label({
+ inputObj:{name:'stat',class:'underlined'},
+ spanObj:{'data-i18n':'stat',role:'heading','aria-level':5}
+ })
+ +attributeOptions
+ +input-label({
+ inputObj:{name:'super',type:'checkbox',value:1,class:'boxed ratio1-1',class:'underlined'},
+ spanObj:{'data-i18n':'super',role:'heading','aria-level':5},
+ divObj:{class:'expanded super'}
+ })
\ No newline at end of file
diff --git a/under_the_neighborhood/source/assets/undertheneighborhood_logo no Kyle Name.png b/under_the_neighborhood/source/assets/undertheneighborhood_logo no Kyle Name.png
new file mode 100644
index 0000000000..93df622165
Binary files /dev/null and b/under_the_neighborhood/source/assets/undertheneighborhood_logo no Kyle Name.png differ
diff --git a/under_the_neighborhood/source/assets/undertheneighborhood_logo.png b/under_the_neighborhood/source/assets/undertheneighborhood_logo.png
new file mode 100644
index 0000000000..76117f7e4e
Binary files /dev/null and b/under_the_neighborhood/source/assets/undertheneighborhood_logo.png differ
diff --git a/under_the_neighborhood/source/javascript/calculators.js b/under_the_neighborhood/source/javascript/calculators.js
new file mode 100644
index 0000000000..6c02ffb0b6
--- /dev/null
+++ b/under_the_neighborhood/source/javascript/calculators.js
@@ -0,0 +1,7 @@
+/*jshint esversion: 11, laxcomma:true, eqeqeq:true*/
+/*jshint -W014,-W084,-W030,-W033*/
+const limitMeterValue = function({trigger,attributes,sections}){
+ const [section,id,field] = k.parseTriggerName(trigger.name);
+ return Math.min(attributes[`${section}_${id}_${field}`],attributes[`${section}_${id}_${field}_max`])
+};
+k.registerFuncs({limitMeterValue});
\ No newline at end of file
diff --git a/under_the_neighborhood/source/javascript/rollfunctions.js b/under_the_neighborhood/source/javascript/rollfunctions.js
new file mode 100644
index 0000000000..a4a8f5624c
--- /dev/null
+++ b/under_the_neighborhood/source/javascript/rollfunctions.js
@@ -0,0 +1,344 @@
+/*jshint esversion: 11, laxcomma:true, eqeqeq:true*/
+/*jshint -W014,-W084,-W030,-W033*/
+//Roll Listeners
+const rollBasicMove = async function({trigger,attributes,sections}){
+ const [,,field,activeEffects,rollObj,dice] = await baseRollInfo({trigger,attributes,sections});
+ let statRef = getMoveResults(field.replace(/-/g,' '),rollObj,attributes);
+ rollObj.roll = assembleRoll(dice,statRef,activeEffects,attributes.is_weird);
+ executeRoll({rollObj});
+};
+k.registerFuncs({rollBasicMove});
+
+const getMoveResults = function(move,rollObj,attributes){
+ const possibleResults = basicMoves[move];
+ ['critical','success','mixed','failure','fumble'].forEach((result)=>{
+ if(result !== 'stat'){
+ rollObj[`${result}_result`] = possibleResults[result] ?
+ `^{${possibleResults[result]}}` :
+ '';
+ }
+ });
+ return attributes[`${move.replace(/\s+/g,'_')}_stat`];
+};
+
+const rollStat = async function({trigger,attributes,sections}){
+ const [,,field,activeEffects,rollObj,dice] = await baseRollInfo({trigger,attributes,sections});
+ rollObj.roll = assembleRoll(dice,`@{${field}}`,activeEffects,attributes.is_weird);
+ executeRoll({rollObj});
+};
+k.registerFuncs({rollStat});
+
+const rollSkill = async function({trigger,attributes,sections}){
+ const [section,rowID,field,activeEffects,rollObj,dice] = await baseRollInfo({trigger,attributes,sections,isSkill:true});
+ let statRef;
+ rollObj.header = attributes[`${section}_${rowID}_${field === 'inability' ? field : 'name'}`];
+ if(attributes.skill_basic_move){
+ const query = k.capitalize(getTranslationByKey('move query'));
+ const options = Object.keys(basicMoves)
+ .map((key) => {
+ return `${k.capitalize(getTranslationByKey(key))},${key}`
+ })
+ .join('|');
+ const move = await k.extractQueryResult(`${query}|${options}`);
+ statRef = getMoveResults(move,rollObj,attributes);
+ }else{
+ await k.pseudoQuery('');
+ statRef = section === 'repeating_skill' ?
+ (/inability/.test(field) ?
+ 'inability_stat' :
+ 'skill_stat'
+ ) :
+ 'stat';
+ statRef = attributes[`${section}_${rowID}_${statRef}`];
+ }
+ k.debug({section,rowID,field});
+
+ rollObj.roll = assembleRoll(dice,statRef,activeEffects,attributes.is_weird);
+ k.debug({skillObj:rollObj});
+ executeRoll({rollObj});
+};
+k.registerFuncs({rollSkill});
+
+const rollHelpFriend = async function({trigger,attributes,sections}){
+ const [section,rowID,field,activeEffects,rollObj,dice] = await baseRollInfo({trigger,attributes,sections});
+ const relationScore = attributes[`${section}_${rowID}_score`];
+ rollObj.header = getTranslationByKey('help a friend roll header').replace('{{0}}',attributes[`${section}_${rowID}_name`]);
+ k.debug({activeEffects});
+ rollObj.roll = assembleRoll(dice,relationScore,activeEffects,attributes.is_weird);
+ executeRoll({rollObj});
+};
+k.registerFuncs({rollHelpFriend});
+
+const rollCharacterMove = async function({trigger,attributes,sections}){
+ const [section,rowID,field,activeEffects,rollObj,dice] = await baseRollInfo({trigger,attributes,sections});
+ rollObj.roll = assembleRoll(dice,'?{query}',activeEffects,attributes.is_weird);
+ const fieldSwitch = {
+ critical_result:attributes[`${section}_${rowID}_critical_success`] || attributes[`${section}_${rowID}_full_success`] || basicMoves['another move'].critical,
+ success_result:attributes[`${section}_${rowID}_full_success`] || basicMoves['another move'].success,
+ mixed_result:attributes[`${section}_${rowID}_mixed_success`] || basicMoves['another move'].mixed,
+ failure_result:attributes[`${section}_${rowID}_failure`] || basicMoves['another move'].failure,
+ fumble_result:attributes[`${section}_${rowID}_critical_failure`] || attributes[`${section}_${rowID}_failure`] || basicMoves['another move'].fumble
+ };
+ rollObj.header = attributes[`${section}_${rowID}_name`];
+ Object.entries(fieldSwitch).forEach(([fieldName,content])=>{
+ rollObj[fieldName] = content;
+ });
+ executeRoll({rollObj});
+};
+k.registerFuncs({rollCharacterMove});
+
+const rollInitiative = async function({trigger,attributes,sections}){
+ const [section,rowID,field,activeEffects,rollObj,dice] = await baseRollInfo({trigger,attributes,sections});
+ rollObj.roll = assembleRoll(dice,attributes.fierce,activeEffects,attributes.is_weird)
+ .replace(/\]\]/,'&{tracker}]]');
+ executeRoll({rollObj});
+};
+k.registerFuncs({rollInitiative});
+
+//Parse Functions
+const baseRollInfo = async function({trigger,attributes,sections,isSkill}){
+ const [section,rowID,button] = k.parseTriggerName(trigger.name);
+ const field = button.replace(/-?action/,'');
+ const baseEffects = {advantage:0,bonus:0,text:[]};
+ if(isSkill){
+ baseEffects.advantage = field === 'inability' ?
+ -1 - attributes[`${section}_${rowID}_${field}_super`] :
+ 1 + attributes[`${section}_${rowID}_super`];
+ }
+ const activeEffects = await countEffects(attributes,sections,baseEffects);
+ const rollObj = {
+ header:`^{${field.replace(/-/g,' ')}}`,
+ result:`[[0[computed value]]]`
+ };
+ if(baseEffects.text?.length){
+ rollObj.description = baseEffects.text.map((text)=>`^{${text}}`).join('\n');
+ }
+ Object.entries(basicMoves['another move']).forEach(([result,effect])=>{
+ if(result !== 'stat'){
+ rollObj[`${result}_result`] = `^{${effect}}`;
+ }
+ });
+ const dice = await parseAdvantage(attributes.advantage,activeEffects,attributes,rollObj);
+ return [section,rowID,field,activeEffects,rollObj,dice];
+};
+
+/*
+ The Compatibility Scores and their effects are as follows:
+ -1 (Frenemies)
+ The other PC rolls with Disadvantage. Each of you gains 1 AP.
+
+ +0 (Acquaintances)
+ The other PC gets +1 on their roll.
+
+ +1 (Friends)
+ CHOOSE 1
+ The other PC gets +1 on their roll.
+ The consequences of the other PC’s actions are “bigger” (Full Successes come with a small bonus, Failures come with an extra complication).
+
+ +2 (Good Friends)
+ CHOOSE 1
+ The other PC gets +2 on their roll.
+ The other PC rolls with Advantage.
+ The consequences of the other PC’s actions are “bigger” (Full Successes come with a small bonus, Failures come with an extra complication).
+
+ +3 (Family)
+ CHOOSE UP TO 2
+ The other PC gets +2 on their roll.
+ The other PC rolls with Advantage.
+ The consequences of the other PC’s move are “bigger” (Full Successes come with a small bonus, Failures come with an extra complication).
+*/
+const compatibilityQuery = async function(baseEffects,attributes,sections){
+ const compatibleEffects = {
+ '-1':{optNum:1,options:['disadvantage']},
+ '0':{optNum:1,options:['+1']},
+ '1':{optNum:1,options:['+1','bigger consequences']},
+ '2':{optNum:1,options:['+1','advantage','bigger consequences']},
+ '3':{optNum:2,options:['+2','advantage','bigger consequences']}
+ }
+ const activeID = sections.repeating_compatibility.reduce((memo,id)=>{
+ return attributes[`repeating_compatibility_${id}_activate`] &&
+ (!memo || attributes[`repeating_compatibility_${id}_score`] < attributes[`repeating_compatibility_${id}_score`]) ?
+ id :
+ memo;
+ },'');
+ const ordinals = {
+ '1':'1st',
+ '2':'2nd'
+ }
+ if(activeID){
+ const activeEffect = compatibleEffects[attributes[`repeating_compatibility_${activeID}_score`]];
+ k.debug({activeID,[`compatibleEffects[${attributes[`repeating_compatibility_${activeID}_score`]}]`]:activeEffect})
+ const selectedEffects = [];
+ if(activeEffect.options.length > 1){
+ const effectWorker = async (iteration = 1)=>{
+ const availableOptions = activeEffect.options.filter((opt)=>selectedEffects.indexOf(opt) === -1);
+ const promptKey = activeEffect.optNum === 1 ?
+ 'Which compatibility effect' :
+ `${ordinals[iteration]} compatible effect`;
+ const selectedOption = await k.extractQueryResult(`${getTranslationByKey(promptKey)}|${availableOptions.map((opt)=>`${getTranslationByKey(`compatibility prompt ${opt}`)},${opt}`).join('|')}`);
+ k.debug({selectedOption});
+ selectedOption.replace(/\+?(.+)/,(match,effect)=>{
+ if(+effect){
+ baseEffects.bonus += +effect;
+ }else if(/dis/.test(effect)){
+ baseEffects.advantage--;
+ }else if(/advantage/.test(effect)){
+ baseEffects.advantage++;
+ }else{
+ baseEffects.text.push(effect);
+ }
+ });
+ selectedEffects.push(selectedOption);
+ iteration++;
+ if(iteration > activeEffect.optNum){
+ return true;
+ }else{
+ return effectWorker(iteration);
+ }
+ }
+ await effectWorker();
+ }else{
+ selectedEffects.push(activeEffect.options[0]);
+ await k.pseudoQuery();
+ }
+ }else{
+ if(attributes.interference){
+ baseEffects.advantage--;
+ baseEffects.text.push('interference roll message');
+ }
+ await k.pseudoQuery();
+ }
+};
+
+const countEffects = async function(attributes,sections,effectObj = {advantage:0,bonus:0}){
+ let idArray;
+ const baseEffects = {...effectObj};
+ if(attributes.use_roll_modifiers){
+ idArray = sections.repeating_modifier;
+ }else{
+ idArray = [];
+ baseEffects.bonus = attributes.generic_bonus;
+ }
+ await compatibilityQuery(baseEffects,attributes,sections);
+ k.debug({baseEffects});
+ return idArray.reduce((memo,id)=>{
+ k.debug({[`attributes[repeating_modifier_${id}_activate]`]:attributes[`repeating_modifier_${id}_activate`]});
+ if(attributes[`repeating_modifier_${id}_activate`]){
+ const effect = attributes[`repeating_modifier_${id}_effect`];
+ k.debug({effect});
+ const advs = effect.match(/(?:^|\s)(?:super )?advantage/ig) || [];
+ const disadvs = effect.match(/(?:super )?disadvantage/ig) || [];
+ const bonuses = effect.match(/(?:[+\-]\s*)?\d+/g) || [];
+ k.debug({advs,disadvs,bonuses});
+ memo.advantage = advs.reduce((total,adv)=>total += /super/.test(adv) ? 2 : 1,memo.advantage);
+ memo.advantage = disadvs.reduce((total,adv)=>total -= /super/.test(adv) ? 2 : 1,memo.advantage);
+ memo.bonus = bonuses.reduce((total,bonus)=>{
+ const val = +bonus.replace(/\+/,'').trim();
+ return total += val;
+ },memo.bonus);
+ }
+ return memo;
+ },{...baseEffects});
+};
+
+const parseAdvantage = async function(advantageState,activeEffects,attributes,rollObj){
+ if(attributes.use_physical_dice){
+ const result = +(await k.extractQueryResult(getTranslationByKey('physical roll query')));
+ if(result === 12){
+ rollObj.result = '[[1]]';
+ }else if(result === 2){
+ rollObj.result = '[[-1]]';
+ }else{
+ rollObj.result = '[[0]]';
+ }
+ return result;
+ }else{
+ await k.pseudoQuery('');
+ if(advantageState.startsWith('?{')){
+ //If the advantage state is a query, translate the query.
+ const options = Object.entries(advantageTypes).map(([aType,aValue])=>{
+ return `${k.capitalize(getTranslationByKey(`${aType} option`))},${aValue}`;
+ }).join('|');
+ return `?{${k.capitalize(getTranslationByKey('roll'))}|${options}}`;
+ }else{
+ //otherwise, just use the advantage as is.
+ return activeEffects.advantage ?
+ (() => {
+ const diceNum = 2 + Math.abs(activeEffects.advantage);
+ const keep = activeEffects.advantage > 0 ?
+ 'kh' :
+ 'kl';
+ return `${diceNum}d6${keep}2`;
+ })() :
+ advantageState;
+ }
+ }
+};
+
+const assembleRoll = function(dice,chosenStat,activeEffects,isWeird){
+ let stat;
+ k.debug({chosenStat,activeEffects});
+ if(`${chosenStat}`.startsWith('?{')){
+ //If the advantage state is a query, translate the query.
+ const options = attributeNames.map((attr)=>{
+ if(attr !== 'weird' || isWeird){
+ return `${k.capitalize(getTranslationByKey(attr))},@{${attr}}`;
+ }else{
+ return null;
+ }
+ })
+ .filter((a)=>a)
+ .join('|');
+ stat = `+ ?{${k.capitalize(getTranslationByKey('which stat'))}|${options}}`;
+ }else{
+ //otherwise, just use the advantage as is.
+ stat = `+ ${chosenStat}`;
+ }
+ const bonus = stringifyBonus(activeEffects.bonus);
+ return `[[${dice}${stat}${bonus}]]`;
+};
+
+const stringifyBonus = function(bonus){
+ k.debug({bonus});
+ return bonus < 0 ?
+ bonus :
+ `+ ${bonus}`.replace(/\+ 0/,'');
+};
+
+const executeRoll = async function({rollObj}){
+ k.debug({toExecute:rollObj});
+ let message = `@{template_start} ${Object.entries(rollObj).map(([field,content])=>`{{${field}=${content}}}`).join('')}`;
+ k.debug({message});
+ const rolled = await startRoll(message);
+ const computeObj = {};
+ if(rolled.results.roll && rolled.results.result && /computed value/.test(rollObj.result)){
+ const expression = rolled.results.roll.expression;
+ let rollState;
+ let rollQuant = 2;
+ expression.replace(/d6k([hl])(\d+)/,(match,target,num)=>{
+ rollState = target;
+ rollQuant = +num || 2;
+ });
+ const diceRoll = rolled.results.roll.rolls[0];
+ const rollTotal = diceRoll.results.sort((a,b)=>{
+ if(rollState === 'l'){
+ return a - b;
+ }else if(rollState === 'h'){
+ return b - a;
+ }else{
+ return 0;
+ }
+ })
+ .slice(0,rollQuant);
+ k.debug({rollTotal});
+ const uniqueRolls = [...new Set(rollTotal)];
+ if(uniqueRolls.length === 1){
+ if(uniqueRolls[0] === 6){
+ computeObj.result = 1;
+ }else if(uniqueRolls[0] === 1){
+ computeObj.result = -1;
+ }
+ }
+ }
+ finishRoll(rolled.rollId,computeObj);
+};
\ No newline at end of file
diff --git a/under_the_neighborhood/source/javascript/setupfunctions.js b/under_the_neighborhood/source/javascript/setupfunctions.js
new file mode 100644
index 0000000000..faf5e20ad4
--- /dev/null
+++ b/under_the_neighborhood/source/javascript/setupfunctions.js
@@ -0,0 +1,14 @@
+/*jshint esversion: 11, laxcomma:true, eqeqeq:true*/
+/*jshint -W014,-W084,-W030,-W033*/
+const setupSheet = function({attributes,sections}){
+ Object.entries(systemDefaults).forEach(([section,fields])=>{
+ fields.forEach((fieldObj)=>{
+ Object.entries(fieldObj).forEach(([field,content])=>{
+ const row = k.generateRowID(section,sections);
+ k.debug({row,fields});
+ attributes[`${row}_${field}`] = content;
+ });
+ });
+ });
+};
+k.registerFuncs({setupSheet},{type:['new']});
\ No newline at end of file
diff --git a/under_the_neighborhood/source/javascript/triggeredfunctions.js b/under_the_neighborhood/source/javascript/triggeredfunctions.js
new file mode 100644
index 0000000000..4521e3cd95
--- /dev/null
+++ b/under_the_neighborhood/source/javascript/triggeredfunctions.js
@@ -0,0 +1,60 @@
+/*jshint esversion: 11, laxcomma:true, eqeqeq:true*/
+/*jshint -W014,-W084,-W030,-W033*/
+const navigateSheet = function({trigger,attributes}){
+ k.debug('navigating sheet');
+ let triggerName = trigger ?
+ trigger.name :
+ attributes.sheet_state;
+ let target = triggerName.replace(/clicked:|nav-/g,'');
+ k.debug({target});
+ $20('.navigable-section,.sheet-nav__tab').removeClass('active');
+ $20(`.sheet-nav__tab.${target},.navigable-section--${target}`).addClass('active');
+ attributes.sheet_state = target;
+};
+k.registerFuncs({navigateSheet},{type:['opener']});
+
+const characterDisplay = function({attributes}){
+ $20('.character-type').addClass('character-type--inactive');
+ k.debug({sheet_type:attributes.sheet_type})
+ const target = `.character-type--${attributes.sheet_type}`;
+ k.debug({target});
+ $20(target).removeClass('character-type--inactive');
+}
+k.registerFuncs({characterDisplay},{type:['opener']})
+
+const sectionDisplay = function({trigger,attributes}){
+ const fieldSwitch = {
+ is_weird:'weird',
+ mon_trainer:'mon',
+ use_initiative:'initiative',
+ has_arc:'arc'
+ };
+ k.debug({trigger});
+ let fields;
+ if(trigger){
+ let[,,attr] = k.parseTriggerName(trigger.name);
+ fields = [attr];
+ }else{
+ fields = Object.keys(fieldSwitch);
+ }
+ fields.forEach((field)=>{
+ const action = attributes[field] ?
+ 'remove' :
+ 'add';
+ $20(`.${fieldSwitch[field]}`)[`${action}Class`]('inactive');
+ });
+}
+k.registerFuncs({sectionDisplay},{type:['opener']});
+
+const modifierDisplay = function({attributes}){
+ if(attributes.use_roll_modifiers){
+ $20('#modifiers').removeClass('inactive');
+ $20('.roll-options').addClass('inactive');
+ attributes.advantage = '2d6';
+ attributes.double = 0;
+ }else{
+ $20('#modifiers').addClass('inactive');
+ $20('.roll-options').removeClass('inactive');
+ }
+};
+k.registerFuncs({modifierDisplay},{type:['opener']})
\ No newline at end of file
diff --git a/under_the_neighborhood/source/javascript/variables.js b/under_the_neighborhood/source/javascript/variables.js
new file mode 100644
index 0000000000..104b7948fd
--- /dev/null
+++ b/under_the_neighborhood/source/javascript/variables.js
@@ -0,0 +1,33 @@
+/*jshint esversion: 11, laxcomma:true, eqeqeq:true*/
+/*jshint -W014,-W084,-W030,-W033*/
+k.version = 0.1;
+k.sheetName = 'Under the Neighborhood';
+const systemDefaults = {
+ 'repeating_basic-advancement':[
+ {description:'Increase a stat by 1 (max:+3)'},
+ {description:'Increase a stat by 1 (max:+3)'},
+ {description:'Increase a stat by 1 (max:+3)'},
+ {description:'Increase a stat by 1 (max:+3)'},
+ {description:'Add Another Skill'},
+ {description:'Add Another Signature Item'},
+ {description:'Add a Move from your Playbook'},
+ {description:'Add a Move from your Playbook'},
+ {description:'Add a Move from your Playbook Or a different Playbook'},
+ {description:'Add a Move from your Playbook Or a different Playbook'}
+ ],
+ 'repeating_super-advancement':[
+ {description:'Upgrade two Playbook Moves to advanced'},
+ {description:'Upgrade two Playbook Moves to advanced'},
+ {description:'Upgrade one Descriptor to advanced'},
+ {description:'Upgrade two Skills into Super Skills'},
+ {description:'Add Another Descriptor'},
+ {description:'Increase the Maximum number of AP stored between adventures by 1'},
+ ],
+ 'repeating_ultra-advancement':[
+ {description:'Increase a stat by 1 (max:+4)'},
+ {description:'Increase the Weird stat by 2 (max:+1)'},
+ {description:'Upgrade all remaining Playbook Moves to Advanced'},
+ {description:'Upgrade all remaining Descriptors to Advanced'},
+ {description:'Add Another Skill'}
+ ]
+}
\ No newline at end of file
diff --git a/under_the_neighborhood/source/mixins/_sheetmixins.pug b/under_the_neighborhood/source/mixins/_sheetmixins.pug
new file mode 100644
index 0000000000..da448952b2
--- /dev/null
+++ b/under_the_neighborhood/source/mixins/_sheetmixins.pug
@@ -0,0 +1,19 @@
+//- An array of all the attributes for the system. We'll iterate through this so we don't have to write the same code 6 times.
+- const attributes = ['heart','books','fierce','slick','weird'];
+- const attributeOpts = attributes.map((attr)=>`${capitalize(attr)},@{${attr}}`).join('|');
+- varObjects.attributeNames = attributes;
+//- A mixin that will create a checkbox for use in controlling the display of extra information on an item. Takes an optional name property to override the name of the checkbox.
+mixin collapse(name='collapse')
+ +checkbox({name,value:1,class:'collapse'})
+//- End Mixin
+mixin attributeOptions(defaultValue)
+ - const baseObj = {value:`?{Which Stat|${attributeOpts}}`,'data-i18n':'query'};
+ if !defaultValue
+ - baseObj.selected = '';
+ +option(baseObj)
+ each value in varObjects.attributeNames
+ - const optObj = {value:`@{${value}}`,'data-i18n':value}
+ if value === defaultValue
+ - optObj.selected = '';
+ +option(optObj)
+//- End Mixin
\ No newline at end of file
diff --git a/under_the_neighborhood/source/mixins/scss/_advancement.scss b/under_the_neighborhood/source/mixins/scss/_advancement.scss
new file mode 100644
index 0000000000..4260790735
--- /dev/null
+++ b/under_the_neighborhood/source/mixins/scss/_advancement.scss
@@ -0,0 +1,27 @@
+#advancement{
+ grid-template-columns:repeat(auto-fill,minmax(250px,1fr));
+ h3{
+ grid-column: 1 / -1;
+ }
+ .repeating-container{
+ align-self:start;
+ gap:var(--half-gap) var(--grid-gap);
+ }
+ .repitem{
+ display:grid;
+ grid-template-columns:1rem 1fr;
+ align-items:start;
+ }
+ input:checked + .adaptive textarea{
+ text-decoration:{
+ line:line-through;
+ style:wavy;
+ color:var(--selectedColor);
+ thickness:2px;
+ }
+ }
+ :is(.adaptive--text) {
+ min-height: 1.5rem;
+ height: auto;
+ }
+}
\ No newline at end of file
diff --git a/under_the_neighborhood/source/mixins/scss/_attributes.scss b/under_the_neighborhood/source/mixins/scss/_attributes.scss
new file mode 100644
index 0000000000..581ae09e42
--- /dev/null
+++ b/under_the_neighborhood/source/mixins/scss/_attributes.scss
@@ -0,0 +1,9 @@
+#attributes{
+ .input-label{
+ justify-content: space-between;
+ button{
+ width:auto;
+ flex:0;
+ }
+ }
+}
\ No newline at end of file
diff --git a/under_the_neighborhood/source/mixins/scss/_clock.scss b/under_the_neighborhood/source/mixins/scss/_clock.scss
new file mode 100644
index 0000000000..f3fb40e964
--- /dev/null
+++ b/under_the_neighborhood/source/mixins/scss/_clock.scss
@@ -0,0 +1,102 @@
+@use '_mixinsandplaceholders';
+$clockSizes:2,3,4,5,6,7,8,9,10;
+$meterValues:(
+ 1:(2,3,4,5,6,7,8,9,10),
+ 2:(3,4,5,6,7,8,9,10),
+ 3:(4,5,6,7,8,9,10),
+ 4:(5,6,7,8,9,10),
+ 5:(6,7,8,9,10),
+ 6:(7,8,9,10),
+ 7:(8,9,10),
+ 8:(9,10),
+ 9:(10)
+);
+/// Mixin to place items on a circle
+/// Adapted from on-circle by the original Authors to work with CSS variables based on a changing control attribute and to make pie slices instead of circular elements on the circle
+/// @author Adapted by Scott Casey
+/// @author Kitty Giraudel
+/// @author Ana Tudor
+@mixin on-circle($item-count) {
+ > * {
+ $angle: (360 / $item-count);
+
+ @for $i from 1 through $item-count {
+ &:nth-of-type(#{$i}) {
+ $rot:($angle * ($i - 1));
+ transform:
+ rotate($rot * 1deg)
+ translate(calc(var(--circleSize) / 4));
+ }
+ }
+ }
+}
+
+#clock{
+ --circleSize:150px;
+ --itemDeg:0deg;
+ --itemCount:10;
+ .repcontainer{
+ display:block;
+ gap:var(--half-gap);
+ column:{
+ width:200px;
+ }
+ }
+ .repitem{
+ display:grid;
+ gap:var(--half-gap);
+ grid-template-areas:
+ 'clock'
+ 'name'
+ ;
+ break-inside:avoid;
+ > input{
+ text-align:center;
+ }
+ }
+ .clock-control--current[value="1"] ~ .clock-progress{
+ --itemDeg:calc(1 / var(--itemCount) * 360deg);
+ }
+ @each $clockSize in $clockSizes{
+ .clock-control--max[value="#{$clockSize}"] + .clock-progress{
+ --itemCount:#{$clockSize};
+ }
+ .clock-control--current[value="#{$clockSize}"] ~ .clock-progress{
+ --itemDeg:calc(#{$clockSize} / var(--itemCount) * 360deg);
+ }
+ }
+ .clock-label{
+ grid-area:clock;
+ max-width:var(--circleSize);
+ box-sizing:border-box;
+ display:flex;
+ place-self:center;
+ align-items:center;
+ z-index:10;
+ > * {
+ color:var(--selectedColor);
+ font-weight:bold;
+ }
+ }
+ .clock-progress{
+ grid-area:clock;
+ border-radius:50%;
+ place-self:center;
+ width: var(--circleSize);
+ height: var(--circleSize);
+ display:grid;
+ grid-template-areas:'content';
+ background: conic-gradient(var(--selectedColor) var(--itemDeg),transparent var(--itemDeg));
+ border:2px solid var(--selectedColor);
+ &:before{
+ content:'';
+ box-sizing:border-box;
+ width:75%;
+ height:75%;
+ place-self:center;
+ border-radius:50%;
+ background-color:var(--backColor);
+ border:2px solid var(--selectedColor);
+ }
+ }
+}
\ No newline at end of file
diff --git a/under_the_neighborhood/source/mixins/scss/_compatibility.scss b/under_the_neighborhood/source/mixins/scss/_compatibility.scss
new file mode 100644
index 0000000000..6bbda5bf30
--- /dev/null
+++ b/under_the_neighborhood/source/mixins/scss/_compatibility.scss
@@ -0,0 +1,20 @@
+@use '_mixinsandplaceholders';
+#compatibility{
+ .repitem,
+ .repeat-column{
+ display:grid;
+ grid-template-columns:1fr 3rem;
+ align-items:end;
+ gap:var(--half-gap);
+ }
+ .repcontainer{
+ gap:var(--half-gap);
+ }
+ h4{
+ white-space: pre-wrap;
+ }
+ button{
+ width:auto;
+ flex:0;
+ }
+}
\ No newline at end of file
diff --git a/under_the_neighborhood/source/mixins/scss/_generalsetup.scss b/under_the_neighborhood/source/mixins/scss/_generalsetup.scss
new file mode 100644
index 0000000000..874c52dc51
--- /dev/null
+++ b/under_the_neighborhood/source/mixins/scss/_generalsetup.scss
@@ -0,0 +1,136 @@
+//This file will store all of our generic styling for the sheet. Styling for specific sections will be done in individual scss files to make the code easier to read.
+@use '_mixinsandplaceholders';
+@import url('https://fonts.googleapis.com/css?family=Material+Icons&display=swap');
+html{
+ font-size:16px;
+}
+.ui-dialog{
+ @extend %universalVariables;
+ .tab-content .charsheet{
+ /*
+ First of all we apply our default styles.
+ Note that because we are using the placeholder, these styles would be first up in the cascade even if we put the extend declaration last.
+ This is done first so that when we read the code, we can easily see that it is applied, and anything after it will overwrite those default styles.
+ */
+
+ // Clears the default Roll20 styling from all elements so that we can style them as we want more easily. We are using 3 classes here because we have to use at least that level of specificity to overcome many of the default Roll20 styles.
+ padding-top:0;
+ //End roll20 clear
+ @extend %defaultStyles;
+ .roller{
+ @extend %h4;
+ display:flex;
+ gap:var(--half-gap);
+ align-items:center;
+ &:before{
+ font-family:dicefontd6;
+ content:'L'
+ }
+ }
+ button{
+ cursor:pointer;
+ }
+ h3{
+ grid-area:header;
+ }
+ .fill-left{
+ display:grid;
+ align-self:center;
+ box-sizing:border-box;
+ box-shadow:inset 0 0 0 1px var(--selectedColor);
+ &:is(:hover,:focus-within,:focus){
+ .fill-left__radio--clearer{
+ background-image: linear-gradient(45deg, transparent 0% 48%, white 48% calc(48% + 1px), transparent calc(48% + 1px)), linear-gradient(-45deg, transparent 0% 48%, white 48% calc(48% + 1px), transparent calc(48% + 1px));
+ border-color: transparent;
+ }
+ }
+ &:not(.no-fill){
+ .fill-left__radio:checked{
+ display:none;
+ }
+ }
+ }
+ .fill-left__radio{
+ appearance:none;
+ -webkit-appearance:none;
+ background-color:var(--selectedColor);
+ order:1;
+ width:100%;
+ height:100%;
+ cursor:pointer;
+ &:checked ~ .fill-left__radio{
+ background-color:var(--unselectedColor2);
+ order:3;
+ }
+ }
+ .fill-left__radio--clearer{
+ order:2;
+ &:checked{
+ display:none;
+ }
+ }
+ .styled-button{
+ @extend %sectionBoxShadow;
+ background-color: var(--backColor);
+ border: 0;
+ border-radius: .5rem;
+ box-sizing: border-box;
+ cursor:pointer;
+ transition:{
+ property:background-color,color;
+ duration:var(--revealTime);
+ }
+ padding-inline:var(--half-gap);
+ text-align:center;
+
+ &:hover {
+ background-color: var(--selectedColor);
+ &,
+ & *{
+ color:var(--dark-secondarytext);
+ }
+ }
+
+ &:focus {
+ outline: 2px solid transparent;
+ outline-offset: 2px;
+ }
+
+ &:focus-visible {
+ box-shadow: none;
+ }
+ }
+ .styled-button--controlled,
+ .styled-button--controlled *{
+ @extend %h4;
+ }
+ .styled-button-control:is(:checked,[type='hidden']:not([value='']):not([value='0'])) + .styled-button--controlled{
+ background-color:var(--selectedColor);
+ &,
+ & *{
+ color:var(--dark-secondarytext);
+ }
+ }
+ }
+}
+.ui-dialog .sheetform{
+ background-color:var(--backColor);
+ transition:{
+ property:background-color;
+ duration:var(--revealTime);
+ }
+}
+.sheet-darkmode .ui-dialog{
+ @extend %darkmodeStyle;
+}
+//the declarations for styles specifically applying to our main element.
+#main{
+ display:grid;
+}
+#character,
+#gamescreen{
+ display:grid;
+ gap:var(--grid-gap);
+ grid-auto-flow:dense;
+ grid-template-columns:repeat(auto-fill,minmax(16rem,1fr));//This one line is our secret sauce for making our sheet responsive to just about any screen width that our sheet might encounter from tiny phone screens up to stretching across several superwide monitors. For more information about auto-fit and auto-fill, I recommend the CSS-tricks summary: https://css-tricks.com/auto-sizing-columns-css-grid-auto-fill-vs-auto-fit/.
+}
\ No newline at end of file
diff --git a/under_the_neighborhood/source/mixins/scss/_header.scss b/under_the_neighborhood/source/mixins/scss/_header.scss
new file mode 100644
index 0000000000..fefdebd04d
--- /dev/null
+++ b/under_the_neighborhood/source/mixins/scss/_header.scss
@@ -0,0 +1,58 @@
+//This file will define how our header section is laid out.
+@use '_mixinsandplaceholders';
+#character-header{
+ display:grid;
+ gap:var(--half-gap);
+ grid-template-columns: 20rem 1fr;
+ grid-template-rows: repeat(2,auto);
+ grid-template-areas:
+ 'image summary'
+ 'sentence summary'
+ ;
+ .header-image{
+ grid-area:image;
+ }
+ > .sentence{
+ display:flex;
+ grid-area:sentence;
+ grid-area:auto;
+ justify-self:center;
+ flex-wrap:wrap;
+ gap:var(--half-gap);
+ > .character-name{
+ min-width:8rem;
+ > input{
+ text-transform:none;
+ }
+ }
+ }
+ .adaptive--input{
+ min-width:6rem;
+ }
+ .input-label,
+ .adaptive--input{
+ flex:1 0 auto;
+ }
+ .character-summary{
+ grid-area:summary;
+ display:grid;
+ grid-template-columns:repeat(2,1fr);
+ grid-template-rows: auto 1fr;
+ gap:var(--grid-gap);
+ }
+ .generic-details{
+ display:flex;
+ gap:var(--grid-gap);
+ flex-wrap:wrap;
+ .adaptive,
+ .input-label{
+ flex:1 0 auto;
+ }
+ }
+ .arc.inactive + .adventure-points{
+ grid-column:1 / -1;
+ }
+ .adventure-points{
+ place-self:center;
+ }
+}
\ No newline at end of file
diff --git a/under_the_neighborhood/source/mixins/scss/_lifeslice.scss b/under_the_neighborhood/source/mixins/scss/_lifeslice.scss
new file mode 100644
index 0000000000..5b8818e990
--- /dev/null
+++ b/under_the_neighborhood/source/mixins/scss/_lifeslice.scss
@@ -0,0 +1,16 @@
+@use '_mixinsandplaceholders';
+#lifeslice{
+ .repcontainer{
+ display:block;
+ gap:var(--half-gap);
+ column:{
+ width:200px;
+ };
+ }
+ .repitem{
+ display:grid;
+ grid-template-columns:auto 1fr;
+ gap:var(--half-gap);
+ break-inside:avoid;
+ }
+}
\ No newline at end of file
diff --git a/under_the_neighborhood/source/mixins/scss/_media.scss b/under_the_neighborhood/source/mixins/scss/_media.scss
new file mode 100644
index 0000000000..401c2d2c2a
--- /dev/null
+++ b/under_the_neighborhood/source/mixins/scss/_media.scss
@@ -0,0 +1,6 @@
+@use '_mixinsandplaceholders';
+@media (min-width:597px){
+ .span-2--if-wide{
+ grid-column:span 2;
+ }
+}
\ No newline at end of file
diff --git a/under_the_neighborhood/source/mixins/scss/_meter.scss b/under_the_neighborhood/source/mixins/scss/_meter.scss
new file mode 100644
index 0000000000..46c91ffc93
--- /dev/null
+++ b/under_the_neighborhood/source/mixins/scss/_meter.scss
@@ -0,0 +1,59 @@
+@use '_mixinsandplaceholders';
+$meterValues:(
+ 1:(2,3,4,5,6,7,8,9,10),
+ 2:(3,4,5,6,7,8,9,10),
+ 3:(4,5,6,7,8,9,10),
+ 4:(5,6,7,8,9,10),
+ 5:(6,7,8,9,10),
+ 6:(7,8,9,10),
+ 7:(8,9,10),
+ 8:(9,10),
+ 9:(10)
+);
+#meter{
+ --meterSquare:2rem;
+ --meterColumns:repeat(10,var(--meterSquare));
+ .repitem,
+ .repeat-column{
+ display:grid;
+ grid-template-columns:1fr var(--half-gap) var(--meterColumns);
+ >h5{
+ place-self:center end;
+ }
+ }
+ .fill-left{
+ grid-column:3 / -1;
+ justify-self:start;
+ border-radius: 15px;
+ overflow: hidden;
+ align-self: center;
+ height: 1rem;
+ align-items:center;
+ grid-template-columns:repeat(10,var(--meterSquare));
+ grid-template-rows: var(--grid-gap);
+ align-content:start;
+ }
+ .fill-left.no-fill{
+ --selectedColor:lightgrey;
+ }
+ h4:first-child{
+ grid-column:span 2;
+ margin-right:var(--half-gap);
+ }
+ button{
+ width:auto;
+ flex:0;
+ }
+ .fill-left__max-control{
+ @each $value,$affected in $meterValues{
+ &[value="#{$value}"] + .fill-left{
+ grid-template-columns: repeat($value,var(--meterSquare));
+ @each $target in $affected{
+ .fill-left__radio[value="#{$target}"]{
+ display:none;
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/under_the_neighborhood/source/mixins/scss/_mixinsandplaceholders.scss b/under_the_neighborhood/source/mixins/scss/_mixinsandplaceholders.scss
new file mode 100644
index 0000000000..08eb1e49a9
--- /dev/null
+++ b/under_the_neighborhood/source/mixins/scss/_mixinsandplaceholders.scss
@@ -0,0 +1,782 @@
+//- This file will store all mixins and placeholders that have generic use
+
+//- A placeholder for creating a stackable grid of elements. Particularly for making stacked input labels.
+%darkmodeStyle{
+ --borderColor: var(--dark-secondarytext);//Our basic border color
+ --fontColor: var(--dark-secondarytext);//Our basic font color
+ --backColor: var(--dark-surface1);//Set our background to match the dark mode background of the sheet
+ --dark-primary:#c33d00;
+ --dark-primary-highlight:#9a644c;
+ --selectedColor:var(--dark-primary);
+ --subHeadBackColor:var(--dark-primary);
+ img{
+ filter: brightness(.8) contrast(1.2);
+ }
+}
+%stackedGrid{
+ &.stacked{
+ flex-direction:column-reverse;
+ &.center>*{
+ text-align: center;
+ }
+ > [name]{
+ width:100%;
+ }
+ }
+}
+//- Some styling to highlight which input is currently being interacted with.
+%inputHighlight{
+ border-width: 1px 3px;
+ border-style: solid;
+ border-color: var(--borderColor);
+ border-radius: 5px;
+ box-sizing: border-box;
+}
+//- Placeholder that allows us to easily set a square aspect ratio
+%ratio1_1{
+ aspect-ratio: 1/1;
+}
+//- Placeholder for applying grid display
+%grid{
+ display:grid;
+}
+
+//Our various fonts that can be used on the sheet. For now, these are all set to Arial until we find fonts we like, which won't be done until the very end.
+%font-vars{
+ --topHeaderFont:Arial;
+ --midHeaderFont:Arial;
+ --contentFont:Arial;
+}
+
+//- Placeholder for applying inline-grid display
+%inline-grid{
+ display: inline-grid;
+}
+//- Placeholder for creating circular shapes easily. Combined with %ratio1_1, makes a perfect circle.
+%circle{
+ border-radius: 50%;
+}
+//- Creates our basic border for inputs
+%base-border{
+ border-width: 1px 3px;
+ border-style: solid;
+ border-radius: 5px;
+ border-color: transparent;
+ box-sizing: border-box;
+}
+//- Our basic button styling
+%base-button{
+ background-color: #DCDCDC33;
+ border-radius: 5px;
+ box-shadow: 0 2px 4px black;
+ border-width: 0;
+ transition:{
+ property:box-shadow,backdrop-filter;
+ duration:200ms;
+ timing-function:ease-out;
+ };
+ backdrop-filter:blur(1px);
+ overflow:hidden;
+ &:is(:hover,:focus){
+ background-color: #85858580;
+ box-shadow: 0 4px 6px black;
+ backdrop-filter:blur(2px);
+ }
+ &:active{
+ background-color: #858585ff;
+ box-shadow: 0 1px 2px black;
+ backdrop-filter:blur(0px);
+ }
+}
+//- Styling for our dice buttons
+%die-button{
+ @extend %base-button;
+ line-height: 14px;
+ /*height to vertically center a 2rem dicefontd10*/
+ font-size: 2rem;
+ font-weight: normal;
+ font-style: normal;
+ padding: 5px 3px 7px;
+}
+//- styling for buttons that have standard text in them
+%text-button{
+ padding: 5px 7px;
+ @extend %base-button;
+}
+//- Styling for our adaptive text fields
+%adaptiveText{
+ .adaptive{
+ @extend %grid;
+ grid-template-columns: auto;
+ grid-template-rows: auto;
+ grid-template-areas: "content";
+ position:relative;
+ >*{
+ grid-area: content;
+ }
+ > span{
+ opacity: 0;
+ z-index: -10;
+ @extend %base-border;
+ text-transform: initial;
+ border-radius:0px;
+ }
+ }
+ .adaptive--text{
+ min-height:4rem;
+ }
+ .adaptive--text__span{
+ white-space: pre-wrap;
+ padding:2px;
+ }
+ .adaptive--text__textarea{
+ width:100%;
+ height:100%;
+ resize: none;
+ }
+ .adaptive--text__textarea,
+ .adaptive--input__input{
+ position:absolute;
+ }
+ .adaptive--input__input{
+ width:100%;
+ }
+ .adaptive--input__span{
+ padding:2px;
+ min-height:1.5rem;
+ }
+}
+
+//- placeholder that can be used to apply uppercase letters to an element
+%uppercase{
+ text-transform:uppercase;
+}
+
+//- Styling for our roll buttons
+%roller{
+ display: inline-flex;
+ align-items: center;
+ align-self:start;
+ gap: var(--half-gap);
+ color:var(--fontColor);
+ &:before{
+ content:'L';
+ font-family:dicefontd6;
+ }
+}
+
+//- Styling to hide elements
+%hidden{
+ display: none!important;//!important is usually a thing to avoid, but if we're applying this, we want it to be gone no matter what.
+}
+
+//- styling for italic words
+%italics{
+ font-style: italic;
+}
+
+//- Our basic setup for text on the sheet.
+%baseText{
+ line-height: normal;
+}
+
+//- Basic styling for headers.
+%baseHeader{
+ @extend %baseText;
+ color:var(--fontColor);
+ display: block;
+ white-space: nowrap;
+ margin-top: 0px;
+ margin-bottom: 0px;
+ font-weight:normal;
+}
+
+//- Headers that should pop!
+%importantHeader{
+ text-transform: uppercase;
+ font-weight: normal;
+}
+
+//- Headers that should be important, but not eye-catching
+%midHeader{
+ &:not(:where(input)){
+ text-transform:capitalize;
+ }
+}
+
+//- The next several placeholders are for styling our various levels of headers (1-5).
+%h1-style{
+ @extend %baseHeader;
+ @extend %importantHeader;
+ font-size: 2rem;
+ font-family: var(--topHeaderFont);
+ &:not(input){
+ text-align: center;
+ }
+}
+%h1{
+ @extend %h1-style;
+ *{
+ @extend %h1-style;
+ }
+}
+%h2-style{
+ @extend %baseHeader;
+ @extend %importantHeader;
+ font-size: 1.75rem;
+ font-family: var(--topHeaderFont);
+ &:not(input){
+ text-align: center;
+ }
+}
+%h2{
+ @extend %h2-style;
+ *{
+ @extend %h2-style;
+ }
+}
+%h3-style{
+ @extend %baseHeader;
+ @extend %importantHeader;
+ font-size: 1.5rem;
+ font-family: var(--topHeaderFont);
+ &:not(input){
+ text-align: center;
+ }
+}
+%h3{
+ @extend %h3-style;
+ *{
+ @extend %h3-style;
+ }
+}
+%h4-style{
+ @extend %baseHeader;
+ @extend %midHeader;
+ font-size: 1.25rem;
+ font-family: var(--midHeaderFont);
+ &:not(input){
+ text-align: center;
+ }
+}
+%h4{
+ @extend %h4-style;
+ *{
+ @extend %h4-style;
+ }
+}
+%h5-style{
+ @extend %baseHeader;
+ @extend %midHeader;
+ font-size: 1rem;
+ font-style:normal;
+ font-family: var(--midHeaderFont);
+ &:not(input){
+ text-align: center;
+ }
+}
+%h5{
+ @extend %h5-style;
+ *{
+ @extend %h5-style;
+ }
+}
+
+//- Placeholder for applying our checked styling to something
+%checked{
+ &:before{
+ content: '\2713';
+ grid-area:content;
+ font-weight:bold;
+ place-self:start center;
+ color: var(--selectedColor);
+ font-size: 1.5rem;
+ line-height: 0.5rem;
+ }
+}
+
+//- Styling for elements that should have a box around them
+%boxed{
+ border: 1px solid black;
+ border-radius:0;
+ box-sizing:border-box;
+}
+//- Styling for items that need the pictos fonts
+%pictos{
+ font-family: pictos;
+ text-transform: initial;
+}
+%pictos3{
+ font-family: pictos three;
+ text-transform: initial;
+}
+%pictosCustom{
+ font-family: pictos custom;
+ text-transform: initial;
+}
+
+%sectionBoxShadow{
+ box-shadow: rgba(0, 0, 0, 0.16) 0px 3px 6px, rgba(0, 0, 0, 0.23) 0px 3px 6px;
+}
+
+%universalVariables{
+ --grid-gap: 1rem;//Variable to define our basic gap between layout elements. Followed by several gaps that are mathematically related to it.
+ --half-gap: calc(var(--grid-gap) / 2);
+ --tiny-gap: calc(var(--half-gap) / 2);
+ --big-gap: calc(var(--grid-gap) * 2);
+
+ --revealTime: 500ms;//Our basic reveal time for transitions
+ --delayTime: 200ms;//Our basic delay time before a transition occurs
+ --inputSwap: opacity var(--revealTime) ease var(--delayTime);//Transition properties for swapping the visibility of two elements
+ --focusTiming: 500ms ease;//our basic timing for the transition used when an element is focused or hovered on
+ --focusTrans: all var(--focusTiming);//The full transition properties for focusing/hovering on an element
+ --borderColor: black;//Our basic border color
+ --fontColor: black;//Our basic font color
+ --backColor: #ffffff;//Our basic background color
+ --spanSectionColumn:1/-1;//How a section should span when it is meant to span large numbers of columns (default span all)
+ --displayBorderWidth: 2px;//Our default border width
+ --subHeadBackColor:grey;//Our sub header background color
+ --unselectedColor1:lightgrey;//Our default font color for items that are currently inactive
+ --unselectedColor2:transparent;
+ --selectedColor:black;
+ @extend %font-vars;
+}
+
+//- Default styles. This Placeholder gets a little complicated. We're going to define several CSS variables in the first lines that will allow us to easily use the same layout principles across the sheet. Additionally, this placeholder is going to apply several styles to various elements. The reason we do this as a placeholder instead of directly in a class' CSS declaration is so that we can easily apply the same styling to our sheet as a whole and to the rolltemplate (note that many of these class specifications have versions prefixed and unprefixed with `.sheet-`
+%defaultStyles{
+ gap: var(--grid-gap);
+ button{
+ cursor: pointer;
+ }
+ .flex-box,
+ .sheet-flex-box{
+ display:flex;
+ }
+ .space-around,
+ .sheet-space-around{
+ justify-content:space-around;
+ }
+ .space-between,
+ .sheet-space-between{
+ justify-content:space-between;
+ }
+ .center,
+ .sheet-center{
+ place-self:center;
+ }
+ textarea{
+ resize: vertical;
+ white-space: pre-wrap;
+ &.fixed{
+ resize: none;
+ overflow: auto;
+ }
+ }
+ .span-2,
+ .sheet-span-2{
+ grid-column:span 2;
+ }
+ .span-all,
+ .sheet-span-all{
+ grid-column:1/-1;
+ }
+ select,
+ .pseudo-select span,
+ .sheet-pseudo-select span,
+ textarea,
+ input:not(:is([type='radio'],[type='checkbox'])),
+ .uneditable-input{
+ padding: 2px;
+ font-size:1rem;
+ }
+ input:is([type="text"],[type="number"]),textarea{
+ cursor:auto;
+ }
+ select,
+ .pseudo-select span,
+ .sheet-pseudo-select span,
+ input:not(:where([type='checkbox'], [type='radio'])),
+ .uneditable-input,
+ textarea{
+ @extend %base-border;
+ }
+ select,
+ .sheet-pseudo-select span,
+ .pseudo-select span{
+ -webkit-apperance: none;
+ appearance: none;
+ text-transform: capitalize;
+ overflow: hidden!important;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ text-align: center;
+ color:var(--fontColor);
+ }
+ input{
+ width: auto;
+ &:placeholder{
+ color: #ededed80;
+ }
+ &.plus-control:not([value*="-"])+span:before{
+ content: '+';
+ }
+ }
+ span,
+ input,
+ textarea{
+ font-size: 1rem;
+ color:var(--fontColor);
+ @extend %baseText;
+ }
+ span[name],input,textarea,button,select{
+ font-family:var(--contentFont);
+ }
+ .sheet-hidden,
+ .hidden,
+ *[hidden]{
+ @extend %hidden;
+ }
+ .sheet-italics,
+ .italics{
+ @extend %italics;
+ }
+ h1,
+ *[aria-level='1']{
+ @extend %h1;
+ }
+ h2,
+ *[aria-level='2']{
+ @extend %h2;
+ }
+ h3,
+ *[aria-level='3']{
+ @extend %h3;
+ }
+ h4,
+ *[aria-level='4']{
+ @extend %h4;
+ }
+ h5,
+ *[aria-level='5']{
+ @extend %h5;
+ }
+ input{
+ &[type='checkbox']{
+ border: 1px solid var(--borderColor);
+ cursor: pointer;
+ -webkit-appearance:none;
+ appearance:none;
+ cursor:pointer;
+ width: 14px;
+ min-width: 14px;
+ height: 14px;
+ min-height: 14px;
+ display:grid;
+ grid-template-columns:1fr;
+ grid-template-areas:"content";
+ &:not(.collapse):checked{
+ @extend %checked;
+ }
+ }
+ &[type='number']{
+ width: 3rem;
+ -moz-appearance: textfield !important;
+ text-align: center;
+ &::-webkit-inner-spin-button,
+ &::-webkit-outer-spin-button{
+ -webkit-appearance: none;
+ margin: 0;
+ }
+ }
+ }
+ .sheet-number,
+ .number{
+ width:2rem;
+ box-sizing:border-box;
+ }
+ section,
+ header{
+ @extend %grid;
+ gap:var(--half-gap);
+ align-self:start;
+ grid-template-areas:'header';
+ border-radius:5px;
+ padding:var(--half-gap);
+ transition:{
+ property:box-shadow;
+ duration:var(--revealTime);
+ timing-function:ease;
+ }
+ &:is(:hover,:focus-within){
+ @extend %sectionBoxShadow;
+ }
+ }
+ .inactive,
+ .character-type--inactive,
+ .navigable-section:not(.active){
+ display:none !important;
+ }
+ .sheet-pictos,
+ .pictos{
+ @extend %pictos;
+ }
+ .pictos3,
+ .sheet-pictos3{
+ @extend %pictos3;
+ }
+ .pictoscustom,
+ .sheet-pictoscustom{
+ @extend %pictosCustom;
+ }
+ .pseudo-button{
+ display: inline;
+ cursor: pointer;
+ border-radius: 5px;
+ text-transform: uppercase;
+ border: 1px solid black;
+ text-align: center;
+ padding: 0 5px;
+ }
+ .input-label{
+ display: inline-flex;
+ gap: var(--half-gap);
+ align-items:center;
+ @extend %stackedGrid;
+ &:not(.stacked){
+ > input[type='text']{
+ width:0;
+ flex:1 1 0;
+ }
+ >:not([name]){
+ flex:0 0 auto;
+ }
+ &.large > [name]{
+ @extend h2;
+ }
+ }
+ }
+ .input-label--dual{
+ grid-template-columns: 1fr auto 1fr;
+ grid-template-areas: 'over over over' 'under under under';
+ >span:last-of-type{
+ grid-column: 1 / -1;
+ }
+ >input{
+ width: 100%;
+ }
+ >.slash{
+ line-height: 0rem;
+ font-weight: normal;
+ }
+ }
+ .headed-textarea{
+ @extend %grid;
+ grid-template-rows: auto 1fr;
+ grid-template-columns: auto;
+ h5{
+ justify-self: start;
+ }
+ }
+ .boxed,
+ .sheet-boxed{
+ @extend %boxed;
+ &.thick-left{
+ border-left-width: 5px;
+ }
+ &.thick-bottom{
+ border-bottom-width: 5px;
+ }
+ &.thick-right{
+ border-right-width: 5px;
+ }
+ &.thick-top{
+ border-top-width: 5px;
+ }
+ }
+ .underlined,
+ .sheet-underlined{
+ @extend %base-border;
+ border-radius: 0;
+ border-bottom: 1px solid var(--borderColor);
+ transition: var(--focusTrans);
+ }
+ :is(.underlined,.boxed){
+ &:not([readonly]):not([type='checkbox']):not([type='radio']):is(:hover, :focus,:focus-within){
+ @extend %inputHighlight;
+ }
+ &:not([readonly]):not([type='checkbox']):not([type='radio']):is(:focus,:focus-within){
+ background-color: var(--subHeadBackColor);
+ }
+ }
+ .underlined--invisible{
+ border-color:transparent !important;
+ }
+ @extend %adaptiveText;
+ .ratio1-1,
+ .sheet-ratio1-1{
+ @extend %ratio1_1;
+ }
+ .short-text,
+ .sheet-short-text{
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ overflow: hidden;
+ text-transform: initial;
+ }
+ .roll-container{
+ @extend %grid;
+ align-self:center;
+ opacity:0;
+ grid-row:1 / 2;
+ }
+ .collapse-container{
+ display:grid;
+ position:relative;
+ }
+ .text-collapse{
+ cursor:pointer;
+ display:flex;
+ justify-content:flex-start;
+ align-items:center;
+ &:before{
+ content:'unfold_less';
+ font-family: 'Material Icons';
+ font-weight: normal;
+ font-style: normal;
+ font-size: 24px; /* Preferred icon size */
+ display: inline-block;
+ line-height: 1;
+ text-transform: none;
+ letter-spacing: normal;
+ word-wrap: normal;
+ white-space: nowrap;
+ direction: ltr;
+
+ /* Support for all WebKit browsers. */
+ -webkit-font-smoothing: antialiased;
+ /* Support for Safari and Chrome. */
+ text-rendering: optimizeLegibility;
+
+ /* Support for Firefox. */
+ -moz-osx-font-smoothing: grayscale;
+ }
+ }
+ .text-collapse__text{
+ display:inline;
+ }
+ .text-collapse__check{
+ &:not(:checked) + .text-collapse{
+ @extend %inputHighlight;
+ }
+ &:checked + .text-collapse:before{
+ content:'unfold_more';
+ }
+ }
+ .repitem,
+ .collapse-container{
+ &:hover{
+ .collapse,.roll-container{
+ opacity:1;
+ }
+ }
+ .collapse{
+ opacity:0;
+ position:absolute;
+ right:-10px;
+ top:0px;
+ border:0px solid black;
+ border-radius:0;
+ color:var(--selectedColor);
+ text-transform:none;
+ background-color:transparent;
+ &:before{
+ content:'y';
+ font-family:pictos;
+ }
+ &:checked{
+ color:var(--unselectedColor1);
+ background-color:transparent;
+ ~ .expanded,
+ ~ .collapse-container .expanded{
+ display:none !important;
+ }
+ ~ .expanded--empty:is(:not([value]),[value='']) + *,
+ ~ .collapse-container ~.expanded--empty:is(:not([value]),[value='']) + *{
+ display:none !important;
+ }
+ }
+ &:not(:checked){
+ ~ .collapsed{
+ display:none !important;
+ }
+ }
+ &:hover{
+ color:var(--selectedColor);
+ }
+ }
+ }
+ .repitem{
+ > input:not([type='checkbox']){
+ width:100%;
+ }
+ .headed-textarea{
+ grid-column:1/-1;
+ }
+ }
+ .repcontainer.editmode{
+ .collapse{
+ display:none;
+ }
+ }
+ .repcontainer{
+ display:grid;
+ gap:var(--grid-gap);
+ }
+ .repeating-container{
+ display:grid;
+ grid-template-areas:'header';
+ > .header{
+ grid-area:header;
+ }
+ &:is(:hover,:focus-within,:focus) .repcontrol-button{
+ opacity:1;
+ }
+ }
+ .repcontrol-button{
+ @extend %text-button;
+ grid-area:header;
+ align-self:center;
+ text-transform:none;
+ opacity:0;
+ transition:{
+ property:opacity,background-color;
+ duration: var(--revealTime);
+ };
+ ~ .repcontrol{
+ display:none;
+ }
+ }
+ .repcontrol_move{
+ display:none;
+ }
+ .repcontrol-button--add{
+ justify-self:start;
+ &:before{
+ font-family:pictos;
+ content:'&';
+ }
+ }
+ .repcontrol-button--edit{
+ justify-self:end;
+ &:before{
+ font-family:pictos;
+ content:'p';
+ }
+ }
+ .sheet-box,
+ .box{
+ border-radius:0;
+ width:10px;
+ height:10px;
+ }
+}
\ No newline at end of file
diff --git a/under_the_neighborhood/source/mixins/scss/_modifiers.scss b/under_the_neighborhood/source/mixins/scss/_modifiers.scss
new file mode 100644
index 0000000000..bd1fe56794
--- /dev/null
+++ b/under_the_neighborhood/source/mixins/scss/_modifiers.scss
@@ -0,0 +1,7 @@
+@use '_mixinsandplaceholders';
+#modifiers{
+ .repitem{
+ display:grid;
+ grid-template-columns:auto 1fr;
+ }
+}
\ No newline at end of file
diff --git a/under_the_neighborhood/source/mixins/scss/_mon.scss b/under_the_neighborhood/source/mixins/scss/_mon.scss
new file mode 100644
index 0000000000..585daa4c80
--- /dev/null
+++ b/under_the_neighborhood/source/mixins/scss/_mon.scss
@@ -0,0 +1,13 @@
+@use '_mixinsandplaceholders';
+#mon{
+ .repitem,
+ .repeat-column{
+ display:grid;
+ grid-template-columns:1fr 1fr 8rem 1fr;
+ gap:var(--half-gap);
+ }
+ button{
+ width:auto;
+ flex:0;
+ }
+}
\ No newline at end of file
diff --git a/under_the_neighborhood/source/mixins/scss/_nav.scss b/under_the_neighborhood/source/mixins/scss/_nav.scss
new file mode 100644
index 0000000000..e0d178b95b
--- /dev/null
+++ b/under_the_neighborhood/source/mixins/scss/_nav.scss
@@ -0,0 +1,48 @@
+@use '_mixinsandplaceholders';
+//Sets the bio/charsheet/a&a tab container to be sticky and float at the top
+ul.nav-tabs.nav{
+ position:sticky;
+ top:0;
+ background-color:var(--backColor);
+ z-index:9999;
+ margin-bottom:0px;
+}
+#main-nav{
+ display:grid;
+ gap:var(--half-gap);
+ grid-template-columns:1fr auto;
+ align-items:center;
+ position:sticky;
+ top:44px;
+ margin-inline:-10px;
+ margin-bottom:10px;
+ padding-inline:10px;
+ padding-block:var(--half-gap);
+ background-color:var(--backColor);
+ z-index:1000;
+ transition:{
+ property:background-color;
+ duration:var(--revealTime);
+ }
+ .sheet-nav__tab{
+ align-self:end;
+ grid-column:2;
+ font-size:1.5rem;
+ &.active{
+ display:none;
+ }
+ }
+ .roll-options,
+ .basic-moves{
+ display:flex;
+ flex-wrap:wrap;
+ grid-gap:var(--half-gap);
+ }
+ .basic-moves{
+ justify-content: space-between;
+ }
+ .roll-options{
+ grid-column:1;
+ justify-content: center;
+ }
+}
\ No newline at end of file
diff --git a/under_the_neighborhood/source/mixins/scss/_playbook.scss b/under_the_neighborhood/source/mixins/scss/_playbook.scss
new file mode 100644
index 0000000000..e403540c5b
--- /dev/null
+++ b/under_the_neighborhood/source/mixins/scss/_playbook.scss
@@ -0,0 +1,13 @@
+@use '_mixinsandplaceholders';
+#playbook{
+ .repitem,
+ .repeat-column{
+ display:grid;
+ grid-template-columns:2fr 8rem 6rem;
+ gap:var(--half-gap);
+ }
+ button{
+ width:auto;
+ flex:0;
+ }
+}
\ No newline at end of file
diff --git a/under_the_neighborhood/source/mixins/scss/_roll20clear.scss b/under_the_neighborhood/source/mixins/scss/_roll20clear.scss
new file mode 100644
index 0000000000..6cfb5b031b
--- /dev/null
+++ b/under_the_neighborhood/source/mixins/scss/_roll20clear.scss
@@ -0,0 +1,10 @@
+.ui-dialog .tab-content .charsheet{
+ select,
+ textarea,
+ input,
+ .uneditable-input,
+ label,
+ button{
+ all: initial;
+ }
+}
\ No newline at end of file
diff --git a/under_the_neighborhood/source/mixins/scss/_rolltemplate.scss b/under_the_neighborhood/source/mixins/scss/_rolltemplate.scss
new file mode 100644
index 0000000000..ceb882702d
--- /dev/null
+++ b/under_the_neighborhood/source/mixins/scss/_rolltemplate.scss
@@ -0,0 +1,132 @@
+//Styling for our rolltemplate. This won't be written yet.
+@use '_mixinsandplaceholders';
+.sheet-rolltemplate-utn{
+ @extend %universalVariables;
+ @extend %defaultStyles;
+ font-size:16px;
+ margin:{
+ left: -40px;
+ right: -5px;
+ }
+ --headerBackground:var(--selectedColor);
+ --templatePad:var(--grid-gap);
+ --criticalColor:green;
+ --fumbleColor:red;
+ --importantColor:blue;
+ &:not(:first-child){
+ margin-top:10px;
+ }
+ &:first-child{
+ .sheet-continuation{
+ margin-top:-9px;
+ }
+ }
+ &.sheet-rolltemplate-darkmode{
+ @extend %darkmodeStyle;
+ }
+ .sheet-header{
+ padding: 5px;
+ display:grid;
+ gap:var(--half-gap);
+ background-color:var(--headerBackground);
+ padding:calc(var(--templatePad) / 2);
+ margin:{
+ left: calc(var(--templatePad) * -1);
+ right: calc(var(--templatePad) * -1);
+ top: calc(var(--templatePad) * -1);
+ };
+ border-radius:5px 5px 0 0;
+ *{
+ color:#e6e6e6;
+ }
+ .sheet-character-name,
+ h3{
+ grid-column:1/-1;
+ white-space: pre-wrap;
+ }
+ .sheet-character_name{
+ place-self:center end;
+ }
+ h3{
+ place-self:center start;
+ }
+ }
+ > .sheet-template{
+ @extend %sectionBoxShadow;
+ border-radius:5px;
+ background-color:var(--backColor);
+ padding:var(--templatePad);
+ h3,
+ h3 *{
+ font-size:1.5em;
+ }
+ h4,
+ h4 *{
+ font-size:1.25em;
+ }
+ h5,
+ h5 *{
+ font-weight:bold;
+ }
+ span,h5{
+ font-size:1em;
+ }
+ .inlinerollresult{
+ font-size:1.25em;
+ &[title*="[computed value]"],
+ &[original-title*="[computed value]"]{
+ pointer-events: none;
+ }
+ &,
+ &.fullcrit,
+ &.fullfail,
+ &.importantroll{
+ border: none;
+ background-color: transparent;
+ }
+ &.fullcrit{
+ color: var(--criticalColor);
+ }
+ &.fullfail{
+ color: var(--fumbleColor);
+ }
+ &.importantroll{
+ color: var(--importantColor);
+ }
+ }
+ .sheet-crit .inlinerollresult{
+ color:var(--criticalColor);
+ }
+ .sheet-fumble .inlinerollresult{
+ color:var(--fumbleColor);
+ }
+ .sheet-regular-roll .inlinerollresult{
+ color:var(--fontColor);
+ }
+ }
+ .sheet-content{
+ display:grid;
+ gap:var(--grid-gap);
+ }
+ .sheet-template-row{
+ display:flex;
+ flex-wrap:wrap;
+ gap:var(--half-gap);
+ justify-content: space-between;
+ align-items:center;
+ &.sheet-center{
+ justify-content:center;
+ }
+ }
+ .sheet-success{
+ color:var(--criticalColor);
+ }
+ .sheet-failure{
+ color:var(--fumbleColor);
+ }
+ .sheet-critical{
+ text-decoration:{
+ line:underline overline;
+ }
+ }
+}
\ No newline at end of file
diff --git a/under_the_neighborhood/source/mixins/scss/_settings.scss b/under_the_neighborhood/source/mixins/scss/_settings.scss
new file mode 100644
index 0000000000..5fc40a50b2
--- /dev/null
+++ b/under_the_neighborhood/source/mixins/scss/_settings.scss
@@ -0,0 +1,17 @@
+@use '_mixinsandplaceholders';
+#settings{
+ display:grid;
+ grid-template-columns:repeat(auto-fill,minmax(250px,1fr));
+ gap:var(--grid-gap);
+ h1{
+ grid-column:1 / -1;
+ }
+ .move-stats{
+ display:grid;
+ gap:var(--grid-gap);
+ grid-template-columns:repeat(auto-fill,minmax(250px,1fr));
+ .input-label{
+ justify-content: space-between;
+ }
+ }
+}
\ No newline at end of file
diff --git a/under_the_neighborhood/source/mixins/scss/_skills.scss b/under_the_neighborhood/source/mixins/scss/_skills.scss
new file mode 100644
index 0000000000..67da8ea3e0
--- /dev/null
+++ b/under_the_neighborhood/source/mixins/scss/_skills.scss
@@ -0,0 +1,33 @@
+@use '_mixinsandplaceholders';
+#skills,
+#signature-item{
+ .super{
+ justify-self:center;
+ }
+ button{
+ width:auto;
+ flex:0;
+ }
+ .repitem,
+ .repeat-column{
+ display:grid;
+ }
+ .expanded{
+ display:flex;
+ justify-content:space-around;
+ }
+}
+#skills{
+ .repitem,
+ .repeat-column{
+ grid-template-columns:repeat(2,1fr);
+ gap:var(--half-gap);
+ }
+}
+#signature-item{
+ justify-self:center;
+ input{
+ width:initial;
+ flex:0;
+ }
+}
\ No newline at end of file
diff --git a/under_the_neighborhood/source/rolltemplates/_utntemplate.pug b/under_the_neighborhood/source/rolltemplates/_utntemplate.pug
new file mode 100644
index 0000000000..a169725d6f
--- /dev/null
+++ b/under_the_neighborhood/source/rolltemplates/_utntemplate.pug
@@ -0,0 +1,53 @@
++templateWrapper(templateName)
+ .header
+ +templateConditionalDisplay('header')
+ h3 {{header}}
+ +characterLink
+ .content
+ +templateConditionalDisplay('roll')
+ .template-row.center
+ +templateConditionalDisplay('roll_name')
+ h5 {{roll_name}}
+ +templateConditionalDisplay('roll_name','invert')
+ h5(data-i18n='roll')
+ +rollGreater({values:'computed::result 0'})
+ span.crit {{roll}}
+ +rollLess({values:'computed::result 0'})
+ span.fumble {{roll}}
+ +rollBetween({values:'computed::result 0 0'})
+ span.regular-roll {{roll}}
+ +templateConditionalDisplay('result')
+ .template-row
+ //- Failure Handling
+ +rollLess({values:'roll 7'})
+ //- Fumble Handling
+ +rollLess({values:'computed::result 0'})
+ h5(data-i18n='critical failure').critical.failure
+ +templateConditionalDisplay('fumble_result')
+ span {{fumble_result}}
+ +templateConditionalDisplay('fumble_result','invert')
+ span {{failure_result}}
+ +rollLess({values:'computed::result 0',invert:true})
+ h5(data-i18n='failure').failure
+ span {{failure_result}}
+ +rollBetween({values:'roll 7 9'})
+ h5(data-i18n='mixed success')
+ span {{mixed_result}}
+ +rollGreater({values:'roll 9'})
+ +rollGreater({values:'computed::result 0'})
+ h5(data-i18n='critical success').critical.success
+ +templateConditionalDisplay('critical_result')
+ span {{critical_result}}
+ +templateConditionalDisplay('critical_result','invert')
+ span {{success_result}}
+ +rollGreater({values:'computed::result 0',invert:true})
+ h5(data-i18n='full success').success
+ span {{success_result}}
+ +templateConditionalDisplay('description')
+ .template-row
+ h5(data-i18n='description')
+ span {{description}}
+ +allProps({values:'header character_name character_id roll_name roll result fumble_result failure_result mixed_result success_result critical_result description'})
+ .template-row
+ h5 {{key}}
+ span {{value}}
\ No newline at end of file
diff --git a/under_the_neighborhood/source/scaffold/_htmlelements.pug b/under_the_neighborhood/source/scaffold/_htmlelements.pug
new file mode 100644
index 0000000000..1faae3247e
--- /dev/null
+++ b/under_the_neighborhood/source/scaffold/_htmlelements.pug
@@ -0,0 +1,1101 @@
+- const varObjects = {repeatingSectionDetails:[],actionAttributes:[],cascades:{attr_character_name:{name:'character_name',type:'text',defaultValue:'',affects:[],triggeredFuncs:['setActionCalls'],listenerFunc:'accessSheet',listener:'change:character_name'}},docs:{js:{},pug:{}}};
+- let selectName = null;
+- let selectTitle = null;
+- let repeatingPrefix = '';
+- const pugDoc = require
+//- Element shortcut mixins
+-
+ varObjects.docs.pug.trigger = {
+ type:'argument',
+ description:"Many of the K-scaffold's mixins utilize the scaffold's trigger system. Triggers can be passed in any mixin that creates an attribute backed element, which is any element that you would give the `name` property to. The trigger is passed as a property of the object that defines the attribute backed element. See the [input mixin](#input) below for a simple example.\nTriggers are how the K-scaffold connects the attributes created in the html of the sheet with the javscript listeners to trigger sheetworker code. When accessed from a sheetworker, a trigger has all of the below properties. When passing a trigger into a mixin, it should only have any or all of the first four properties; `affects`,`initialFunc`, `calculation`, `triggeredFuncs`, and `listenerFunc`. Note that except for `listenerFunc`, the function that is called is passed arguments using the [destructuring assignment pattern](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment). This means that the functions referenced in ,`initialFunc`, `calculation`, and `triggeredFuncs` must be passed in an object associated with a key named as indicated in the arguments below.",
+ arguments:[
+ {type:'Array',name:'affects',description:'An array of attribute names that this attribute might affect. As an example, the `strength` attribute in a 5e based sheet would have `strength_mod` in its affects array.'},
+ {type:'string',name:'initialFunc',description:'The name of a function that has been registered with the K-scaffold using [k.registerFuncs](https://github.com/Kurohyou/Roll20-Snippets/blob/main/K_Scaffold/readme_docs/k_scaffold_js_documentation.md#registerFuncs). This function will only be called if the change to the attribute is directly caused by the user as opposed to being changed by the sheetworkers.\nThe K-scaffold sends this function the following arguments:\n- `trigger`: The trigger object for the attribute that is being changed\n- `attributes`: the attributes object containing the entire state of the sheet.\n- `sections`: An object containing the row IDs of all the repeating sections, indexed by section name (e.g. repeating_equipment).'},
+ {type:'string',name:'calculation',description:'The name of a function that has been registered with the K-scaffold using [k.registerFuncs](https://github.com/Kurohyou/Roll20-Snippets/blob/main/K_Scaffold/readme_docs/k_scaffold_js_documentation.md#registerFuncs). This function must return the calculated value of the attribute it is for, and is only called if the attribute is not changed directly by the user. Ideally, this function should be written as a pure function, reacting only to the arguments it is passed, and returning a value without modifying any of the arguments it is passed.\nThe K-scaffold sends this function the following arguments:\n- `trigger`: The trigger object for the attribute that is being changed\n- `attributes`: the attributes object containing the entire state of the sheet.\n- `sections`: An object containing the row IDs of all the repeating sections, indexed by section name (e.g. repeating_equipment).'},
+ {type:'Array',name:'triggeredFuncs',description:'An array of function names that have been registered with the K-scaffold using [k.registerFuncs](https://github.com/Kurohyou/Roll20-Snippets/blob/main/K_Scaffold/readme_docs/k_scaffold_js_documentation.md#registerFuncs). These functions are called any time an attribute is changed, regardless of the source of the change, do not return any values, and will likely modify the arguments that are passed to them. These functions are used for doing complex logic to determine what should be done in response to a change.\nThe K-scaffold sends these functions the following arguments:\n- `trigger`: The trigger object for the attribute that is being changed\n- `attributes`: the attributes object containing the entire state of the sheet.\n- `sections`: An object containing the row IDs of all the repeating sections, indexed by section name (e.g. repeating_equipment), and in the order that they appear on the sheet.'},
+ {type:'string',name:'listenerFunc',description:'The name of a function that has been registered with the K-scaffold using [k.registerFuncs](https://github.com/Kurohyou/Roll20-Snippets/blob/main/K_Scaffold/readme_docs/k_scaffold_js_documentation.md#registerFuncs). This function will be called directly from the event listener for this attribute instead of the default [accessSheet](https://github.com/Kurohyou/Roll20-Snippets/blob/main/K_Scaffold/readme_docs/k_scaffold_js_documentation.md#accesssheet) listener function. If a `listenerFunc` is specified, the automated handling of the rest of the previous trigger properties will be disabled. You can still use these properties, but will need to code that handling yourself.\nThe K-scaffold sends this function the following argument:\n- `event`: The event information object that is passed to the callback in the [sheet worker event listener](https://wiki.roll20.net/Sheet_Worker_Scripts#Event_listener). This is **NOT** passed using the object destructuring pattern, unlike the previous three function types.'},
+ {type:'string',name:'name',description:'The name of the attribute. When accessed from inside the default function cascade of the K-scaffold, or the callback of [getAllAttrs](https://github.com/Kurohyou/Roll20-Snippets/blob/main/K_Scaffold/readme_docs/k_scaffold_js_documentation.md#getAllAttrs), repeating attributes will include their specific row id for that instance of the attribute.'},
+ {type:'string',name:'type',description:'What type of attribute is this. This is determined by the type of element that first created the trigger. Can be `text`,`number`,`checkbox`,`select`,or `radio`, but will usually be `text` or `number` based on the type of default value that is assigned to the attribute.'},
+ {type:'string|number',name:'defaultValue',description:'What the default value of the attribute is. The k-scaffold uses this to return the default value in the event the value of the attribute becomes corrupted. For checkboxes and radios, this is determined based on the check state of the element. For selects, this is determined by which option has the `selected` property.'}
+ ]
+ };
+-
+ varObjects.docs.pug.input = {
+ type:'mixin',
+ description:'A generic mixin to create an input. The mixin will replace spaces in the attribute name with underscores and will add a title property if one isn\'t supplied that will inform the user what the attribute call for the attribute is.',
+ arguments:[
+ {type:'object',name:'obj',description:'An object containing all of the properties to apply to the element. Must have the properties; `name` and `type`. Can have any property that is valid for an input element. May also have a [trigger](#trigger) property'},
+ {type:'string',name:'obj.name',}
+ ],
+ example:[
+ [
+ `+input({name:'my attribute',type:'text',class:'some-class',trigger:{affects:['other_attribute']}})'`,
+ ` '`
+ ]
+ ]
+ };
+mixin input(obj)
+ - obj.class = obj.class ? replaceProblems(obj.class) : undefined;
+ - obj.name = replaceSpaces(obj.name);
+ - obj.title = obj.title || attrTitle(obj.name);
+ - obj.name = `attr_${obj.name}`;
+ - const elementObj = makeElementObj(obj);
+ - addFieldToFieldsetObj(obj);
+ - storeTrigger(obj);
+ input&attributes(elementObj)&attributes(attributes)
+//-End Mixin
+-
+ varObjects.docs.pug.text = {
+ type:'mixin',
+ description:'Alias for [input](#input) that makes a text input.',
+ example:[
+ [
+ `+text({name:'my text',class:'some-class',trigger:{affects:['other_attribute']}})`,
+ ' '
+ ]
+ ]
+ };
+mixin text(obj)
+ - obj.type = 'text';
+ +input(obj)
+//-End Mixin
+-
+ varObjects.docs.pug.checkbox = {
+ type:'mixin',
+ description:'Alias for [input](#input) that makes a checkbox input.',
+ example:[
+ [
+ `+checkbox({name:'my checkbox',class:'some-class',trigger:{affects:['other_attribute']}})`,
+ ' '
+ ]
+ ]
+ };
+mixin checkbox(obj)
+ - obj.type = 'checkbox';
+ +input(obj)&attributes(attributes)
+//-End Mixin
+-
+ varObjects.docs.pug.collapse = {
+ type:'mixin',
+ description:'Alias for [checkbox](#checkbox) that creates a checkbox for use in collapsing/expanding a section. Sets the checkbox to unchecked with a checked value of `1` and a class of `collapse`. Additional classes/ids can be applied by applying them inline to the mixin call.',
+ arguments:[
+ {type:'string',name:'name',description:'The name to assign to the collapse button. Defaults to `collapse`'}
+ ]
+ };
+mixin collapse(name='collapse')
+ +checkbox({name,value:1,class:'collapse'})&attributes(attributes)
+-
+ varObjects.docs.pug.radio = {
+ type:'mixin',
+ description:'Alias for [input](#input) that makes a radio input.',
+ example:[
+ [
+ `+radio({name:'my radio',class:'some-class',trigger:{affects:['other_attribute']}})`,
+ ' '
+ ]
+ ]
+ };
+mixin radio(obj)
+ - obj.type = 'radio';
+ +input(obj)
+//-End Mixin
+-
+ varObjects.docs.pug.number = {
+ type:'mixin',
+ description:'Alias for [input](#input) that makes a number input.',
+ example:[
+ [
+ `+number({name:'my number',class:'some-class',trigger:{affects:['other_attribute']}})`,
+ ' '
+ ]
+ ]
+ };
+mixin number(obj)
+ - obj.type = 'number';
+ +input(obj)
+//-End Mixin
+-
+ varObjects.docs.pug.range = {
+ type:'mixin',
+ description:'Alias for [input](#input) that makes a range input.',
+ example:[
+ [
+ `+range({name:'my range',class:'some-class',trigger:{affects:['other_attribute']}})`,
+ ' '
+ ]
+ ]
+ };
+mixin range(obj)
+ - obj.type = 'range';
+ +input(obj)
+//-End Mixin
+-
+ varObjects.docs.pug.hidden = {
+ type:'mixin',
+ description:'Alias for [input](#input) that makes a hidden input.',
+ example:[
+ [
+ `+hidden({name:'my hidden',class:'some-class',trigger:{affects:['other_attribute']}})`,
+ ' '
+ ]
+ ]
+ };
+mixin hidden(obj)
+ - obj.type = 'hidden';
+ +input(obj)
+//-End Mixin
+mixin radioGroup(objArray)
+ each obj in objArray
+ - obj.class = obj.class ? replaceProblems(obj.class) : undefined;
+ - obj.type='radio';
+ +input(obj)
+//-End Mixin
+-
+ varObjects.docs.pug.fillLeft = {
+ type:'mixin',
+ description:`A mixin that creates a construction that can be used to recreate sheet mechanics that fill to the left. Will apply BEM style classes in addition to the classes specified. BEM classes that can be applied are
+ - \`fill-left\`: The class for the fill-left container.
+ - \`fill-left__radio\`: The class applied to all the radio buttons in the container
+ - \`fill-left__radio--clearer\`: The class applied to the clear value radio button`,
+ arguments:[
+ {type:'object',name:'radioObj',description:'The object containing the details of the radio input to create. Similar to the [radio mixin](#radio), but the value property passed is used as the default checked value.'},
+ {type:'object',name:'divObj',description:'Optional object containing any details of the div to be applied such as class, id, or other properties. Class and ID can also be supplied by attaching them to the mixin invocation just like with a regular div.'},
+ {type:'array',name:'valueArray',description:'Array containing the values to be used for the fill to left construction. These should be in the order that they should be displayed left to right.'},
+ {name:'noClear',type:'boolean',description:'Optional argument that tells the mixin whether or not to apply the `fill-left__radio--clearer` class to the first radio button value. If falsy (or not passed), the class is applied. If truthy, the class is not applied.'}
+ ],
+ example:[
+ [
+ `+fillLeft({radioObj:{name:'track',class:'some-class',value:"0"},valueArray:[0,1,2]})`,
+ `
\n \n \n \n
`
+ ],
+ [
+ `+fillLeft({radioObj:{name:'track',class:'some-class',value:"0"},valueArray:[0,1,2],noClear:true})`,
+ `\n \n \n \n
`
+ ],
+ [
+ `+fillLeft({radioObj:{name:'track',class:'some-class',value:"0"},valueArray:[0,1,2],divObj:{'aria-label':'Accessible tracker'}}).custom-tracker-class`,
+ `\n \n \n \n
`
+ ],
+ ]
+ };
+mixin fillLeft({radioObj,divObj,valueArray,noClear})
+ - divObj = divObj || {};
+ .fill-left&attributes(divObj)&attributes(attributes)
+ each value,index in valueArray
+ - const usedObj = {...radioObj,value};
+ -
+ usedObj.class = usedObj.class ?
+ `${usedObj.class} fill-left__radio` :
+ `fill-left__radio`;
+ if value === radioObj.value
+ - usedObj.checked = '';
+ if !noClear && index === 0
+ - usedObj.class = `${usedObj.class} fill-left__radio--clearer`;
+
+ +radio(usedObj)
+//- End Mixin
+-
+ varObjects.docs.pug.textarea = {
+ type:'mixin',
+ description:'Functions like [input](#input), but creates a textarea instead.',
+ example:[
+ [
+ `+textarea({name:'my hidden',class:'some-class',placeholder:'Placeholder text',trigger:{affects:['other_attribute']}})`,
+ ''
+ ]
+ ]
+ };
+mixin textarea(obj)
+ - obj.class = obj.class ? replaceProblems(obj.class) : undefined;
+ - obj.name = replaceSpaces(obj.name);
+ - obj.title = obj.title || attrTitle(obj.name);
+ - obj.name = `attr_${obj.name}`;
+ - addFieldToFieldsetObj(obj);
+ - storeTrigger(obj);
+ - const elementObj = makeElementObj(obj);
+ textarea&attributes(elementObj)
+//-End Mixin
+-
+ varObjects.docs.pug.option = {
+ type:'mixin',
+ description:'Creates an option attribute. Also stores the trigger for the select that the option is part of. See [select](#select) for example uses.'
+ };
+mixin option(obj)
+ - obj.class = obj.class ? replaceProblems(obj.class) : undefined;
+ - const triggerObj = {...obj,name:selectName,title:selectTitle,type:'select'};
+ - const elementObj = makeElementObj(obj);
+ - storeTrigger(triggerObj);
+ option&attributes(elementObj)
+ block
+//-End Mixin
+-
+ varObjects.docs.pug.select = {
+ type:'mixin',
+ description:'Functions like [input](#input), but creates a select instead.',
+ example:[
+ [
+ `+select({name:'my select',class:'some-class'})\n +option({value:'option 1','data-i18n':'some-text',trigger:{affects:['some-attribute']}})\n +option({value:'option 2','data-i18n':'other-text'})`,
+ '\n \n \n '
+ ]
+ ]
+ };
+mixin select(obj)
+ - obj.class = obj.class ? replaceProblems(obj.class) : undefined;
+ - obj.name = replaceSpaces(obj.name);
+ - obj.title = obj.title || attrTitle(obj.name);
+ - obj.name = `attr_${obj.name}`;
+ - selectName = obj.name;
+ - selectTitle = obj.title;
+ - addFieldToFieldsetObj(obj);
+ select&attributes(obj)
+ if block
+ block
+ else
+ +option({value:'empty select, PEBKAC',selected:true})
+//-End Mixin
+-
+ varObjects.docs.pug.img = {
+ type:'mixin',
+ description:'Functions like [input](#input), but creates an img instead. The name property is optional.',
+ example:[
+ [
+ `+img({name:'my image',src:'my_image.jpg',class:'some-class'})`,
+ ' '
+ ]
+ ]
+ };
+mixin img(obj)
+ - obj.class = obj.class ? replaceProblems(obj.class) : undefined;
+ - obj['data-i18n-alt'] = obj['data-i18n-alt'] || obj.name;
+ - obj.name = replaceSpaces(obj.name);
+ - obj.title = obj.title || attrTitle(obj.name);
+ - obj.name = `attr_${obj.name}`;
+ img&attributes(obj)
+//-End Mixin
+-
+ varObjects.docs.pug.img = {
+ type:'mixin',
+ description:'Functions like [input](#input), but creates a span instead. The name property is optional.',
+ example:[
+ [
+ `+span({name:'my span',class:'some-class'})`,
+ ' '
+ ]
+ ]
+ };
+mixin span(obj)
+ - obj.class = obj.class ? replaceProblems(obj.class) : undefined;
+ if obj.name
+ - obj.name = replaceSpaces(obj.name);
+ - obj.title = obj.title || attrTitle(obj.name);
+ - obj.name = `attr_${obj.name}`;
+ - addFieldToFieldsetObj(obj);
+ - const elementObj = makeElementObj(obj);
+ span&attributes(elementObj)
+ block
+ if obj.name
+ - obj.type = 'span';
+ - storeTrigger(obj);
+//-End Mixin
+-
+ varObjects.docs.pug.datalist = {
+ type:'mixin',
+ description:'Functions like [input](#input), but creates a datalist instead. Note that an ID should never be put inside a repeating section, although you can reference one from inside the repeating section.',
+ example:[
+ [
+ `+select({name:'my select',list:'my-data'})\n+datalist({id:'my-data'})\n +option({value:'option 1','data-i18n':'some-text',trigger:{affects:['some-attribute']}})\n +option({value:'option 2','data-i18n':'other-text'})`,
+ '\n \n \n '
+ ]
+ ]
+ };
+mixin datalist(obj)
+ - obj.class = obj.class ? replaceProblems(obj.class) : undefined;
+ datalist&attributes(obj)
+ block
+//-End Mixin
+mixin div(obj)
+ - obj.class = obj.class ? replaceProblems(obj.class) : undefined;
+ if obj.name
+ - obj.name = replaceSpaces(obj.name);
+ - obj.title = obj.title || attrTitle(obj.name);
+ - obj.name = `attr_${obj.name}`;
+ div&attributes(obj)
+ block
+//-End Mixin
+mixin h1(obj)
+ - obj.class = obj.class ? replaceProblems(obj.class) : undefined;
+ h1&attributes(obj)
+ block
+//-End Mixin
+mixin h2(obj)
+ - obj.class = obj.class ? replaceProblems(obj.class) : undefined;
+ h2&attributes(obj)
+ block
+//-End Mixin
+mixin h3(obj)
+ - obj.class = obj.class ? replaceProblems(obj.class) : undefined;
+ h3&attributes(obj)
+ block
+//-End Mixin
+mixin h4(obj)
+ - obj.class = obj.class ? replaceProblems(obj.class) : undefined;
+ h4&attributes(obj)
+ block
+//-End Mixin
+mixin h5(obj)
+ - obj.class = obj.class ? replaceProblems(obj.class) : undefined;
+ h5&attributes(obj)
+ block
+//-End Mixin
+mixin h6(obj)
+ - obj.class = obj.class ? replaceProblems(obj.class) : undefined;
+ h6&attributes(obj)
+ block
+//-End Mixin
+mixin p(obj)
+ - obj.class = obj.class ? replaceProblems(obj.class) : undefined;
+ p&attributes(obj)
+ block
+//-End Mixin
+-
+ varObjects.docs.pug.button = {
+ type:'mixin',
+ description:'Creates a button element. Valid types are `roll` or `action`. If a type is not specified in the object argument, a roll button is created. If an action button is created, spaces in the name are replaced with dashes instead of underscores.',
+ example:[
+ [
+ `+button({name:'my button',value:'/r 3d10'})`,
+ ' '
+ ],
+ [
+ `+button({name:'my button',type:'action','data-i18n':'action button'})`,
+ ' '
+ ]
+ ]
+ };
+mixin button(obj)
+ - obj.class = obj.class ? replaceProblems(obj.class) : undefined;
+ - obj.name = replaceSpaces(obj.name);
+ - obj.title = obj.title || buttonTitle(obj.name);
+ if obj.type === 'action'
+ - obj.name = `act_${obj.name}`;
+ else
+ - obj.type = 'roll';
+ - obj.name = `roll_${obj.name}`;
+ - const elementObj = makeElementObj(obj);
+ if obj.type !== 'roll'
+ - storeTrigger(obj);
+ button&attributes(elementObj)
+ block
+//-End Mixin
+-
+ varObjects.docs.pug.action = {
+ type:'mixin',
+ description:'Alias for [button](#button) that creates a button element with a type of `action`. Spaces in the name are replaced with dashes instead of underscores.',
+ example:[
+ [
+ `+action({name:'my button','data-i18n':'action button'})`,
+ ' '
+ ]
+ ]
+ };
+mixin action(obj)
+ - obj.class = obj.class ? replaceProblems(obj.class) : undefined;
+ - obj.type = 'action';
+ - obj.name = obj.name.replace(/\s+/g,'-');
+ +button(obj)
+ block
+//- End Mixin
+-
+ varObjects.docs.pug.navButton = {
+ type:'mixin',
+ description:'Alias for [button](#button) that creates a button element with a type of `action` for use in nav buttons. Spaces in the name are replaced with dashes instead of underscores. The name is prefixed with `nav_`. A [trigger](#trigger) object should be passed',
+ example:[
+ [
+ `+navButton({name:'my button','data-i18n':'action button'})`,
+ ' '
+ ]
+ ]
+ };
+mixin navButton(obj)
+ - addIfUnique(obj.name,'navButtons');
+ - obj.name = `nav ${obj.name}`;
+ +action(obj)
+ block
+//- End Mixin
+mixin rollerExtras(obj)
+ - obj.class = obj.class ? `${replaceProblems(obj.class)} roller` : 'roller';
+ - let attrName = replaceSpaces(actionInputName(obj.name));
+ - let actionName = actionButtonName(attrName);
+ - obj.value = `@{${attrName}}`;
+ - actionObj = {name:actionName,hidden:''};
+ - actionObj.trigger = obj.trigger || {listenerFunc:'initiateRoll'};
+ block
+ - addIfUnique(`${repeatingPrefix}${attrName}`,'actionAttributes');
+ +action(actionObj)
+ +hidden({name:attrName})
+//- End Mixin
+-
+ varObjects.docs.pug.navButton = {
+ type:'mixin',
+ description:'Alias for [button](#button) that creates a combination roll button and action button element to get around the limitation that action buttons cannot be dragged to the macro quickbar. Requires sheetworker infrastructure to work, which should be triggered `on(sheet:opened)` and `on("change:character_name")`.',
+ example:[
+ [
+ `+roller({name:'my roll'})`,
+ ' \n \n '
+ ]
+ ]
+ };
+mixin roller(obj)
+ +rollerExtras(obj)
+ - let rollObj = {...obj};
+ - delete rollObj.trigger;
+ +button(obj)
+ block
+mixin section(obj)
+ - obj.class = obj.class ? replaceProblems(obj.class) : undefined;
+ section&attributes(obj)
+ block
+//-End Mixin
+mixin main(obj)
+ - obj.class = obj.class ? replaceProblems(obj.class) : undefined;
+ main&attributes(obj)
+ block
+//-End Mixin
+mixin header(obj)
+ - obj.class = obj.class ? replaceProblems(obj.class) : undefined;
+ header&attributes(obj)
+ block
+//-End Mixin
+mixin footer(obj)
+ - obj.class = obj.class ? replaceProblems(obj.class) : undefined;
+ footer&attributes(obj)
+ block
+//-End Mixin
+mixin details(obj)
+ - obj.class = obj.class ? replaceProblems(obj.class) : undefined;
+ details&attributes(obj)
+ block
+//-End Mixin
+mixin summary(obj)
+ - obj.class = obj.class ? replaceProblems(obj.class) : undefined;
+ summary&attributes(obj)
+ block
+//-End Mixin
+mixin label(obj)
+ - obj.class = obj.class ? replaceProblems(obj.class) : undefined;
+ label&attributes(obj)
+ block
+//-End Mixin
+mixin repeating_section(name,header,columnArr,id)
+ section(class=`repeating-container--${name} repeating-container section` id=`${(id||name).replace(/\s+/g,'-')}`)
+ if header
+ h2(data-i18n=header)
+ if columnArr
+ .repeat-columns
+ each head in columnArr
+ h5(data-i18n=head)
+ +customControlFieldset(name)
+ block
+//- End Mixin
+-
+ varObjects.docs.pug.customControlFieldset = {
+ type:'mixin',
+ description:'Alias for [fieldset](#fieldset) that creates to custom action buttons to add/remove rows to the repeating section. Useful when you need to trigger a sheetworker when a row is added. This also prevents the occassional error of a new row disappearing immediately after the user has clicked the button to create one. Proper use of this will require css to hide the default buttons that fieldsets create automatically. Note that currently this assumes the existence of an addItem and editSection sheetworker function.',
+ arguments:[
+ {name:'name',type:'string',description:'The name of the repeating section. Will be prefixed with `repeating_` and spaces will be replaced with dashes (`-`).'},
+ {name:'trigger',type:'object',description:'Trigger that defines how to handle the removal of a row from the fieldset. `Optional`'},
+ {name:'addClass',type:'string',description:'Any additional classes that should be used for the repeating section. Note that these are not added to the fieldset itself as adding additional classes to the fieldset itself interferes with calling action buttons from chat, but are added to a span that precedes the fieldset. This allows styling of the repcontainer via a css declaration like `.bonus-class + fieldset + .repcontainer`.'}
+ ],
+ example:[
+ [
+ `+customControlFieldset({name:'equipment'})\n +text({name:'name',class:'underlined'})`,
+
+ ' \n \n\n \n '
+ ]
+ ]
+ };
+mixin customControlFieldset({name,trigger,addClass})
+ +action({name:`add ${name}`,class:'repcontrol-button repcontrol-button--add',trigger:{listenerFunc:'addItem'}})
+ +action({name:`edit ${name}`,class:'repcontrol-button repcontrol-button--edit',trigger:{listenerFunc:'editSection'}})
+ +fieldset({name,trigger,addClass})
+ block
+//- End Mixin
+mixin inlineFieldset({name,trigger,addClass})
+ - addClass = addClass ? `${addClass} inline-fieldset` : 'inline-fieldset';
+ - varObjects.inlineFieldsets = varObjects.inlineFieldsets || [];
+ - varObjects.inlineFieldsets.push(name);
+ +fieldset({name,trigger,addClass})
+ +radio({name:'display state',class:'display-control',value:'short-display',hidden:''})
+ .inline-fieldset__summary.display-target
+ label.pointer
+ +checkbox({name:'collapse',value:1,hidden:'',trigger:{triggeredFuncs:['collapseSection']}})
+ +span({name:'name',class:'inline-fieldset__summary__text'})
+ +radio({name:'display state',class:'display-control',value:'display',checked:'',hidden:''})
+ .inline-fieldset__detail.display-target
+ +pseudoCollapse('collapse',{'data-i18n-title':collapseTitle})
+ block
+ +action({name:`add ${name}`,class:'repcontrol-button repcontrol-button--add repcontrol-button--inline',trigger:{listenerFunc:'sectionInteract'}})
+ +action({name:`edit ${name}`,class:'repcontrol-button repcontrol-button--edit repcontrol-button--inline',trigger:{listenerFunc:'sectionInteract'}})
+//- End Mixin
+-
+ varObjects.docs.pug.fieldset = {
+ type:'mixin',
+ description:'A mixin that creates a fieldset for the creation of a repeating section. The mixin prefixes the name with `repeating_` and replaces problem characters (e.g. spaces are replaced with dashes). Additionally, the auto-generated title properties from the K-scaffold\'s mixins will include the proper repeating section information.',
+ arguments:[
+ {name:'name',type:'string',description:'The name of the repeating section. Will be prefixed with `repeating_` and spaces will be replaced with dashes (`-`).'},
+ {name:'trigger',type:'object',description:'Trigger that defines how to handle the removal of a row from the fieldset. `Optional`'},
+ {name:'addClass',type:'string',description:'Any additional classes that should be used for the repeating section. Note that these are not added to the fieldset itself as adding additional classes to the fieldset itself interferes with calling action buttons from chat, but are added to a span that precedes the fieldset. This allows styling of the repcontainer via a css declaration like `.bonus-class + fieldset + .repcontainer`.'}
+ ],
+ example:[
+ [
+ `+fieldset({name:'equipment'})\n +text({name:'name',class:'underlined'})`,
+
+ '\n \n
'
+ ],
+ [
+ `+fieldset({name:'equipment',trigger:{listenerFunc:'handleDeletedRow'},addClass:'class-1 class-2'})\n +text({name:'name',class:'underlined'})`,
+
+ ` \n\n \n
`
+ ]
+ ]
+ };
+mixin fieldset({name,trigger,addClass})
+ - name = name.replace(/\s+/g,'-');
+ - let section = `repeating_${name}`;
+ - repeatingPrefix = `${section}_$X_`;
+ - createFieldsetObj(section)
+ if trigger
+ - storeTrigger({name:section,type:'fieldset',trigger})
+ if addClass
+ span(hidden="" class=addClass)
+ fieldset(class=`${section}`)
+ block
+ - repeatingPrefix = '';
+//-End Mixin
+-
+ varObjects.docs.pug['pseudo-button'] = {
+ type:'mixin',
+ description:'A mixin for creating pseudo buttons of paired checkboxes/radios and spans such as those that had to be used to create [tabbed sheets](https://wiki.roll20.net/CSS_Wizardry#Show.2FHide_Areas) before action buttons were introduced.',
+ arguments:[
+ {name:'label',type:'string',description:'The `data-i18n` translation key to use for the displayed text.'},
+ {name:'inputObj',type:'object',description:'An object describing the input to be paired with the span. This is the same object that you would pass to [input](#input).'},
+ ],
+ example:[
+ [
+ `+pseudo-button('page 1',{name:'page',type:'radio',value:1,trigger:{triggeredFuncs:['changePage']}})`,
+
+ ` \n \n `
+ ]
+ ]
+ };
+mixin pseudo-button({label,inputObj,divObj,spanObj})
+ - inputObj.class = inputObj.class ? replaceProblems(inputObj.class) : undefined;
+ label.pseudo-button&attributes(divObj)
+ - inputObj.class = inputObj.class ? `${inputObj.class} pseudo-button__input` : 'pseudo-button__input';
+ +input(inputObj)
+ span(data-i18n=label class='pseudo-button__span')&attributes(spanObj)
+//-End Mixin
+-
+ varObjects.docs.pug['button-label'] = {
+ type:'mixin',
+ description:'A mixin to create a combined button and input that are within the same container. Similar to [input-label](#input-label), but does not use a label.',
+ arguments:[
+ {name:'inputObj',type:'object',description:'An object describing the input to be paired with the button. This is the same object that you would pass to [input](#input).'},
+ {name:'buttonObj',type:'object',description:'An object describing the button to be paired with the input. This is the same object that you would pass to [button](#button).'},
+ {name:'divObj',type:'object',description:'An object describing the container div. Similar to the first two objects, but will most likely only have a `class` property if it is passed at all.'}
+ ],
+ example:[
+ [
+ `+button-label({inputObj:{name:'strength',type:'number',class:'underlined',value:10,trigger:{affects:['athletics']}},buttonObj:{name:'strength_roll',type:'roll',value:'/r 1d20+@{strength}'},divObj:{class:'strength'}})`,
+
+ `\n \n \n
\n \n \n \n \n`
+ ]
+ ]
+ };
+mixin select-label({label,inputObj,divObj,spanObj})
+ if divObj
+ - divObj.class = divObj.class ? replaceProblems(divObj.class) : undefined;
+ if inputObj
+ - inputObj.class = inputObj.class ? replaceProblems(inputObj.class) : undefined;
+ if spanObj
+ - spanObj.class = spanObj.class ? replaceProblems(spanObj.class) : undefined;
+ label.input-label&attributes(divObj)
+ - inputObj.name = inputObj.name.replace(/\s+/g,'_');
+ - inputObj.class = inputObj.class ? `${inputObj.class} input-label__input` : 'input-label__input';
+ if spanObj
+ - spanObj.class = spanObj.class ? `${spanObj.class} input-label__text` : 'input-label__text';
+ span(data-i18n=label)&attributes(spanObj)
+ +select(inputObj)
+ block
+//-End Mixin
+-
+ varObjects.docs.pug['input-label'] = {
+ type:'mixin',
+ description:'Creates a construction that nests the input and span in a label so that they are connected. This is beneficial for screen readers as well as making it easier to interact with the input via mouse by clicking on the text associated with it.',
+ arguments:[
+ {name:'label',type:'string',description:'The `data-i18n` translation key to add to the span in the label.'},
+ {name:'inputObj',type:'object',description:'An object describing the input to be paired with the button. This is the same object that you would pass to [input](#input).'},
+ {name:'divObj',type:'object',description:'An object describing the container label. Similar to the inputObj, but will most likely only have a `class` property if it is passed at all.'},
+ {name:'spanObj',type:'object',description:'An object describing the span to be paired with the input. This is the same object that you would pass to [span](#span).'},
+ {name:'block',type:'block',description:'The mixin uses the pug block as the content of the select.'}
+ ],
+ example:[
+ [
+ `+input-label({label:'strength',inputObj:{name:'strength',type:'number'},divObj:{class:'div-class'},spanObj:{class:'span-class'}})`,
+
+ ` \n \n `
+ ]
+ ]
+ };
+mixin input-label({label,inputObj,divObj,spanObj})
+ if divObj
+ - divObj.class = divObj.class ? replaceProblems(divObj.class) : undefined;
+ if inputObj
+ - inputObj.class = inputObj.class ? replaceProblems(inputObj.class) : undefined;
+ if spanObj
+ - spanObj.class = spanObj.class ? replaceProblems(spanObj.class) : undefined;
+ label.input-label&attributes(divObj)
+ - inputObj.name = inputObj.name.replace(/\s+/g,'_');
+ - inputObj.class = inputObj.class ? `${inputObj.class} input-label__input` : 'input-label__input';
+ if spanObj
+ - spanObj.class = spanObj.class ? `${spanObj.class} input-label__text` : 'input-label__text';
+ span(data-i18n=label)&attributes(spanObj)
+ +input(inputObj)
+//-End Mixin
+mixin dual-input-label({label,inputArr,divObj,spanObj})
+ if divObj
+ - divObj.class = divObj.class ? replaceProblems(divObj.class) : undefined;
+ if spanObj
+ - spanObj.class = spanObj.class ? replaceProblems(spanObj.class) : undefined;
+ .input-label.input-label--dual&attributes(divObj)
+ span(data-i18n=label)&attributes(spanObj)
+ each inputObj,i in inputArr
+ if inputObj
+ - inputObj.class = inputObj.class ? replaceProblems(inputObj.class) : undefined;
+ - inputObj.name = inputObj.name.replace(/\s+/g,'_');
+ +input(inputObj)
+ if i<1
+ span(class='slash h2')='/'
+//-End Mixin
+-
+ varObjects.docs.pug.headedTextarea = {
+ type:'mixin',
+ description:'Creates a construction for pairing a header with a textarea. Currently is locked to creating an `h3`. This mixin also accepts classes and IDs appended directly to it (see the second example)',
+ arguments:[
+ {name:'textObj',type:'object',description:'The object describing the textarea as per [textarea](#textarea)'},
+ {name:'header',type:'string',description:'The `data-i18n` translation key to use for the header'}
+ ],
+ example:[
+ [
+ `+headedTextarea({textObj:{name:'character description','data-i18n-placeholder':'The description of your character'},header:'description'})`,
+
+ `\n
\n \n`
+ ],
+ [
+ `+headedTextarea({textObj:{name:'character description','data-i18n-placeholder':'The description of your character'},header:'description'}).character-description`,
+
+ `\n
\n \n`
+ ]
+ ]
+ };
+mixin headedTextarea({textObj,header})
+ .headed-textarea&attributes(attributes)
+ - textObj.class = textObj.class ? `${textObj.class} headed-textarea__textarea` : 'headed-textarea__textarea';
+ +h3({'data-i18n':header,class:'headed-textarea__header'})
+ +textarea(textObj)
+//-End Mixin
+-
+ varObjects.docs.pug.script = {
+ type:'mixin',
+ description:'Creates a generic [Roll20 script block](https://wiki.roll20.net/Building_Character_Sheets#JavaScript_2) for use with the sheetworker system.'
+ };
+-
+ varObjects.docs.pug.adaptiveTextarea = {
+ type:'mixin',
+ description:'Creates an html construction for creating a [content-scaled](https://wiki.roll20.net/CSS_Wizardry#Content-scaled_Inputs) textarea.',
+ arguments:[
+ {name:'textObj',type:'object',description:'The object describing the textarea as per the [textarea](#textarea) mixin. You can apply classes and IDs to the container div by appending them to the mixin call (see the second example).'}
+ ],
+ example:[
+ [
+ `+adaptiveTextarea({name:'character description'})`,
+
+ ` \n \n
`
+ ],
+ [
+ `+adaptiveTextarea({name:'character description'}).custom-class`,
+
+ ` \n \n
`
+ ]
+ ]
+ };
+mixin adaptiveTextarea(textObj)
+ .adaptive.adaptive--text&attributes(attributes)
+ - let spanObj = {name:textObj.name,class:'adaptive--text__span'};
+ - textObj.class = textObj.class ? `${textObj.class} adaptive--text__textarea` : 'adaptive--text__textarea';
+ +span(spanObj)
+ +textarea(textObj)
+//- End Mixin
+//-
+ varObjects.docs.pug.adaptiveInput = {
+ type:'mixin',
+ description:'Creates an html construction for creating a [content-scaled](https://wiki.roll20.net/CSS_Wizardry#Content-scaled_Inputs) input.',
+ arguments:[
+ {name:'textObj',type:'object',description:'The object describing the input as per the [input](#input) mixin. You can apply classes and IDs to the container div by appending them to the mixin call (see the second example).'}
+ ],
+ example:[
+ [
+ `+adaptiveInput({name:'character description',type:'text'})`,
+
+ ` \n \n
`
+ ],
+ [
+ `+adaptiveInput({name:'character description',type:'text'}).custom-class`,
+
+ ` \n \n
`
+ ]
+ ]
+ };
+mixin adaptiveInput(textObj)
+ .adaptive.adaptive--input&attributes(attributes)
+ - let spanObj = {name:textObj.name,class:'adaptive--input__span','max-width':maxWidth};
+ - textObj.class = textObj.class ? `${textObj.class} adaptive--input__input` : 'adaptive--input__input';
+ +span(spanObj)
+ +input(textObj)
+//- End Mixin
+-
+ varObjects.docs.pug.compendiumAttributes = {
+ type:'mixin',
+ description:'Creates a set of compendium drop target attributes. Defaults to creating target attributes for the `Name` and `data` compendium attributes.',
+ arguments:[
+ {name:'prefix',type:'string',description:'A prefix to attach to the default attribute names.'},
+ {name:'lookupAttributes',type:'array',description:'An array of the lookup attributes to create targets for. The target attributes are named based on the compendium attribute they are for. lookupAttributes defaults to `["Name","data"]`.'},
+ {name:'triggerAccept',type:'string',description:'The compendium attribute that should trigger the sheetworkers to handle the compendium drop.'},
+ {name:'trigger',type:'object',description:'The trigger object. Defaults to `{listenerFunc:"handleCompendiumDrop"}`'}
+ ],
+ example:[
+ [
+ `+compendiumAttributes({})`,
+
+ ` \n `
+ ],
+ [
+ `+compendiumAttributes({prefix:'prefix'})`,
+
+ ` \n `
+ ],
+ [
+ `+compendiumAttributes({lookupAttributes:['Name','data','Category'],prefix:'prefix})`,
+
+ ` \n \n `
+ ]
+ ]
+ };
+mixin compendiumAttributes({prefix,lookupAttributes = ['Name','data'],triggerAccept = 'Name',trigger = {listenerFunc:'handleCompendiumDrop'}})
+ - prefix = prefix ? `${prefix} ` : '';
+ each accept in lookupAttributes
+ - let inputObj = {name:`${prefix}drop ${accept.toLowerCase()}`,accept,value:''};
+ if accept === triggerAccept
+ - inputObj.trigger = trigger;
+ +hidden(inputObj)
+//- End Mixin
+//- Functions
+-
+ varObjects.docs.pug.attrTitle = {
+ type:'function',
+ description:'Converts an attribute name into an attribute call for that attribute. Converts `_max` attribute names to the proper attribute call syntax for `_max` attributes (see second example). If called from inside the block of a [fieldset](#fieldset) mixin, will also add the appropriate information for calling a repeating attribute.',
+ arguments:[
+ {name:'attrName',type:'string',description:'The attribute name to create an attribute call for.'}
+ ],
+ example:[
+ [
+ `- attrTitle('strength') => "@{strength}"`
+ ],
+ [
+ `- attrTitle('hp_max') => "@{hp|max}"`
+ ],
+ [
+ `+fieldset({name:'equipment'})\n - attrTitle('weight') => "@{repeating_equipment_$X_weight}"`
+ ]
+ ],
+ retValue:{
+ type:'string'
+ }
+ };
+mixin script
+ script(type='text/worker')
+ block
+//-End Mixin
+-
+ varObjects.docs.pug.kscript = {
+ type:'mixin',
+ description:'Similar to [script](#script), but includes the K-scaffold\'s javascript function library.'
+ };
+mixin kscript(document=false)
+ +script
+ |const k = (function(){
+ |const kFuncs = {};
+ |const document = #{document};
+ //- The below declarations import variables from the pug file and mixins into the sheetworker code
+ - const propArray = ['cascades','repeatingSectionDetails','docs'];
+ each prop in propArray
+ |
+ |const !{prop} = !{JSON.stringify(varObjects[prop])};
+ |
+ |kFuncs.!{prop} = !{prop};
+ - delete varObjects[prop];
+ |
+ |
+ include kvariables.js
+ include utility.js
+ include attribute_proxy.js
+ include accessSheet.js
+ include parse_cascade.js
+ include sheetworker_aliases.js
+ include listeners.js
+ |return kFuncs;
+ |}());
+ |
+ each content,prop in varObjects
+ |
+ if typeof content === 'object'
+ |const !{prop} = !{JSON.stringify(content)};
+ else
+ |let !{prop} = !{content};
+ |
+ |
+ block
+//- End Mixin
+- const attrTitle = (string) => `@{${repeatingPrefix}${replaceSpaces(string).replace(/_max$/,'|max')}}`;
+-
+ varObjects.docs.pug.buttonTitle = {
+ type:'function',
+ description:'Converts an ability name into an ability call for that attribute. If called from inside the block of a [fieldset](#fieldset) mixin, will also add the appropriate information for calling a repeating attribute.',
+ arguments:[
+ {name:'abilityName',type:'string',description:'The ability name to create an attribute call for.'}
+ ],
+ example:[
+ [
+ `- buttonTitle('strength') => "%{strength}"`
+ ],
+ [
+ `+fieldset({name:'equipment'})\n - buttonTitle('use') => "%{repeating_equipment_$X_use}"`
+ ]
+ ],
+ retValue:{
+ type:'string'
+ }
+ };
+- const buttonTitle = (string) => `%{${repeatingPrefix}${replaceSpaces(string)}}`;
+-
+ varObjects.docs.pug.replaceSpaces = {
+ type:'function',
+ description:'Replaces spaces in a string with underscores (`_`).',
+ arguments:[
+ {name:'string',type:'string',description:'The string to work on'}
+ ],
+ example:[
+ [
+ `- replaceSpaces('attribute name') => "attribute_name"`
+ ]
+ ],
+ retValue:{
+ type:'string'
+ }
+ };
+- const replaceSpaces = (string) => string.replace(/\s+/g,'_');
+-
+ varObjects.docs.pug.replaceProblems = {
+ type:'function',
+ description:'Escapes problem characters in a string for use as a regex.',
+ arguments:[
+ {name:'string',type:'string',description:'The string to work on'}
+ ],
+ example:[
+ [
+ `- replaceProblems("Here's a problem => [") => "Here's a problem => \["`
+ ]
+ ],
+ retValue:{
+ type:'string'
+ }
+ };
+- const replaceProblems = (string) => string.replace(/[\(\)\[\]\|\/\\]/g,'-');
+-
+ varObjects.docs.pug.capitalize = {
+ type:'function',
+ description:'Capitalizes the first let of words in a string.',
+ arguments:[
+ {name:'string',type:'string',description:'The string to apply capitalization to.'}
+ ],
+ retValue:{type:'string'}
+ };
+-
+ const capitalize = (string)=> string.replace(/(?:^|\s+|\/)[a-z]/ig,(letter)=>letter.toUpperCase());
+
+- const actionButtonName = (name) => `${name.replace(/_|\s+/g,'-')}`;
+- const actionInputName = (name) => `${name}_action`.replace(/roll_action/,'action');
+- const titleToName = (string) => string.replace(/[@%]\{|\}/g,'');
+-
+ const addIfUnique = (item,arrName) => {
+ varObjects[arrName] = varObjects[arrName] || [];
+ if(varObjects[arrName].indexOf(item) === -1){
+ varObjects[arrName].push(item);
+ }
+ };
+-
+ const storeTrigger = function(element){
+ let trigger = element.trigger || {};
+ const namePrefix = {
+ roll:'roll_',
+ action:'act_',
+ fieldset:'fieldset_'
+ };
+ const typeDefs = {
+ select:'',
+ radio:0,
+ checkbox:0,
+ number:0,
+ text:'',
+ span:''
+ };
+ const eventTypes = {
+ roll:'clicked',
+ action:'clicked',
+ fieldset:'remove'
+ };
+ let elementName = element.title ?
+ titleToName(element.title) :
+ element.name;
+ trigger.name = elementName.replace(/\|/g,'_');
+ let cascName = `${namePrefix[element.type] || 'attr_'}${trigger.name}`;
+ let match = trigger.name.match(/(repeating_[^_]+)_[^_]+_(.+)/);
+ let [,section,field] = match || [,,trigger.name];
+ let eventType = eventTypes[element.type] || 'change';
+ if(!varObjects.cascades[cascName]){
+ if(trigger.listener || trigger.triggeredFuncs || trigger.listenerFunc || trigger.initialFunc || trigger.affects){
+ trigger.listener = trigger.listener || `${eventType}:${section ? `${section}:` : ''}${field}`;
+ trigger.listenerFunc = trigger.listenerFunc || 'accessSheet';
+ }
+ trigger.type = element.type;
+ if(!namePrefix[element.type]){
+ trigger.defaultValue = trigger.hasOwnProperty('defaultValue') ?
+ trigger.defaultValue :
+ (element.type === 'checkbox' && !element.hasOwnProperty('checked')) ?
+ 0 :
+ element.hasOwnProperty('value') ?
+ element.value :
+ typeDefs.hasOwnProperty(element.type) ?
+ typeDefs[element.type] :
+ '';
+ trigger.triggeredFuncs = trigger.triggeredFuncs || [];
+ if(trigger.affects){
+ trigger.affects = trigger.affects.map((affect)=>replaceSpaces(affect));
+ }else{
+ trigger.affects = [];
+ }
+ }
+ varObjects.cascades[cascName] = {...trigger};
+ }else{
+ if(!namePrefix[varObjects.cascades[cascName].type]){
+ varObjects.cascades[cascName].triggeredFuncs = trigger.triggeredFuncs ?
+ [...new Set([...varObjects.cascades[cascName].triggeredFuncs,...trigger.triggeredFuncs])] :
+ varObjects.cascades[cascName].triggeredFuncs;
+ varObjects.cascades[cascName].affects = trigger.affects ?
+ [...new Set([...varObjects.cascades[cascName].affects,...trigger.affects])] :
+ varObjects.cascades[cascName].affects;
+ varObjects.cascades[cascName].calculation = varObjects.cascades[cascName].calculation ||
+ trigger.calculation;
+ }
+ if(trigger.listenerFunc || trigger.triggeredFuncs || trigger.affects){
+ varObjects.cascades[cascName].listener = varObjects.cascades[cascName].listener || trigger.listener || `${eventType}:${section ? `${section}:` : ''}${field}`;
+ varObjects.cascades[cascName].listenerFunc = varObjects.cascades[cascName].listenerFunc || trigger.listenerFunc || 'accessSheet';
+ }
+ }
+ };
+-
+ const getSectionDetails = function(section){
+ return varObjects.repeatingSectionDetails.find((obj)=>obj.section === section);
+ };
+-
+ const createFieldsetObj = function(section){
+ !getSectionDetails(section) ?
+ varObjects.repeatingSectionDetails.push({section,fields:[]}) :
+ null;
+ };
+-
+ const addFieldToFieldsetObj = function(obj){
+ let section = repeatingPrefix.replace(/_[^_]+_$/,'');
+ let sectionDetails = getSectionDetails(section);
+ let name = obj.name.replace(/^attr_/,'');
+ if(sectionDetails && sectionDetails.fields.indexOf(name) < 0){
+ sectionDetails.fields.push(name);
+ }
+ };
+-
+ const makeElementObj = function(obj){
+ const newObj = {...obj};
+ delete newObj.trigger;
+ return newObj;
+ };
\ No newline at end of file
diff --git a/under_the_neighborhood/source/scaffold/_kpug.pug b/under_the_neighborhood/source/scaffold/_kpug.pug
new file mode 100644
index 0000000000..c56258d585
--- /dev/null
+++ b/under_the_neighborhood/source/scaffold/_kpug.pug
@@ -0,0 +1,2 @@
+include _htmlelements.pug
+include _rolltemplate_mixins.pug
\ No newline at end of file
diff --git a/under_the_neighborhood/source/scaffold/_rolltemplate_mixins.pug b/under_the_neighborhood/source/scaffold/_rolltemplate_mixins.pug
new file mode 100644
index 0000000000..35cdffa3bb
--- /dev/null
+++ b/under_the_neighborhood/source/scaffold/_rolltemplate_mixins.pug
@@ -0,0 +1,69 @@
+//-Roll Template Mixins to be included in Sheet files
+mixin rolltemplate(name)
+ rolltemplate(class=`sheet-rolltemplate-${name}`)
+ block
+//- End Mixin
+mixin templateWrapper(name)
+ +rolltemplate(name)
+ div(class=`template ${name}`)
+ block
+//- End Mixin
+mixin multiPartTemplate(name)
+ rolltemplate(class=`sheet-rolltemplate-${name}`)
+ +templateHelperFunction({func:'rollBetween',values:'computed::finished 0 0',invert:true})
+ span(class='finished')
+ div(class=`template ${name}{{#continuation}} continuation{{/continuation}}{{#first}} first{{/first}} finished`)
+ block
+//- End Mixin
+mixin characterLink
+ +templateConditionalDisplay('character_name')
+ +templateConditionalDisplay('character_id')
+ h4.character_name [{{character_name}}](http://journal.roll20.net/character/{{character_id}})
+ +templateConditionalDisplay('character_id',true)
+ h4.character_name {{character_name}}
+//-End mixin
+mixin templateConditionalDisplay(fieldBool,invert)
+ !=`{{${invert ? '^' : '#'}${fieldBool}}}`
+ block
+ !=`{{/${fieldBool}}}`
+//- End mixin
+mixin templateHelperFunction(helperObj)
+ !=`{{${helperObj.invert ? '#^':'#'}${helperObj.func ? `${helperObj.func}() `:''}${helperObj.values ? `${helperObj.values}` : ''}}}`
+ block
+ !=`{{/${(helperObj.invert) ? '^' : ''}${helperObj.func ? `${helperObj.func}() `:''}${helperObj.values}}}`
+//- End Mixin
+mixin rollWasCrit(helperObj)
+ - helperObj.func = 'rollWasCrit';
+ +templateHelperFunction(helperObj)
+ block
+//- End Mixin
+mixin rollWasFumble(helperObj)
+ - helperObj.func = 'rollWasFumble';
+ +templateHelperFunction(helperObj)
+ block
+//- End Mixin
+mixin rollTotal(helperObj)
+ - helperObj.func = 'rollTotal';
+ +templateHelperFunction(helperObj)
+ block
+//- End Mixin
+mixin rollGreater(helperObj)
+ - helperObj.func = 'rollGreater';
+ +templateHelperFunction(helperObj)
+ block
+//- End Mixin
+mixin rollLess(helperObj)
+ - helperObj.func = 'rollLess';
+ +templateHelperFunction(helperObj)
+ block
+//- End Mixin
+mixin rollBetween(helperObj)
+ - helperObj.func = 'rollBetween';
+ +templateHelperFunction(helperObj)
+ block
+//- End Mixin
+mixin allProps(helperObj)
+ - helperObj.func = 'allprops';
+ +templateHelperFunction(helperObj)
+ block
+//- End Mixin
\ No newline at end of file
diff --git a/under_the_neighborhood/source/scaffold/accessSheet.js b/under_the_neighborhood/source/scaffold/accessSheet.js
new file mode 100644
index 0000000000..ab394d8014
--- /dev/null
+++ b/under_the_neighborhood/source/scaffold/accessSheet.js
@@ -0,0 +1,128 @@
+/*jshint esversion: 11, laxcomma:true, eqeqeq:true*/
+/*jshint -W014,-W084,-W030,-W033*/
+//Sheet Updaters and styling functions
+/**
+ * An object to store your update functions in. Functions should be index by the version they are for (e.g. the update handler for version 1.01 would be indexed to '1.01'). These update functions will be iterated through based on the previous version of the sheet (as stored in the `sheet_version` attribute of the sheet) and the new version of the sheet (as stored in {@link k.version}).
+ * @type {object}
+ */
+const updateSheet = function(){
+ log('updating sheet');
+ getAllAttrs({props:['debug_mode',...baseGet],callback:(attributes,sections,casc)=>{
+ kFuncs.debugMode = kFuncs.debugMode || !!attributes.debug_mode;
+ debug({sheet_version:attributes.sheet_version});
+ if(!attributes.sheet_version){
+ Object.entries(initialSetups).forEach(([funcName,handler])=>{
+ if(typeof funcs[funcName] === 'function'){
+ debug(`running ${funcName}`);
+ funcs[funcName]({attributes,sections,casc});
+ }else{
+ debug(`!!!Warning!!! no function named ${funcName} found. Initial sheet setup not performed.`);
+ }
+ });
+ }else{
+ Object.entries(updateHandlers).forEach(([ver,handler])=>{
+ if(attributes.version < +ver){
+ handler({attributes,sections,casc});
+ }
+ });
+ }
+ Object.entries(openHandlers).forEach(([funcName,func])=>{
+ if(typeof funcs[funcName] === 'function'){
+ debug(`running ${funcName}`);
+ funcs[funcName]({attributes,sections,casc});
+ }else{
+ debug(`!!!Warning!!! no function named ${funcName} found. Sheet open handling not performed.`);
+ }
+ });
+ setActionCalls({attributes,sections});
+ attributes.sheet_version = kFuncs.version;
+ log(`Sheet Update applied. Current Sheet Version ${kFuncs.version}`);
+ if(document){
+ ['pug','js'].forEach((type)=>{
+ log(docGen(type));
+ });
+ }
+ attributes.set();
+ log('Sheet ready for use');
+ }});
+};
+
+const docGen = function(language){
+ debug(`generating documentation for ${language}`);
+ let docHead = [`# K Scaffold ${language.toUpperCase()} documentation`];
+ Object.keys(docs[language])
+ .sort((nameA,nameB) => nameA.localeCompare(nameB))
+ .forEach((funcName)=>{
+ docHead.push(`- [${funcName}](#${funcName.replace(/\./g,'').replace(/\s/g,'-')})`);
+ });
+ return Object.entries(docs[language])
+ .sort(([nameA,docObjA],[nameB,docObjB]) => nameA.localeCompare(nameB))
+ .reduce((text,[name,docObj]) => {
+ let type = docObj.type;
+ text.push(`## ${docObj.name || name}`,`\`${type}\`\n`);
+ if(docObj.invocation){
+ text.push(`\`\`\`js\n${docObj.invocation}\n\`\`\``);
+ }
+ if(docObj.description){
+ text.push(docObj.description);
+ }
+ let args = Array.isArray(docObj.arguments) ? docObj.arguments : [];
+ if(args.length){
+ text.push('|Argument|type|description|','|---|---|---|')
+ }
+ args.forEach((a)=>{
+ text.push(`|${a.name}|\`${a.type}\`|${a.description || ''}|`)
+ });
+ if(args.length){
+ text.push('\n')
+ }
+ let example = Array.isArray(docObj.example) ? docObj.example : [];
+ if(example.length){
+ text.push(`### Example${example.length > 1 ? 's' : ''}`);
+ }
+ example.forEach((eArr)=>{
+ text.push(
+ `**${language.toUpperCase()}**`,
+ `\`\`\`js`,
+ eArr[0],
+ `\`\`\``
+ );
+ if(type === 'mixin'){
+ text.push(
+ '**HTML**',
+ `\`\`\`html`,
+ eArr[1],
+ `\`\`\``
+ );
+ }
+ });
+ if(docObj.retValue){
+ text.push(`returns \`${docObj.retValue.type}\` ${
+ docObj.retValue.description ?
+ `- ${docObj.retValue.description}` :
+ ''
+ }`
+ );
+ }
+ return text;
+ },docHead).join('\n');
+};
+
+const initialSetup = function(attributes,sections){
+ debug('Initial sheet setup');
+};
+
+/**
+ * This is the default listener function for attributes that the K-Scaffold uses. It utilizes the `triggerFuncs`, `listenerFunc`, `calculation`, and `affects` properties of the K-scaffold trigger object (see the Pug section of the scaffold for more details).
+ * @param {Roll20Event} event
+ * @returns {void}
+ */
+const accessSheet = function(event){
+ debug({funcs:Object.keys(funcs)});
+ debug({event});
+ getAllAttrs({event,callback:(attributes,sections,casc)=>{
+ let trigger = attributes.getCascObj(event,casc);
+ attributes.processChange({event,trigger,attributes,sections,casc});
+ }});
+};
+funcs.accessSheet = accessSheet;
\ No newline at end of file
diff --git a/under_the_neighborhood/source/scaffold/attribute_proxy.js b/under_the_neighborhood/source/scaffold/attribute_proxy.js
new file mode 100644
index 0000000000..42ca3acfbe
--- /dev/null
+++ b/under_the_neighborhood/source/scaffold/attribute_proxy.js
@@ -0,0 +1,221 @@
+/*jshint esversion: 11, laxcomma:true, eqeqeq:true*/
+/*jshint -W014,-W084,-W030,-W033*/
+//# Attribute Obj Proxy handler
+const createAttrProxy = function(attrs){
+ //creates a proxy for the attributes object so that values can be worked with more easily.
+ const getCascObj = function(event,casc){
+ const eventName = event.triggerName || event.sourceAttribute;
+ let typePrefix = eventName.startsWith('clicked:') ?
+ 'act_' :
+ event.removedInfo ?
+ 'fieldset_' :
+ 'attr_';
+ let cascName = `${typePrefix}${eventName.replace(/clicked:/,'')}`;
+ let cascObj = casc[cascName];
+ return cascObj;
+ };
+
+ const triggerFunctions = function(obj,attributes,sections){
+ if(obj.triggeredFuncs && obj.triggeredFuncs.length){
+ debug(`triggering functions for ${obj.name}`);
+ obj.triggeredFuncs && obj.triggeredFuncs.forEach(func=>funcs[func] ?
+ funcs[func]({trigger:obj,attributes,sections}) :
+ debug(`!!!Warning!!! no function named ${func} found. Triggered function not called for ${obj.name}`,true));
+ }
+ };
+
+ const initialFunction = function(obj,attributes,sections){
+ if(obj.initialFunc){
+ debug(`initial functions for ${obj.name}`);
+ funcs[obj.initialFunc] ?
+ funcs[obj.initialFunc]({trigger:obj,attributes,sections}) :
+ debug(`!!!Warning!!! no function named ${obj.initialFunc} found. Initial function not called for ${obj.name}`,true);
+ }
+ };
+ const processChange = function({event,trigger,attributes,sections,casc}){
+ debug({trigger});
+ if(event && !trigger){
+ debug('initial change detected. No trigger found');
+ return;
+ }
+ if(!attributes || !sections || !casc){
+ debug(`!!! Insufficient arguments || attributes > ${!!attributes} | sections > ${!!sections} | casc > ${!!casc} !!!`);
+ return;
+ }
+ //store the queue in attributes.
+ if(event){
+ debug('checking for initial functions');
+ initialFunction(trigger,attributes,sections);//functions that should only be run if the attribute was the thing changed by the user
+ }
+ if(trigger){
+ debug(`processing ${trigger.name}`);
+ triggerFunctions(trigger,attributes,sections);
+ if(!event && trigger.calculation && funcs[trigger.calculation]){
+ attributes[trigger.name] = funcs[trigger.calculation]({trigger,attributes,sections,casc});
+ }else if(trigger.calculation && !funcs[trigger.calculation]){
+ debug(`K-Scaffold Error: No function named ${trigger.calculation} found`);
+ }
+ if(Array.isArray(trigger.affects)){
+ attributes.queue.push(...trigger.affects);
+ }
+ }
+ attributes.set({attributes,sections,casc});
+ };
+ const attrTarget = {
+ updates:{},
+ attributes:{...attrs},
+ repOrders:{},
+ queue: [],
+ casc:{},
+ processChange,
+ triggerFunctions,
+ initialFunction,
+ getCascObj
+ };
+ const attrHandler = {
+ get:function(obj,prop){//gets the most value of the attribute.
+ //If it is a repeating order, returns the array, otherwise returns the update value or the original value
+ if(prop === 'set'){
+ return function(){
+ let {attributes,sections,casc,callback,vocal} = arguments[0] ? arguments[0] : {};
+ if(attributes && attributes.queue.length && sections && casc){
+ let triggerName = attributes.queue.shift();
+ let trigger = getCascObj({sourceAttribute:triggerName},casc);
+ attributes.processChange({trigger,attributes,sections,casc});
+ }else{
+ debug({updates:obj.updates});
+ let trueCallback = Object.keys(obj.repOrders).length ?
+ function(){
+ Object.entries(obj.repOrders).forEach(([section,order])=>{
+ _setSectionOrder(section,order,)
+ });
+ callback && callback();
+ }:
+ callback;
+ Object.keys(obj.updates).forEach((key)=>obj.attributes[key] = obj.updates[key]);
+ const update = obj.updates;
+ obj.updates = {};
+ set(update,vocal,trueCallback);
+ }
+ }
+ }else if(Object.keys(obj).some(key=>key===prop)){
+ return Reflect.get(...arguments)
+ }else{
+ let retValue;
+ switch(true){
+ case obj.repOrders.hasOwnProperty(prop):
+ retValue = obj.repOrders[prop];
+ break;
+ case obj.updates.hasOwnProperty(prop):
+ retValue = obj.updates[prop];
+ break;
+ default:
+ retValue = obj.attributes[prop];
+ break;
+ }
+ let cascRef = `attr_${prop.replace(/(repeating_[^_]+_)[^_]+/,'$1\$X')}`;
+ let numRetVal = +retValue;
+ if(!Number.isNaN(numRetVal) && retValue !== ''){
+ retValue = numRetVal;
+ }else if(cascades[cascRef] && (typeof cascades[cascRef].defaultValue === 'number' || cascades[cascRef].type === 'number')){
+ retValue = cascades[cascRef].defaultValue;
+ }
+ return retValue;
+ }
+ },
+ set:function(obj,prop,value){
+ //Sets the value. Also verifies that the value is a valid attribute value
+ //e.g. not undefined, null, or NaN
+ if(value || value===0 || value===''){
+ if(/reporder|^repeating_[^_]+$/.test(prop)){
+ let section = prop.replace(/_reporder_/,'');
+ obj.repOrders[section] = value;
+ }else if(`${obj.attributes}` !== `${value}` ||
+ (obj.updates[prop] && `${obj.updates}` !== `${value}`)
+ ){
+ obj.updates[prop] = value;
+ }
+ }else{
+ debug(`!!!Warning: Attempted to set ${prop} to an invalid value:${value}; value not stored!!!`);
+ }
+ return true;
+ },
+ deleteProperty(obj,prop){
+ //removes the property from the original attributes, updates, and the reporders
+ Object.keys(obj).forEach((key)=>{
+ delete obj[key][prop.toLowerCase()];
+ });
+ }
+ };
+ return new Proxy(attrTarget,attrHandler);
+};
+
+/**
+ * Function that registers a function for being called via the funcs object. Returns true if the function was successfully registered, and false if it could not be registered for any reason.
+ * @param {object} funcObj - Object with keys that are names to register functions under and values that are functions.
+ * @param {object} optionsObj - Object that contains options to use for this registration.
+ * @param {string[]} optionsObj.type - Array that contains the types of specialized functions that apply to the functions being registered. Valid types are `"opener"`, `"updater"`, and `"default"`. `"default"` is always used, and never needs to be passed.
+ * @returns {boolean} - True if the registration succeeded, false if it failed.
+ */
+const registerFuncs = function(funcObj,optionsObj = {}){
+ if(typeof funcObj !== 'object' || typeof optionsObj !== 'object'){
+ debug(`!!!! K-scaffold error: Improper arguments to register functions !!!!`);
+ return false;
+ }
+ const typeArr = optionsObj.type ? ['default',...optionsObj.type] : ['default'];
+ const typeSwitch = {
+ 'opener':openHandlers,
+ 'updater':updateHandlers,
+ 'new':initialSetups,
+ 'default':funcs
+ };
+ let setState;
+ Object.entries(funcObj).map(([prop,value])=>{
+ typeArr.forEach((type)=>{
+ if(typeSwitch[type][prop]){
+ debug(`!!! Duplicate function name for ${prop} as ${type}!!!`);
+ setState = false;
+ }else if(typeof value === 'function'){
+ typeSwitch[type][prop] = value;
+ setState = setState !== false ? true : false;
+ }else{
+ debug(`!!! K-scaffold error: Function registration requires a function. Invalid value to register as ${type} !!!`);
+ setState = false;
+ }
+ });
+ });
+ return setState;
+};
+kFuncs.registerFuncs = registerFuncs;
+
+const setActionCalls = function({attributes,sections}){
+ actionAttributes.forEach((base)=>{
+ let [section,,field] = k.parseTriggerName(base);
+ let fieldAction = field.replace(/_/g,'-');
+ if(section){
+ sections[section].forEach((id)=>{
+ attributes[`${section}_${id}_${field}`] = `%{${attributes.character_name}|${section}_${id}_${fieldAction}}`;
+ });
+ }else{
+ attributes[`${field}`] = `%{${attributes.character_name}|${fieldAction}}`;
+ }
+ });
+};
+funcs.setActionCalls = setActionCalls;
+
+/**
+ * Function to call a function previously registered to the funcs object. May not be used that much. Either returns the function or null if no function exists.
+ * @param {string} funcName - The name of the function to invoke.
+ * @param {...any} args - The arguments to call the function with.
+ * @returns {any}
+ */
+const callFunc = function(funcName,...args){
+ if(funcs[funcName]){
+ debug(`calling ${funcName}`);
+ return funcs[funcName](...args);
+ }else{
+ debug(`Invalid function name: ${funcName}`);
+ return null;
+ }
+};
+kFuncs.callFunc = callFunc;
\ No newline at end of file
diff --git a/under_the_neighborhood/source/scaffold/docs.html b/under_the_neighborhood/source/scaffold/docs.html
new file mode 100644
index 0000000000..70d22ac6cf
--- /dev/null
+++ b/under_the_neighborhood/source/scaffold/docs.html
@@ -0,0 +1,992 @@
+
+
\ No newline at end of file
diff --git a/under_the_neighborhood/source/scaffold/docs.pug b/under_the_neighborhood/source/scaffold/docs.pug
new file mode 100644
index 0000000000..94632c5f11
--- /dev/null
+++ b/under_the_neighborhood/source/scaffold/docs.pug
@@ -0,0 +1,2 @@
+include _kpug.pug
++kscript(true)
\ No newline at end of file
diff --git a/under_the_neighborhood/source/scaffold/kvariables.js b/under_the_neighborhood/source/scaffold/kvariables.js
new file mode 100644
index 0000000000..67b906e27e
--- /dev/null
+++ b/under_the_neighborhood/source/scaffold/kvariables.js
@@ -0,0 +1,42 @@
+/**
+ * This stores the name of your sheet for use in the logging functions {@link log} and {@link debug}
+ * * @var
+ * @type {string}
+ */
+docs.js['k.sheetName'] = {
+ type:'string',
+ description:'This stores the name of your sheet for use in the logging functions [k.log](#klog) and [k.debug](#kdebug).'
+};
+let sheetName = 'kScaffold Powered Sheet';
+kFuncs.sheetName = sheetName;
+/**
+ * This stores the version of your sheet for use in the logging functions{@link log} and {@link debug}. It is also stored in the sheet_version attribute on your character sheet.
+ * @var
+ * @type {number}
+ */
+docs.js['k.version'] = {
+ type:'number',
+ description:'This stores the version of your sheet for use in the logging functions [k.log](#klog) and [k.debug](#kdebug), and in the K-scaffolds sheet versioning handling. It is also stored in the sheet_version attribute on your character sheet.'
+};
+let version = 0;
+kFuncs.version = version;
+/**
+ * A boolean flag that tells the script whether to enable or disable {@link debug} calls. If the version of the sheet is `0`, or an attribute named `debug_mode` is found on opening this is set to true for your entire session. Otherwise, it remains false.
+ * @var
+ * @type {boolean}
+ */
+docs.js['k.debugMode'] = {
+ type:'boolean',
+ description:'A boolean flag that tells the script whether to enable or disable [k.debug](#kdebug) calls. If the version of the sheet is `0`, or an attribute named `debug_mode` is found on opening this is set to true for all sheets you open from that point on. Otherwise, it remains false.'
+};
+let debugMode = false;
+kFuncs.debugMode = debugMode;
+const funcs = {};
+kFuncs.funcs = funcs;
+const updateHandlers = {};
+const openHandlers = {};
+const initialSetups = {};
+const addFuncs = {};
+
+const kscaffoldJSVersion = 0.30;
+const kscaffoldPUGVersion = 0.30;
diff --git a/under_the_neighborhood/source/scaffold/listeners.js b/under_the_neighborhood/source/scaffold/listeners.js
new file mode 100644
index 0000000000..857d80a4f4
--- /dev/null
+++ b/under_the_neighborhood/source/scaffold/listeners.js
@@ -0,0 +1,59 @@
+/*jshint esversion: 11, laxcomma:true, eqeqeq:true*/
+/*jshint -W014,-W084,-W030,-W033*/
+const listeners = {};
+const baseGet = Object.entries(cascades).reduce((memo,[attrName,detailObj])=>{
+ if(!/repeating/.test(attrName) && detailObj.type !== 'action'){
+ memo.push(detailObj.name);
+ }
+ if(detailObj.listener){
+ listeners[detailObj.listener] = detailObj.listenerFunc;
+ }
+ return memo;
+},[]);
+kFuncs.baseGet = baseGet;
+const registerEventHandlers = function(){
+ on('sheet:opened',updateSheet);
+ debug({funcKeys:Object.keys(funcs),funcs});
+ //Roll20 change and click listeners
+ Object.entries(listeners).forEach(([event,funcName])=>{
+ if(funcs[funcName]){
+ on(event,funcs[funcName]);
+ }else{
+ debug(`!!!Warning!!! no function named ${funcName} found. No listener created for ${event}`,true);
+ }
+ });
+ log(`kScaffold Loaded`);
+};
+setTimeout(registerEventHandlers,0);//Delay the execution of event registration to ensure all event properties are present.
+
+const addItem = function(event){
+ let [,,section] = parseClickTrigger(event.triggerName);
+ section = section.replace(/add-/,'');
+ getAllAttrs({
+ callback:(attributes,sections,casc) => {
+ let row = _generateRowID(section,sections);
+ debug({row});
+ attributes[`${row}_name`] = '';
+ setActionCalls({attributes,sections});
+ const trigger = cascades[`fieldset_repeating_${section}`];
+ if(trigger && trigger.addFuncs){
+ trigger.addFuncs.forEach((funcName) => {
+ if(funcs[funcName]){
+ funcs[funcName]({attributes,sections,casc,trigger});
+ }
+ });
+ }
+ attributes.set({attributes,sections,casc});
+ }
+ });
+};
+funcs.addItem = addItem;
+
+const editSection = function(event){
+ let [,,section] = parseClickTrigger(event.triggerName);
+ section = section.replace(/edit-/,'');
+ let target = `fieldset.repeating_${section} + .repcontainer`;
+ $20(target).toggleClass('ui-sortable');
+ $20(target).toggleClass('editmode');
+};
+registerFuncs({editSection});
\ No newline at end of file
diff --git a/under_the_neighborhood/source/scaffold/parse_cascade.js b/under_the_neighborhood/source/scaffold/parse_cascade.js
new file mode 100644
index 0000000000..463acb711e
--- /dev/null
+++ b/under_the_neighborhood/source/scaffold/parse_cascade.js
@@ -0,0 +1,62 @@
+/*jshint esversion: 11, laxcomma:true, eqeqeq:true*/
+/*jshint -W014,-W084,-W030,-W033*/
+/*
+Cascade Expansion functions
+*/
+//Expands the repeating section templates in cascades to reflect the rows actually available
+const expandCascade = function(cascade,sections,attributes){
+ return _.keys(cascade).reduce((memo,key)=>{//iterate through cascades and replace references to repeating attributes with correct row ids.
+ if(/^(?:act|attr)_repeating_/.test(key)){//If the attribute is a repeating attribute, do special logic
+ expandRepeating(memo,key,cascade,sections,attributes);
+ }else if(key){//for non repeating attributes do this logic
+ expandNormal(memo,key,cascade,sections);
+ }
+ return memo;
+ },{});
+};
+
+const expandRepeating = function(memo,key,cascade,sections,attributes){
+ key.replace(/((?:attr|act)_)(repeating_[^_]+)_[^_]+?_(.+)/,(match,type,section,field)=>{
+ (sections[section]||[]).forEach((id)=>{
+ memo[`${type}${section}_${id}_${field}`]=_.clone(cascade[key]);//clone the details so that each row's attributes have correct ids
+ memo[`${type}${section}_${id}_${field}`].name = `${section}_${id}_${field}`;
+ if(key.startsWith('attr_')){
+ memo[`${type}${section}_${id}_${field}`].affects = memo[`${type}${section}_${id}_${field}`].affects.reduce((m,affected)=>{
+ if(section === affected){//otherwise if the affected attribute is in the same section, simply set the affected attribute to have the same row id.
+ m.push(applyID(affected,id));
+ }else if(/repeating/.test(affected)){//If the affected attribute isn't in the same repeating section but is still a repeating attribute, add all the rows of that section
+ addAllRows(affected,m,sections);
+ }else{//otherwise the affected attribute is a non repeating attribute. Simply add it to the computed affected array
+ m.push(affected);
+ }
+ return m;
+ },[]);
+ }
+ });
+ });
+};
+
+const applyID = function(affected,id){
+ return affected.replace(/(repeating_[^_]+_)[^_]+(.+)/,`$1${id}$2`);
+};
+
+const expandNormal = function(memo,key,cascade,sections){
+ memo[key] = _.clone(cascade[key]);
+ if(key.startsWith('attr_')){
+ memo[key].affects = memo[key].affects || [];
+ memo[key].affects = memo[key].affects.reduce((m,a)=>{
+ if(/^repeating/.test(a)){
+ addAllRows(a,m,sections);
+ }else{
+ m.push(a);
+ }
+ return m;
+ },[]);
+ }
+};
+
+const addAllRows = function(affected,memo,sections){
+ affected.replace(/(repeating_[^_]+?)_[^_]+?_(.+)/,(match,section,field)=>{
+ sections[section].forEach(id=>memo.push(`${section}_${id}_${field}`));
+ });
+};
\ No newline at end of file
diff --git a/under_the_neighborhood/source/scaffold/scaffold_type_definitions/attributesProxy.js b/under_the_neighborhood/source/scaffold/scaffold_type_definitions/attributesProxy.js
new file mode 100644
index 0000000000..a306cc894b
--- /dev/null
+++ b/under_the_neighborhood/source/scaffold/scaffold_type_definitions/attributesProxy.js
@@ -0,0 +1,41 @@
+/**
+ * @typedef {object} attributesProxy
+ * @property {string|number} [name_of_attribute] - Your attribute values are stored in the attribute proxy under their attribute names. You can reference the current value of the attribute by calling it directly from the proxy (e.g. `attributes.my_attribute`). This will return either the original value if no changes have been made, or the updated value if changes have previously been applied. Storing an updated value is done by simple assignment (e.g. `attributes.my_attributes = 5`).
+ * @property {function} set - Applies any updates to the sheet. Also applies any repeating section reorders. This uses ES6 destructuring function argument assignment so all arguments should be passed in as properties of a single object with the indicated keys (e.g. `attributes.set({vocal:true,callback:() => log('attributes were set')})`)
+ * @property {boolean=} set.vocal - set will not be silent. Inverts the standard behavior of setAttrs options object
+ * @property {function=} set.callback - A callback to be invoked once the setAttrs is completed
+ * @property {attributesProxy=} set.attributes - The instance of the attributeProxy to use for further set operations
+ * @property {object} set.sections - An object containing the idArrays for each repeating section
+ * @property {object} set.casc - as [the casc property]{@link attributesProxy#casc}
+ * @property {object} attributes - The original attribute values from the getAttrs call
+ * @property {object} updates - Any changes that are made by the script are stored here
+ * @property {object} repOrders - any changes to repeating section orders stored here
+ * @property {string[]} queue - queue of attributes affected to iterate through. Only used if the whole proxy is passed to attributes.set()
+ * @property {object} casc - The details of how to handle all attributes as created from the {@link k.cascades} object by [k.getAllAttrs()]{@link k.getAllAttrs}.
+ * @property {function} processChange - function to iterate through attribute changes for default handling
+ * @property {function} triggerFunctions - Calls functions that are triggered whenever an attribute is changed or affected
+ * @property {function} initialFunction - Calls functions that are only triggered when an attribute is the triggering event
+ * @property {function} getCascObj - Gets the appropriate cascade object for a given attribute or action button
+ */
+
+/**
+ * The attributes object that is generated via [createAttrProxy()]{@link createAttrProxy}, and used in [k.getAttrs()]{@link k.getAttrs} & [k.getAllAttrs()]{@link k.getAllAttrs}. This is a proxy for the basic attributes object passed to the callback in the sheetworker [getAttrs()](https://wiki.roll20.net/Sheet_Worker_Scripts#getAttrs.28attributeNameArray.2C_callback.29).
+ * @const {object}
+ * @property {string|number} [name_of_attribute] - Your attribute values are stored in the attribute proxy under their attribute names. You can reference the current value of the attribute by calling it directly from the proxy (e.g. `attributes.my_attribute`). This will return either the original value if no changes have been made, or the updated value if changes have previously been applied. Storing an updated value is done by simple assignment (e.g. `attributes.my_attributes = 5`).
+ * @property {function} set - Applies any updates to the sheet. Also applies any repeating section reorders. This uses ES6 destructuring function argument assignment so all arguments should be passed in as properties of a single object with the indicated keys (e.g. `attributes.set({vocal:true,callback:() => log('attributes were set')})`)
+ * @property {boolean=} set.vocal - set will not be silent. Inverts the standard behavior of setAttrs options object
+ * @property {function=} set.callback - A callback to be invoked once the setAttrs is completed
+ * @property {attributesProxy=} set.attributes - The instance of the attributeProxy to use for further set operations
+ * @property {object} set.sections - An object containing the idArrays for each repeating section
+ * @property {object} set.casc - as [the casc property]{@link attributesProxy#casc}
+ * @property {object} attributes - The original attribute values from the getAttrs call
+ * @property {object} updates - Any changes that are made by the script are stored here
+ * @property {object} repOrders - any changes to repeating section orders stored here
+ * @property {string[]} queue - queue of attributes affected to iterate through. Only used if the whole proxy is passed to attributes.set()
+ * @property {object} casc - The details of how to handle all attributes
+ * @property {function} processChange - function to iterate through attribute changes for default handling
+ * @property {function} triggerFunctions - Calls functions that are triggered whenever an attribute is changed or affected
+ * @property {function} initialFunction - Calls functions that are only triggered when an attribute is the triggering event
+ * @property {function} getCascObj - Gets the appropriate cascade object for a given attribute or action button
+ */
+const attributesProxy = {};
\ No newline at end of file
diff --git a/under_the_neighborhood/source/scaffold/scaffold_type_definitions/constant_definitions.js b/under_the_neighborhood/source/scaffold/scaffold_type_definitions/constant_definitions.js
new file mode 100644
index 0000000000..e0fdb33f7c
--- /dev/null
+++ b/under_the_neighborhood/source/scaffold/scaffold_type_definitions/constant_definitions.js
@@ -0,0 +1,30 @@
+/**
+ * @typedef Roll20Event
+ * @type {object}
+ * @property {string} sourceAttribute - The attribute name of the change; undefined for button events.
+ * @property {string} triggerName - The name of the trigger including the trigger type. E.g. `change:attribute_name` or `clicked:button-name`
+ * @property {string} sourceType - Who made the change; player (or API) or sheetworker
+ * @property {string|number} previousValue - The value of the attribute before the change was made
+ * @property {string|number} newValue - What the attribute value was changed to
+ * @property {object} removedInfo - The contents of the removed repeating row
+ */
+
+/**
+ * An object containing arrays of section ids indexed by section (e.g. `{repeating_inventory:['id1','id2']}`). Is auto generated by the PUG side of the kscaffold.
+ * @typedef {object} sectionObj
+ * @property {...sectionIDS[]} sectionname
+ */
+
+/**
+ * @typedef {object} expandedCascade
+ * @property {...object} itemName - Object with details of how to handle the attribute or button. Attribute names are used as the key with a prefix of `attr_` and button names are used as the key with a prefix of `act_`. Repeating attributes/buttons should use $x or $X in place of an explicit ID. This will be expanded by the scaffold in response to an event.
+ * @property {string} itemName.name - The name of the attribute without the `act_` or `attr_` prefix.
+ * @property {string} itemName.type - The type of the attribute. Autopopulated from the `type` property from the attribute definition in the PUG.
+ * @property {string=} [itemName.listener] - the listener text to be used in a [roll20 event listener](https://wiki.roll20.net/Sheet_Worker_Scripts#Event_listener).
+ * @property {string|number} [defaultValue] - The default value of the attribute. Not used for buttons. Autopopulated from the `value` property of the PUG definition of the attribute.
+ * @property {string=} [itemName.listenerFunc] - Name of function to call from the [roll20 event listener](https://wiki.roll20.net/Sheet_Worker_Scripts#Event_listener). Is used as a string to call the function from the {@link funcs} object.
+ * @property {string} [itemName.initialFunc] - Name of function to call only when the attribute is the change event that triggered the listener.
+ * @property {string} [itemName.calculation] - Name of function to call to calculate the value of this attribute.
+ * @property {string[]} [itemName.triggeredFuncs] - Array of function names that should be called any time this attribute is changed or affected.
+ * @property {...*} [itemName.yourPropertyName] - Additional properties can be added to this setup via the K-Scaffold's PUG interface.
+ */
\ No newline at end of file
diff --git a/under_the_neighborhood/source/scaffold/sheetworker_aliases.js b/under_the_neighborhood/source/scaffold/sheetworker_aliases.js
new file mode 100644
index 0000000000..f66618b7b0
--- /dev/null
+++ b/under_the_neighborhood/source/scaffold/sheetworker_aliases.js
@@ -0,0 +1,216 @@
+/*jshint esversion: 11, laxcomma:true, eqeqeq:true*/
+/*jshint -W014,-W084,-W030,-W033*/
+/**
+ * Alias for [setSectionOrder()](https://wiki.roll20.net/Sheet_Worker_Scripts#setSectionOrder.28.3CRepeating_Section_Name.3E.2C_.3CSection_Array.3E.2C_.3CCallback.3E.29) that allows you to use the section name in either `repeating_section` or `section` formats.
+ * @name setSectionOrder
+ * @param {string} section
+ * @param {string[]} order
+ * @returns {void}
+ */
+docs.js['k.setSectionOrder'] = {
+ type:'function',
+ description:'Alias for [setSectionOrder()](https://wiki.roll20.net/Sheet_Worker_Scripts#setSectionOrder.28.3CRepeating_Section_Name.3E.2C_.3CSection_Array.3E.2C_.3CCallback.3E.29) that allows you to use the section name in either `repeating_section` or `section` formats. Note that the Roll20 sheetworker [setSectionOrder](https://wiki.roll20.net/Sheet_Worker_Scripts#setSectionOrder.28.3CRepeating_Section_Name.3E.2C_.3CSection_Array.3E.2C_.3CCallback.3E.29) currently causes some display issues on sheets.',
+ arguments:[
+ {type:'string',name:'section',description:'The name of the section to change the order in. Accepts the section name with or without the `repeating_` prefix.'},
+ {type:'[\'string\']',name:'order',description:'Array of the row ids in the order that the rows need to be placed.'}
+ ]
+};
+const _setSectionOrder = function(section,order){
+ let trueSection = section.replace(/repeating_/,'');
+ setSectionOrder(trueSection,order);
+};
+kFuncs.setSectionOrder = _setSectionOrder;
+
+/**
+ * Alias for [removeRepeatingRow](https://wiki.roll20.net/Sheet_Worker_Scripts#removeRepeatingRow.28_RowID_.29) that also removes the row from the current object of attribute values and array of section IDs to ensure that erroneous updates are not issued.
+ * @name removeRepeatingRow
+ * @param {string} row - The row id to be removed
+ * @param {attributesProxy} attributes - The attribute values currently in memory
+ * @param {object} sections - Object that contains arrays of all the IDs in sections on the sheet indexed by repeating name.
+ * @returns {void}
+ */
+docs.js['k.removeRepeatingRow'] = {
+ type:'function',
+ description:'Alias for [removeRepeatingRow](https://wiki.roll20.net/Sheet_Worker_Scripts#removeRepeatingRow.28_RowID_.29) that also removes the row from the current object of attribute values and array of section IDs to ensure that erroneous updates are not issued.',
+ arguments:[
+ {type:'string',name:'row',description:'The row id including the section name, e.g. `repeating_equipment_-oiuLKJ987ulkj`.'},
+ {type:'object',name:'attributes',description:'The attributes object passed to the callback in [k.getAllAttrs()](#getAllAttrs), [k.getAttrs()](#getAttrs), or [getAttrs()](https://wiki.roll20.net/Sheet_Worker_Scripts#getAttrs.28attributeNameArray.2C_callback.29)'},
+ {type:'object',name:'sections',description:'Object that contains arrays of all the IDs in sections on the sheet indexed by repeating name.'}
+ ]
+};
+const _removeRepeatingRow = function(row,attributes,sections){
+ debug(`removing ${row}`);
+ Object.keys(attributes.attributes).forEach((key)=>{
+ if(key.startsWith(row)){
+ delete attributes[key];
+ }
+ });
+ let [,section,rowID] = row.match(/(repeating_[^_]+)_(.+)/,'');
+ sections[section] = sections[section].filter((id)=>id!==rowID);
+ removeRepeatingRow(row);
+};
+kFuncs.removeRepeatingRow = _removeRepeatingRow;
+
+/**
+ * Alias for [getAttrs()](https://wiki.roll20.net/Sheet_Worker_Scripts#getAttrs.28attributeNameArray.2C_callback.29) that converts the default object of attribute values into an {@link attributesProxy} and passes that back to the callback function.
+ * @name getAttrs
+ * @param {Object} args
+ * @param {string[]} [args.props=baseGet] - Array of attribute names to get the value of. Defaults to {@link baseGet} if not passed.
+ * @param {function(attributesProxy)} args.callback - The function to call after the attribute values have been gotten. An {@link attributesProxy} is passed to the callback.
+ */
+docs.js['k.getAttrs'] = {
+ type:'function',
+ invocation:`k.getAttrs({props,callback})`,
+ description:'Alias for [getAttrs](https://wiki.roll20.net/Sheet_Worker_Scripts#getAttrs.28_RowID_.29) that converts the default object of attribute values into a K-scaffold attributes object and passes that back to the callback function.',
+ arguments:[
+ {type:'array',name:'props',description:'Array of attribute names to get the values of as per the [getAttrs() sheetworker](https://wiki.roll20.net/Sheet_Worker_Scripts#getAttrs.28attributeNameArray.2C_callback.29). If not passed, gets all the attributes contained in the cascades object.'},
+ {type:'function',name:'callback',description:'The function to call after the attribute values have been gotten. Works the same as the callback for the [getAttrs() sheetworker](https://wiki.roll20.net/Sheet_Worker_Scripts#getAttrs.28attributeNameArray.2C_callback.29).'}
+ ]
+};
+const _getAttrs = function({props=baseGet,callback}){
+ getAttrs(props,(values)=>{
+ const attributes = createAttrProxy(values);
+ callback(attributes);
+ });
+};
+kFuncs.getAttrs = _getAttrs;
+
+/**
+ * Alias for [getAttrs()](https://wiki.roll20.net/Sheet_Worker_Scripts#getAttrs.28attributeNameArray.2C_callback.29) and [getSectionIDs](https://wiki.roll20.net/Sheet_Worker_Scripts#getSectionIDs.28section_name.2Ccallback.29) that combines the actions of both sheetworker functions and converts the default object of attribute values into an {@link attributesProxy}. Also gets the details on how to handle all attributes from the master {@link cascades} object and.
+ * @param {Object} args
+ * @param {string[]} [args.props=baseGet] - Array of attribute names to get the value of. Defaults to {@link baseGet} if not passed.
+ * @param {repeatingSectionDetails} sectionDetails - Array of details about a section to get the IDs for and attributes that need to be gotten.
+ * @param {function(attributesProxy,sectionObj,expandedCascade):void} args.callback - The function to call after the attribute values have been gotten. An {@link attributesProxy} is passed to the callback along with a {@link sectionObj} and {@link expandedCascade}.
+ */
+docs.js['k.getAllAttrs'] = {
+ type:'function',
+ invocation:`k.getAllAttrs({props,sectionDetails,callback})`,
+ description:'Alias for [getAttrs()](https://wiki.roll20.net/Sheet_Worker_Scripts#getAttrs.28attributeNameArray.2C_callback.29) and [getSectionIDs()](https://wiki.roll20.net/Sheet_Worker_Scripts#getSectionIDs.28section_name.2Ccallback.29) that combines the actions of both sheetworker functions and converts the default object of attribute values into a K-scaffold attributes object. ',
+ arguments:[
+ {type:'array',name:'props',description:'Array of attribute names to get the values of as per the [getAttrs() sheetworker](https://wiki.roll20.net/Sheet_Worker_Scripts#getAttrs.28attributeNameArray.2C_callback.29).'},
+ {type:'array',name:'sectionDetails',description:'An array of objects that contain the details on how to handle a given repeating section. See [k.getSections](#getSections) for more details.'},
+ {type:'function',name:'callback(attributes,sections,casc)',description:'The function to call after the attribute values have been gotten. Three arguments are passed to the callback; `attributes`, `sections`, and `casc`. `sections` is an object that holds arrays of row ids, indexed by repeating section name. `casc` is the expanded version of the cascades object with repeating attributes including their row IDs.'}
+ ]
+};
+const getAllAttrs = function({props=baseGet,sectionDetails=repeatingSectionDetails,callback}){
+ getSections(sectionDetails,(repeats,sections)=>{
+ getAttrs([...props,...repeats],(values)=>{
+ const attributes = createAttrProxy(values);
+ orderSections(attributes,sections);
+ const casc = expandCascade(cascades,sections,attributes);
+ callback(attributes,sections,casc);
+ })
+ });
+};
+kFuncs.getAllAttrs = getAllAttrs;
+
+/**
+ * Alias for [getSectionIDs()](https://wiki.roll20.net/Sheet_Worker_Scripts#getSectionIDs.28section_name.2Ccallback.29) that allows you to iterate through several functions at once. Also assembles an array of repeating attributes to get.
+ * @param {object[]} sectionDetails - Array of details about a section to get the IDs for and attributes that need to be gotten.
+ * @param {string} sectionDetails.section - The full name of the repeating section including the `repeating_` prefix.
+ * @param {string[]} sectionDetails.fields - Array of field names that need to be gotten from the repeating section
+ * @param {function(string[],sectionObj)} callback - The function to call once all IDs have been gotten and the array of repating attributes to get has been assembled. The callback is passed the array of repating attributes to get and a {@link sectionObj}.
+ * @returns {void}
+ */
+docs.js['k.getSections'] = {
+ type:'function',
+ invocation:`k.getSections(sectionDetails,callback)`,
+ description:'Alias for [getSectionIDs()](https://wiki.roll20.net/Sheet_Worker_Scripts#getSectionIDs.28section_name.2Ccallback.29) that allows you to iterate through several sections at once. Also assembles an array of repeating attributes to get.',
+ arguments:[
+ {type:'array',name:'props',description:'Array of attribute names to get the values of as per the [getAttrs() sheetworker](https://wiki.roll20.net/Sheet_Worker_Scripts#getAttrs.28attributeNameArray.2C_callback.29).'},
+ {type:'array',name:'sectionDetails',description:'An array of objects that contain the details on how to handle a given repeating section. See [k.repeatingSectionDetails](#krepeatingsectiondetails) for more details.'},
+ {type:'function',name:'callback(repeatAttrs,sections)',description:'The function to call after the attribute values have been gotten. Two arguments are passed to the callback; `repeatAttrs` and `sections`. `repeatAttrs` is an array of repeating attributes ready to be used in a [getAttrs](https://wiki.roll20.net/Sheet_Worker_Scripts#getAttrs.28attributeNameArray.2C_callback.29), or [k.getAttrs](#kgetattrs) call. `sections` is an object that holds arrays of row ids, indexed by repeating section name.'}
+ ]
+};
+const getSections = function(sectionDetails,callback){
+ let queueClone = _.clone(sectionDetails);
+ const worker = (queue,repeatAttrs=[],sections={})=>{
+ let detail = queue.shift();
+ getSectionIDs(detail.section,(IDs)=>{
+ sections[detail.section] = IDs;
+ IDs.forEach((id)=>{
+ detail.fields.forEach((f)=>{
+ repeatAttrs.push(`${detail.section}_${id}_${f}`);
+ });
+ });
+ repeatAttrs.push(`_reporder_${detail.section}`);
+ if(queue.length){
+ worker(queue,repeatAttrs,sections);
+ }else{
+ callback(repeatAttrs,sections);
+ }
+ });
+ };
+ if(!queueClone[0]){
+ callback([],{});
+ }else{
+ worker(queueClone);
+ }
+};
+kFuncs.getSections = getSections;
+
+// Sets the attributes while always calling with {silent:true}
+// Can be awaited to get the values returned from _setAttrs
+/**
+ * Alias for [setAttrs()](https://wiki.roll20.net/Sheet_Worker_Scripts#setAttrs.28values.2Coptions.2Ccallback.29) that sets silently by default.
+ * @name setAttrs
+ * @param {object} obj - The object containting attributes to set
+ * @param {boolean} [vocal=false] - Whether to set silently (default value) or not.
+ * @param {function()} [callback] - The callback function to invoke after the setting has been completed. No arguments are passed to the callback function.
+ * @returns {void}
+ */
+docs.js['k.setAttrs'] = {
+ type:'function',
+ invocation:`k.setAttrs(setObj,vocal,callback)`,
+ description:'Alias for [setAttrs()](https://wiki.roll20.net/Sheet_Worker_Scripts#setAttrs.28values.2Coptions.2Ccallback.29) that sets silently by default.',
+ arguments:[
+ {type:'object',name:'setObj',description:'Object with key/value pairs of attributes to set on the sheet. See [the wiki page](https://wiki.roll20.net/Sheet_Worker_Scripts#setAttrs.28values.2Coptions.2Ccallback.29) for more information.'},
+ {type:'boolean',name:'vocal',description:'Whether to set silently (default value) or not.'},
+ {type:'function',name:'callback()',description:'The callback function to invoke after the setting has been completed. No arguments are passed to the callback function.'}
+ ]
+};
+const set = function(obj,vocal=false,callback){
+ setAttrs(obj,{silent:!vocal},callback);
+};
+kFuncs.setAttrs = set;
+
+const generateCustomID = function(string){
+ if(!string.startsWith('-')){
+ string = `-${string}`;
+ }
+ rowID = generateRowID();
+ let re = new RegExp(`^.{${string.length}}`);
+ return `${string}${rowID.replace(re,'')}`;
+};
+
+
+/**
+ * Alias for generateRowID that adds the new id to the {@link sectionObj}. Also allows for creation of custom IDs that conform to the section ID requirements.
+ * @name generateRowID
+ * @param {string} section - The section name to create an ID for. The `repeating_` prefix is optional so both `repeating_equipment` and `equipment` are valid.
+ * @param {sectionObj} sections
+ * @param {string} [customText] - Custom text to start the ID with. This text should not be longer than the standard repeating section ID format.
+ * @returns {any}
+ */
+docs.js['k.generateRowID'] = {
+ type:'function',
+ invocation:`k.generateRowID(section,sections,customText)`,
+ description:'Alias for generateRowID that adds the new id to the sections object. Also allows for creation of custom IDs that conform to the section ID requirements.',
+ arguments:[
+ {type:'string',name:'setObj',description:'The section name to create an ID for. The `repeating_` prefix is optional so both `repeating_equipment` and `equipment` are valid.'},
+ {type:'object',name:'vocal',description:'Object containing the IDs for the repeating sections, indexed by repeating section name.'},
+ {type:'string',name:'customText',description:'Custom text to start the ID with. This text should not be longer than the standard repeating section ID format.'}
+ ]
+};
+const _generateRowID = function(section,sections,customText){
+ let rowID = customText ?
+ generateCustomID(customText) :
+ generateRowID();
+ section = section.match(/^repeating_[^_]+$/) ?
+ section :
+ `repeating_${section}`;
+ sections[section] = sections[section] || [];
+ sections[section].push(rowID);
+ return `${section}_${rowID}`;
+};
+kFuncs.generateRowID = _generateRowID;
\ No newline at end of file
diff --git a/under_the_neighborhood/source/scaffold/utility.js b/under_the_neighborhood/source/scaffold/utility.js
new file mode 100644
index 0000000000..6592b4deaf
--- /dev/null
+++ b/under_the_neighborhood/source/scaffold/utility.js
@@ -0,0 +1,249 @@
+/*jshint esversion: 11, laxcomma:true, eqeqeq:true*/
+/*jshint -W014,-W084,-W030,-W033*/
+docs.js['k.sanitizeForRegex'] = {
+ type:'function',
+ invocation:`k.sanitizeForRegex(text)`,
+ description:'Replaces problem characters to use a string as a regex.',
+ arguments:[
+ {type:'string',name:'text',description:'The text to replace characters in.'}
+ ]
+};
+const sanitizeForRegex = function(text){
+ return text.replace(/\.|\||\(|\)|\[|\]|\-|\+|\?|\/|\{|\}|\^|\$|\*/g,'\\$&');
+};
+kFuncs.sanitizeForRegex = sanitizeForRegex;
+
+docs.js['k.value'] = {
+ type:'function',
+ invocation:`k.value(val,def)`,
+ description:'Converts a value to a number, it\'s default value, or `0` if no default value passed.',
+ arguments:[
+ {type:'any',name:'val',description:'The value to coerce into a number.'},
+ {type:'number',name:'def',description:'A default value to use, if not passed, 0 is used instead.'}
+ ]
+};
+const value = function(val,def){
+ return (+val||def||0);
+};
+kFuncs.value = value;
+
+docs.js['k.parseRepeatName'] = {
+ type:'function',
+ invocation:`k.parseRepeatName(string)`,
+ description:'Extracts the section (e.g. `repeating_equipment`), rowID (e.g `-;lkj098J:LKj`), and field name (e.g. `bulk`) from a repeating attribute name.',
+ arguments:[
+ {type:'string',name:'string',description:'The attribute name to parse.'}
+ ],
+ retValue:{
+ type:'array',
+ description:'For a repeating attribute named `repeating_equipment_-LKJhpoi98;lj_weight`, the array will be `[\'repeating_equipment\',\'-LKJhpoi98;lj\',\'weight\']`.'
+ }
+};
+const parseRepeatName = function(string){
+ let match = string.match(/(repeating_[^_]+)_([^_]+)(?:_(.+))?/);
+ match.shift();
+ return match;
+};
+kFuncs.parseRepeatName = parseRepeatName;
+
+docs.js['k.parseTriggerName'] = {
+ type:'function',
+ invocation:`k.parseTriggerName(string)`,
+ description:'Parses out the components of a trigger name similar to [parseRepeatName](#parserepeatname). Aliases: parseClickTrigger.\n\nAliases: `k.parseClickTrigger`',
+ arguments:[
+ {type:'string',name:'string',description:'The triggerName property of the [event](https://wiki.roll20.net/Sheet_Worker_Scripts#eventInfo_Object).'}
+ ],
+ retValue:{
+ type:'array',
+ description:'For a repeating button named `repeating_equipment_-LKJhpoi98;lj_roll`, the array will be `[\'repeating_equipment\',\'-LKJhpoi98;lj\',\'roll\']`. For a non repeating button named `roll`, the array will be `[undefined,undefined,\'roll\']`'
+ }
+};
+const parseTriggerName = function(string){
+ let match = string.replace(/^clicked:/,'').match(/(?:(repeating_[^_]+)_([^_]+)_)?(.+)/);
+ match.shift();
+ return match;
+};
+kFuncs.parseTriggerName = parseTriggerName;
+const parseClickTrigger = parseTriggerName;
+kFuncs.parseClickTrigger = parseClickTrigger;
+
+docs.js['k.parseHTMLName'] = {
+ type:'function',
+ invocation:`k.parseHTMLName(string)`,
+ description:'Parses out the attribute name from the htmlattribute name.',
+ arguments:[
+ {type:'string',name:'string',description:'The triggerName property of the [event](https://wiki.roll20.net/Sheet_Worker_Scripts#eventInfo_Object).'}
+ ],
+ retValue:{
+ type:'array',
+ description:'For a repeating button named `act_repeating_equipment_-LKJhpoi98;lj_roll`, the array will be `[\'repeating_equipment\',\'-LKJhpoi98;lj\',\'roll\']`. For a non repeating button named `act_roll`, the array will be `[undefined,undefined,\'roll\']`'
+ }
+};
+const parseHTMLName = function(string){
+ let match = string.match(/(?:attr|act|roll)_(.+)/);
+ match.shift();
+ return match[0];
+};
+kFuncs.parseHTMLName = parseHTMLName;
+
+docs.js['k.capitalize'] = {
+ type:'function',
+ invocation:`k.capitalize(string)`,
+ description:'Capitalize each word in a string.',
+ arguments:[
+ {type:'string',name:'string',description:'The string to capitalize'},
+ ],
+ retValue:{
+ type:'string',
+ description:'The capitalized string'
+ }
+};
+const capitalize = function(string){
+ return string.replace(/(?:^|\s+|\/)[a-z]/ig,(letter)=>letter.toUpperCase());
+};
+kFuncs.capitalize = capitalize;
+
+docs.js['k.extractQueryResult'] = {
+ type:'function',
+ invocation:`k.extractQueryResult(section,sections,customText)`,
+ description:'Extracts a roll query result for use in later functions. Must be awaited as per [startRoll documentation](https://wiki.roll20.net/Sheet_Worker_Scripts#Roll_Parsing.28NEW.29). Stolen from [Oosh\'s Adventures with Startroll thread](https://app.roll20.net/forum/post/10346883/adventures-with-startroll).',
+ arguments:[
+ {type:'string',name:'query',description:'The query should be just the text as the `?{` and `}` at the start/end of the query are added by the function.'}
+ ],
+ retValue:{
+ type:'string',
+ description:'The selected value from the roll query'
+ }
+};
+const extractQueryResult = async function(query){
+ debug('entering extractQueryResult');
+ let queryRoll = await startRoll(`!{{query=[[0[response=?{${query}}]]]}}`);
+ finishRoll(queryRoll.rollId);
+ return queryRoll.results.query.expression.replace(/^.+?response=|\]$/g,'');
+};
+kFuncs.extractQueryResult = extractQueryResult;
+
+docs.js['k.pseudoQuery'] = {
+ type:'function',
+ invocation:`k.pseudoQuery(section,sections,customText)`,
+ description:'Simulates a query for ensuring that async/await works correctly in the sheetworker environment when doing conditional startRolls. E.g. if you have an if/else and only one of the conditions results in `startRoll` being called (and thus an `await`), the sheetworker environment would normally crash. Awaiting this in the condition that does not actually need to call `startRoll` will keep the environment in sync.',
+ arguments:[
+ {type:'number|string',name:'value',description:'The value to return. Optional.'}
+ ],
+ retValue:{
+ type:'string',
+ description:'The `value` passed to the function is returned after startRoll resolves.'
+ }
+};
+const pseudoQuery = async function(value){
+ debug('entering pseudoQuery');
+ let queryRoll = await startRoll(`!{{query=[[0[response=${value}]]]}}`);
+ finishRoll(queryRoll.rollId);
+ return queryRoll.results.query.expression.replace(/^.+?response=|\]$/g,'');
+};
+kFuncs.pseudoQuery = pseudoQuery;
+
+docs.js['k.log'] = {
+ type:'function',
+ invocation:`k.log(msg)`,
+ description:'An alias for console.log.',
+ arguments:[
+ {type:'string|object|array',name:'msg',description:'The message can be a straight string, an object, or an array. If it is an object or array, the object will be broken down so that each key is used as a label to output followed by the value of that key. If the value of the key is an object or array, it will be output via `console.table`.'}
+ ]
+};
+const log = function(msg){
+ if(typeof msg === 'string'){
+ console.log(`%c${kFuncs.sheetName} log| ${msg}`,"background-color:#159ccf");
+ }else if(typeof msg === 'object'){
+ Object.keys(msg).forEach((m)=>{
+ if(typeof msg[m] === 'string'){
+ console.log(`%c${kFuncs.sheetName} log| ${m}: ${msg[m]}`,"background-color:#159ccf");
+ }else{
+ console.log(`%c${kFuncs.sheetName} log| ${typeof msg[m]} ${m}`,"background-color:#159ccf");
+ console.table(msg[m]);
+ }
+ });
+ }
+};
+kFuncs.log = log;
+
+docs.js['k.debug'] = {
+ type:'function',
+ invocation:`k.debug(msg,force)`,
+ description:'Alias for console.log that only triggers when debug mode is enabled or when the sheet\'s version is `0`.',
+ arguments:[
+ {type:'string',name:'setObj',description:'See [k.log](#klog)'},
+ {type:'boolean',name:'force',description:'Pass as a truthy value to force the debug output to be output to the console regardless of debug mode.'}
+ ]
+};
+const debug = function(msg,force){
+ if(!kFuncs.debugMode && !force && kFuncs.version > 0) return;
+ if(typeof msg === 'string'){
+ console.log(`%c${kFuncs.sheetName} DEBUG| ${msg}`,"background-color:tan;color:red;");
+ }else if(typeof msg === 'object'){
+ Object.keys(msg).forEach((m)=>{
+ if(typeof msg[m] === 'string'){
+ console.log(`%c${kFuncs.sheetName} DEBUG| ${m}: ${msg[m]}`,"background-color:tan;color:red;");
+ }else{
+ console.log(`%c${kFuncs.sheetName} DEBUG| ${typeof msg[m]} ${m}`,"background-color:tan;color:red;font-weight:bold;");
+ console.table(msg[m]);
+ }
+ });
+ }
+};
+kFuncs.debug = debug;
+
+docs.js['k.orderSections'] = {
+ type:'function',
+ invocation:`k.orderSections(attributes,sections)`,
+ description:'Orders the section id arrays for all sections in the `sections` object to match the repOrder attribute.',
+ arguments:[
+ {type:'object',name:'attributes',description:'The attributes object that must have a value for the reporder for each section.'},
+ {type:'object',name:'sections',description:'Object containing the IDs for the repeating sections, indexed by repeating section name.'}
+ ]
+};
+const orderSections = function(attributes,sections){
+ Object.keys(sections).forEach((section)=>{
+ attributes.attributes[`_reporder_${section}`] = commaArray(attributes[`_reporder_${section}`]);
+ orderSection(attributes.attributes[`_reporder_${section}`],sections[section]);
+ });
+};
+kFuncs.orderSections = orderSections;
+
+docs.js['k.orderSection'] = {
+ type:'function',
+ invocation:`k.orderSection(repOrder,IDs)`,
+ description:'Orders a single ID array.',
+ arguments:[
+ {type:'array',name:'setObj',description:'Array of IDs in the order they are in on the sheet.'},
+ {type:'array',name:'vocal',description:'Array of IDs to be ordered.'}
+ ]
+};
+const orderSection = function(repOrder,IDs=[]){
+ IDs.sort((a,b)=>{
+ return repOrder.indexOf(a.toLowerCase()) - repOrder.indexOf(b.toLowerCase());
+ });
+};
+kFuncs.orderSection = orderSection;
+
+/**
+ * Splits a comma delimited string into an array
+ * @param {string} [string='']
+ * @returns {string[]}
+ */
+docs.js['k.commaArray'] = {
+ type:'function',
+ invocation:`k.commaArray(string)`,
+ description:'Splits a comma delimited string into an array',
+ arguments:[
+ {type:'string',name:'setObj',description:'The string to split.'}
+ ],
+ retValue:{
+ type:'array',
+ description:'The string segments of the comma delimited list.'
+ }
+};
+const commaArray = function(string=''){
+ return string.toLowerCase().split(/\s*,\s*/);
+};
+kFuncs.commaArray = commaArray;
\ No newline at end of file
diff --git a/under_the_neighborhood/source/system.pug b/under_the_neighborhood/source/system.pug
new file mode 100644
index 0000000000..3a636f0a0a
--- /dev/null
+++ b/under_the_neighborhood/source/system.pug
@@ -0,0 +1,153 @@
+//- The .pug file type is not required when including pug files, but I find that it makes reading the code easier if I can see at a glance what the file type is
+include scaffold/_kpug.pug
+include mixins/_sheetmixins.pug
+-
+ const basicMoves = {
+ 'see into their heart':{
+ stat:'heart',
+ success:'see into their heart success result',
+ mixed:'see into their heart mixed result',
+ failure:'see into their heart failure result'
+ },
+ 'help somebody':{
+ stat:'heart',
+ success:'help somebody success result',
+ mixed:'help somebody mixed result',
+ failure:'help somebody failure result'
+ },
+ 'touch the other side':{
+ stat:'books',
+ success:'touch the other side success result',
+ mixed:'touch the other side mixed result',
+ failure:'touch the other side failure result'
+ },
+ 'understand':{
+ stat:'books',
+ success:'understand success result',
+ mixed:'understand mixed result',
+ failure:'understand failure result'
+ },
+ 'take a swing':{
+ stat:'fierce',
+ success:'take a swing success result',
+ mixed:'take a swing mixed result',
+ failure:'take a swing failure result'
+ },
+ 'take action':{
+ stat:'fierce',
+ success:'take action success result',
+ mixed:'take action mixed result',
+ failure:'take action failure result'
+ },
+ 'sneak':{
+ stat:'slick',
+ success:'sneak success result',
+ mixed:'sneak mixed result',
+ failure:'sneak failure result'
+ },
+ 'keep your cool':{
+ stat:'slick',
+ success:'keep your cool success result',
+ mixed:'keep your cool mixed result',
+ failure:'keep your cool failure result'
+ },
+ 'convince somebody':{
+ stat:null,
+ success:'convince somebody success result',
+ mixed:'convince somebody mixed result',
+ failure:'convince somebody failure result'
+ },
+ 'another move':{
+ stat:null,
+ critical:`basic critical result`,
+ success:`basic success result`,
+ mixed:`basic mixed result`,
+ failure:`basic failure result`,
+ fumble:`basic fumble result`
+ }
+ };
+-
+ varObjects.basicMoves = {
+ ...basicMoves,
+ };
+-
+ const advantageTypes = {
+ advantage:'[[3+@{double}]]d6kh2',
+ normal:'2d6',
+ disadvantage:'[[3+@{double}]]d6kl2',
+ query:'?{Roll with|Double Advantage,4d6kh2|Advantage,3d6kh2|Normal,2d6|Disadvantage,3d6kl2|Double Disadvantage,4d6kl2}'
+ };
+-
+ varObjects.advantageTypes = {
+ 'double advantage':'4d6kh2',
+ advantage:'3d6kh2',
+ normal:'2d6',
+ disadvantage:'3d6kl2',
+ 'double disadvantage':'4d6kl2'
+ };
+- const meterValues = [1,2,3,4,5,6,7,8,9,10];
+- varObjects.meterValues = [0,...meterValues];
+//- We define our rolltemplate name here so that we can use it as a variable elsewhere. We'll be using a single template for the entire system
+- const templateName = 'utn';
+//-
+ template_start will allow us to include repetitive message information without needing to type it all out every time. The parts of the template included here are:
+ - @{whisper}: An attribute that we will create in the settings page of the sheet to allow users to set their rolls to always whisper to the gm or not.
+ - &{template:${templateName}}: The template declaration that we'll use in our macros
+ - @{character_name}: the character's name. We'll combine it with the character id so that a link to open the character sheet is output with every macro
+ - @{character_id}: the character's Roll20 ID. Combined with the character name, this allows the template to output a link to open the character from chat.
++hidden({name:'template start',value:`@{whisper}&{template:${templateName}} {{character_name=@{character_name}}} {{character_id=@{character_id}}}`})
+//- sheet_state will contain which display of the sheet is currently enabled
++hidden({name:'sheet version',value:0})
++hidden({name:'sheet state',value:'settings'})
+//- collapsed will contain the sections that are currently collapsed as a space separated list
++hidden({name:'collapsed',value:''})
+//- Now, for our actual sheet. We'll wrap this in a main element and give it an ID of main.
+main#main
+ //- Because this is going to be a tabbed sheet, we'll need to have a navigation section. We will use a nav element so that our html will be semantic html. This will help us make our sheet more accessible to those using assistive technologies like screen readers.
+ nav#main-nav.sheet-nav
+ .basic-moves.span-all.character-type.character-type--character.character-type--mon
+ each content,name in basicMoves
+ +roller({name,'data-i18n':name,class:'styled-button basic-roll',trigger:{triggeredFuncs:['rollBasicMove']}})
+ +roller({name:'initiative','data-i18n':'initiative',class:'styled-button basic-roll initiative',trigger:{triggeredFuncs:['rollInitiative']}})
+ .roll-options.character-type.character-type--character.character-type--mon
+ label.input-label.double-label
+ +checkbox({name:'double',hidden:'',value:1,class:'styled-button-control'})
+ span(data-i18n='double').styled-button.styled-button--controlled
+ each value,name in advantageTypes
+ - const checkObj = {name:'advantage',hidden:'',type:'radio',value,class:'styled-button-control'};
+ if name === 'normal'
+ - checkObj.checked = ''
+ label.input-label
+ +radio(checkObj)
+ span(data-i18n=name).styled-button.styled-button--controlled
+ +hidden({name:'generic bonus',value:'',class:'styled-button-control',trigger:{defaultValue:0}}).styled-button-control
+ +input-label({
+ inputObj:{name:'generic bonus',type:'number'},
+ spanObj:{'data-i18n':'bonus'},
+ divObj:{class:'styled-button styled-button--controlled'}
+ })
+ //- Our nav section is going to contain a series of action buttons that will eventually trigger our navigation javascript function. We'll create the buttons now, but write their functions at a later point.
+ //- This will be our button to access the settings page. it's written separately because it has some different requirements
+ +action({name:'nav settings',class:'pictos active sheet-nav__tab settings','data-i18n-title':'open the settings page',role:'heading','aria-level':5,trigger:{triggeredFuncs:['navigateSheet']}})
+ |y
+ +action({name:'nav character',class:'pictos sheet-nav__tab character character-type character-type--character character-type--mon','data-i18n-title':'open the character sheet',role:'heading','aria-level':5,trigger:{triggeredFuncs:['navigateSheet']}})
+ |N
+ +action({name:'nav gamescreen',class:'pictos sheet-nav__tab gamescreen character-type character-type--gamescreen','data-i18n-title':'open the confrontation sheet',role:'heading','aria-level':5,trigger:{triggeredFuncs:['navigateSheet']}})
+ |N
+ //- Includes all the code for the PC character sheet
+ - let headerType = 'character';
+ include articles/_character.pug
+ //- Includes all the code for the game screen sheet
+ - headerType = 'screen';
+ include articles/_game_screen.pug
+ //- This will eventually expand to become our settings page for the sheet, but we'll start with the PC sheet
+ include articles/_settings.pug
+//- This will eventually become our roll template. It is empty for now
+include rolltemplates/_utntemplate.pug
++kscript
+ //- All additional javascript files should start here
+ include javascript/variables.js
+ include javascript/setupfunctions.js
+ include javascript/calculators.js
+ include javascript/triggeredfunctions.js
+ include javascript/rollfunctions.js
\ No newline at end of file
diff --git a/under_the_neighborhood/source/system.scss b/under_the_neighborhood/source/system.scss
new file mode 100644
index 0000000000..6b7d930dc9
--- /dev/null
+++ b/under_the_neighborhood/source/system.scss
@@ -0,0 +1,17 @@
+@use 'mixins/scss/roll20clear';
+@use 'mixins/scss/_generalsetup';
+@use 'mixins/scss/_nav';
+@use 'mixins/scss/settings';
+@use 'mixins/scss/_media';
+@use 'mixins/scss/_header';
+@use 'mixins/scss/_attributes';
+@use 'mixins/scss/_skills';
+@use 'mixins/scss/_compatibility';
+@use 'mixins/scss/_playbook';
+@use 'mixins/scss/_mon';
+@use 'mixins/scss/_meter';
+@use 'mixins/scss/_modifiers';
+@use 'mixins/scss/_advancement';
+@use 'mixins/scss/_lifeslice';
+@use 'mixins/scss/_clock';
+@use 'mixins/scss/_rolltemplate';
\ No newline at end of file
diff --git a/under_the_neighborhood/system.css b/under_the_neighborhood/system.css
new file mode 100644
index 0000000000..d3b0908b17
--- /dev/null
+++ b/under_the_neighborhood/system.css
@@ -0,0 +1,1558 @@
+
+@import url("https://fonts.googleapis.com/css?family=Material+Icons&display=swap");
+.ui-dialog .tab-content .charsheet select,
+.ui-dialog .tab-content .charsheet textarea,
+.ui-dialog .tab-content .charsheet input,
+.ui-dialog .tab-content .charsheet .uneditable-input,
+.ui-dialog .tab-content .charsheet label,
+.ui-dialog .tab-content .charsheet button {
+ all: initial;
+}
+
+.sheet-rolltemplate-utn.sheet-rolltemplate-darkmode, .sheet-darkmode .ui-dialog {
+ --borderColor: var(--dark-secondarytext);
+ --fontColor: var(--dark-secondarytext);
+ --backColor: var(--dark-surface1);
+ --dark-primary:#c33d00;
+ --dark-primary-highlight:#9a644c;
+ --selectedColor:var(--dark-primary);
+ --subHeadBackColor:var(--dark-primary);
+}
+.sheet-rolltemplate-utn.sheet-rolltemplate-darkmode img, .sheet-darkmode .ui-dialog img {
+ filter: brightness(0.8) contrast(1.2);
+}
+
+.sheet-rolltemplate-utn .stacked.input-label, .ui-dialog .tab-content .charsheet .stacked.input-label {
+ flex-direction: column-reverse;
+}
+.sheet-rolltemplate-utn .stacked.center.input-label > *, .ui-dialog .tab-content .charsheet .stacked.center.input-label > * {
+ text-align: center;
+}
+.sheet-rolltemplate-utn .stacked.input-label > [name], .ui-dialog .tab-content .charsheet .stacked.input-label > [name] {
+ width: 100%;
+}
+
+.sheet-rolltemplate-utn .text-collapse__check:not(:checked) + .text-collapse, .ui-dialog .tab-content .charsheet .text-collapse__check:not(:checked) + .text-collapse, .sheet-rolltemplate-utn :is(.underlined, .boxed):not([readonly]):not([type=checkbox]):not([type=radio]):is(:hover, :focus, :focus-within), .ui-dialog .tab-content .charsheet :is(.underlined, .boxed):not([readonly]):not([type=checkbox]):not([type=radio]):is(:hover, :focus, :focus-within) {
+ border-width: 1px 3px;
+ border-style: solid;
+ border-color: var(--borderColor);
+ border-radius: 5px;
+ box-sizing: border-box;
+}
+
+.sheet-rolltemplate-utn .ratio1-1, .ui-dialog .tab-content .charsheet .ratio1-1,
+.sheet-rolltemplate-utn .sheet-ratio1-1,
+.ui-dialog .tab-content .charsheet .sheet-ratio1-1 {
+ aspect-ratio: 1/1;
+}
+
+.sheet-rolltemplate-utn .roll-container, .ui-dialog .tab-content .charsheet .roll-container, .sheet-rolltemplate-utn .headed-textarea, .ui-dialog .tab-content .charsheet .headed-textarea, .sheet-rolltemplate-utn section, .ui-dialog .tab-content .charsheet section,
+.sheet-rolltemplate-utn header,
+.ui-dialog .tab-content .charsheet header, .sheet-rolltemplate-utn .adaptive, .ui-dialog .tab-content .charsheet .adaptive {
+ display: grid;
+}
+
+.sheet-rolltemplate-utn, .ui-dialog {
+ --topHeaderFont:Arial;
+ --midHeaderFont:Arial;
+ --contentFont:Arial;
+}
+
+.sheet-rolltemplate-utn .underlined, .ui-dialog .tab-content .charsheet .underlined,
+.sheet-rolltemplate-utn .sheet-underlined,
+.ui-dialog .tab-content .charsheet .sheet-underlined, .sheet-rolltemplate-utn select, .ui-dialog .tab-content .charsheet select,
+.sheet-rolltemplate-utn .pseudo-select span,
+.ui-dialog .tab-content .charsheet .pseudo-select span,
+.sheet-rolltemplate-utn .sheet-pseudo-select span,
+.ui-dialog .tab-content .charsheet .sheet-pseudo-select span,
+.sheet-rolltemplate-utn input:not(:where([type="checkbox"], [type="radio"])),
+.ui-dialog .tab-content .charsheet input:not(:where([type="checkbox"], [type="radio"])),
+.sheet-rolltemplate-utn .uneditable-input,
+.ui-dialog .tab-content .charsheet .uneditable-input,
+.sheet-rolltemplate-utn textarea,
+.ui-dialog .tab-content .charsheet textarea, .sheet-rolltemplate-utn .adaptive > span, .ui-dialog .tab-content .charsheet .adaptive > span {
+ border-width: 1px 3px;
+ border-style: solid;
+ border-radius: 5px;
+ border-color: transparent;
+ box-sizing: border-box;
+}
+
+.sheet-rolltemplate-utn .repcontrol-button, .ui-dialog .tab-content .charsheet .repcontrol-button {
+ background-color: #DCDCDC33;
+ border-radius: 5px;
+ box-shadow: 0 2px 4px black;
+ border-width: 0;
+ transition-property: box-shadow, backdrop-filter;
+ transition-duration: 200ms;
+ transition-timing-function: ease-out;
+ backdrop-filter: blur(1px);
+ overflow: hidden;
+}
+.sheet-rolltemplate-utn .repcontrol-button:is(:hover, :focus), .ui-dialog .tab-content .charsheet .repcontrol-button:is(:hover, :focus) {
+ background-color: #85858580;
+ box-shadow: 0 4px 6px black;
+ backdrop-filter: blur(2px);
+}
+.sheet-rolltemplate-utn .repcontrol-button:active, .ui-dialog .tab-content .charsheet .repcontrol-button:active {
+ background-color: #858585ff;
+ box-shadow: 0 1px 2px black;
+ backdrop-filter: blur(0px);
+}
+
+.sheet-rolltemplate-utn .repcontrol-button, .ui-dialog .tab-content .charsheet .repcontrol-button {
+ padding: 5px 7px;
+}
+
+.sheet-rolltemplate-utn .adaptive, .ui-dialog .tab-content .charsheet .adaptive {
+ grid-template-columns: auto;
+ grid-template-rows: auto;
+ grid-template-areas: "content";
+ position: relative;
+}
+.sheet-rolltemplate-utn .adaptive > *, .ui-dialog .tab-content .charsheet .adaptive > * {
+ grid-area: content;
+}
+.sheet-rolltemplate-utn .adaptive > span, .ui-dialog .tab-content .charsheet .adaptive > span {
+ opacity: 0;
+ z-index: -10;
+ text-transform: initial;
+ border-radius: 0px;
+}
+.sheet-rolltemplate-utn .adaptive--text, .ui-dialog .tab-content .charsheet .adaptive--text {
+ min-height: 4rem;
+}
+.sheet-rolltemplate-utn .adaptive--text__span, .ui-dialog .tab-content .charsheet .adaptive--text__span {
+ white-space: pre-wrap;
+ padding: 2px;
+}
+.sheet-rolltemplate-utn .adaptive--text__textarea, .ui-dialog .tab-content .charsheet .adaptive--text__textarea {
+ width: 100%;
+ height: 100%;
+ resize: none;
+}
+.sheet-rolltemplate-utn .adaptive--text__textarea, .ui-dialog .tab-content .charsheet .adaptive--text__textarea,
+.sheet-rolltemplate-utn .adaptive--input__input,
+.ui-dialog .tab-content .charsheet .adaptive--input__input {
+ position: absolute;
+}
+.sheet-rolltemplate-utn .adaptive--input__input, .ui-dialog .tab-content .charsheet .adaptive--input__input {
+ width: 100%;
+}
+.sheet-rolltemplate-utn .adaptive--input__span, .ui-dialog .tab-content .charsheet .adaptive--input__span {
+ padding: 2px;
+ min-height: 1.5rem;
+}
+
+.sheet-rolltemplate-utn .sheet-hidden, .ui-dialog .tab-content .charsheet .sheet-hidden,
+.sheet-rolltemplate-utn .hidden,
+.ui-dialog .tab-content .charsheet .hidden,
+.sheet-rolltemplate-utn *[hidden],
+.ui-dialog .tab-content .charsheet *[hidden] {
+ display: none !important;
+}
+
+.sheet-rolltemplate-utn .sheet-italics, .ui-dialog .tab-content .charsheet .sheet-italics,
+.sheet-rolltemplate-utn .italics,
+.ui-dialog .tab-content .charsheet .italics {
+ font-style: italic;
+}
+
+.sheet-rolltemplate-utn span, .ui-dialog .tab-content .charsheet span,
+.sheet-rolltemplate-utn input,
+.ui-dialog .tab-content .charsheet input,
+.sheet-rolltemplate-utn textarea,
+.ui-dialog .tab-content .charsheet textarea, .sheet-rolltemplate-utn h5 *, .ui-dialog .tab-content .charsheet h5 *,
+.sheet-rolltemplate-utn *[aria-level="5"] *,
+.ui-dialog .tab-content .charsheet *[aria-level="5"] *, .sheet-rolltemplate-utn h5, .ui-dialog .tab-content .charsheet h5,
+.sheet-rolltemplate-utn *[aria-level="5"],
+.ui-dialog .tab-content .charsheet *[aria-level="5"], .ui-dialog .tab-content .charsheet .roller *, .ui-dialog .tab-content .charsheet .styled-button--controlled *, .sheet-rolltemplate-utn h4 *, .ui-dialog .tab-content .charsheet h4 *,
+.sheet-rolltemplate-utn *[aria-level="4"] *,
+.ui-dialog .tab-content .charsheet *[aria-level="4"] *, .ui-dialog .tab-content .charsheet .roller, .ui-dialog .tab-content .charsheet .styled-button--controlled, .sheet-rolltemplate-utn h4, .ui-dialog .tab-content .charsheet h4,
+.sheet-rolltemplate-utn *[aria-level="4"],
+.ui-dialog .tab-content .charsheet *[aria-level="4"], .sheet-rolltemplate-utn h3 *, .ui-dialog .tab-content .charsheet h3 *,
+.sheet-rolltemplate-utn *[aria-level="3"] *,
+.ui-dialog .tab-content .charsheet *[aria-level="3"] *, .sheet-rolltemplate-utn h3, .ui-dialog .tab-content .charsheet h3,
+.sheet-rolltemplate-utn *[aria-level="3"],
+.ui-dialog .tab-content .charsheet *[aria-level="3"], .sheet-rolltemplate-utn h2 *, .ui-dialog .tab-content .charsheet h2 *, .sheet-rolltemplate-utn .input-label:not(.stacked).large > [name] *, .ui-dialog .tab-content .charsheet .input-label:not(.stacked).large > [name] *,
+.sheet-rolltemplate-utn *[aria-level="2"] *,
+.ui-dialog .tab-content .charsheet *[aria-level="2"] *, .sheet-rolltemplate-utn h2, .ui-dialog .tab-content .charsheet h2, .sheet-rolltemplate-utn .input-label:not(.stacked).large > [name], .ui-dialog .tab-content .charsheet .input-label:not(.stacked).large > [name],
+.sheet-rolltemplate-utn *[aria-level="2"],
+.ui-dialog .tab-content .charsheet *[aria-level="2"], .sheet-rolltemplate-utn h1 *, .ui-dialog .tab-content .charsheet h1 *,
+.sheet-rolltemplate-utn *[aria-level="1"] *,
+.ui-dialog .tab-content .charsheet *[aria-level="1"] *, .sheet-rolltemplate-utn h1, .ui-dialog .tab-content .charsheet h1,
+.sheet-rolltemplate-utn *[aria-level="1"],
+.ui-dialog .tab-content .charsheet *[aria-level="1"] {
+ line-height: normal;
+}
+
+.sheet-rolltemplate-utn h5 *, .ui-dialog .tab-content .charsheet h5 *,
+.sheet-rolltemplate-utn *[aria-level="5"] *,
+.ui-dialog .tab-content .charsheet *[aria-level="5"] *, .sheet-rolltemplate-utn h5, .ui-dialog .tab-content .charsheet h5,
+.sheet-rolltemplate-utn *[aria-level="5"],
+.ui-dialog .tab-content .charsheet *[aria-level="5"], .ui-dialog .tab-content .charsheet .roller *, .ui-dialog .tab-content .charsheet .styled-button--controlled *, .sheet-rolltemplate-utn h4 *, .ui-dialog .tab-content .charsheet h4 *,
+.sheet-rolltemplate-utn *[aria-level="4"] *,
+.ui-dialog .tab-content .charsheet *[aria-level="4"] *, .ui-dialog .tab-content .charsheet .roller, .ui-dialog .tab-content .charsheet .styled-button--controlled, .sheet-rolltemplate-utn h4, .ui-dialog .tab-content .charsheet h4,
+.sheet-rolltemplate-utn *[aria-level="4"],
+.ui-dialog .tab-content .charsheet *[aria-level="4"], .sheet-rolltemplate-utn h3 *, .ui-dialog .tab-content .charsheet h3 *,
+.sheet-rolltemplate-utn *[aria-level="3"] *,
+.ui-dialog .tab-content .charsheet *[aria-level="3"] *, .sheet-rolltemplate-utn h3, .ui-dialog .tab-content .charsheet h3,
+.sheet-rolltemplate-utn *[aria-level="3"],
+.ui-dialog .tab-content .charsheet *[aria-level="3"], .sheet-rolltemplate-utn h2 *, .ui-dialog .tab-content .charsheet h2 *, .sheet-rolltemplate-utn .input-label:not(.stacked).large > [name] *, .ui-dialog .tab-content .charsheet .input-label:not(.stacked).large > [name] *,
+.sheet-rolltemplate-utn *[aria-level="2"] *,
+.ui-dialog .tab-content .charsheet *[aria-level="2"] *, .sheet-rolltemplate-utn h2, .ui-dialog .tab-content .charsheet h2, .sheet-rolltemplate-utn .input-label:not(.stacked).large > [name], .ui-dialog .tab-content .charsheet .input-label:not(.stacked).large > [name],
+.sheet-rolltemplate-utn *[aria-level="2"],
+.ui-dialog .tab-content .charsheet *[aria-level="2"], .sheet-rolltemplate-utn h1 *, .ui-dialog .tab-content .charsheet h1 *,
+.sheet-rolltemplate-utn *[aria-level="1"] *,
+.ui-dialog .tab-content .charsheet *[aria-level="1"] *, .sheet-rolltemplate-utn h1, .ui-dialog .tab-content .charsheet h1,
+.sheet-rolltemplate-utn *[aria-level="1"],
+.ui-dialog .tab-content .charsheet *[aria-level="1"] {
+ color: var(--fontColor);
+ display: block;
+ white-space: nowrap;
+ margin-top: 0px;
+ margin-bottom: 0px;
+ font-weight: normal;
+}
+
+.sheet-rolltemplate-utn h3 *, .ui-dialog .tab-content .charsheet h3 *,
+.sheet-rolltemplate-utn *[aria-level="3"] *,
+.ui-dialog .tab-content .charsheet *[aria-level="3"] *, .sheet-rolltemplate-utn h3, .ui-dialog .tab-content .charsheet h3,
+.sheet-rolltemplate-utn *[aria-level="3"],
+.ui-dialog .tab-content .charsheet *[aria-level="3"], .sheet-rolltemplate-utn h2 *, .ui-dialog .tab-content .charsheet h2 *, .sheet-rolltemplate-utn .input-label:not(.stacked).large > [name] *, .ui-dialog .tab-content .charsheet .input-label:not(.stacked).large > [name] *,
+.sheet-rolltemplate-utn *[aria-level="2"] *,
+.ui-dialog .tab-content .charsheet *[aria-level="2"] *, .sheet-rolltemplate-utn h2, .ui-dialog .tab-content .charsheet h2, .sheet-rolltemplate-utn .input-label:not(.stacked).large > [name], .ui-dialog .tab-content .charsheet .input-label:not(.stacked).large > [name],
+.sheet-rolltemplate-utn *[aria-level="2"],
+.ui-dialog .tab-content .charsheet *[aria-level="2"], .sheet-rolltemplate-utn h1 *, .ui-dialog .tab-content .charsheet h1 *,
+.sheet-rolltemplate-utn *[aria-level="1"] *,
+.ui-dialog .tab-content .charsheet *[aria-level="1"] *, .sheet-rolltemplate-utn h1, .ui-dialog .tab-content .charsheet h1,
+.sheet-rolltemplate-utn *[aria-level="1"],
+.ui-dialog .tab-content .charsheet *[aria-level="1"] {
+ text-transform: uppercase;
+ font-weight: normal;
+}
+
+.sheet-rolltemplate-utn h5 :not(:where(input)), .ui-dialog .tab-content .charsheet h5 :not(:where(input)),
+.sheet-rolltemplate-utn *[aria-level="5"] :not(:where(input)),
+.ui-dialog .tab-content .charsheet *[aria-level="5"] :not(:where(input)), .sheet-rolltemplate-utn h5:not(:where(input)), .ui-dialog .tab-content .charsheet h5:not(:where(input)),
+.sheet-rolltemplate-utn [aria-level="5"]:not(:where(input)),
+.ui-dialog .tab-content .charsheet [aria-level="5"]:not(:where(input)), .ui-dialog .tab-content .charsheet .roller :not(:where(input)), .ui-dialog .tab-content .charsheet .styled-button--controlled :not(:where(input)), .sheet-rolltemplate-utn h4 :not(:where(input)), .ui-dialog .tab-content .charsheet h4 :not(:where(input)),
+.sheet-rolltemplate-utn *[aria-level="4"] :not(:where(input)),
+.ui-dialog .tab-content .charsheet *[aria-level="4"] :not(:where(input)), .ui-dialog .tab-content .charsheet .roller:not(:where(input)), .ui-dialog .tab-content .charsheet .styled-button--controlled:not(:where(input)), .sheet-rolltemplate-utn h4:not(:where(input)), .ui-dialog .tab-content .charsheet h4:not(:where(input)),
+.sheet-rolltemplate-utn [aria-level="4"]:not(:where(input)),
+.ui-dialog .tab-content .charsheet [aria-level="4"]:not(:where(input)) {
+ text-transform: capitalize;
+}
+
+.sheet-rolltemplate-utn h1 *, .ui-dialog .tab-content .charsheet h1 *,
+.sheet-rolltemplate-utn *[aria-level="1"] *,
+.ui-dialog .tab-content .charsheet *[aria-level="1"] *, .sheet-rolltemplate-utn h1, .ui-dialog .tab-content .charsheet h1,
+.sheet-rolltemplate-utn *[aria-level="1"],
+.ui-dialog .tab-content .charsheet *[aria-level="1"] {
+ font-size: 2rem;
+ font-family: var(--topHeaderFont);
+}
+.sheet-rolltemplate-utn h1 :not(input), .ui-dialog .tab-content .charsheet h1 :not(input),
+.sheet-rolltemplate-utn *[aria-level="1"] :not(input),
+.ui-dialog .tab-content .charsheet *[aria-level="1"] :not(input), .sheet-rolltemplate-utn h1:not(input), .ui-dialog .tab-content .charsheet h1:not(input),
+.sheet-rolltemplate-utn [aria-level="1"]:not(input),
+.ui-dialog .tab-content .charsheet [aria-level="1"]:not(input) {
+ text-align: center;
+}
+
+.sheet-rolltemplate-utn h2 *, .ui-dialog .tab-content .charsheet h2 *, .sheet-rolltemplate-utn .input-label:not(.stacked).large > [name] *, .ui-dialog .tab-content .charsheet .input-label:not(.stacked).large > [name] *,
+.sheet-rolltemplate-utn *[aria-level="2"] *,
+.ui-dialog .tab-content .charsheet *[aria-level="2"] *, .sheet-rolltemplate-utn h2, .ui-dialog .tab-content .charsheet h2, .sheet-rolltemplate-utn .input-label:not(.stacked).large > [name], .ui-dialog .tab-content .charsheet .input-label:not(.stacked).large > [name],
+.sheet-rolltemplate-utn *[aria-level="2"],
+.ui-dialog .tab-content .charsheet *[aria-level="2"] {
+ font-size: 1.75rem;
+ font-family: var(--topHeaderFont);
+}
+.sheet-rolltemplate-utn h2 :not(input), .ui-dialog .tab-content .charsheet h2 :not(input), .sheet-rolltemplate-utn .input-label:not(.stacked).large > [name] :not(input), .ui-dialog .tab-content .charsheet .input-label:not(.stacked).large > [name] :not(input),
+.sheet-rolltemplate-utn *[aria-level="2"] :not(input),
+.ui-dialog .tab-content .charsheet *[aria-level="2"] :not(input), .sheet-rolltemplate-utn h2:not(input), .ui-dialog .tab-content .charsheet h2:not(input), .sheet-rolltemplate-utn .input-label:not(.stacked).large > [name]:not(input), .ui-dialog .tab-content .charsheet .input-label:not(.stacked).large > [name]:not(input),
+.sheet-rolltemplate-utn [aria-level="2"]:not(input),
+.ui-dialog .tab-content .charsheet [aria-level="2"]:not(input) {
+ text-align: center;
+}
+
+.sheet-rolltemplate-utn h3 *, .ui-dialog .tab-content .charsheet h3 *,
+.sheet-rolltemplate-utn *[aria-level="3"] *,
+.ui-dialog .tab-content .charsheet *[aria-level="3"] *, .sheet-rolltemplate-utn h3, .ui-dialog .tab-content .charsheet h3,
+.sheet-rolltemplate-utn *[aria-level="3"],
+.ui-dialog .tab-content .charsheet *[aria-level="3"] {
+ font-size: 1.5rem;
+ font-family: var(--topHeaderFont);
+}
+.sheet-rolltemplate-utn h3 :not(input), .ui-dialog .tab-content .charsheet h3 :not(input),
+.sheet-rolltemplate-utn *[aria-level="3"] :not(input),
+.ui-dialog .tab-content .charsheet *[aria-level="3"] :not(input), .sheet-rolltemplate-utn h3:not(input), .ui-dialog .tab-content .charsheet h3:not(input),
+.sheet-rolltemplate-utn [aria-level="3"]:not(input),
+.ui-dialog .tab-content .charsheet [aria-level="3"]:not(input) {
+ text-align: center;
+}
+
+.ui-dialog .tab-content .charsheet .roller *, .ui-dialog .tab-content .charsheet .styled-button--controlled *, .sheet-rolltemplate-utn h4 *, .ui-dialog .tab-content .charsheet h4 *,
+.sheet-rolltemplate-utn *[aria-level="4"] *,
+.ui-dialog .tab-content .charsheet *[aria-level="4"] *, .ui-dialog .tab-content .charsheet .roller, .ui-dialog .tab-content .charsheet .styled-button--controlled, .sheet-rolltemplate-utn h4, .ui-dialog .tab-content .charsheet h4,
+.sheet-rolltemplate-utn *[aria-level="4"],
+.ui-dialog .tab-content .charsheet *[aria-level="4"] {
+ font-size: 1.25rem;
+ font-family: var(--midHeaderFont);
+}
+.ui-dialog .tab-content .charsheet .roller :not(input), .ui-dialog .tab-content .charsheet .styled-button--controlled :not(input), .sheet-rolltemplate-utn h4 :not(input), .ui-dialog .tab-content .charsheet h4 :not(input),
+.sheet-rolltemplate-utn *[aria-level="4"] :not(input),
+.ui-dialog .tab-content .charsheet *[aria-level="4"] :not(input), .ui-dialog .tab-content .charsheet .roller:not(input), .ui-dialog .tab-content .charsheet .styled-button--controlled:not(input), .sheet-rolltemplate-utn h4:not(input), .ui-dialog .tab-content .charsheet h4:not(input),
+.sheet-rolltemplate-utn [aria-level="4"]:not(input),
+.ui-dialog .tab-content .charsheet [aria-level="4"]:not(input) {
+ text-align: center;
+}
+
+.sheet-rolltemplate-utn h5 *, .ui-dialog .tab-content .charsheet h5 *,
+.sheet-rolltemplate-utn *[aria-level="5"] *,
+.ui-dialog .tab-content .charsheet *[aria-level="5"] *, .sheet-rolltemplate-utn h5, .ui-dialog .tab-content .charsheet h5,
+.sheet-rolltemplate-utn *[aria-level="5"],
+.ui-dialog .tab-content .charsheet *[aria-level="5"] {
+ font-size: 1rem;
+ font-style: normal;
+ font-family: var(--midHeaderFont);
+}
+.sheet-rolltemplate-utn h5 :not(input), .ui-dialog .tab-content .charsheet h5 :not(input),
+.sheet-rolltemplate-utn *[aria-level="5"] :not(input),
+.ui-dialog .tab-content .charsheet *[aria-level="5"] :not(input), .sheet-rolltemplate-utn h5:not(input), .ui-dialog .tab-content .charsheet h5:not(input),
+.sheet-rolltemplate-utn [aria-level="5"]:not(input),
+.ui-dialog .tab-content .charsheet [aria-level="5"]:not(input) {
+ text-align: center;
+}
+
+.sheet-rolltemplate-utn input[type=checkbox]:not(.collapse):checked:before, .ui-dialog .tab-content .charsheet input[type=checkbox]:not(.collapse):checked:before {
+ content: "✓";
+ grid-area: content;
+ font-weight: bold;
+ place-self: start center;
+ color: var(--selectedColor);
+ font-size: 1.5rem;
+ line-height: 0.5rem;
+}
+
+.sheet-rolltemplate-utn .boxed, .ui-dialog .tab-content .charsheet .boxed,
+.sheet-rolltemplate-utn .sheet-boxed,
+.ui-dialog .tab-content .charsheet .sheet-boxed {
+ border: 1px solid black;
+ border-radius: 0;
+ box-sizing: border-box;
+}
+
+.sheet-rolltemplate-utn .sheet-pictos, .ui-dialog .tab-content .charsheet .sheet-pictos,
+.sheet-rolltemplate-utn .pictos,
+.ui-dialog .tab-content .charsheet .pictos {
+ font-family: pictos;
+ text-transform: initial;
+}
+
+.sheet-rolltemplate-utn .pictos3, .ui-dialog .tab-content .charsheet .pictos3,
+.sheet-rolltemplate-utn .sheet-pictos3,
+.ui-dialog .tab-content .charsheet .sheet-pictos3 {
+ font-family: pictos three;
+ text-transform: initial;
+}
+
+.sheet-rolltemplate-utn .pictoscustom, .ui-dialog .tab-content .charsheet .pictoscustom,
+.sheet-rolltemplate-utn .sheet-pictoscustom,
+.ui-dialog .tab-content .charsheet .sheet-pictoscustom {
+ font-family: pictos custom;
+ text-transform: initial;
+}
+
+.sheet-rolltemplate-utn > .sheet-template, .ui-dialog .tab-content .charsheet .styled-button, .sheet-rolltemplate-utn section:is(:hover, :focus-within), .ui-dialog .tab-content .charsheet section:is(:hover, :focus-within),
+.sheet-rolltemplate-utn header:is(:hover, :focus-within),
+.ui-dialog .tab-content .charsheet header:is(:hover, :focus-within) {
+ box-shadow: rgba(0, 0, 0, 0.16) 0px 3px 6px, rgba(0, 0, 0, 0.23) 0px 3px 6px;
+}
+
+.sheet-rolltemplate-utn, .ui-dialog {
+ --grid-gap: 1rem;
+ --half-gap: calc(var(--grid-gap) / 2);
+ --tiny-gap: calc(var(--half-gap) / 2);
+ --big-gap: calc(var(--grid-gap) * 2);
+ --revealTime: 500ms;
+ --delayTime: 200ms;
+ --inputSwap: opacity var(--revealTime) ease var(--delayTime);
+ --focusTiming: 500ms ease;
+ --focusTrans: all var(--focusTiming);
+ --borderColor: black;
+ --fontColor: black;
+ --backColor: #ffffff;
+ --spanSectionColumn:1/-1;
+ --displayBorderWidth: 2px;
+ --subHeadBackColor:grey;
+ --unselectedColor1:lightgrey;
+ --unselectedColor2:transparent;
+ --selectedColor:black;
+}
+
+.sheet-rolltemplate-utn, .ui-dialog .tab-content .charsheet {
+ gap: var(--grid-gap);
+}
+.sheet-rolltemplate-utn button, .ui-dialog .tab-content .charsheet button {
+ cursor: pointer;
+}
+.sheet-rolltemplate-utn .flex-box, .ui-dialog .tab-content .charsheet .flex-box,
+.sheet-rolltemplate-utn .sheet-flex-box,
+.ui-dialog .tab-content .charsheet .sheet-flex-box {
+ display: flex;
+}
+.sheet-rolltemplate-utn .space-around, .ui-dialog .tab-content .charsheet .space-around,
+.sheet-rolltemplate-utn .sheet-space-around,
+.ui-dialog .tab-content .charsheet .sheet-space-around {
+ justify-content: space-around;
+}
+.sheet-rolltemplate-utn .space-between, .ui-dialog .tab-content .charsheet .space-between,
+.sheet-rolltemplate-utn .sheet-space-between,
+.ui-dialog .tab-content .charsheet .sheet-space-between {
+ justify-content: space-between;
+}
+.sheet-rolltemplate-utn .center, .ui-dialog .tab-content .charsheet .center,
+.sheet-rolltemplate-utn .sheet-center,
+.ui-dialog .tab-content .charsheet .sheet-center {
+ place-self: center;
+}
+.sheet-rolltemplate-utn textarea, .ui-dialog .tab-content .charsheet textarea {
+ resize: vertical;
+ white-space: pre-wrap;
+}
+.sheet-rolltemplate-utn textarea.fixed, .ui-dialog .tab-content .charsheet textarea.fixed {
+ resize: none;
+ overflow: auto;
+}
+.sheet-rolltemplate-utn .span-2, .ui-dialog .tab-content .charsheet .span-2,
+.sheet-rolltemplate-utn .sheet-span-2,
+.ui-dialog .tab-content .charsheet .sheet-span-2 {
+ grid-column: span 2;
+}
+.sheet-rolltemplate-utn .span-all, .ui-dialog .tab-content .charsheet .span-all,
+.sheet-rolltemplate-utn .sheet-span-all,
+.ui-dialog .tab-content .charsheet .sheet-span-all {
+ grid-column: 1/-1;
+}
+.sheet-rolltemplate-utn select, .ui-dialog .tab-content .charsheet select,
+.sheet-rolltemplate-utn .pseudo-select span,
+.ui-dialog .tab-content .charsheet .pseudo-select span,
+.sheet-rolltemplate-utn .sheet-pseudo-select span,
+.ui-dialog .tab-content .charsheet .sheet-pseudo-select span,
+.sheet-rolltemplate-utn textarea,
+.ui-dialog .tab-content .charsheet textarea,
+.sheet-rolltemplate-utn input:not(:is([type=radio], [type=checkbox])),
+.ui-dialog .tab-content .charsheet input:not(:is([type=radio], [type=checkbox])),
+.sheet-rolltemplate-utn .uneditable-input,
+.ui-dialog .tab-content .charsheet .uneditable-input {
+ padding: 2px;
+ font-size: 1rem;
+}
+.sheet-rolltemplate-utn input:is([type=text], [type=number]), .ui-dialog .tab-content .charsheet input:is([type=text], [type=number]), .sheet-rolltemplate-utn textarea, .ui-dialog .tab-content .charsheet textarea {
+ cursor: auto;
+}
+.sheet-rolltemplate-utn select, .ui-dialog .tab-content .charsheet select,
+.sheet-rolltemplate-utn .sheet-pseudo-select span,
+.ui-dialog .tab-content .charsheet .sheet-pseudo-select span,
+.sheet-rolltemplate-utn .pseudo-select span,
+.ui-dialog .tab-content .charsheet .pseudo-select span {
+ -webkit-apperance: none;
+ appearance: none;
+ text-transform: capitalize;
+ overflow: hidden !important;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ text-align: center;
+ color: var(--fontColor);
+}
+.sheet-rolltemplate-utn input, .ui-dialog .tab-content .charsheet input {
+ width: auto;
+}
+.sheet-rolltemplate-utn input:placeholder, .ui-dialog .tab-content .charsheet input:placeholder {
+ color: #ededed80;
+}
+.sheet-rolltemplate-utn input.plus-control:not([value*="-"]) + span:before, .ui-dialog .tab-content .charsheet input.plus-control:not([value*="-"]) + span:before {
+ content: "+";
+}
+.sheet-rolltemplate-utn span, .ui-dialog .tab-content .charsheet span,
+.sheet-rolltemplate-utn input,
+.ui-dialog .tab-content .charsheet input,
+.sheet-rolltemplate-utn textarea,
+.ui-dialog .tab-content .charsheet textarea {
+ font-size: 1rem;
+ color: var(--fontColor);
+}
+.sheet-rolltemplate-utn span[name], .ui-dialog .tab-content .charsheet span[name], .sheet-rolltemplate-utn input, .ui-dialog .tab-content .charsheet input, .sheet-rolltemplate-utn textarea, .ui-dialog .tab-content .charsheet textarea, .sheet-rolltemplate-utn button, .ui-dialog .tab-content .charsheet button, .sheet-rolltemplate-utn select, .ui-dialog .tab-content .charsheet select {
+ font-family: var(--contentFont);
+}
+.sheet-rolltemplate-utn input[type=checkbox], .ui-dialog .tab-content .charsheet input[type=checkbox] {
+ border: 1px solid var(--borderColor);
+ cursor: pointer;
+ -webkit-appearance: none;
+ appearance: none;
+ cursor: pointer;
+ width: 14px;
+ min-width: 14px;
+ height: 14px;
+ min-height: 14px;
+ display: grid;
+ grid-template-columns: 1fr;
+ grid-template-areas: "content";
+}
+.sheet-rolltemplate-utn input[type=number], .ui-dialog .tab-content .charsheet input[type=number] {
+ width: 3rem;
+ -moz-appearance: textfield !important;
+ text-align: center;
+}
+.sheet-rolltemplate-utn input[type=number]::-webkit-inner-spin-button, .ui-dialog .tab-content .charsheet input[type=number]::-webkit-inner-spin-button, .sheet-rolltemplate-utn input[type=number]::-webkit-outer-spin-button, .ui-dialog .tab-content .charsheet input[type=number]::-webkit-outer-spin-button {
+ -webkit-appearance: none;
+ margin: 0;
+}
+.sheet-rolltemplate-utn .sheet-number, .ui-dialog .tab-content .charsheet .sheet-number,
+.sheet-rolltemplate-utn .number,
+.ui-dialog .tab-content .charsheet .number {
+ width: 2rem;
+ box-sizing: border-box;
+}
+.sheet-rolltemplate-utn section, .ui-dialog .tab-content .charsheet section,
+.sheet-rolltemplate-utn header,
+.ui-dialog .tab-content .charsheet header {
+ gap: var(--half-gap);
+ align-self: start;
+ grid-template-areas: "header";
+ border-radius: 5px;
+ padding: var(--half-gap);
+ transition-property: box-shadow;
+ transition-duration: var(--revealTime);
+ transition-timing-function: ease;
+}
+.sheet-rolltemplate-utn .inactive, .ui-dialog .tab-content .charsheet .inactive,
+.sheet-rolltemplate-utn .character-type--inactive,
+.ui-dialog .tab-content .charsheet .character-type--inactive,
+.sheet-rolltemplate-utn .navigable-section:not(.active),
+.ui-dialog .tab-content .charsheet .navigable-section:not(.active) {
+ display: none !important;
+}
+.sheet-rolltemplate-utn .pseudo-button, .ui-dialog .tab-content .charsheet .pseudo-button {
+ display: inline;
+ cursor: pointer;
+ border-radius: 5px;
+ text-transform: uppercase;
+ border: 1px solid black;
+ text-align: center;
+ padding: 0 5px;
+}
+.sheet-rolltemplate-utn .input-label, .ui-dialog .tab-content .charsheet .input-label {
+ display: inline-flex;
+ gap: var(--half-gap);
+ align-items: center;
+}
+.sheet-rolltemplate-utn .input-label:not(.stacked) > input[type=text], .ui-dialog .tab-content .charsheet .input-label:not(.stacked) > input[type=text] {
+ width: 0;
+ flex: 1 1 0;
+}
+.sheet-rolltemplate-utn .input-label:not(.stacked) > :not([name]), .ui-dialog .tab-content .charsheet .input-label:not(.stacked) > :not([name]) {
+ flex: 0 0 auto;
+}
+.sheet-rolltemplate-utn .input-label--dual, .ui-dialog .tab-content .charsheet .input-label--dual {
+ grid-template-columns: 1fr auto 1fr;
+ grid-template-areas: "over over over" "under under under";
+}
+.sheet-rolltemplate-utn .input-label--dual > span:last-of-type, .ui-dialog .tab-content .charsheet .input-label--dual > span:last-of-type {
+ grid-column: 1/-1;
+}
+.sheet-rolltemplate-utn .input-label--dual > input, .ui-dialog .tab-content .charsheet .input-label--dual > input {
+ width: 100%;
+}
+.sheet-rolltemplate-utn .input-label--dual > .slash, .ui-dialog .tab-content .charsheet .input-label--dual > .slash {
+ line-height: 0rem;
+ font-weight: normal;
+}
+.sheet-rolltemplate-utn .headed-textarea, .ui-dialog .tab-content .charsheet .headed-textarea {
+ grid-template-rows: auto 1fr;
+ grid-template-columns: auto;
+}
+.sheet-rolltemplate-utn .headed-textarea h5, .ui-dialog .tab-content .charsheet .headed-textarea h5 {
+ justify-self: start;
+}
+.sheet-rolltemplate-utn .boxed.thick-left, .ui-dialog .tab-content .charsheet .boxed.thick-left,
+.sheet-rolltemplate-utn .sheet-boxed.thick-left,
+.ui-dialog .tab-content .charsheet .sheet-boxed.thick-left {
+ border-left-width: 5px;
+}
+.sheet-rolltemplate-utn .boxed.thick-bottom, .ui-dialog .tab-content .charsheet .boxed.thick-bottom,
+.sheet-rolltemplate-utn .sheet-boxed.thick-bottom,
+.ui-dialog .tab-content .charsheet .sheet-boxed.thick-bottom {
+ border-bottom-width: 5px;
+}
+.sheet-rolltemplate-utn .boxed.thick-right, .ui-dialog .tab-content .charsheet .boxed.thick-right,
+.sheet-rolltemplate-utn .sheet-boxed.thick-right,
+.ui-dialog .tab-content .charsheet .sheet-boxed.thick-right {
+ border-right-width: 5px;
+}
+.sheet-rolltemplate-utn .boxed.thick-top, .ui-dialog .tab-content .charsheet .boxed.thick-top,
+.sheet-rolltemplate-utn .sheet-boxed.thick-top,
+.ui-dialog .tab-content .charsheet .sheet-boxed.thick-top {
+ border-top-width: 5px;
+}
+.sheet-rolltemplate-utn .underlined, .ui-dialog .tab-content .charsheet .underlined,
+.sheet-rolltemplate-utn .sheet-underlined,
+.ui-dialog .tab-content .charsheet .sheet-underlined {
+ border-radius: 0;
+ border-bottom: 1px solid var(--borderColor);
+ transition: var(--focusTrans);
+}
+.sheet-rolltemplate-utn :is(.underlined, .boxed):not([readonly]):not([type=checkbox]):not([type=radio]):is(:focus, :focus-within), .ui-dialog .tab-content .charsheet :is(.underlined, .boxed):not([readonly]):not([type=checkbox]):not([type=radio]):is(:focus, :focus-within) {
+ background-color: var(--subHeadBackColor);
+}
+.sheet-rolltemplate-utn .underlined--invisible, .ui-dialog .tab-content .charsheet .underlined--invisible {
+ border-color: transparent !important;
+}
+.sheet-rolltemplate-utn .short-text, .ui-dialog .tab-content .charsheet .short-text,
+.sheet-rolltemplate-utn .sheet-short-text,
+.ui-dialog .tab-content .charsheet .sheet-short-text {
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ overflow: hidden;
+ text-transform: initial;
+}
+.sheet-rolltemplate-utn .roll-container, .ui-dialog .tab-content .charsheet .roll-container {
+ align-self: center;
+ opacity: 0;
+ grid-row: 1/2;
+}
+.sheet-rolltemplate-utn .collapse-container, .ui-dialog .tab-content .charsheet .collapse-container {
+ display: grid;
+ position: relative;
+}
+.sheet-rolltemplate-utn .text-collapse, .ui-dialog .tab-content .charsheet .text-collapse {
+ cursor: pointer;
+ display: flex;
+ justify-content: flex-start;
+ align-items: center;
+}
+.sheet-rolltemplate-utn .text-collapse:before, .ui-dialog .tab-content .charsheet .text-collapse:before {
+ content: "unfold_less";
+ font-family: "Material Icons";
+ font-weight: normal;
+ font-style: normal;
+ font-size: 24px;
+ /* Preferred icon size */
+ display: inline-block;
+ line-height: 1;
+ text-transform: none;
+ letter-spacing: normal;
+ word-wrap: normal;
+ white-space: nowrap;
+ direction: ltr;
+ /* Support for all WebKit browsers. */
+ -webkit-font-smoothing: antialiased;
+ /* Support for Safari and Chrome. */
+ text-rendering: optimizeLegibility;
+ /* Support for Firefox. */
+ -moz-osx-font-smoothing: grayscale;
+}
+.sheet-rolltemplate-utn .text-collapse__text, .ui-dialog .tab-content .charsheet .text-collapse__text {
+ display: inline;
+}
+.sheet-rolltemplate-utn .text-collapse__check:checked + .text-collapse:before, .ui-dialog .tab-content .charsheet .text-collapse__check:checked + .text-collapse:before {
+ content: "unfold_more";
+}
+.sheet-rolltemplate-utn .repitem:hover .collapse, .ui-dialog .tab-content .charsheet .repitem:hover .collapse, .sheet-rolltemplate-utn .repitem:hover .roll-container, .ui-dialog .tab-content .charsheet .repitem:hover .roll-container,
+.sheet-rolltemplate-utn .collapse-container:hover .collapse,
+.ui-dialog .tab-content .charsheet .collapse-container:hover .collapse,
+.sheet-rolltemplate-utn .collapse-container:hover .roll-container,
+.ui-dialog .tab-content .charsheet .collapse-container:hover .roll-container {
+ opacity: 1;
+}
+.sheet-rolltemplate-utn .repitem .collapse, .ui-dialog .tab-content .charsheet .repitem .collapse,
+.sheet-rolltemplate-utn .collapse-container .collapse,
+.ui-dialog .tab-content .charsheet .collapse-container .collapse {
+ opacity: 0;
+ position: absolute;
+ right: -10px;
+ top: 0px;
+ border: 0px solid black;
+ border-radius: 0;
+ color: var(--selectedColor);
+ text-transform: none;
+ background-color: transparent;
+}
+.sheet-rolltemplate-utn .repitem .collapse:before, .ui-dialog .tab-content .charsheet .repitem .collapse:before,
+.sheet-rolltemplate-utn .collapse-container .collapse:before,
+.ui-dialog .tab-content .charsheet .collapse-container .collapse:before {
+ content: "y";
+ font-family: pictos;
+}
+.sheet-rolltemplate-utn .repitem .collapse:checked, .ui-dialog .tab-content .charsheet .repitem .collapse:checked,
+.sheet-rolltemplate-utn .collapse-container .collapse:checked,
+.ui-dialog .tab-content .charsheet .collapse-container .collapse:checked {
+ color: var(--unselectedColor1);
+ background-color: transparent;
+}
+.sheet-rolltemplate-utn .repitem .collapse:checked ~ .expanded, .ui-dialog .tab-content .charsheet .repitem .collapse:checked ~ .expanded,
+.sheet-rolltemplate-utn .repitem .collapse:checked ~ .collapse-container .expanded,
+.ui-dialog .tab-content .charsheet .repitem .collapse:checked ~ .collapse-container .expanded,
+.sheet-rolltemplate-utn .collapse-container .collapse:checked ~ .expanded,
+.ui-dialog .tab-content .charsheet .collapse-container .collapse:checked ~ .expanded,
+.sheet-rolltemplate-utn .collapse-container .collapse:checked ~ .collapse-container .expanded,
+.ui-dialog .tab-content .charsheet .collapse-container .collapse:checked ~ .collapse-container .expanded {
+ display: none !important;
+}
+.sheet-rolltemplate-utn .repitem .collapse:checked ~ .expanded--empty:is(:not([value]), [value=""]) + *, .ui-dialog .tab-content .charsheet .repitem .collapse:checked ~ .expanded--empty:is(:not([value]), [value=""]) + *,
+.sheet-rolltemplate-utn .repitem .collapse:checked ~ .collapse-container ~ .expanded--empty:is(:not([value]), [value=""]) + *,
+.ui-dialog .tab-content .charsheet .repitem .collapse:checked ~ .collapse-container ~ .expanded--empty:is(:not([value]), [value=""]) + *,
+.sheet-rolltemplate-utn .collapse-container .collapse:checked ~ .expanded--empty:is(:not([value]), [value=""]) + *,
+.ui-dialog .tab-content .charsheet .collapse-container .collapse:checked ~ .expanded--empty:is(:not([value]), [value=""]) + *,
+.sheet-rolltemplate-utn .collapse-container .collapse:checked ~ .collapse-container ~ .expanded--empty:is(:not([value]), [value=""]) + *,
+.ui-dialog .tab-content .charsheet .collapse-container .collapse:checked ~ .collapse-container ~ .expanded--empty:is(:not([value]), [value=""]) + * {
+ display: none !important;
+}
+.sheet-rolltemplate-utn .repitem .collapse:not(:checked) ~ .collapsed, .ui-dialog .tab-content .charsheet .repitem .collapse:not(:checked) ~ .collapsed,
+.sheet-rolltemplate-utn .collapse-container .collapse:not(:checked) ~ .collapsed,
+.ui-dialog .tab-content .charsheet .collapse-container .collapse:not(:checked) ~ .collapsed {
+ display: none !important;
+}
+.sheet-rolltemplate-utn .repitem .collapse:hover, .ui-dialog .tab-content .charsheet .repitem .collapse:hover,
+.sheet-rolltemplate-utn .collapse-container .collapse:hover,
+.ui-dialog .tab-content .charsheet .collapse-container .collapse:hover {
+ color: var(--selectedColor);
+}
+.sheet-rolltemplate-utn .repitem > input:not([type=checkbox]), .ui-dialog .tab-content .charsheet .repitem > input:not([type=checkbox]) {
+ width: 100%;
+}
+.sheet-rolltemplate-utn .repitem .headed-textarea, .ui-dialog .tab-content .charsheet .repitem .headed-textarea {
+ grid-column: 1/-1;
+}
+.sheet-rolltemplate-utn .repcontainer.editmode .collapse, .ui-dialog .tab-content .charsheet .repcontainer.editmode .collapse {
+ display: none;
+}
+.sheet-rolltemplate-utn .repcontainer, .ui-dialog .tab-content .charsheet .repcontainer {
+ display: grid;
+ gap: var(--grid-gap);
+}
+.sheet-rolltemplate-utn .repeating-container, .ui-dialog .tab-content .charsheet .repeating-container {
+ display: grid;
+ grid-template-areas: "header";
+}
+.sheet-rolltemplate-utn .repeating-container > .header, .ui-dialog .tab-content .charsheet .repeating-container > .header {
+ grid-area: header;
+}
+.sheet-rolltemplate-utn .repeating-container:is(:hover, :focus-within, :focus) .repcontrol-button, .ui-dialog .tab-content .charsheet .repeating-container:is(:hover, :focus-within, :focus) .repcontrol-button {
+ opacity: 1;
+}
+.sheet-rolltemplate-utn .repcontrol-button, .ui-dialog .tab-content .charsheet .repcontrol-button {
+ grid-area: header;
+ align-self: center;
+ text-transform: none;
+ opacity: 0;
+ transition-property: opacity, background-color;
+ transition-duration: var(--revealTime);
+}
+.sheet-rolltemplate-utn .repcontrol-button ~ .repcontrol, .ui-dialog .tab-content .charsheet .repcontrol-button ~ .repcontrol {
+ display: none;
+}
+.sheet-rolltemplate-utn .repcontrol_move, .ui-dialog .tab-content .charsheet .repcontrol_move {
+ display: none;
+}
+.sheet-rolltemplate-utn .repcontrol-button--add, .ui-dialog .tab-content .charsheet .repcontrol-button--add {
+ justify-self: start;
+}
+.sheet-rolltemplate-utn .repcontrol-button--add:before, .ui-dialog .tab-content .charsheet .repcontrol-button--add:before {
+ font-family: pictos;
+ content: "&";
+}
+.sheet-rolltemplate-utn .repcontrol-button--edit, .ui-dialog .tab-content .charsheet .repcontrol-button--edit {
+ justify-self: end;
+}
+.sheet-rolltemplate-utn .repcontrol-button--edit:before, .ui-dialog .tab-content .charsheet .repcontrol-button--edit:before {
+ font-family: pictos;
+ content: "p";
+}
+.sheet-rolltemplate-utn .sheet-box, .ui-dialog .tab-content .charsheet .sheet-box,
+.sheet-rolltemplate-utn .box,
+.ui-dialog .tab-content .charsheet .box {
+ border-radius: 0;
+ width: 10px;
+ height: 10px;
+}
+
+html {
+ font-size: 16px;
+}
+
+.ui-dialog .tab-content .charsheet {
+ /*
+ First of all we apply our default styles.
+ Note that because we are using the placeholder, these styles would be first up in the cascade even if we put the extend declaration last.
+ This is done first so that when we read the code, we can easily see that it is applied, and anything after it will overwrite those default styles.
+ */
+ padding-top: 0;
+}
+.ui-dialog .tab-content .charsheet .roller {
+ display: flex;
+ gap: var(--half-gap);
+ align-items: center;
+}
+.ui-dialog .tab-content .charsheet .roller:before {
+ font-family: dicefontd6;
+ content: "L";
+}
+.ui-dialog .tab-content .charsheet button {
+ cursor: pointer;
+}
+.ui-dialog .tab-content .charsheet h3 {
+ grid-area: header;
+}
+.ui-dialog .tab-content .charsheet .fill-left {
+ display: grid;
+ align-self: center;
+ box-sizing: border-box;
+ box-shadow: inset 0 0 0 1px var(--selectedColor);
+}
+.ui-dialog .tab-content .charsheet .fill-left:is(:hover, :focus-within, :focus) .fill-left__radio--clearer {
+ background-image: linear-gradient(45deg, transparent 0% 48%, white 48% calc(48% + 1px), transparent calc(48% + 1px)), linear-gradient(-45deg, transparent 0% 48%, white 48% calc(48% + 1px), transparent calc(48% + 1px));
+ border-color: transparent;
+}
+.ui-dialog .tab-content .charsheet .fill-left:not(.no-fill) .fill-left__radio:checked {
+ display: none;
+}
+.ui-dialog .tab-content .charsheet .fill-left__radio {
+ appearance: none;
+ -webkit-appearance: none;
+ background-color: var(--selectedColor);
+ order: 1;
+ width: 100%;
+ height: 100%;
+ cursor: pointer;
+}
+.ui-dialog .tab-content .charsheet .fill-left__radio:checked ~ .fill-left__radio {
+ background-color: var(--unselectedColor2);
+ order: 3;
+}
+.ui-dialog .tab-content .charsheet .fill-left__radio--clearer {
+ order: 2;
+}
+.ui-dialog .tab-content .charsheet .fill-left__radio--clearer:checked {
+ display: none;
+}
+.ui-dialog .tab-content .charsheet .styled-button {
+ background-color: var(--backColor);
+ border: 0;
+ border-radius: 0.5rem;
+ box-sizing: border-box;
+ cursor: pointer;
+ transition-property: background-color, color;
+ transition-duration: var(--revealTime);
+ padding-inline: var(--half-gap);
+ text-align: center;
+}
+.ui-dialog .tab-content .charsheet .styled-button:hover {
+ background-color: var(--selectedColor);
+}
+.ui-dialog .tab-content .charsheet .styled-button:hover, .ui-dialog .tab-content .charsheet .styled-button:hover * {
+ color: var(--dark-secondarytext);
+}
+.ui-dialog .tab-content .charsheet .styled-button:focus {
+ outline: 2px solid transparent;
+ outline-offset: 2px;
+}
+.ui-dialog .tab-content .charsheet .styled-button:focus-visible {
+ box-shadow: none;
+}
+.ui-dialog .tab-content .charsheet .styled-button-control:is(:checked, [type=hidden]:not([value=""]):not([value="0"])) + .styled-button--controlled {
+ background-color: var(--selectedColor);
+}
+.ui-dialog .tab-content .charsheet .styled-button-control:is(:checked, [type=hidden]:not([value=""]):not([value="0"])) + .styled-button--controlled, .ui-dialog .tab-content .charsheet .styled-button-control:is(:checked, [type=hidden]:not([value=""]):not([value="0"])) + .styled-button--controlled * {
+ color: var(--dark-secondarytext);
+}
+
+.ui-dialog .sheetform {
+ background-color: var(--backColor);
+ transition-property: background-color;
+ transition-duration: var(--revealTime);
+}
+
+#main {
+ display: grid;
+}
+
+#character,
+#gamescreen {
+ display: grid;
+ gap: var(--grid-gap);
+ grid-auto-flow: dense;
+ grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
+}
+
+ul.nav-tabs.nav {
+ position: sticky;
+ top: 0;
+ background-color: var(--backColor);
+ z-index: 9999;
+ margin-bottom: 0px;
+}
+
+#main-nav {
+ display: grid;
+ gap: var(--half-gap);
+ grid-template-columns: 1fr auto;
+ align-items: center;
+ position: sticky;
+ top: 44px;
+ margin-inline: -10px;
+ margin-bottom: 10px;
+ padding-inline: 10px;
+ padding-block: var(--half-gap);
+ background-color: var(--backColor);
+ z-index: 1000;
+ transition-property: background-color;
+ transition-duration: var(--revealTime);
+}
+#main-nav .sheet-nav__tab {
+ align-self: end;
+ grid-column: 2;
+ font-size: 1.5rem;
+}
+#main-nav .sheet-nav__tab.active {
+ display: none;
+}
+#main-nav .roll-options,
+#main-nav .basic-moves {
+ display: flex;
+ flex-wrap: wrap;
+ grid-gap: var(--half-gap);
+}
+#main-nav .basic-moves {
+ justify-content: space-between;
+}
+#main-nav .roll-options {
+ grid-column: 1;
+ justify-content: center;
+}
+
+#settings {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
+ gap: var(--grid-gap);
+}
+#settings h1 {
+ grid-column: 1/-1;
+}
+#settings .move-stats {
+ display: grid;
+ gap: var(--grid-gap);
+ grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
+}
+#settings .move-stats .input-label {
+ justify-content: space-between;
+}
+
+@media (min-width: 597px) {
+ .span-2--if-wide {
+ grid-column: span 2;
+ }
+}
+#character-header {
+ display: grid;
+ gap: var(--half-gap);
+ grid-template-columns: 20rem 1fr;
+ grid-template-rows: repeat(2, auto);
+ grid-template-areas: "image summary" "sentence summary";
+}
+#character-header .header-image {
+ grid-area: image;
+}
+#character-header > .sentence {
+ display: flex;
+ grid-area: sentence;
+ grid-area: auto;
+ justify-self: center;
+ flex-wrap: wrap;
+ gap: var(--half-gap);
+}
+#character-header > .sentence > .character-name {
+ min-width: 8rem;
+}
+#character-header > .sentence > .character-name > input {
+ text-transform: none;
+}
+#character-header .adaptive--input {
+ min-width: 6rem;
+}
+#character-header .input-label,
+#character-header .adaptive--input {
+ flex: 1 0 auto;
+}
+#character-header .character-summary {
+ grid-area: summary;
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ grid-template-rows: auto 1fr;
+ gap: var(--grid-gap);
+}
+#character-header .generic-details {
+ display: flex;
+ gap: var(--grid-gap);
+ flex-wrap: wrap;
+}
+#character-header .generic-details .adaptive,
+#character-header .generic-details .input-label {
+ flex: 1 0 auto;
+}
+#character-header .arc.inactive + .adventure-points {
+ grid-column: 1/-1;
+}
+#character-header .adventure-points {
+ place-self: center;
+}
+
+#attributes .input-label {
+ justify-content: space-between;
+}
+#attributes .input-label button {
+ width: auto;
+ flex: 0;
+}
+
+#skills .super,
+#signature-item .super {
+ justify-self: center;
+}
+#skills button,
+#signature-item button {
+ width: auto;
+ flex: 0;
+}
+#skills .repitem,
+#skills .repeat-column,
+#signature-item .repitem,
+#signature-item .repeat-column {
+ display: grid;
+}
+#skills .expanded,
+#signature-item .expanded {
+ display: flex;
+ justify-content: space-around;
+}
+
+#skills .repitem,
+#skills .repeat-column {
+ grid-template-columns: repeat(2, 1fr);
+ gap: var(--half-gap);
+}
+
+#signature-item {
+ justify-self: center;
+}
+#signature-item input {
+ width: initial;
+ flex: 0;
+}
+
+#compatibility .repitem,
+#compatibility .repeat-column {
+ display: grid;
+ grid-template-columns: 1fr 3rem;
+ align-items: end;
+ gap: var(--half-gap);
+}
+#compatibility .repcontainer {
+ gap: var(--half-gap);
+}
+#compatibility h4 {
+ white-space: pre-wrap;
+}
+#compatibility button {
+ width: auto;
+ flex: 0;
+}
+
+#playbook .repitem,
+#playbook .repeat-column {
+ display: grid;
+ grid-template-columns: 2fr 8rem 6rem;
+ gap: var(--half-gap);
+}
+#playbook button {
+ width: auto;
+ flex: 0;
+}
+
+#mon .repitem,
+#mon .repeat-column {
+ display: grid;
+ grid-template-columns: 1fr 1fr 8rem 1fr;
+ gap: var(--half-gap);
+}
+#mon button {
+ width: auto;
+ flex: 0;
+}
+
+#meter {
+ --meterSquare:2rem;
+ --meterColumns:repeat(10,var(--meterSquare));
+}
+#meter .repitem,
+#meter .repeat-column {
+ display: grid;
+ grid-template-columns: 1fr var(--half-gap) var(--meterColumns);
+}
+#meter .repitem > h5,
+#meter .repeat-column > h5 {
+ place-self: center end;
+}
+#meter .fill-left {
+ grid-column: 3/-1;
+ justify-self: start;
+ border-radius: 15px;
+ overflow: hidden;
+ align-self: center;
+ height: 1rem;
+ align-items: center;
+ grid-template-columns: repeat(10, var(--meterSquare));
+ grid-template-rows: var(--grid-gap);
+ align-content: start;
+}
+#meter .fill-left.no-fill {
+ --selectedColor:lightgrey;
+}
+#meter h4:first-child {
+ grid-column: span 2;
+ margin-right: var(--half-gap);
+}
+#meter button {
+ width: auto;
+ flex: 0;
+}
+#meter .fill-left__max-control[value="1"] + .fill-left {
+ grid-template-columns: repeat(1, var(--meterSquare));
+}
+#meter .fill-left__max-control[value="1"] + .fill-left .fill-left__radio[value="2"] {
+ display: none;
+}
+#meter .fill-left__max-control[value="1"] + .fill-left .fill-left__radio[value="3"] {
+ display: none;
+}
+#meter .fill-left__max-control[value="1"] + .fill-left .fill-left__radio[value="4"] {
+ display: none;
+}
+#meter .fill-left__max-control[value="1"] + .fill-left .fill-left__radio[value="5"] {
+ display: none;
+}
+#meter .fill-left__max-control[value="1"] + .fill-left .fill-left__radio[value="6"] {
+ display: none;
+}
+#meter .fill-left__max-control[value="1"] + .fill-left .fill-left__radio[value="7"] {
+ display: none;
+}
+#meter .fill-left__max-control[value="1"] + .fill-left .fill-left__radio[value="8"] {
+ display: none;
+}
+#meter .fill-left__max-control[value="1"] + .fill-left .fill-left__radio[value="9"] {
+ display: none;
+}
+#meter .fill-left__max-control[value="1"] + .fill-left .fill-left__radio[value="10"] {
+ display: none;
+}
+#meter .fill-left__max-control[value="2"] + .fill-left {
+ grid-template-columns: repeat(2, var(--meterSquare));
+}
+#meter .fill-left__max-control[value="2"] + .fill-left .fill-left__radio[value="3"] {
+ display: none;
+}
+#meter .fill-left__max-control[value="2"] + .fill-left .fill-left__radio[value="4"] {
+ display: none;
+}
+#meter .fill-left__max-control[value="2"] + .fill-left .fill-left__radio[value="5"] {
+ display: none;
+}
+#meter .fill-left__max-control[value="2"] + .fill-left .fill-left__radio[value="6"] {
+ display: none;
+}
+#meter .fill-left__max-control[value="2"] + .fill-left .fill-left__radio[value="7"] {
+ display: none;
+}
+#meter .fill-left__max-control[value="2"] + .fill-left .fill-left__radio[value="8"] {
+ display: none;
+}
+#meter .fill-left__max-control[value="2"] + .fill-left .fill-left__radio[value="9"] {
+ display: none;
+}
+#meter .fill-left__max-control[value="2"] + .fill-left .fill-left__radio[value="10"] {
+ display: none;
+}
+#meter .fill-left__max-control[value="3"] + .fill-left {
+ grid-template-columns: repeat(3, var(--meterSquare));
+}
+#meter .fill-left__max-control[value="3"] + .fill-left .fill-left__radio[value="4"] {
+ display: none;
+}
+#meter .fill-left__max-control[value="3"] + .fill-left .fill-left__radio[value="5"] {
+ display: none;
+}
+#meter .fill-left__max-control[value="3"] + .fill-left .fill-left__radio[value="6"] {
+ display: none;
+}
+#meter .fill-left__max-control[value="3"] + .fill-left .fill-left__radio[value="7"] {
+ display: none;
+}
+#meter .fill-left__max-control[value="3"] + .fill-left .fill-left__radio[value="8"] {
+ display: none;
+}
+#meter .fill-left__max-control[value="3"] + .fill-left .fill-left__radio[value="9"] {
+ display: none;
+}
+#meter .fill-left__max-control[value="3"] + .fill-left .fill-left__radio[value="10"] {
+ display: none;
+}
+#meter .fill-left__max-control[value="4"] + .fill-left {
+ grid-template-columns: repeat(4, var(--meterSquare));
+}
+#meter .fill-left__max-control[value="4"] + .fill-left .fill-left__radio[value="5"] {
+ display: none;
+}
+#meter .fill-left__max-control[value="4"] + .fill-left .fill-left__radio[value="6"] {
+ display: none;
+}
+#meter .fill-left__max-control[value="4"] + .fill-left .fill-left__radio[value="7"] {
+ display: none;
+}
+#meter .fill-left__max-control[value="4"] + .fill-left .fill-left__radio[value="8"] {
+ display: none;
+}
+#meter .fill-left__max-control[value="4"] + .fill-left .fill-left__radio[value="9"] {
+ display: none;
+}
+#meter .fill-left__max-control[value="4"] + .fill-left .fill-left__radio[value="10"] {
+ display: none;
+}
+#meter .fill-left__max-control[value="5"] + .fill-left {
+ grid-template-columns: repeat(5, var(--meterSquare));
+}
+#meter .fill-left__max-control[value="5"] + .fill-left .fill-left__radio[value="6"] {
+ display: none;
+}
+#meter .fill-left__max-control[value="5"] + .fill-left .fill-left__radio[value="7"] {
+ display: none;
+}
+#meter .fill-left__max-control[value="5"] + .fill-left .fill-left__radio[value="8"] {
+ display: none;
+}
+#meter .fill-left__max-control[value="5"] + .fill-left .fill-left__radio[value="9"] {
+ display: none;
+}
+#meter .fill-left__max-control[value="5"] + .fill-left .fill-left__radio[value="10"] {
+ display: none;
+}
+#meter .fill-left__max-control[value="6"] + .fill-left {
+ grid-template-columns: repeat(6, var(--meterSquare));
+}
+#meter .fill-left__max-control[value="6"] + .fill-left .fill-left__radio[value="7"] {
+ display: none;
+}
+#meter .fill-left__max-control[value="6"] + .fill-left .fill-left__radio[value="8"] {
+ display: none;
+}
+#meter .fill-left__max-control[value="6"] + .fill-left .fill-left__radio[value="9"] {
+ display: none;
+}
+#meter .fill-left__max-control[value="6"] + .fill-left .fill-left__radio[value="10"] {
+ display: none;
+}
+#meter .fill-left__max-control[value="7"] + .fill-left {
+ grid-template-columns: repeat(7, var(--meterSquare));
+}
+#meter .fill-left__max-control[value="7"] + .fill-left .fill-left__radio[value="8"] {
+ display: none;
+}
+#meter .fill-left__max-control[value="7"] + .fill-left .fill-left__radio[value="9"] {
+ display: none;
+}
+#meter .fill-left__max-control[value="7"] + .fill-left .fill-left__radio[value="10"] {
+ display: none;
+}
+#meter .fill-left__max-control[value="8"] + .fill-left {
+ grid-template-columns: repeat(8, var(--meterSquare));
+}
+#meter .fill-left__max-control[value="8"] + .fill-left .fill-left__radio[value="9"] {
+ display: none;
+}
+#meter .fill-left__max-control[value="8"] + .fill-left .fill-left__radio[value="10"] {
+ display: none;
+}
+#meter .fill-left__max-control[value="9"] + .fill-left {
+ grid-template-columns: repeat(9, var(--meterSquare));
+}
+#meter .fill-left__max-control[value="9"] + .fill-left .fill-left__radio[value="10"] {
+ display: none;
+}
+
+#modifiers .repitem {
+ display: grid;
+ grid-template-columns: auto 1fr;
+}
+
+#advancement {
+ grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
+}
+#advancement h3 {
+ grid-column: 1/-1;
+}
+#advancement .repeating-container {
+ align-self: start;
+ gap: var(--half-gap) var(--grid-gap);
+}
+#advancement .repitem {
+ display: grid;
+ grid-template-columns: 1rem 1fr;
+ align-items: start;
+}
+#advancement input:checked + .adaptive textarea {
+ text-decoration-line: line-through;
+ text-decoration-style: wavy;
+ text-decoration-color: var(--selectedColor);
+ text-decoration-thickness: 2px;
+}
+#advancement :is(.adaptive--text) {
+ min-height: 1.5rem;
+ height: auto;
+}
+
+#lifeslice .repcontainer {
+ display: block;
+ gap: var(--half-gap);
+ column-width: 200px;
+}
+#lifeslice .repitem {
+ display: grid;
+ grid-template-columns: auto 1fr;
+ gap: var(--half-gap);
+ break-inside: avoid;
+}
+
+#clock {
+ --circleSize:150px;
+ --itemDeg:0deg;
+ --itemCount:10;
+}
+#clock .repcontainer {
+ display: block;
+ gap: var(--half-gap);
+ column-width: 200px;
+}
+#clock .repitem {
+ display: grid;
+ gap: var(--half-gap);
+ grid-template-areas: "clock" "name";
+ break-inside: avoid;
+}
+#clock .repitem > input {
+ text-align: center;
+}
+#clock .clock-control--current[value="1"] ~ .clock-progress {
+ --itemDeg:calc(1 / var(--itemCount) * 360deg);
+}
+#clock .clock-control--max[value="2"] + .clock-progress {
+ --itemCount:2;
+}
+#clock .clock-control--current[value="2"] ~ .clock-progress {
+ --itemDeg:calc(2 / var(--itemCount) * 360deg);
+}
+#clock .clock-control--max[value="3"] + .clock-progress {
+ --itemCount:3;
+}
+#clock .clock-control--current[value="3"] ~ .clock-progress {
+ --itemDeg:calc(3 / var(--itemCount) * 360deg);
+}
+#clock .clock-control--max[value="4"] + .clock-progress {
+ --itemCount:4;
+}
+#clock .clock-control--current[value="4"] ~ .clock-progress {
+ --itemDeg:calc(4 / var(--itemCount) * 360deg);
+}
+#clock .clock-control--max[value="5"] + .clock-progress {
+ --itemCount:5;
+}
+#clock .clock-control--current[value="5"] ~ .clock-progress {
+ --itemDeg:calc(5 / var(--itemCount) * 360deg);
+}
+#clock .clock-control--max[value="6"] + .clock-progress {
+ --itemCount:6;
+}
+#clock .clock-control--current[value="6"] ~ .clock-progress {
+ --itemDeg:calc(6 / var(--itemCount) * 360deg);
+}
+#clock .clock-control--max[value="7"] + .clock-progress {
+ --itemCount:7;
+}
+#clock .clock-control--current[value="7"] ~ .clock-progress {
+ --itemDeg:calc(7 / var(--itemCount) * 360deg);
+}
+#clock .clock-control--max[value="8"] + .clock-progress {
+ --itemCount:8;
+}
+#clock .clock-control--current[value="8"] ~ .clock-progress {
+ --itemDeg:calc(8 / var(--itemCount) * 360deg);
+}
+#clock .clock-control--max[value="9"] + .clock-progress {
+ --itemCount:9;
+}
+#clock .clock-control--current[value="9"] ~ .clock-progress {
+ --itemDeg:calc(9 / var(--itemCount) * 360deg);
+}
+#clock .clock-control--max[value="10"] + .clock-progress {
+ --itemCount:10;
+}
+#clock .clock-control--current[value="10"] ~ .clock-progress {
+ --itemDeg:calc(10 / var(--itemCount) * 360deg);
+}
+#clock .clock-label {
+ grid-area: clock;
+ max-width: var(--circleSize);
+ box-sizing: border-box;
+ display: flex;
+ place-self: center;
+ align-items: center;
+ z-index: 10;
+}
+#clock .clock-label > * {
+ color: var(--selectedColor);
+ font-weight: bold;
+}
+#clock .clock-progress {
+ grid-area: clock;
+ border-radius: 50%;
+ place-self: center;
+ width: var(--circleSize);
+ height: var(--circleSize);
+ display: grid;
+ grid-template-areas: "content";
+ background: conic-gradient(var(--selectedColor) var(--itemDeg), transparent var(--itemDeg));
+ border: 2px solid var(--selectedColor);
+}
+#clock .clock-progress:before {
+ content: "";
+ box-sizing: border-box;
+ width: 75%;
+ height: 75%;
+ place-self: center;
+ border-radius: 50%;
+ background-color: var(--backColor);
+ border: 2px solid var(--selectedColor);
+}
+
+.sheet-rolltemplate-utn {
+ font-size: 16px;
+ margin-left: -40px;
+ margin-right: -5px;
+ --headerBackground:var(--selectedColor);
+ --templatePad:var(--grid-gap);
+ --criticalColor:green;
+ --fumbleColor:red;
+ --importantColor:blue;
+}
+.sheet-rolltemplate-utn:not(:first-child) {
+ margin-top: 10px;
+}
+.sheet-rolltemplate-utn:first-child .sheet-continuation {
+ margin-top: -9px;
+}
+.sheet-rolltemplate-utn .sheet-header {
+ padding: 5px;
+ display: grid;
+ gap: var(--half-gap);
+ background-color: var(--headerBackground);
+ padding: calc(var(--templatePad) / 2);
+ margin-left: calc(var(--templatePad) * -1);
+ margin-right: calc(var(--templatePad) * -1);
+ margin-top: calc(var(--templatePad) * -1);
+ border-radius: 5px 5px 0 0;
+}
+.sheet-rolltemplate-utn .sheet-header * {
+ color: #e6e6e6;
+}
+.sheet-rolltemplate-utn .sheet-header .sheet-character-name,
+.sheet-rolltemplate-utn .sheet-header h3 {
+ grid-column: 1/-1;
+ white-space: pre-wrap;
+}
+.sheet-rolltemplate-utn .sheet-header .sheet-character_name {
+ place-self: center end;
+}
+.sheet-rolltemplate-utn .sheet-header h3 {
+ place-self: center start;
+}
+.sheet-rolltemplate-utn > .sheet-template {
+ border-radius: 5px;
+ background-color: var(--backColor);
+ padding: var(--templatePad);
+}
+.sheet-rolltemplate-utn > .sheet-template h3,
+.sheet-rolltemplate-utn > .sheet-template h3 * {
+ font-size: 1.5em;
+}
+.sheet-rolltemplate-utn > .sheet-template h4,
+.sheet-rolltemplate-utn > .sheet-template h4 * {
+ font-size: 1.25em;
+}
+.sheet-rolltemplate-utn > .sheet-template h5,
+.sheet-rolltemplate-utn > .sheet-template h5 * {
+ font-weight: bold;
+}
+.sheet-rolltemplate-utn > .sheet-template span, .sheet-rolltemplate-utn > .sheet-template h5 {
+ font-size: 1em;
+}
+.sheet-rolltemplate-utn > .sheet-template .inlinerollresult {
+ font-size: 1.25em;
+}
+.sheet-rolltemplate-utn > .sheet-template .inlinerollresult[title*="[computed value]"], .sheet-rolltemplate-utn > .sheet-template .inlinerollresult[original-title*="[computed value]"] {
+ pointer-events: none;
+}
+.sheet-rolltemplate-utn > .sheet-template .inlinerollresult, .sheet-rolltemplate-utn > .sheet-template .inlinerollresult.fullcrit, .sheet-rolltemplate-utn > .sheet-template .inlinerollresult.fullfail, .sheet-rolltemplate-utn > .sheet-template .inlinerollresult.importantroll {
+ border: none;
+ background-color: transparent;
+}
+.sheet-rolltemplate-utn > .sheet-template .inlinerollresult.fullcrit {
+ color: var(--criticalColor);
+}
+.sheet-rolltemplate-utn > .sheet-template .inlinerollresult.fullfail {
+ color: var(--fumbleColor);
+}
+.sheet-rolltemplate-utn > .sheet-template .inlinerollresult.importantroll {
+ color: var(--importantColor);
+}
+.sheet-rolltemplate-utn > .sheet-template .sheet-crit .inlinerollresult {
+ color: var(--criticalColor);
+}
+.sheet-rolltemplate-utn > .sheet-template .sheet-fumble .inlinerollresult {
+ color: var(--fumbleColor);
+}
+.sheet-rolltemplate-utn > .sheet-template .sheet-regular-roll .inlinerollresult {
+ color: var(--fontColor);
+}
+.sheet-rolltemplate-utn .sheet-content {
+ display: grid;
+ gap: var(--grid-gap);
+}
+.sheet-rolltemplate-utn .sheet-template-row {
+ display: flex;
+ flex-wrap: wrap;
+ gap: var(--half-gap);
+ justify-content: space-between;
+ align-items: center;
+}
+.sheet-rolltemplate-utn .sheet-template-row.sheet-center {
+ justify-content: center;
+}
+.sheet-rolltemplate-utn .sheet-success {
+ color: var(--criticalColor);
+}
+.sheet-rolltemplate-utn .sheet-failure {
+ color: var(--fumbleColor);
+}
+.sheet-rolltemplate-utn .sheet-critical {
+ text-decoration-line: underline overline;
+}
\ No newline at end of file
diff --git a/under_the_neighborhood/system.html b/under_the_neighborhood/system.html
new file mode 100644
index 0000000000..8435c4fefd
--- /dev/null
+++ b/under_the_neighborhood/system.html
@@ -0,0 +1,2274 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ y
+
+ N
+
+ N
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{#roll}}
+
{{#roll_name}}
+
{{roll_name}} {{/roll_name}}{{^roll_name}}
+ {{/roll_name}}{{#rollGreater() computed::result 0}}{{roll}} {{/rollGreater() computed::result 0}}{{#rollLess() computed::result 0}}{{roll}} {{/rollLess() computed::result 0}}{{#rollBetween() computed::result 0 0}}{{roll}} {{/rollBetween() computed::result 0 0}}
+ {{#result}}
+
{{#rollLess() roll 7}}{{#rollLess() computed::result 0}}
+
{{#fumble_result}}{{fumble_result}} {{/fumble_result}}{{^fumble_result}}{{failure_result}} {{/fumble_result}}{{/rollLess() computed::result 0}}{{#^rollLess() computed::result 0}}
+ {{failure_result}} {{/^rollLess() computed::result 0}}{{/rollLess() roll 7}}{{#rollBetween() roll 7 9}}
+ {{mixed_result}} {{/rollBetween() roll 7 9}}{{#rollGreater() roll 9}}{{#rollGreater() computed::result 0}}
+ {{#critical_result}}{{critical_result}} {{/critical_result}}{{^critical_result}}{{success_result}} {{/critical_result}}{{/rollGreater() computed::result 0}}{{#^rollGreater() computed::result 0}}
+ {{success_result}} {{/^rollGreater() computed::result 0}}{{/rollGreater() roll 9}}
+ {{/result}}{{/roll}}{{#description}}
+
+
{{description}}
+ {{/description}}{{#allprops() header character_name character_id roll_name roll result fumble_result failure_result mixed_result success_result critical_result description}}
+
+
{{key}} {{value}}
+ {{/allprops() header character_name character_id roll_name roll result fumble_result failure_result mixed_result success_result critical_result description}}
+
+
+
+
\ No newline at end of file
diff --git a/under_the_neighborhood/translation.json b/under_the_neighborhood/translation.json
new file mode 100644
index 0000000000..9290a306a5
--- /dev/null
+++ b/under_the_neighborhood/translation.json
@@ -0,0 +1,142 @@
+{
+ "character sentence order":"the,playbook,who,descriptor",
+ "sentence the":"the",
+ "sentence who":"who",
+ "character sheet":"character sheet",
+ "npc sheet":"npc sheet",
+ "pronouns":"pronouns",
+ "age":"age",
+ "appearance":"appearance",
+ "homeworld":"homeworld",
+ "want":"want",
+ "adventure points":"adventure points",
+ "stats":"stats",
+ "stat":"stat",
+ "heart":"heart",
+ "books":"books",
+ "fierce":"fierce",
+ "slick":"slick",
+ "weird":"weird",
+ "skills and inabilities":"skills and inabilities",
+ "skill":"skill",
+ "inability":"inability",
+ "signature item":"signature item",
+ "compatibility scores":"compatibility",
+ "character":"character",
+ "character moves":"character moves",
+ "name":"name",
+ "cost":"cost",
+ "source":"source",
+ "select one":"select one",
+ "playbook":"playbook",
+ "descriptor":"descriptor",
+ "arc":"arc",
+ "description":"description",
+ "meters":"meters",
+ "advancements":"advancements",
+ "playbook placeholder":"playbook",
+ "descriptor placeholder":"descriptor",
+ "mon information":"mon information",
+ "element":"element",
+ "basic":"basic",
+ "super":"super",
+ "ultra":"ultra",
+ "max":"max",
+ "open the settings page":"open the settings page",
+ "open the character sheet":"open the character sheet",
+ "settings":"settings",
+ "is weird":"is weird",
+ "mon trainer":"mon trainer",
+ "use initiative":"use initiative",
+ "see into their heart":"see into their heart",
+ "help somebody":"help somebody",
+ "touch the other side":"touch the other side",
+ "understand":"understand",
+ "take a swing":"take a swing",
+ "take action":"take action",
+ "sneak":"sneak",
+ "keep your cool":"keep your cool",
+ "convince somebody":"convince somebody",
+ "another move":"another move",
+ "double":"double",
+ "advantage":"advantage",
+ "normal":"normal",
+ "disadvantage":"disadvantage",
+ "query":"query",
+ "score":"score",
+ "initiative":"initiative",
+ "basic move stats":"basic move stats",
+ "whisper to gm":"whisper to gm",
+ "never":"never",
+ "always":"always",
+ "roll":"roll",
+ "critical failure":"critical failure",
+ "failure":"failure",
+ "mixed success":"mixed success",
+ "full success":"full success",
+ "critical success":"critical success",
+ "which stat":"which stat",
+ "effects":"effects",
+ "roll modifiers":"roll modifiers",
+ "use roll modifiers":"use roll modifiers",
+ "use physical dice":"use physical dice",
+ "physical roll query":"what was the result of your roll (without modifiers)",
+ "bonus":"bonus",
+ "help a friend roll header":"Help {{0}}",
+ "basic critical result":"You succeed beyond your expectations.",
+ "basic success result":"You succeed.",
+ "basic mixed result":"You succeed, but at a cost or not quite in the way you want. The GM makes a Soft Move.",
+ "basic failure result":"The result is bad for you. The GM makes a Hard Move, and you gain 1 AP (see: Adventure Points).",
+ "basic fumble result":"The result is REALLY bad for you. The GM makes a Hard Move, and you gain 0 AP.",
+ "see into their heart success result":"Ask the GM a simple question about your target’s inner thoughts. They must answer truthfully.",
+ "see into their heart mixed result":"Ask the GM a simple question about your target’s inner thoughts. They cannot lie and they cannot say nothing, but they can omit important information.",
+ "see into their heart failure result":"Your target gets important insight into you. The GM is able to ask you a simple question about your inner thoughts on behalf of your target, which you must answer truthfully.",
+ "help somebody success result":"You help your target succeed at their goal.",
+ "help somebody mixed result":"With your help, your target succeeds, but not fully or at a cost.",
+ "help somebody failure result":"Despite your help (or perhaps due to it), your target fails.",
+ "touch the other side success result":"You “Touch the Other Side” successfully, but the unfamiliar process still comes with complications. The PC chooses 1 Drawback\n• It takes longer than expected to work\n• It works, but not quite how you expected\n• It breaks after being used.",
+ "touch the other side mixed result":"You touch the other side and do… fine. The PC chooses 2 Drawbacks\n• It takes longer than expected to work\n• It works, but not quite how you expected\n• It breaks after being used.",
+ "touch the other side failure result":"You “Touch the Other Side,” but at great cost. The GM chooses 2 Drawbacks\n• It takes much longer than expected to work\n• It works, but in a way that’s harmful to you\n• It breaks after or while being used.",
+ "understand success result":"Ask a simple question about your target.",
+ "understand mixed result":"Ask a simple question about your target. The GM holds another question; this is something that your PC fails to understand.",
+ "understand failure result":"You misunderstand something vital about your target.",
+ "take a swing success result":"You hit with precision and force. The PC chooses 1 Benefit\n• You hit exactly where you want. Take Advantage Forward or +1 Ongoing against your target for the rest of the scene\n• You temporarily immobilize your target. If it’s a device, it stops functioning how it should for an appropriate amount of time\n• Your target moves exactly where you want them to. Choose a nearby position. That’s exactly where your target is.",
+ "take a swing mixed result":"On a Mixed Success: You smack your target, but not super well. The PC chooses 1 Benefit and 1 Drawback\n**Benefit*\n• You hit where you want. Take +1 Forward against your target\n• You temporarily daze your target. If it’s a device, it functions about half as well as it should for an appropriate amount of time\n• Your target moves where you want them to. Choose a nearby position. Your target is somewhere near there\n**Drawback*\n• You use too much or too little force\n• You hit someone or something you didn’t intend to\n• You injure yourself or leave yourself in a bad position in the process. Take -1 Forward.",
+ "take a swing failure result":"Not only do you fail to effectively hit your target, but your attack backfires. The GM chooses 1 Drawback\n• You use way too much or too little force\n• You hit someone or something you didn’t intend to.\n• You injure yourself or leave yourself in a very bad position in the process. Take Disadvantage Forward or -1 Ongoing for the rest of the scene.",
+ "take action success result":"You spring into action and are able to react just in time.",
+ "take action mixed result":"You’re able to act, but just a few seconds later than you’d like.",
+ "take action failure result":"You’re caught with your pants down. Any actions you take are far too late or they make the situation worse.",
+ "sneak success result":"You sneak through, and no one is the wiser.",
+ "sneak mixed result":"You sneak through, but aren’t silent in the process. The PC chooses 1 Drawback\n• You arouse the suspicions of those around you. You haven’t been seen, but those around you are on high alert.\n• You leave behind evidence of your presence\n• Someone other than your target(s) is aware of your stealth, but doesn’t say anything yet.",
+ "sneak failure result":"You’re spotted. Everyone (or at least the worst possible people) around you are aware of your presence.",
+ "keep your cool success result":"You successfully keep your cool, and, if you were trying to complete a task, you complete it.",
+ "keep your cool mixed result":"You almost keep your cool, but your composure slips, just a little bit. If you were trying to complete a task, you complete it, but not as effectively as you would have liked. The PC chooses 1 Drawback\n• You were sloppy\n• You were loud\n• You were slow.",
+ "keep your cool failure result":"You fail to keep your cool. If you were trying to complete a task, you fail.",
+ "convince somebody success result":"Your target agrees with what you have to say and helps however they can.",
+ "convince somebody mixed result":"Your target mostly agrees with you, but there’s a cost. The PC chooses 1 Drawback\n• Your target is convinced to do what you want, but something is preventing them from doing so\n• Your target asks for something in return\n• Your target mostly understands you, but makes one inconvenient misunderstanding\n• Your target is upset with you and will act appropriately until time has passed or you’ve made up for your actions. Take -1 Forward for any roll related to your target.",
+ "convince somebody failure result":"You fail to convince your target. Even worse, simply trying to convince them has made things worse. The GM chooses 1 Drawback\n• Your target actively believes the opposite of what you’re saying, and, if you were asking for help, actively tries to interfere with you\n• Your target demands something as a result of your meddling\n• Your target makes a vital misunderstanding of what you are trying to say\n• Your target is VERY upset with you and will act appropriately until significant time has passed or you’ve made up for your actions. Take -1 Ongoing for all rolls related to your target until you reconcile with them.",
+ "customize results":"customize results",
+ "character arc/flaw":"character arc/flaw",
+ "has arc/flaw":"has arc/flaw",
+ "skills use moves":"skills use moves",
+ "move query":"which move would you like to use",
+ "interference":"interference",
+ "signature stat":"signature stat",
+ "sheet type":"sheet type",
+ "mon":"mon",
+ "game screen":"game screen",
+ "Under the Neighborhood by Kyle Decker":"Under the Neighborhood by Kyle Decker",
+ "slice of life complications":"slice of life complications",
+ "clocks":"clocks",
+ "Which compatibility effect":"Which compatibility effect",
+ "1st compatible effect":"Select the 1st compatible effect",
+ "2nd compatible effect":"Select the 2nd compatible effect",
+ "compatibility prompt disadvantage":"You roll with disadvantage",
+ "compatibility prompt advantage":"You roll with advantage",
+ "compatibility prompt +1":"You receive a +1 bonus to your roll",
+ "compatibility prompt +2":"You receive a +2 bonus to your roll",
+ "compatibility prompt bigger consequences":"The effect of your roll is bigger",
+ "bigger consequences":"The consequences of your actions are “bigger” (Full Successes come with a small bonus, Failures come with an extra complication).",
+ "interference roll message":"Interference: You gain 1 AP",
+ "":""
+}
\ No newline at end of file