Under the Neighborhood v1

This commit is contained in:
Scott Casey
2022-06-15 13:30:18 -06:00
parent d83dc53c12
commit 9600e28287
64 changed files with 10608 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
{
"editor.autoClosingBrackets": "always",
"editor.fontFamily": "Fira Code",
"editor.fontLigatures": true
}
View File
+946
View File
@@ -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"
}
}
]
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

+9
View File
@@ -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
}
@@ -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
@@ -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
@@ -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')
@@ -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'})
@@ -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}
})
@@ -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})
@@ -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
@@ -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'})
@@ -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'})
@@ -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'})
@@ -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'})
@@ -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'})
@@ -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'}
})
Binary file not shown.

After

Width:  |  Height:  |  Size: 553 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 705 KiB

@@ -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});
@@ -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);
};
@@ -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']});
@@ -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']})
@@ -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'}
]
}
@@ -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
@@ -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;
}
}
@@ -0,0 +1,9 @@
#attributes{
.input-label{
justify-content: space-between;
button{
width:auto;
flex:0;
}
}
}
@@ -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);
}
}
}
@@ -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;
}
}
@@ -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/.
}
@@ -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;
}
}
@@ -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;
}
}
@@ -0,0 +1,6 @@
@use '_mixinsandplaceholders';
@media (min-width:597px){
.span-2--if-wide{
grid-column:span 2;
}
}
@@ -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;
}
}
}
}
}
}
@@ -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;
}
}
@@ -0,0 +1,7 @@
@use '_mixinsandplaceholders';
#modifiers{
.repitem{
display:grid;
grid-template-columns:auto 1fr;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -0,0 +1,10 @@
.ui-dialog .tab-content .charsheet{
select,
textarea,
input,
.uneditable-input,
label,
button{
all: initial;
}
}
@@ -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;
}
}
}
@@ -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;
}
}
}
@@ -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;
}
}
@@ -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}}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,2 @@
include _htmlelements.pug
include _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
@@ -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;
@@ -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;
File diff suppressed because one or more lines are too long
@@ -0,0 +1,2 @@
include _kpug.pug
+kscript(true)
@@ -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;
@@ -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});
@@ -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}`));
});
};
@@ -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 = {};
@@ -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.
*/
@@ -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;
@@ -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;
+153
View File
@@ -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
+17
View File
@@ -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';
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+142
View File
@@ -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",
"":""
}