From b7f63225df7b3304cc1e17a752fee000f7943ccd Mon Sep 17 00:00:00 2001 From: Zakkon Date: Thu, 11 Apr 2024 15:39:31 +0200 Subject: [PATCH] adding character builder to 5etools --- charbuilder.html | 79 + charbuilder/5etools-js/main.css | 1 + charbuilder/5etools-js/parser.js | 3697 +++ charbuilder/5etools-js/render-dice.js | 2268 ++ charbuilder/5etools-js/render.js | 12559 ++++++++++ charbuilder/css/charbuilder.css | 341 + charbuilder/css/charselect.css | 171 + charbuilder/css/plutonium.css | 7421 ++++++ charbuilder/css/sheet.css | 778 + .../fonts/glyphicons-halflings-regular.eot | Bin 0 -> 14079 bytes .../fonts/glyphicons-halflings-regular.svg | 228 + .../fonts/glyphicons-halflings-regular.ttf | Bin 0 -> 41280 bytes .../fonts/glyphicons-halflings-regular.woff | Bin 0 -> 16448 bytes .../fonts/glyphicons-halflings-regular.woff2 | Bin 0 -> 18028 bytes charbuilder/js/arrayextensions.js | 342 + charbuilder/js/charexport.js | 994 + charbuilder/js/charselect.js | 139 + charbuilder/js/helperfunctions.js | 25 + charbuilder/js/main.js | 818 + charbuilder/js/plutonium/brewutil.js | 1836 ++ charbuilder/js/plutonium/charactermancer.js | 20228 ++++++++++++++++ charbuilder/js/plutonium/config.js | 3869 +++ charbuilder/js/plutonium/filter.js | 9504 ++++++++ charbuilder/js/plutonium/importlist.js | 82 + charbuilder/js/plutonium/jqueryutil.js | 296 + charbuilder/js/plutonium/proxy.js | 558 + charbuilder/js/plutonium/render_addons.js | 178 + charbuilder/js/plutonium/sidedata.js | 530 + charbuilder/js/plutonium/sourcemodal.js | 1376 ++ charbuilder/js/plutonium/utils-entity.js | 1005 + charbuilder/js/plutonium/utils.js | 12978 ++++++++++ charbuilder/js/plutonium/vetools.js | 1271 + charbuilder/js/roll.js | 29 + charbuilder/js/sheet.js | 1567 ++ charbuilder/js/stringextensions.js | 222 + charbuilder/js/vetools/parser.js | 3693 +++ charbuilder/js/vetools/render.js | 16436 +++++++++++++ charbuilder/js/vetools/utils-dataloader.js | 2187 ++ charbuilder/js/vetools/utils.js | 7570 ++++++ js/navigation.js | 1 + js/utils.js | 1 + 41 files changed, 115278 insertions(+) create mode 100644 charbuilder.html create mode 100644 charbuilder/5etools-js/main.css create mode 100644 charbuilder/5etools-js/parser.js create mode 100644 charbuilder/5etools-js/render-dice.js create mode 100644 charbuilder/5etools-js/render.js create mode 100644 charbuilder/css/charbuilder.css create mode 100644 charbuilder/css/charselect.css create mode 100644 charbuilder/css/plutonium.css create mode 100644 charbuilder/css/sheet.css create mode 100644 charbuilder/fonts/glyphicons-halflings-regular.eot create mode 100644 charbuilder/fonts/glyphicons-halflings-regular.svg create mode 100644 charbuilder/fonts/glyphicons-halflings-regular.ttf create mode 100644 charbuilder/fonts/glyphicons-halflings-regular.woff create mode 100644 charbuilder/fonts/glyphicons-halflings-regular.woff2 create mode 100644 charbuilder/js/arrayextensions.js create mode 100644 charbuilder/js/charexport.js create mode 100644 charbuilder/js/charselect.js create mode 100644 charbuilder/js/helperfunctions.js create mode 100644 charbuilder/js/main.js create mode 100644 charbuilder/js/plutonium/brewutil.js create mode 100644 charbuilder/js/plutonium/charactermancer.js create mode 100644 charbuilder/js/plutonium/config.js create mode 100644 charbuilder/js/plutonium/filter.js create mode 100644 charbuilder/js/plutonium/importlist.js create mode 100644 charbuilder/js/plutonium/jqueryutil.js create mode 100644 charbuilder/js/plutonium/proxy.js create mode 100644 charbuilder/js/plutonium/render_addons.js create mode 100644 charbuilder/js/plutonium/sidedata.js create mode 100644 charbuilder/js/plutonium/sourcemodal.js create mode 100644 charbuilder/js/plutonium/utils-entity.js create mode 100644 charbuilder/js/plutonium/utils.js create mode 100644 charbuilder/js/plutonium/vetools.js create mode 100644 charbuilder/js/roll.js create mode 100644 charbuilder/js/sheet.js create mode 100644 charbuilder/js/stringextensions.js create mode 100644 charbuilder/js/vetools/parser.js create mode 100644 charbuilder/js/vetools/render.js create mode 100644 charbuilder/js/vetools/utils-dataloader.js create mode 100644 charbuilder/js/vetools/utils.js diff --git a/charbuilder.html b/charbuilder.html new file mode 100644 index 0000000..74ccdc7 --- /dev/null +++ b/charbuilder.html @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + 5e Character Builder + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+
+ + + + + + + + diff --git a/charbuilder/5etools-js/main.css b/charbuilder/5etools-js/main.css new file mode 100644 index 0000000..acbf15c --- /dev/null +++ b/charbuilder/5etools-js/main.css @@ -0,0 +1 @@ +๏ปฟ@font-face{font-family:"Convergence";font-style:normal;font-weight:400;src:local("Convergence-Regular"),url("../fonts/Convergence-Regular.woff2") format("woff2")}@font-face{font-family:"Roboto";font-style:normal;font-weight:400;src:local("Roboto"),url("../fonts/Roboto-Regular.woff2") format("woff2")}@font-face{font-family:"Glyphicons Halflings";font-style:normal;font-weight:400;src:local("glyphicons-halflings-regular"),url("../fonts/glyphicons-halflings-regular.woff2") format("woff2")}@font-face{font-family:"Blambot Casual";src:local("Blambot-Casual"),url("../fonts/Blambot-Casual-Regular.woff2") format("woff2")}@keyframes kf-fade-out{from{opacity:1}to{opacity:0}}.linked-titles .rd__h--0 .entry-title-inner:hover::before{font-size:50%}.linked-titles .rd__h--1 .entry-title-inner:hover::before{font-size:55%}.linked-titles .rd__h--2 .entry-title-inner:hover::before{font-size:60%}.linked-titles .rd__h .entry-title-inner{cursor:copy}.linked-titles .rd__h .entry-title-inner:hover::before{content:" ๐Ÿ”—";color:rgba(0,0,0,.2);position:relative;float:left;width:14px;height:14px;right:20px;margin-right:-30px;font-size:85%}@font-face{font-family:"Convergence";font-style:normal;font-weight:400;src:local("Convergence-Regular"),url("../fonts/Convergence-Regular.woff2") format("woff2")}@font-face{font-family:"Roboto";font-style:normal;font-weight:400;src:local("Roboto"),url("../fonts/Roboto-Regular.woff2") format("woff2")}@font-face{font-family:"Glyphicons Halflings";font-style:normal;font-weight:400;src:local("glyphicons-halflings-regular"),url("../fonts/glyphicons-halflings-regular.woff2") format("woff2")}@font-face{font-family:"Blambot Casual";src:local("Blambot-Casual"),url("../fonts/Blambot-Casual-Regular.woff2") format("woff2")}@keyframes kf-fade-out{from{opacity:1}to{opacity:0}}.linked-titles .rd__h--0 .entry-title-inner:hover::before{font-size:50%}.linked-titles .rd__h--1 .entry-title-inner:hover::before{font-size:55%}.linked-titles .rd__h--2 .entry-title-inner:hover::before{font-size:60%}.linked-titles .rd__h .entry-title-inner{cursor:copy}.linked-titles .rd__h .entry-title-inner:hover::before{content:" ๐Ÿ”—";color:rgba(0,0,0,.2);position:relative;float:left;width:14px;height:14px;right:20px;margin-right:-30px;font-size:85%}:root{--rgb-font: #333;--rgb-font--muted: #777;--rgb-name: #822000;--rgb-bg: white;--rgb-bg--alt: whitesmoke;--rgb-border--statblock: #e69a28}:root.night-mode{--rgb-font: #bbb;--rgb-name: #d29a38;--rgb-bg: #222;--rgb-bg--alt: #383838;--rgb-border--statblock: #565656}@font-face{font-family:"Convergence";font-style:normal;font-weight:400;src:local("Convergence-Regular"),url("../fonts/Convergence-Regular.woff2") format("woff2")}@font-face{font-family:"Roboto";font-style:normal;font-weight:400;src:local("Roboto"),url("../fonts/Roboto-Regular.woff2") format("woff2")}@font-face{font-family:"Glyphicons Halflings";font-style:normal;font-weight:400;src:local("glyphicons-halflings-regular"),url("../fonts/glyphicons-halflings-regular.woff2") format("woff2")}@font-face{font-family:"Blambot Casual";src:local("Blambot-Casual"),url("../fonts/Blambot-Casual-Regular.woff2") format("woff2")}@keyframes kf-fade-out{from{opacity:1}to{opacity:0}}.linked-titles .rd__h--0 .entry-title-inner:hover::before{font-size:50%}.linked-titles .rd__h--1 .entry-title-inner:hover::before{font-size:55%}.linked-titles .rd__h--2 .entry-title-inner:hover::before{font-size:60%}.linked-titles .rd__h .entry-title-inner{cursor:copy}.linked-titles .rd__h .entry-title-inner:hover::before{content:" ๐Ÿ”—";color:rgba(0,0,0,.2);position:relative;float:left;width:14px;height:14px;right:20px;margin-right:-30px;font-size:85%}:root{--safe-area-inset-top: 0;--safe-area-inset-right: 0;--safe-area-inset-bottom: 0;--safe-area-inset-left: 0}.glyphicon-send{top:2px;right:1px}.glyphicon--top-2p{top:2px}.roller{color:#337ab7;cursor:pointer}.text-muted a{color:#7096b7}.font-ui{font-family:Arial,sans-serif}@media(max-width: 780px){.help--hover{cursor:default !important;text-decoration:none !important}}body{min-height:100vh;position:relative;overflow-x:hidden;padding:var(--safe-area-inset-top) var(--safe-area-inset-right) var(--safe-area-inset-bottom) var(--safe-area-inset-left)}body.is-fullscreen .page__header{display:none}body.is-fullscreen .page__nav{display:none}input{min-width:0}input[type=checkbox],input[type=radio]{margin:0}main{padding:10px 15px}footer{padding:20px 15px;font-size:90%}pre,textarea{tab-size:2}hr{flex-shrink:0}@-moz-document url-prefix(){*{scrollbar-width:thin}}::-webkit-scrollbar{width:9px;height:9px}::-webkit-scrollbar-track{background:rgba(0,0,0,0)}::-webkit-scrollbar-thumb{background:#cbcbcb}body{scrollbar-width:auto}body::-webkit-scrollbar{width:15px}.container{position:relative}.input-xs{height:22px;padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.form-control--minimal{border-radius:0;padding:0 2px}.ve-flex-label{display:inline-flex;align-items:center}.ve-flex-label>input[type=checkbox],.ve-flex-label>input[type=radio]{margin:0 0 0 5px}.btn-xxs{padding:0 2px;font-size:12px;line-height:1.5;border-radius:3px}.btn-primary--half{background:repeating-linear-gradient(135deg, #337ab7, #337ab7 16px, #b8b8b8 16px, #b8b8b8 32px)}.dropdown-menu--side{top:-10px;left:100%;max-height:calc(100vh - 130px);overflow-y:auto}.nav>li>a{padding:5px 14px 6px}@media(min-width: 992px){.nav>li>a{border-top-left-radius:0;border-top-right-radius:0}}@media(max-width: 991px){.nav>li{margin-top:2px;margin-bottom:2px}}.page__nav-inner>li.active>a,.page__nav-inner>li.active>a:focus,.page__nav-inner>li.active>a:hover{background-color:#006bc4;border-top:0}@media(min-width: 992px){.page__nav-inner>li.active>a,.page__nav-inner>li.active>a:focus,.page__nav-inner>li.active>a:hover{border-left:1px solid rgba(0,0,0,0);border-right:1px solid rgba(0,0,0,0);border-bottom:1px solid #999}}.nav>li>a:focus,.nav>li>a:hover{background-color:rgba(0,0,0,.1)}.nav .open>a,.nav .open>a:focus,.nav .open>a:hover{background-color:rgba(0,0,0,.1)}.row{margin-right:0;margin-left:0}.dropdown-menu>li>a.dropdown-ext-link{display:flex;justify-content:space-between}.dropdown-menu>li>a.dropdown-ext-link:hover{padding-right:10px}.dropdown-menu>li>span{cursor:pointer;display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.42857143;color:#333;white-space:nowrap}.dropdown-menu>li>span:focus,.dropdown-menu>li>span:hover{color:#262626;text-decoration:none;background-color:#f5f5f5}.dropdown-menu>.disabled>span{pointer-events:none;color:#777}.dropdown-menu>.disabled>span:focus,.dropdown-menu>.disabled>span:hover{color:#777;text-decoration:none;cursor:not-allowed;background-color:rgba(0,0,0,0);background-image:none}.dropdown-menu>.ctx-danger>span{background:#d9534f;color:#fff}.dropdown-menu>.ctx-danger>span:focus,.dropdown-menu>.ctx-danger>span:hover{color:#fff;text-decoration:none;background-color:#ac2925}.dropdown-ext-link>.glyphicon{top:3px;display:none}.dropdown-ext-link:hover>.glyphicon{display:inline-block}.caret--right{transform:rotate(270deg)}.input-group>input.form-control,.input-group>label,.input-group>button,.input-group>a.btn{border-radius:0;border-right:0}.input-group>input.form-control:first-child,.input-group>label:first-child,.input-group>button:first-child,.input-group>a.btn:first-child{border-top-left-radius:3px;border-bottom-left-radius:3px}.input-group>input.form-control:last-child,.input-group>label:last-child,.input-group>button:last-child,.input-group>a.btn:last-child{border-top-right-radius:3px;border-bottom-right-radius:3px;border-right:1px solid #ccc}.input-group--top input.form-control:first-child,.input-group--top label:first-child,.input-group--top button:first-child,.input-group--top a.btn:first-child{border-bottom-left-radius:0}.input-group--top input.form-control:last-child,.input-group--top label:last-child,.input-group--top button:last-child,.input-group--top a.btn:last-child{border-bottom-right-radius:0}.input-group--middle input.form-control,.input-group--middle label,.input-group--middle button,.input-group--middle a.btn{border-top:0}.input-group--middle input.form-control:first-child,.input-group--middle label:first-child,.input-group--middle button:first-child,.input-group--middle a.btn:first-child{border-radius:0}.input-group--middle input.form-control:last-child,.input-group--middle label:last-child,.input-group--middle button:last-child,.input-group--middle a.btn:last-child{border-radius:0}.input-group--bottom input.form-control,.input-group--bottom label,.input-group--bottom button,.input-group--bottom a.btn{border-top:0}.input-group--bottom input.form-control:first-child,.input-group--bottom label:first-child,.input-group--bottom button:first-child,.input-group--bottom a.btn:first-child{border-top-left-radius:0}.input-group--bottom input.form-control:last-child,.input-group--bottom label:last-child,.input-group--bottom button:last-child,.input-group--bottom a.btn:last-child{border-top-right-radius:0}.night-mode .input-group>input.form-control:last-child,.night-mode .input-group>label:last-child,.night-mode .input-group>button:last-child,.night-mode .input-group>a.btn:last-child{border-right-color:#555}.col-0-1,.col-0-2,.col-0-3,.col-0-4,.col-0-5,.col-0-6,.col-0-7,.col-0-8,.col-0-9,.col-1-1,.col-1-2,.col-1-3,.col-1-4,.col-1-5,.col-1-6,.col-1-7,.col-1-8,.col-1-9,.col-1,.col-2-1,.col-2-2,.col-2-3,.col-2-4,.col-2-5,.col-2-6,.col-2-7,.col-2-8,.col-2-9,.col-2,.col-3-1,.col-3-2,.col-3-3,.col-3-4,.col-3-5,.col-3-6,.col-3-7,.col-3-8,.col-3-9,.col-3,.col-4-1,.col-4-2,.col-4-3,.col-4-4,.col-4-5,.col-4-6,.col-4-7,.col-4-8,.col-4-9,.col-4,.col-5-1,.col-5-2,.col-5-3,.col-5-4,.col-5-5,.col-5-6,.col-5-7,.col-5-8,.col-5-9,.col-5,.col-6-1,.col-6-2,.col-6-3,.col-6-4,.col-6-5,.col-6-6,.col-6-7,.col-6-8,.col-6-9,.col-6,.col-7-1,.col-7-2,.col-7-3,.col-7-4,.col-7-5,.col-7-6,.col-7-7,.col-7-8,.col-7-9,.col-7,.col-8-1,.col-8-2,.col-8-3,.col-8-4,.col-8-5,.col-8-6,.col-8-7,.col-8-8,.col-8-9,.col-8,.col-9-1,.col-9-2,.col-9-3,.col-9-4,.col-9-5,.col-9-6,.col-9-7,.col-9-8,.col-9-9,.col-9,.col-10-1,.col-10-2,.col-10-3,.col-10-4,.col-10-5,.col-10-6,.col-10-7,.col-10-8,.col-10-9,.col-10,.col-11-1,.col-11-2,.col-11-3,.col-11-4,.col-11-5,.col-11-6,.col-11-7,.col-11-8,.col-11-9,.col-11,.col-12{position:relative;min-height:1px}.col-12{width:100% !important}.col-11{width:91.6666666667% !important}.col-11-9{width:99.1666666667% !important}.col-11-8{width:98.3333333333% !important}.col-11-7{width:97.5% !important}.col-11-6{width:96.6666666667% !important}.col-11-5{width:95.8333333333% !important}.col-11-4{width:95% !important}.col-11-3{width:94.1666666667% !important}.col-11-2{width:93.3333333333% !important}.col-11-1{width:92.5% !important}.col-10{width:83.3333333333% !important}.col-10-9{width:90.8333333333% !important}.col-10-8{width:90% !important}.col-10-7{width:89.1666666667% !important}.col-10-6{width:88.3333333333% !important}.col-10-5{width:87.5% !important}.col-10-4{width:86.6666666667% !important}.col-10-3{width:85.8333333333% !important}.col-10-2{width:85% !important}.col-10-1{width:84.1666666667% !important}.col-9{width:75% !important}.col-9-9{width:82.5% !important}.col-9-8{width:81.6666666667% !important}.col-9-7{width:80.8333333333% !important}.col-9-6{width:80% !important}.col-9-5{width:79.1666666667% !important}.col-9-4{width:78.3333333333% !important}.col-9-3{width:77.5% !important}.col-9-2{width:76.6666666667% !important}.col-9-1{width:75.8333333333% !important}.col-8{width:66.6666666667% !important}.col-8-9{width:74.1666666667% !important}.col-8-8{width:73.3333333333% !important}.col-8-7{width:72.5% !important}.col-8-6{width:71.6666666667% !important}.col-8-5{width:70.8333333333% !important}.col-8-4{width:70% !important}.col-8-3{width:69.1666666667% !important}.col-8-2{width:68.3333333333% !important}.col-8-1{width:67.5% !important}.col-7{width:58.3333333333% !important}.col-7-9{width:65.8333333333% !important}.col-7-8{width:65% !important}.col-7-7{width:64.1666666667% !important}.col-7-6{width:63.3333333333% !important}.col-7-5{width:62.5% !important}.col-7-4{width:61.6666666667% !important}.col-7-3{width:60.8333333333% !important}.col-7-2{width:60% !important}.col-7-1{width:59.1666666667% !important}.col-6{width:50% !important}.col-6-9{width:57.5% !important}.col-6-8{width:56.6666666667% !important}.col-6-7{width:55.8333333333% !important}.col-6-6{width:55% !important}.col-6-5{width:54.1666666667% !important}.col-6-4{width:53.3333333333% !important}.col-6-3{width:52.5% !important}.col-6-2{width:51.6666666667% !important}.col-6-1{width:50.8333333333% !important}.col-5{width:41.6666666667% !important}.col-5-9{width:49.1666666667% !important}.col-5-8{width:48.3333333333% !important}.col-5-7{width:47.5% !important}.col-5-6{width:46.6666666667% !important}.col-5-5{width:45.8333333333% !important}.col-5-4{width:45% !important}.col-5-3{width:44.1666666667% !important}.col-5-2{width:43.3333333333% !important}.col-5-1{width:42.5% !important}.col-4{width:33.3333333333% !important}.col-4-9{width:40.8333333333% !important}.col-4-8{width:40% !important}.col-4-7{width:39.1666666667% !important}.col-4-6{width:38.3333333333% !important}.col-4-5{width:37.5% !important}.col-4-4{width:36.6666666667% !important}.col-4-3{width:35.8333333333% !important}.col-4-2{width:35% !important}.col-4-1{width:34.1666666667% !important}.col-3{width:25% !important}.col-3-9{width:32.5% !important}.col-3-8{width:31.6666666667% !important}.col-3-7{width:30.8333333333% !important}.col-3-6{width:30% !important}.col-3-5{width:29.1666666667% !important}.col-3-4{width:28.3333333333% !important}.col-3-3{width:27.5% !important}.col-3-2{width:26.6666666667% !important}.col-3-1{width:25.8333333333% !important}.col-2{width:16.6666666667% !important}.col-2-9{width:24.1666666667% !important}.col-2-8{width:23.3333333333% !important}.col-2-7{width:22.5% !important}.col-2-6{width:21.6666666667% !important}.col-2-5{width:20.8333333333% !important}.col-2-4{width:20% !important}.col-2-3{width:19.1666666667% !important}.col-2-2{width:18.3333333333% !important}.col-2-1{width:17.5% !important}.col-1{width:8.3333333333% !important}.col-1-9{width:15.8333333333% !important}.col-1-8{width:15% !important}.col-1-7{width:14.1666666667% !important}.col-1-6{width:13.3333333333% !important}.col-1-5{width:12.5% !important}.col-1-4{width:11.6666666667% !important}.col-1-3{width:10.8333333333% !important}.col-1-2{width:10% !important}.col-1-1{width:9.1666666667% !important}.col-0-9{width:7.5% !important}.col-0-8{width:6.6666666667% !important}.col-0-7{width:5.8333333333% !important}.col-0-6{width:5% !important}.col-0-5{width:4.1666666667% !important}.col-0-4{width:3.3333333333% !important}.col-0-3{width:2.5% !important}.col-0-2{width:1.6666666667% !important}.col-0-1{width:.8333333333% !important}@font-face{font-family:"Convergence";font-style:normal;font-weight:400;src:local("Convergence-Regular"),url("../fonts/Convergence-Regular.woff2") format("woff2")}@font-face{font-family:"Roboto";font-style:normal;font-weight:400;src:local("Roboto"),url("../fonts/Roboto-Regular.woff2") format("woff2")}@font-face{font-family:"Glyphicons Halflings";font-style:normal;font-weight:400;src:local("glyphicons-halflings-regular"),url("../fonts/glyphicons-halflings-regular.woff2") format("woff2")}@font-face{font-family:"Blambot Casual";src:local("Blambot-Casual"),url("../fonts/Blambot-Casual-Regular.woff2") format("woff2")}@keyframes kf-fade-out{from{opacity:1}to{opacity:0}}.linked-titles .rd__h--0 .entry-title-inner:hover::before{font-size:50%}.linked-titles .rd__h--1 .entry-title-inner:hover::before{font-size:55%}.linked-titles .rd__h--2 .entry-title-inner:hover::before{font-size:60%}.linked-titles .rd__h .entry-title-inner{cursor:copy}.linked-titles .rd__h .entry-title-inner:hover::before{content:" ๐Ÿ”—";color:rgba(0,0,0,.2);position:relative;float:left;width:14px;height:14px;right:20px;margin-right:-30px;font-size:85%}.b-0{border:0 !important}.b-1{border-width:.25rem !important}.b-2{border-width:.5rem !important}.b-3{border-width:1rem !important}.b-4{border-width:1.5rem !important}.b-5{border-width:3rem !important}.b-1p{border:1px solid #ccc !important}.bt-0{border-top-width:0 !important}.bt-1{border-top-width:.25rem !important}.bt-2{border-top-width:.5rem !important}.bt-3{border-top-width:1rem !important}.bt-4{border-top-width:1.5rem !important}.bt-5{border-top-width:3rem !important}.bt-1p{border-top:1px solid #ccc !important}.br-0{border-right-width:0 !important}.br-1{border-right-width:.25rem !important}.br-2{border-right-width:.5rem !important}.br-3{border-right-width:1rem !important}.br-4{border-right-width:1.5rem !important}.br-5{border-right-width:3rem !important}.br-1p{border-right:1px solid #ccc !important}.bb-0{border-bottom-width:0 !important}.bb-1{border-bottom-width:.25rem !important}.bb-2{border-bottom-width:.5rem !important}.bb-3{border-bottom-width:1rem !important}.bb-4{border-bottom-width:1.5rem !important}.bb-5{border-bottom-width:3rem !important}.bb-1p{border-bottom:1px solid #ccc !important}.bb-1p-trans{border-bottom:1px solid rgba(204,204,204,.6274509804) !important}.bl-0{border-left-width:0 !important}.bl-1{border-left-width:.25rem !important}.bl-2{border-left-width:.5rem !important}.bl-3{border-left-width:1rem !important}.bl-4{border-left-width:1.5rem !important}.bl-5{border-left-width:3rem !important}.bl-1p{border-left:1px solid #ccc !important}.by-0{border-top-width:0 !important;border-bottom-width:0 !important}.by-1{border-top-width:.25rem !important;border-bottom-width:.25rem !important}.by-2{border-top-width:.5rem !important;border-bottom-width:.5rem !important}.by-3{border-top-width:1rem !important;border-bottom-width:1rem !important}.by-4{border-top-width:1.5rem !important;border-bottom-width:1.5rem !important}.by-5{border-top-width:3rem !important;border-bottom-width:3rem !important}.bx-0{border-right-width:0 !important;border-left-width:0 !important}.bx-1{border-right-width:.25rem !important;border-left-width:.25rem !important}.bx-2{border-right-width:.5rem !important;border-left-width:.5rem !important}.bx-3{border-right-width:1rem !important;border-left-width:1rem !important}.bx-4{border-right-width:1.5rem !important;border-left-width:1.5rem !important}.bx-5{border-right-width:3rem !important;border-left-width:3rem !important}.btl-0{border-top-left-radius:0 !important}.btl-5p{border-top-left-radius:5px !important}.btr-0{border-top-right-radius:0 !important}.btr-5p{border-top-right-radius:5px !important}.bbr-0{border-bottom-right-radius:0 !important}.bbr-5p{border-bottom-right-radius:5px !important}.bbl-0{border-bottom-left-radius:0 !important}.bbl-5p{border-bottom-left-radius:5px !important}.hr-0{margin-top:0 !important;margin-bottom:0 !important;width:100%}.hr-1{margin-top:.25rem !important;margin-bottom:.25rem !important;width:100%}.hr-2{margin-top:.5rem !important;margin-bottom:.5rem !important;width:100%}.hr-3{margin-top:1rem;margin-bottom:1rem;width:100%}.hr-4{margin-top:1.5rem !important;margin-bottom:1.5rem !important;width:100%}.hr-5{margin-top:3rem !important;margin-bottom:3rem !important;width:100%}.vr-0{width:1px;height:100%;border-left:1px solid #ccc;margin-right:0 !important;margin-left:0 !important}.vr-1{width:1px;height:100%;border-left:1px solid #ccc;margin-right:.25rem !important;margin-left:.25rem !important}.vr-2{width:1px;height:100%;border-left:1px solid #ccc;margin-right:.5rem !important;margin-left:.5rem !important}.vr-3{width:1px;height:100%;border-left:1px solid #ccc;margin-right:1rem !important;margin-left:1rem !important}.vr-4{width:1px;height:100%;border-left:1px solid #ccc;margin-right:1.5rem !important;margin-left:1.5rem !important}.vr-5{width:1px;height:100%;border-left:1px solid #ccc;margin-right:3rem !important;margin-left:3rem !important}.m-auto{margin:auto !important}.m-0{margin:0 !important}.m-1{margin:.25rem !important}.m-2{margin:.5rem !important}.m-3{margin:1rem !important}.m-4{margin:1.5rem !important}.m-5{margin:3rem !important}.my-auto{margin-top:auto !important;margin-bottom:auto !important}.my-0{margin-top:0 !important;margin-bottom:0 !important}.my-1{margin-top:.25rem !important;margin-bottom:.25rem !important}.my-2{margin-top:.5rem !important;margin-bottom:.5rem !important}.my-3{margin-top:1rem !important;margin-bottom:1rem !important}.my-4{margin-top:1.5rem !important;margin-bottom:1.5rem !important}.my-5{margin-top:3rem !important;margin-bottom:3rem !important}.mx-auto{margin-right:auto !important;margin-left:auto !important}.mx-0{margin-right:0 !important;margin-left:0 !important}.mx-1{margin-right:.25rem !important;margin-left:.25rem !important}.mx-2{margin-right:.5rem !important;margin-left:.5rem !important}.mx-3{margin-right:1rem !important;margin-left:1rem !important}.mx-4{margin-right:1.5rem !important;margin-left:1.5rem !important}.mx-5{margin-right:3rem !important;margin-left:3rem !important}.my-1p{margin-top:1px;margin-bottom:1px}.mx-2p{margin-right:2px !important;margin-left:2px !important}.mt-auto{margin-top:auto !important}.mt-0{margin-top:0 !important}.mt-1{margin-top:.25rem !important}.mt-n1{margin-top:-0.25rem !important}.mt-2{margin-top:.5rem !important}.mt-n2{margin-top:-0.5rem !important}.mt-3{margin-top:1rem !important}.mt-4{margin-top:1.5rem !important}.mt-5{margin-top:3rem !important}.mt-1p{margin-top:1px !important}.mr-auto{margin-right:auto !important}.mr-0{margin-right:0 !important}.mr-1{margin-right:.25rem !important}.mr-n1{margin-right:-0.25rem !important}.mr-2{margin-right:.5rem !important}.mr-n2{margin-right:-0.5rem !important}.mr-3{margin-right:1rem !important}.mr-4{margin-right:1.5rem !important}.mr-5{margin-right:3rem !important}.mr-3p{margin-right:3px !important}.mb-auto{margin-bottom:auto !important}.mb-0{margin-bottom:0 !important}.mb-1{margin-bottom:.25rem !important}.mb-n1{margin-bottom:-0.25rem !important}.mb-2{margin-bottom:.5rem !important}.mb-n2{margin-bottom:-0.5rem !important}.mb-3{margin-bottom:1rem !important}.mb-4{margin-bottom:1.5rem !important}.mb-5{margin-bottom:3rem !important}.ml-auto{margin-left:auto !important}.ml-0{margin-left:0 !important}.ml-1{margin-left:.25rem !important}.ml-n1{margin-left:-0.25rem !important}.ml-2{margin-left:.5rem !important}.ml-n2{margin-left:-0.5rem !important}.ml-3{margin-left:1rem !important}.ml-4{margin-left:1.5rem !important}.ml-5{margin-left:3rem !important}.p-0{padding:0 !important}.p-1{padding:.25rem !important}.p-2{padding:.5rem !important}.p-3{padding:1rem !important}.p-4{padding:1.5rem !important}.p-5{padding:3rem !important}.p-1p{padding:1px !important}.py-0{padding-top:0 !important;padding-bottom:0 !important}.py-1{padding-top:.25rem !important;padding-bottom:.25rem !important}.py-2{padding-top:.5rem !important;padding-bottom:.5rem !important}.py-3{padding-top:1rem !important;padding-bottom:1rem !important}.py-4{padding-top:1.5rem !important;padding-bottom:1.5rem !important}.py-5{padding-top:3rem !important;padding-bottom:3rem !important}.px-0{padding-right:0 !important;padding-left:0 !important}.px-1{padding-right:.25rem !important;padding-left:.25rem !important}.px-2{padding-right:.5rem !important;padding-left:.5rem !important}.px-3{padding-right:1rem !important;padding-left:1rem !important}.px-4{padding-right:1.5rem !important;padding-left:1.5rem !important}.px-5{padding-right:3rem !important;padding-left:3rem !important}.py-1p{padding-top:1px !important;padding-bottom:1px !important}.py-2p{padding-top:2px !important;padding-bottom:2px !important}.px-1p{padding-right:1px !important;padding-left:1px !important}.px-2p{padding-right:2px !important;padding-left:2px !important}.pt-0{padding-top:0 !important}.pt-1{padding-top:.25rem !important}.pt-2{padding-top:.5rem !important}.pt-3{padding-top:1rem !important}.pt-4{padding-top:1.5rem !important}.pt-5{padding-top:3rem !important}.pt-1p{padding-top:1px !important}.pr-0{padding-right:0 !important}.pr-1{padding-right:.25rem !important}.pr-2{padding-right:.5rem !important}.pr-3{padding-right:1rem !important}.pr-4{padding-right:1.5rem !important}.pr-5{padding-right:3rem !important}.pr-1p{padding-right:1px !important}.pb-0{padding-bottom:0 !important}.pb-1{padding-bottom:.25rem !important}.pb-2{padding-bottom:.5rem !important}.pb-3{padding-bottom:1rem !important}.pb-4{padding-bottom:1.5rem !important}.pb-5{padding-bottom:3rem !important}.pb-1p{padding-bottom:1px !important}.pl-0{padding-left:0 !important}.pl-1{padding-left:.25rem !important}.pl-2{padding-left:.5rem !important}.pl-3{padding-left:1rem !important}.pl-4{padding-left:1.5rem !important}.pl-5{padding-left:3rem !important}.pl-1p{padding-left:1px !important}.z-index-1{z-index:1 !important}.top-n1p{top:-1px}.right-0{right:0 !important}@font-face{font-family:"Convergence";font-style:normal;font-weight:400;src:local("Convergence-Regular"),url("../fonts/Convergence-Regular.woff2") format("woff2")}@font-face{font-family:"Roboto";font-style:normal;font-weight:400;src:local("Roboto"),url("../fonts/Roboto-Regular.woff2") format("woff2")}@font-face{font-family:"Glyphicons Halflings";font-style:normal;font-weight:400;src:local("glyphicons-halflings-regular"),url("../fonts/glyphicons-halflings-regular.woff2") format("woff2")}@font-face{font-family:"Blambot Casual";src:local("Blambot-Casual"),url("../fonts/Blambot-Casual-Regular.woff2") format("woff2")}@keyframes kf-fade-out{from{opacity:1}to{opacity:0}}.linked-titles .rd__h--0 .entry-title-inner:hover::before{font-size:50%}.linked-titles .rd__h--1 .entry-title-inner:hover::before{font-size:55%}.linked-titles .rd__h--2 .entry-title-inner:hover::before{font-size:60%}.linked-titles .rd__h .entry-title-inner{cursor:copy}.linked-titles .rd__h .entry-title-inner:hover::before{content:" ๐Ÿ”—";color:rgba(0,0,0,.2);position:relative;float:left;width:14px;height:14px;right:20px;margin-right:-30px;font-size:85%}input[type=checkbox]:checked{filter:grayscale(100%)}input[type=radio]:checked{filter:grayscale(100%)}.code{font-family:monospace !important}.dnd-font{font-family:"Times New Roman",serif;font-variant:small-caps;font-weight:500}.ve-small{font-size:85% !important}.font-size-24p{font-size:24px !important}.ve-muted{color:#777 !important}.bold{font-weight:bold !important}.ve-bolder{font-weight:bolder !important}.italic{font-style:italic !important}i>i{font-style:initial}.underline{text-decoration:underline !important}.no-underline{text-decoration:none !important}.help{cursor:help !important;text-decoration:underline !important;text-decoration-style:dotted !important}.help:hover,.help:active,.help:focus{text-decoration:underline !important;text-decoration-style:dotted !important}.help-subtle{cursor:help !important}.no-wrap{white-space:nowrap !important}.text-clip-ellipsis{white-space:nowrap !important;text-overflow:ellipsis !important;overflow:hidden !important}.whitespace-normal{white-space:normal}.whitespace-pre{white-space:pre}.word-break-all{word-break:break-all}.small-caps{font-variant:small-caps}.capitalize{text-transform:capitalize}.no-breaks{break-before:auto;break-after:auto;break-inside:avoid}.text-left{text-align:left !important}.text-right{text-align:right !important}.ve-text-center{text-align:center !important}.text-rtl{direction:rtl}.trans-x-flip{transform:scaleX(-1) !important}.clickable{cursor:pointer !important}.not-clickable{cursor:default !important}.copyable{cursor:copy !important}.ve-draggable{cursor:grab}.no-events{pointer-events:none !important}.events-initial{pointer-events:initial !important}.no-select{user-select:none !important}.user-select-text{user-select:text !important}.user-select-all{user-select:all !important}.smooth-scroll{transform:translateZ(0) !important}.scrollbar-stable{scrollbar-gutter:stable}.overflow-auto{overflow-x:auto;overflow-y:auto}.overflow-y-auto{overflow-y:auto}.overflow-y-scroll{overflow-y:scroll}.overflow-y-hidden{overflow-y:hidden}.overflow-x-auto{overflow-x:auto}.overflow-x-scroll{overflow-x:scroll}.overflow-x-hidden{overflow-x:hidden}.overflow-hidden{overflow:hidden}.overflow-ellipsis{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.overflow-overlay{overflow:auto;overflow:overlay}.resize-vertical{resize:vertical}.resize-none{resize:none}.w-10{width:10% !important}.w-15{width:15% !important}.w-20{width:20% !important}.w-25{width:25% !important}.w-30{width:30% !important}.w-33{width:33.3333333% !important}.w-40{width:40% !important}.w-50{width:50% !important}.w-50--mr-2{width:calc(50% - 0.5rem) !important}.w-60{width:60% !important}.w-66{width:66.6666666% !important}.w-70{width:70% !important}.w-75{width:75% !important}.w-80{width:80% !important}.w-90{width:90% !important}.w-100{width:100% !important}.w-100w{width:100vw !important}.w-initial{width:initial !important}.w-20p{width:20px !important}.w-24p{width:24px !important}.w-30p{width:30px !important}.w-40p{width:40px !important}.w-48p{width:48px !important}.w-50p{width:50px !important}.w-70p{width:70px !important}.w-80p{width:80px !important}.w-90p{width:90px !important}.w-100p{width:100px !important}.w-140p{width:140px !important}.w-200p{width:200px !important}.w-640p{width:640px !important}.min-w-0{min-width:0 !important}.min-w-80{min-width:80% !important}.min-w-100{min-width:100% !important}.min-w-20p{min-width:20px !important}.min-w-100p{min-width:100px !important}.min-w-200p{min-width:200px !important}.max-w-25{max-width:25% !important}.max-w-33{max-width:33.3333333% !important}.max-w-80{max-width:80% !important}.max-w-100{max-width:100% !important}.max-w-80p{max-width:80px !important}.max-w-100p{max-width:100px !important}.max-w-200p{max-width:200px !important}.max-w-300p{max-width:300px !important}.max-w-640p{max-width:640px !important}.h-initial{height:initial !important}.h-50{height:50% !important}.h-100{height:100% !important}.h-100h{height:100vh !important}.h-20p{height:20px !important}.h-21p{height:21px !important}.h-25p{height:25px !important}.h-27p{height:27px !important}.h-30p{height:30px !important}.h-100p{height:100px !important}.h-120p{height:120px !important}.h-ipt-xs{height:22px}.min-h-0{min-height:0 !important}.min-h-100{min-height:100% !important}.min-h-24p{min-height:24px !important}.min-h-100p{min-height:100px !important}.max-h-40{max-height:40% !important}.max-h-unset{max-height:unset !important}.relative{position:relative !important}.absolute{position:absolute !important}.sticky{position:sticky !important}.ve-grid{display:grid !important}.block{display:block !important}.ve-block{display:block !important}.inline-block{display:inline-block !important}.ve-inline-block{display:inline-block !important}.inline{display:inline !important}.ve-inline-flex{display:inline-flex !important}.ve-flex{display:flex !important}.ve-flex-col{display:flex !important;flex-direction:column !important}.ve-flex-v-center{display:flex !important;align-items:center !important}.ve-inline-flex-v-center{display:inline-flex !important;align-items:center !important}.ve-flex-v-top{display:flex;align-items:flex-start}.ve-flex-v-baseline{display:flex !important;align-items:baseline !important}.ve-flex-v-end{display:flex !important;align-items:flex-end !important}.ve-flex-v-stretch{display:flex !important;align-items:stretch !important}.ve-flex-h-center{display:flex !important;justify-content:center !important}.ve-flex-h-right{display:flex !important;justify-content:flex-end !important}.ve-flex-vh-center{display:flex !important;align-items:center !important;justify-content:center !important}.ve-flex-vh-center-around{display:flex;align-items:center;justify-content:space-around}.ve-flex-inline-col{display:inline-flex !important;flex-direction:column !important}.ve-flex-inline-v-center{display:inline-flex !important;align-items:center !important;justify-content:center !important}.ve-self-flex-start{align-self:flex-start !important}.ve-self-flex-center{align-self:center !important}.ve-self-flex-end{align-self:flex-end !important}.ve-self-flex-stretch{align-self:stretch !important}.ve-flex-fill{flex-basis:100%}.ve-grow{flex-grow:1 !important}.no-shrink{flex-shrink:0 !important}.no-grow{flex-grow:0 !important}.ve-flex-1{flex:1 !important}.ve-flex-2{flex:2 !important}.ve-flex-3{flex:3 !important}.ve-flex-4{flex:4 !important}.ve-flex-5{flex:5 !important}.ve-flex-6{flex:6 !important}.ve-flex-7{flex:7 !important}.ve-shrink-10{flex-shrink:10 !important}.ve-flex-wrap{display:flex !important;flex-wrap:wrap !important}.split{display:flex !important;justify-content:space-between !important}.split-v-center{display:flex !important;justify-content:space-between !important;align-items:center !important}.inline-split-v-center{display:inline-flex !important;justify-content:space-between;align-items:center}.split-v-end{display:flex !important;justify-content:space-between !important;align-items:flex-end !important}.split-child{width:50%;flex-shrink:0;flex-grow:0}.split-column{display:flex;justify-content:space-between;flex-direction:column}.split-column--inline{display:inline-flex}.columns-2{column-count:2;break-inside:avoid-column;column-gap:1.75rem}.columns-2>*{break-inside:avoid-column}@media(max-width: 768px){.columns-2{column-count:1}}.columns-3{column-count:3;break-inside:avoid-column;column-gap:1.75rem}.columns-3>*{break-inside:avoid-column}@media(max-width: 768px){.columns-3{column-count:2}}@media(max-width: 480px){.columns-3{column-count:1}}.columns-4{column-count:4;break-inside:avoid-column;column-gap:1.75rem}.columns-4>*{break-inside:avoid-column}@media(max-width: 768px){.columns-4{column-count:3}}@media(max-width: 480px){.columns-4{column-count:2}}.columns-5{column-count:5;break-inside:avoid-column;column-gap:1.75rem}.columns-5>*{break-inside:avoid-column}@media(max-width: 768px){.columns-5{column-count:3}}@media(max-width: 480px){.columns-5{column-count:2}}.columns-6{column-count:6;break-inside:avoid-column;column-gap:1.75rem}.columns-6>*{break-inside:avoid-column}@media(max-width: 768px){.columns-6{column-count:3}}@media(max-width: 480px){.columns-6{column-count:2}}.table-layout-fixed{table-layout:fixed !important}.hr--dotted{border-style:dashed;border-left:0;border-right:0}.hr--heavy{border-bottom-width:2px;border-top-width:3px;border-style:outset}.border-dotted{border-style:dotted !important}.opacity-50{opacity:.5 !important}.ve-hidden{display:none !important}@font-face{font-family:"Convergence";font-style:normal;font-weight:400;src:local("Convergence-Regular"),url("../fonts/Convergence-Regular.woff2") format("woff2")}@font-face{font-family:"Roboto";font-style:normal;font-weight:400;src:local("Roboto"),url("../fonts/Roboto-Regular.woff2") format("woff2")}@font-face{font-family:"Glyphicons Halflings";font-style:normal;font-weight:400;src:local("glyphicons-halflings-regular"),url("../fonts/glyphicons-halflings-regular.woff2") format("woff2")}@font-face{font-family:"Blambot Casual";src:local("Blambot-Casual"),url("../fonts/Blambot-Casual-Regular.woff2") format("woff2")}@keyframes kf-fade-out{from{opacity:1}to{opacity:0}}.linked-titles .rd__h--0 .entry-title-inner:hover::before{font-size:50%}.linked-titles .rd__h--1 .entry-title-inner:hover::before{font-size:55%}.linked-titles .rd__h--2 .entry-title-inner:hover::before{font-size:60%}.linked-titles .rd__h .entry-title-inner{cursor:copy}.linked-titles .rd__h .entry-title-inner:hover::before{content:" ๐Ÿ”—";color:rgba(0,0,0,.2);position:relative;float:left;width:14px;height:14px;right:20px;margin-right:-30px;font-size:85%}.clickable--link{color:#337ab7 !important}.plain{font-weight:initial !important;font-style:initial !important;text-decoration:none !important}.fade{transition:opacity 51ms linear}.float-clear{clear:both}.stripe-even:nth-child(even){background:rgba(136,136,136,.0941176471)}.stripe-even--faint:nth-child(even){background:rgba(187,187,187,.0941176471)}.stripe-odd:nth-child(odd){background:rgba(136,136,136,.0941176471)}.stripe-odd--faint:nth-child(odd){background:rgba(187,187,187,.0941176471)}.stripe-child-even-first:nth-child(even)>:first-child{background:rgba(136,136,136,.0941176471)}.veapp__ele-hoverable:hover{background:#f5f5f5}.last-mr-0:last-child{margin-right:0 !important}.hidden{display:none !important}@font-face{font-family:"Convergence";font-style:normal;font-weight:400;src:local("Convergence-Regular"),url("../fonts/Convergence-Regular.woff2") format("woff2")}@font-face{font-family:"Roboto";font-style:normal;font-weight:400;src:local("Roboto"),url("../fonts/Roboto-Regular.woff2") format("woff2")}@font-face{font-family:"Glyphicons Halflings";font-style:normal;font-weight:400;src:local("glyphicons-halflings-regular"),url("../fonts/glyphicons-halflings-regular.woff2") format("woff2")}@font-face{font-family:"Blambot Casual";src:local("Blambot-Casual"),url("../fonts/Blambot-Casual-Regular.woff2") format("woff2")}@keyframes kf-fade-out{from{opacity:1}to{opacity:0}}.linked-titles .rd__h--0 .entry-title-inner:hover::before{font-size:50%}.linked-titles .rd__h--1 .entry-title-inner:hover::before{font-size:55%}.linked-titles .rd__h--2 .entry-title-inner:hover::before{font-size:60%}.linked-titles .rd__h .entry-title-inner{cursor:copy}.linked-titles .rd__h .entry-title-inner:hover::before{content:" ๐Ÿ”—";color:rgba(0,0,0,.2);position:relative;float:left;width:14px;height:14px;right:20px;margin-right:-30px;font-size:85%}.night-mode .stripe-even:nth-child(even){background:rgba(170,170,170,.1333333333)}.night-mode .stripe-odd:nth-child(odd){background:rgba(170,170,170,.1333333333)}.night-mode .stripe-child-even-first:nth-child(even)>:first-child{background:rgba(170,170,170,.1333333333)}.night-mode .veapp__ele-hoverable:hover{background:#383838}@font-face{font-family:"Convergence";font-style:normal;font-weight:400;src:local("Convergence-Regular"),url("../fonts/Convergence-Regular.woff2") format("woff2")}@font-face{font-family:"Roboto";font-style:normal;font-weight:400;src:local("Roboto"),url("../fonts/Roboto-Regular.woff2") format("woff2")}@font-face{font-family:"Glyphicons Halflings";font-style:normal;font-weight:400;src:local("glyphicons-halflings-regular"),url("../fonts/glyphicons-halflings-regular.woff2") format("woff2")}@font-face{font-family:"Blambot Casual";src:local("Blambot-Casual"),url("../fonts/Blambot-Casual-Regular.woff2") format("woff2")}@keyframes kf-fade-out{from{opacity:1}to{opacity:0}}.linked-titles .rd__h--0 .entry-title-inner:hover::before{font-size:50%}.linked-titles .rd__h--1 .entry-title-inner:hover::before{font-size:55%}.linked-titles .rd__h--2 .entry-title-inner:hover::before{font-size:60%}.linked-titles .rd__h .entry-title-inner{cursor:copy}.linked-titles .rd__h .entry-title-inner:hover::before{content:" ๐Ÿ”—";color:rgba(0,0,0,.2);position:relative;float:left;width:14px;height:14px;right:20px;margin-right:-30px;font-size:85%}@media only screen and (min-width: 1201px){.mobile-ish__visible{display:none !important}}@media only screen and (max-width: 1200px){.mobile-ish__hidden{display:none !important}.mobile-ish__ve-flex-col{display:flex !important;flex-direction:column !important}.mobile-ish__ve-flex-ai-start{align-items:flex-start !important}.mobile-ish__w-100{width:100% !important}.mobile-ish__mr-0{margin-right:0 !important}.mobile-ish__mb-2{margin-bottom:.5rem !important}}@media only screen and (min-width: 769px){.mobile__visible{display:none !important}}@media only screen and (max-width: 768px){.mobile__hidden{display:none !important}.mobile__text-center{text-align:center !important}.mobile__text-clip-ellipsis{white-space:nowrap !important;text-overflow:ellipsis !important;overflow:hidden !important}.mobile__ve-flex-col{display:flex !important;flex-direction:column !important}.mobile__ve-flex-row{display:flex !important;flex-direction:row !important}.mobile__ve-flex-col-reverse{display:flex !important;flex-direction:column-reverse !important}.mobile__ve-flex-ai-start{align-items:flex-start !important}.mobile__w-100{width:100% !important}.mobile__max-w-100{max-width:100% !important}.mobile__h-initial{height:initial !important}.mobile__m-auto{margin:auto !important}.mobile__m-0{margin:0 !important}.mobile__m-1{margin:.25rem !important}.mobile__m-2{margin:.5rem !important}.mobile__m-3{margin:1rem !important}.mobile__m-4{margin:1.5rem !important}.mobile__m-5{margin:3rem !important}.mobile__mt-auto{margin-top:auto !important}.mobile__mt-0{margin-top:0 !important}.mobile__mt-1{margin-top:.25rem !important}.mobile__mt-2{margin-top:.5rem !important}.mobile__mt-3{margin-top:1rem !important}.mobile__mt-4{margin-top:1.5rem !important}.mobile__mt-5{margin-top:3rem !important}.mobile__mr-auto{margin-right:auto !important}.mobile__mr-0{margin-right:0 !important}.mobile__mr-1{margin-right:.25rem !important}.mobile__mr-2{margin-right:.5rem !important}.mobile__mr-3{margin-right:1rem !important}.mobile__mr-4{margin-right:1.5rem !important}.mobile__mr-5{margin-right:3rem !important}.mobile__mb-auto{margin-bottom:auto !important}.mobile__mb-0{margin-bottom:0 !important}.mobile__mb-1{margin-bottom:.25rem !important}.mobile__mb-2{margin-bottom:.5rem !important}.mobile__mb-3{margin-bottom:1rem !important}.mobile__mb-4{margin-bottom:1.5rem !important}.mobile__mb-5{margin-bottom:3rem !important}.mobile__ml-auto{margin-left:auto !important}.mobile__ml-0{margin-left:0 !important}.mobile__ml-1{margin-left:.25rem !important}.mobile__ml-2{margin-left:.5rem !important}.mobile__ml-3{margin-left:1rem !important}.mobile__ml-4{margin-left:1.5rem !important}.mobile__ml-5{margin-left:3rem !important}.mobile__my-auto{margin-top:auto !important;margin-bottom:auto !important}.mobile__my-0{margin-top:0 !important;margin-bottom:0 !important}.mobile__my-1{margin-top:.25rem !important;margin-bottom:.25rem !important}.mobile__my-2{margin-top:.5rem !important;margin-bottom:.5rem !important}.mobile__my-3{margin-top:1rem !important;margin-bottom:1rem !important}.mobile__my-4{margin-top:1.5rem !important;margin-bottom:1.5rem !important}.mobile__my-5{margin-top:3rem !important;margin-bottom:3rem !important}.mobile__mx-auto{margin-right:auto !important;margin-left:auto !important}.mobile__mx-0{margin-right:0 !important;margin-left:0 !important}.mobile__mx-1{margin-right:.25rem !important;margin-left:.25rem !important}.mobile__mx-2{margin-right:.5rem !important;margin-left:.5rem !important}.mobile__mx-3{margin-right:1rem !important;margin-left:1rem !important}.mobile__mx-4{margin-right:1.5rem !important;margin-left:1.5rem !important}.mobile__mx-5{margin-right:3rem !important;margin-left:3rem !important}.mobile__p-0{padding:0 !important}.mobile__p-1{padding:.25rem !important}.mobile__p-2{padding:.5rem !important}.mobile__p-3{padding:1rem !important}.mobile__p-4{padding:1.5rem !important}.mobile__p-5{padding:3rem !important}.mobile__p-1p{padding:1px !important}.mobile__pt-0{padding-top:0 !important}.mobile__pt-1{padding-top:.25rem !important}.mobile__pt-2{padding-top:.5rem !important}.mobile__pt-3{padding-top:1rem !important}.mobile__pt-4{padding-top:1.5rem !important}.mobile__pt-5{padding-top:3rem !important}.mobile__pt-1p{padding-top:1px !important}.mobile__pr-0{padding-right:0 !important}.mobile__pr-1{padding-right:.25rem !important}.mobile__pr-2{padding-right:.5rem !important}.mobile__pr-3{padding-right:1rem !important}.mobile__pr-4{padding-right:1.5rem !important}.mobile__pr-5{padding-right:3rem !important}.mobile__pr-1p{padding-right:1px !important}.mobile__pb-0{padding-bottom:0 !important}.mobile__pb-1{padding-bottom:.25rem !important}.mobile__pb-2{padding-bottom:.5rem !important}.mobile__pb-3{padding-bottom:1rem !important}.mobile__pb-4{padding-bottom:1.5rem !important}.mobile__pb-5{padding-bottom:3rem !important}.mobile__pb-1p{padding-bottom:1px !important}.mobile__pl-0{padding-left:0 !important}.mobile__pl-1{padding-left:.25rem !important}.mobile__pl-2{padding-left:.5rem !important}.mobile__pl-3{padding-left:1rem !important}.mobile__pl-4{padding-left:1.5rem !important}.mobile__pl-5{padding-left:3rem !important}.mobile__pl-1p{padding-left:1px !important}.mobile__py-0{padding-top:0 !important;padding-bottom:0 !important}.mobile__py-1{padding-top:.25rem !important;padding-bottom:.25rem !important}.mobile__py-2{padding-top:.5rem !important;padding-bottom:.5rem !important}.mobile__py-3{padding-top:1rem !important;padding-bottom:1rem !important}.mobile__py-4{padding-top:1.5rem !important;padding-bottom:1.5rem !important}.mobile__py-5{padding-top:3rem !important;padding-bottom:3rem !important}.mobile__px-0{padding-right:0 !important;padding-left:0 !important}.mobile__px-1{padding-right:.25rem !important;padding-left:.25rem !important}.mobile__px-2{padding-right:.5rem !important;padding-left:.5rem !important}.mobile__px-3{padding-right:1rem !important;padding-left:1rem !important}.mobile__px-4{padding-right:1.5rem !important;padding-left:1.5rem !important}.mobile__px-5{padding-right:3rem !important;padding-left:3rem !important}.mobile__py-1p{padding-top:1px !important;padding-bottom:1px !important}}.ve-popwindow .ve-popwindow__hidden{display:none !important}@font-face{font-family:"Convergence";font-style:normal;font-weight:400;src:local("Convergence-Regular"),url("../fonts/Convergence-Regular.woff2") format("woff2")}@font-face{font-family:"Roboto";font-style:normal;font-weight:400;src:local("Roboto"),url("../fonts/Roboto-Regular.woff2") format("woff2")}@font-face{font-family:"Glyphicons Halflings";font-style:normal;font-weight:400;src:local("glyphicons-halflings-regular"),url("../fonts/glyphicons-halflings-regular.woff2") format("woff2")}@font-face{font-family:"Blambot Casual";src:local("Blambot-Casual"),url("../fonts/Blambot-Casual-Regular.woff2") format("woff2")}@keyframes kf-fade-out{from{opacity:1}to{opacity:0}}.linked-titles .rd__h--0 .entry-title-inner:hover::before{font-size:50%}.linked-titles .rd__h--1 .entry-title-inner:hover::before{font-size:55%}.linked-titles .rd__h--2 .entry-title-inner:hover::before{font-size:60%}.linked-titles .rd__h .entry-title-inner{cursor:copy}.linked-titles .rd__h .entry-title-inner:hover::before{content:" ๐Ÿ”—";color:rgba(0,0,0,.2);position:relative;float:left;width:14px;height:14px;right:20px;margin-right:-30px;font-size:85%}:root{--sz-font-h0: 1.8em;--sz-font-h1: 1.5em;--sz-font-h2: 1.35em;--h-mb-p: 5px;--h-mb-p-inline: 0;--h-mb-quote-line: 5px;--h-mb-quote-line-last: 5px;--h-mb-li: 3px;--w-text-indent-inline-p: 0.7em;--w-pl-list: 24px;--w-pl-list-no-bullets: 10px}@keyframes rd__spin{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}.rd__b p{margin-bottom:var(--h-mb-p)}.rd__b--0,.rd__b--1,.rd__b--2,.rd__b--3,.rd__b--4{margin-bottom:var(--h-mb-p)}.rd__b--0:last-child,.rd__b--1:last-child,.rd__b--2:last-child,.rd__b--3:last-child,.rd__b--4:last-child{margin-bottom:0}.rd__b--0>*:last-child,.rd__b--1>*:last-child,.rd__b--2>*:last-child,.rd__b--3>*:last-child,.rd__b--4>*:last-child{margin-bottom:0}.rd__hr{border-color:rgba(170,170,170,.4);margin:17px 0 5px}.rd__hr--section{margin:30px 0 5px}.rd__list{margin-top:0;margin-bottom:var(--h-mb-p);padding-left:var(--w-pl-list)}.rd__list>.rd__list:last-child{margin-bottom:0}.rd__list>.rd__list-name{margin-left:calc(-1*var(--w-pl-list))}.rd__list-name{margin:0 0 var(--h-mb-li);font-weight:bold;list-style-type:none}.rd__li{margin-bottom:var(--h-mb-p)}.rd__compact-stats{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:flex-start;align-content:flex-start}.rd__title-link{opacity:.3;font-size:12px;font-weight:normal}.rd__title-link--inset{font-size:12px}.rd__wrp-image{margin:5px auto 0;text-align:center}.rd__image{max-width:100%;max-height:60vh;cursor:zoom-in}.rd__wrp-map{max-width:33%;margin:0 auto}.rd__wrp-gallery{display:flex;flex-wrap:wrap;justify-content:center;align-items:flex-end}.rd__wrp-gallery-image{padding:0 10px 10px;max-width:33%}.rd__gallery-name{font-style:italic;text-decoration:underline}.rd__quote-line{margin-bottom:var(--h-mb-quote-line)}.rd__quote-line--last{margin-bottom:var(--h-mb-quote-line-last)}.rd__quote-by{width:100%;text-align:right;display:block}.rd__p-list-item{font-style:initial}.rd__p-cont-indent{display:block;text-indent:1em}.rd__tab-indent{width:1em;display:inline-block}.rd__image-title{width:100%;text-align:center;font-style:italic;margin-top:3px}.rd__image-title-inner{display:inline-block;text-decoration:underline;margin:2px 0}.rd__image-btn-viewer{font-style:initial;white-space:normal;font-size:inherit;line-height:1.7}.rd__image-credit{font-size:80%}.rd__scroller-viewer{scrollbar-width:auto}.rd__scroller-viewer::-webkit-scrollbar{width:15px;height:15px}.rd__prerequisite{font-style:italic;display:block}.rd__li-spell{margin:0}.rd__list-hang-notitle{padding:0;list-style:none}.rd__list-hang-notitle>.rd__li{margin-bottom:var(--h-mb-li);text-indent:-1.1em;margin-left:1.1em}.rd__list-hang-notitle>.rd__li a,.rd__list-hang-notitle>.rd__li span{text-indent:initial}.rd__list-hang-notitle>.rd__li>*{margin:0 0 var(--h-mb-li)}.rd__list-hang-notitle>.rd__li>ul{text-indent:0}.rd__list-hang{list-style:none}.rd__list-hang>.rd__list-name{margin-left:calc(-1*var(--w-pl-list))}.rd__list-hang>li>*:not(::marker){text-indent:-1.1em;margin-left:1.1em}.rd__list-decimal{list-style:decimal}.rd__list-lower-roman{list-style:lower-roman}.rd__list-upper-roman{list-style:upper-roman}.rd__list-no-bullets{list-style:none;padding:0 0 0 var(--w-pl-list-no-bullets)}.rd__list-no-bullets>.rd__list-name{margin-left:calc(-1*var(--w-pl-list-no-bullets))}.rd__list-italic{font-style:italic}.rd__quote-pull{padding:10px 15px;text-align:center;font-size:125%}.rd__h{margin:0;line-height:inherit}.rd__h--0{color:#822000;font-family:"Times New Roman",serif;font-variant:small-caps;font-weight:500;display:flex;justify-content:space-between;align-items:center;font-size:var(--sz-font-h0)}.rd__h--1{color:#822000;font-family:"Times New Roman",serif;font-variant:small-caps;font-weight:500;display:flex;justify-content:space-between;align-items:center;font-size:var(--sz-font-h1);border-bottom:1px solid #822000;margin:0 0 .2em}.rd__h--2{color:#822000;font-family:"Times New Roman",serif;font-variant:small-caps;font-weight:500;display:flex;justify-content:space-between;align-items:center;font-size:var(--sz-font-h2)}.rd__h--2-inset{font-variant:small-caps;font-weight:bolder;font-size:1.1em;display:flex;justify-content:space-between;align-items:center}.rd__h--2-inset-no-name{justify-content:flex-end;float:right}.rd__h--2-flow-block{display:block;font-variant:small-caps;font-weight:bolder;font-size:1.1em;text-align:center}.rd__h--2-inset>h4,.rd__h--2-flow-block>h4{font-size:inherit;font-weight:inherit;line-height:1.42857143;margin:0}.rd__h--3{font-weight:bold;font-style:italic}.rd__h--4{font-style:italic}.rd__h-toggle{font-family:Arial,sans-serif;font-size:12px;opacity:.3;font-weight:normal}.rd__ele-toggled-hidden{display:none !important}.rd__b--3>p,.rd__b--4>p{text-indent:var(--w-text-indent-inline-p);margin-bottom:var(--h-mb-p-inline)}.rd__b--3>p:first-of-type,.rd__b--4>p:first-of-type{display:inline}.rd__b-inset>p{text-indent:var(--w-text-indent-inline-p);margin-bottom:0}.rd__b-inset>p:first-of-type{text-indent:0}.rd__b-inset{margin:7px 15px;padding:5px 10px;box-shadow:0 0 4px 0 #988e7c;border:1px solid #656565;border-top:2px solid #656565;border-bottom:2px solid #656565;background-color:#e9ecda}.rd__b-inset>*:last-of-type{margin-bottom:0}.rd__b-inset--readaloud{box-shadow:0 0 4px 0 #988e7c;border:1px solid #656565;border-left:2px solid #656565;border-right:2px solid #656565;background-color:#eef0f3}.rd__b-inset-inner{margin-top:10px}.rd__b-data{border:3px solid #e69a28;border-left-width:1px;border-right-width:1px;margin:5px;width:calc(100% - 12px);table-layout:fixed}.rd__b-data--inset{box-shadow:0 0 4px 0 #988e7c;border:1px solid #656565;background-color:rgba(156,150,120,.1)}.rd__li>.rd__b-data{margin:0}.rd__data-embed-header{cursor:pointer;font-family:"Times New Roman",serif;font-variant:small-caps;text-transform:uppercase;font-weight:bold}.rd__data-embed-header:hover{background:rgba(100,100,100,.08)}.rd__data-embed-toggle{font-family:Arial,sans-serif;float:right}.rd__wrp-loadbrew--ready{cursor:pointer;text-decoration:underline}.rd__loadbrew-icon{text-indent:0;margin-left:2px;transition-property:transform;transition-duration:1s}.rd__loadbrew-icon--active{animation-name:rd__spin;animation-duration:1.2s;animation-iteration-count:infinite;animation-timing-function:linear}.rd__table{width:100%;margin-bottom:var(--h-mb-p)}.rd__table>caption{text-align:left}.rd__comic{font-family:"Blambot Casual",sans-serif;color:#1942be}.rd__comic--h1{font-size:140%;font-variant:small-caps}.rd__comic--h2{font-size:130%}.rd__comic--h3{font-size:120%}.rd__comic--h4{font-size:110%}.rd__comic--note{opacity:.7}.rd__comic-img-speaker{margin-top:-5px;margin-bottom:-5px}.rd__comic-img-speaker--left{float:left;margin-right:0;margin-left:-20px}.rd__comic-img-speaker--right{float:right;margin-right:-20px;margin-left:0}.rd__comic-img-speaker::after{content:"";clear:both;display:block}.rd__img-small{max-width:25vw;max-height:25vh}.rd__s-v-flow{height:15px;width:0;border-left:1px solid #656565;border-right:1px solid #656565;margin:0 auto}.rd__b-flow{margin:0 15px;padding:5px 10px;box-shadow:0 0 4px 0 #988e7c;border:1px solid #656565;border-top:2px solid #656565;border-bottom:2px solid #656565;background-color:#ece4da}.rd__b-flow>*:last-of-type{margin-bottom:0}.rd__stats-name-page{font-family:"Convergence",Arial,sans-serif;font-size:12px;color:#333;font-weight:100}.rd__stats-name-brew-link{font-size:13px;font-weight:initial}.rd__pre-wrap{white-space:pre-wrap}.rd__highlight{background-color:#ff0}.rd__color a{color:inherit !important}.rd-item__type-rarity-attunement{color:#333}.rd-spell__level-school-ritual{font-style:italic;color:#333}.rd-ability-icon{max-width:100px}.rd-ability-icon__fill-primary{fill:#333}.rd-ability-icon__fill-bg{fill:#fff}.rd-ability-icon__stroke-bg{stroke:#fff}.rd-homebrew__b{background-color:rgba(255,0,0,.1019607843);clear:both}.rd-homebrew__wrp-notice{float:right;border:1px dotted;margin-bottom:5px;margin-left:5px;padding-right:2px;padding-left:2px;text-indent:0}.rd-homebrew__disp-notice::before{content:"Homebrew"}.rd-homebrew__disp-old-content{color:#a00;margin-left:5px}.rd-homebrew__disp-inline{background-color:rgba(255,0,0,.1019607843);text-decoration:underline dotted}td>.rd__b:last-child{margin-bottom:0}.rd-recipes__wrp-recipe .rd__b--3>p,.rd-recipes__wrp-recipe .rd__b--4>p{text-indent:0}.rd-recipes__wrp-instructions .rd__h--3{font-style:initial;font-variant:small-caps}.rd-recipes__wrp-instructions .rd__b--3>p,.rd-recipes__wrp-instructions .rd__b--4>p{margin-bottom:10px}.rd-recipes__wrp-instructions .rd__b--3>p:nth-of-type(2),.rd-recipes__wrp-instructions .rd__b--4>p:nth-of-type(2){margin-top:10px}.rd-recipes__wrp-ingredients .rd__h--2{font-size:1em;font-family:Roboto,Helvetica,sans-serif;color:inherit;font-weight:bold}.rd-recipes__wrp-ingredients .rd__b p{margin-bottom:0}@font-face{font-family:"Convergence";font-style:normal;font-weight:400;src:local("Convergence-Regular"),url("../fonts/Convergence-Regular.woff2") format("woff2")}@font-face{font-family:"Roboto";font-style:normal;font-weight:400;src:local("Roboto"),url("../fonts/Roboto-Regular.woff2") format("woff2")}@font-face{font-family:"Glyphicons Halflings";font-style:normal;font-weight:400;src:local("glyphicons-halflings-regular"),url("../fonts/glyphicons-halflings-regular.woff2") format("woff2")}@font-face{font-family:"Blambot Casual";src:local("Blambot-Casual"),url("../fonts/Blambot-Casual-Regular.woff2") format("woff2")}@keyframes kf-fade-out{from{opacity:1}to{opacity:0}}.linked-titles .rd__h--0 .entry-title-inner:hover::before{font-size:50%}.linked-titles .rd__h--1 .entry-title-inner:hover::before{font-size:55%}.linked-titles .rd__h--2 .entry-title-inner:hover::before{font-size:60%}.linked-titles .rd__h .entry-title-inner{cursor:copy}.linked-titles .rd__h .entry-title-inner:hover::before{content:" ๐Ÿ”—";color:rgba(0,0,0,.2);position:relative;float:left;width:14px;height:14px;right:20px;margin-right:-30px;font-size:85%}.night-mode .rd__h--0,.night-mode .rd__h--1,.night-mode .rd__h--2{color:#d29a38}.night-mode .rd__h--1{border-bottom-color:#d29a38}.night-mode .rd__h--4{color:#c2c2c2}.night-mode .rd__h--3{color:#c2c2c2}.night-mode .rd__-image-title-inner{border-color:#555}.night-mode .rd__b-inset{background-color:#323431}.night-mode .rd__b-inset--readaloud{background-color:#28303a}.night-mode .rd__b-data{border-color:#565656}.night-mode .rd__b-flow{background-color:#38352f}.night-mode .rd__comic{color:#95aaea}.night-mode .rd__stats-name-page{color:#bbb}.night-mode .rd__highlight{background-color:#cc0;color:#222}.night-mode .rd-item__type-rarity-attunement{color:#bbb}.night-mode .rd-spell__level-school-ritual{color:#bbb}.night-mode .rd-ability-icon__fill-primary{fill:#bbb}.night-mode .rd-ability-icon__fill-bg{fill:#222}.night-mode .rd-ability-icon__stroke-bg{stroke:#222}.night-mode .rd-homebrew__b{background-color:rgba(255,0,0,.15)}.night-mode .rd-homebrew__disp-old-content{color:#f99}.night-mode .rd-homebrew__disp-inline{background-color:rgba(255,0,0,.15)}@font-face{font-family:"Convergence";font-style:normal;font-weight:400;src:local("Convergence-Regular"),url("../fonts/Convergence-Regular.woff2") format("woff2")}@font-face{font-family:"Roboto";font-style:normal;font-weight:400;src:local("Roboto"),url("../fonts/Roboto-Regular.woff2") format("woff2")}@font-face{font-family:"Glyphicons Halflings";font-style:normal;font-weight:400;src:local("glyphicons-halflings-regular"),url("../fonts/glyphicons-halflings-regular.woff2") format("woff2")}@font-face{font-family:"Blambot Casual";src:local("Blambot-Casual"),url("../fonts/Blambot-Casual-Regular.woff2") format("woff2")}@keyframes kf-fade-out{from{opacity:1}to{opacity:0}}.linked-titles .rd__h--0 .entry-title-inner:hover::before{font-size:50%}.linked-titles .rd__h--1 .entry-title-inner:hover::before{font-size:55%}.linked-titles .rd__h--2 .entry-title-inner:hover::before{font-size:60%}.linked-titles .rd__h .entry-title-inner{cursor:copy}.linked-titles .rd__h .entry-title-inner:hover::before{content:" ๐Ÿ”—";color:rgba(0,0,0,.2);position:relative;float:left;width:14px;height:14px;right:20px;margin-right:-30px;font-size:85%}@keyframes kf-fade-in{from{opacity:0}to{opacity:1}}.hwin{position:fixed;width:600px;max-width:92vw;min-width:150px;z-index:200;box-shadow:0 0 12px 0 #000;animation-name:kf-fade-in;animation-duration:150ms;display:flex;flex-direction:column;background:#f5f5f5}.hwin--minified .hoverborder__resize-n,.hwin--minified .hoverborder__resize-ne,.hwin--minified .hoverborder__resize-e,.hwin--minified .hoverborder__resize-se,.hwin--minified .hoverborder__resize-s,.hwin--minified .hoverborder__resize-sw,.hwin--minified .hoverborder__resize-w,.hwin--minified .hoverborder__resize-nw{display:none}.hwin--popout{box-shadow:initial;width:100%;animation-duration:initial;overflow-y:scroll;height:100%;max-width:initial;max-height:initial}@media(max-width: 1023px){.hwin{max-width:95vw}}.hwin::-webkit-scrollbar-track{background:#a0a0a0}.hwin::-webkit-scrollbar{width:4px}.hwin__wrp-table{max-height:92vh;min-height:20px;overflow-y:auto;background:#f5f5f5;transform:translateZ(0)}.hwin p{margin-bottom:5px}.hwin .rnd-name{font-size:22.4px}.hwin td div.border{height:2px;background-color:#822000;margin:0 3px;padding:0;border-right:5px rgba(0,0,0,0)}.hoverborder{position:relative;min-height:3px;max-height:16px;text-align:right}.hoverborder--btm{cursor:ns-resize}.hoverborder--top{cursor:move;user-select:none;display:flex;justify-content:space-between}.hoverborder .hwin__top-border-icon{display:none}.hoverborder[data-perm=true] .hwin__top-border-icon{display:block}.hoverborder .window-title{max-width:calc(100% - 45px);text-align:left;margin-left:4px;padding:1px 0;font-size:12px;display:none;font-family:"Times New Roman",serif;font-variant:small-caps;text-transform:uppercase;font-weight:bold}.hoverborder[data-perm=true] .window-title{display:block}.hoverborder__resize-n{position:absolute;top:-4px;right:4px;left:4px;height:4px;cursor:ns-resize}.hoverborder__resize-ne{position:absolute;top:-6px;right:-6px;height:10px;width:10px;cursor:ne-resize}.hoverborder__resize-e{position:absolute;top:4px;right:-4px;bottom:4px;width:4px;cursor:ew-resize}.hoverborder__resize-se{position:absolute;right:-6px;bottom:-6px;height:10px;width:10px;cursor:se-resize}.hoverborder__resize-s{position:absolute;top:3px;right:4px;left:4px;height:2px}.hoverborder__resize-sw{position:absolute;bottom:-6px;left:-6px;height:10px;width:10px;cursor:sw-resize}.hoverborder__resize-w{position:absolute;top:4px;bottom:4px;left:-4px;width:4px;cursor:ew-resize}.hoverborder__resize-nw{position:absolute;top:-6px;left:-6px;height:10px;width:10px;cursor:nw-resize}.hoverborder[data-display-title=true]~.hwin__wrp-table,.hoverborder[data-display-title=true]~.hoverborder{display:none}@font-face{font-family:"Convergence";font-style:normal;font-weight:400;src:local("Convergence-Regular"),url("../fonts/Convergence-Regular.woff2") format("woff2")}@font-face{font-family:"Roboto";font-style:normal;font-weight:400;src:local("Roboto"),url("../fonts/Roboto-Regular.woff2") format("woff2")}@font-face{font-family:"Glyphicons Halflings";font-style:normal;font-weight:400;src:local("glyphicons-halflings-regular"),url("../fonts/glyphicons-halflings-regular.woff2") format("woff2")}@font-face{font-family:"Blambot Casual";src:local("Blambot-Casual"),url("../fonts/Blambot-Casual-Regular.woff2") format("woff2")}@keyframes kf-fade-out{from{opacity:1}to{opacity:0}}.linked-titles .rd__h--0 .entry-title-inner:hover::before{font-size:50%}.linked-titles .rd__h--1 .entry-title-inner:hover::before{font-size:55%}.linked-titles .rd__h--2 .entry-title-inner:hover::before{font-size:60%}.linked-titles .rd__h .entry-title-inner{cursor:copy}.linked-titles .rd__h .entry-title-inner:hover::before{content:" ๐Ÿ”—";color:rgba(0,0,0,.2);position:relative;float:left;width:14px;height:14px;right:20px;margin-right:-30px;font-size:85%}.night-mode .hwin{background:#222}@font-face{font-family:"Convergence";font-style:normal;font-weight:400;src:local("Convergence-Regular"),url("../fonts/Convergence-Regular.woff2") format("woff2")}@font-face{font-family:"Roboto";font-style:normal;font-weight:400;src:local("Roboto"),url("../fonts/Roboto-Regular.woff2") format("woff2")}@font-face{font-family:"Glyphicons Halflings";font-style:normal;font-weight:400;src:local("glyphicons-halflings-regular"),url("../fonts/glyphicons-halflings-regular.woff2") format("woff2")}@font-face{font-family:"Blambot Casual";src:local("Blambot-Casual"),url("../fonts/Blambot-Casual-Regular.woff2") format("woff2")}@keyframes kf-fade-out{from{opacity:1}to{opacity:0}}.linked-titles .rd__h--0 .entry-title-inner:hover::before{font-size:50%}.linked-titles .rd__h--1 .entry-title-inner:hover::before{font-size:55%}.linked-titles .rd__h--2 .entry-title-inner:hover::before{font-size:60%}.linked-titles .rd__h .entry-title-inner{cursor:copy}.linked-titles .rd__h .entry-title-inner:hover::before{content:" ๐Ÿ”—";color:rgba(0,0,0,.2);position:relative;float:left;width:14px;height:14px;right:20px;margin-right:-30px;font-size:85%}.source-category-site{color:#e50711 !important;border-color:#e50711 !important;text-decoration-color:#e50711 !important}.source-category-extras{color:#9d4c4f !important;border-color:#9d4c4f !important;text-decoration-color:#9d4c4f !important}.source-category-homebrew{color:#8c3b96 !important;border-color:#8c3b96 !important;text-decoration-color:#8c3b96 !important}.source-category-homebrew--local{color:#4b40ed !important;border-color:#4b40ed !important;text-decoration-color:#4b40ed !important}.source-category-spicy{color:#1d965d !important;border-color:#1d965d !important;text-decoration-color:#1d965d !important}.source-category-spicy--local{color:#54ce19 !important;border-color:#54ce19 !important;text-decoration-color:#54ce19 !important}.sourcePHB{color:#4a6898 !important;border-color:#4a6898 !important;text-decoration-color:#4a6898 !important}.sourceDMG{color:purple !important;border-color:purple !important;text-decoration-color:purple !important}.sourceMM{color:green !important;border-color:green !important;text-decoration-color:green !important}.sourceSCAG{color:#76af76 !important;border-color:#76af76 !important;text-decoration-color:#76af76 !important}.sourceVGM{color:gray !important;border-color:gray !important;text-decoration-color:gray !important}.sourceOGA{color:#933d0f !important;border-color:#933d0f !important;text-decoration-color:#933d0f !important}.sourceXGE,.sourceTTP{color:#ba7c00 !important;border-color:#ba7c00 !important;text-decoration-color:#ba7c00 !important}.sourceXMtS{color:#830051 !important;border-color:#830051 !important;text-decoration-color:#830051 !important}.sourceHotDQ{color:#ad8eba !important;border-color:#ad8eba !important;text-decoration-color:#ad8eba !important}.sourceRoT{color:#ff2900 !important;border-color:#ff2900 !important;text-decoration-color:#ff2900 !important}.sourceCoS{color:purple !important;border-color:purple !important;text-decoration-color:purple !important}.sourceOotA{color:gray !important;border-color:gray !important;text-decoration-color:gray !important}.sourceSKT{color:#008b8b !important;border-color:#008b8b !important;text-decoration-color:#008b8b !important}.sourcePotA,.sourceEEPC{color:#57b6c6 !important;border-color:#57b6c6 !important;text-decoration-color:#57b6c6 !important}.sourceLMoP{color:#8da851 !important;border-color:#8da851 !important;text-decoration-color:#8da851 !important}.sourceTftYP{color:#c94029 !important;border-color:#c94029 !important;text-decoration-color:#c94029 !important}.sourceToA{color:#666f30 !important;border-color:#666f30 !important;text-decoration-color:#666f30 !important}.sourceMTF{color:#1f6e7b !important;border-color:#1f6e7b !important;text-decoration-color:#1f6e7b !important}.sourceWDH{color:#d4af37 !important;border-color:#d4af37 !important;text-decoration-color:#d4af37 !important}.sourceGGR,.sourceKKW{color:#bfa76c !important;border-color:#bfa76c !important;text-decoration-color:#bfa76c !important}.sourceWDMM{color:#a2201f !important;border-color:#a2201f !important;text-decoration-color:#a2201f !important}.sourceLLK{color:#6e7a71 !important;border-color:#6e7a71 !important;text-decoration-color:#6e7a71 !important}.sourceAZfyT{color:#4667a7 !important;border-color:#4667a7 !important;text-decoration-color:#4667a7 !important}.sourceGoS{color:#3d695a !important;border-color:#3d695a !important;text-decoration-color:#3d695a !important}.sourceAI,.sourceOoW{color:#5baf04 !important;border-color:#5baf04 !important;text-decoration-color:#5baf04 !important}.sourceESK,.sourceDIP,.sourceDC,.sourceSDW,.sourceSLW{color:#6b909a !important;border-color:#6b909a !important;text-decoration-color:#6b909a !important}.sourceBGDIA{color:#752418 !important;border-color:#752418 !important;text-decoration-color:#752418 !important}.sourceERLW,.sourceEFR{color:#983426 !important;border-color:#983426 !important;text-decoration-color:#983426 !important}.sourceRMR,.sourceRMBRE{color:#5c7c27 !important;border-color:#5c7c27 !important;text-decoration-color:#5c7c27 !important}.sourceMFF{color:#92817f !important;border-color:#92817f !important;text-decoration-color:#92817f !important}.sourceLR{color:#78613c !important;border-color:#78613c !important;text-decoration-color:#78613c !important}.sourceIMR{color:#a19364 !important;border-color:#a19364 !important;text-decoration-color:#a19364 !important}.sourceSADS{color:#333bab !important;border-color:#333bab !important;text-decoration-color:#333bab !important}.sourceEGW,.sourceFS,.sourceDD,.sourceUS,.sourceToR{color:#855a6e !important;border-color:#855a6e !important;text-decoration-color:#855a6e !important}.sourceMOT{color:#556b2e !important;border-color:#556b2e !important;text-decoration-color:#556b2e !important}.sourceIDRotF{color:#8fb8c0 !important;border-color:#8fb8c0 !important;text-decoration-color:#8fb8c0 !important}.sourceTCE{color:#a24d08 !important;border-color:#a24d08 !important;text-decoration-color:#a24d08 !important}.sourceAL{color:#e50711 !important;border-color:#e50711 !important;text-decoration-color:#e50711 !important}.sourceHF{color:#ac9544 !important;border-color:#ac9544 !important;text-decoration-color:#ac9544 !important}.sourceCM{color:#e6585e !important;border-color:#e6585e !important;text-decoration-color:#e6585e !important}.sourceVRGR,.sourceHoL{color:#bd000f !important;border-color:#bd000f !important;text-decoration-color:#bd000f !important}.sourceRtG{color:#8a536a !important;border-color:#8a536a !important;text-decoration-color:#8a536a !important}.sourceAitFR{color:#6e5ab9 !important;border-color:#6e5ab9 !important;text-decoration-color:#6e5ab9 !important}.sourceAitFR-ISF,.sourceAitFR-THP,.sourceAitFR-AVT,.sourceAitFR-DN,.sourceAitFR-FCD{color:#6e5ab9 !important;border-color:#6e5ab9 !important;text-decoration-color:#6e5ab9 !important}.sourceWBtW{color:#7151b6 !important;border-color:#7151b6 !important;text-decoration-color:#7151b6 !important}.sourceDoD{color:#fe4935 !important;border-color:#fe4935 !important;text-decoration-color:#fe4935 !important}.sourceMaBJoV{color:#7a2854 !important;border-color:#7a2854 !important;text-decoration-color:#7a2854 !important}.sourceFTD{color:#b82a15 !important;border-color:#b82a15 !important;text-decoration-color:#b82a15 !important}.sourceNRH{color:#bd335b !important;border-color:#bd335b !important;text-decoration-color:#bd335b !important}.sourceNRH-TCMC,.sourceNRH-AVitW,.sourceNRH-ASS,.sourceNRH-CoI,.sourceNRH-TLT,.sourceNRH-AWoL,.sourceNRH-AT{color:#bd335b !important;border-color:#bd335b !important;text-decoration-color:#bd335b !important}.sourceSCC{color:#be9c56 !important;border-color:#be9c56 !important;text-decoration-color:#be9c56 !important}.sourceSCC-CK,.sourceSCC-HfMT,.sourceSCC-TMM,.sourceSCC-ARiR{color:#be9c56 !important;border-color:#be9c56 !important;text-decoration-color:#be9c56 !important}.sourceMPMM{color:#5c758d !important;border-color:#5c758d !important;text-decoration-color:#5c758d !important}.sourceCRCotN{color:#ac4a70 !important;border-color:#ac4a70 !important;text-decoration-color:#ac4a70 !important}.sourceJttRC{color:#cf48e2 !important;border-color:#cf48e2 !important;text-decoration-color:#cf48e2 !important}.sourceSjA,.sourceSAiS,.sourceAAG,.sourceBAM,.sourceLoX{color:#056b97 !important;border-color:#056b97 !important;text-decoration-color:#056b97 !important}.sourceDoSI{color:#478bb8 !important;border-color:#478bb8 !important;text-decoration-color:#478bb8 !important}.sourceDSotDQ{color:#851e20 !important;border-color:#851e20 !important;text-decoration-color:#851e20 !important}.sourcePSA{color:#d76404 !important;border-color:#d76404 !important;text-decoration-color:#d76404 !important}.sourcePSD{color:#5db7da !important;border-color:#5db7da !important;text-decoration-color:#5db7da !important}.sourcePSI{color:#5d4696 !important;border-color:#5d4696 !important;text-decoration-color:#5d4696 !important}.sourcePSK{color:#a27135 !important;border-color:#a27135 !important;text-decoration-color:#a27135 !important}.sourcePSX{color:#bb2722 !important;border-color:#bb2722 !important;text-decoration-color:#bb2722 !important}.sourcePSZ{color:#6f8a2d !important;border-color:#6f8a2d !important;text-decoration-color:#6f8a2d !important}.sourceKftGV{color:#876e38 !important;border-color:#876e38 !important;text-decoration-color:#876e38 !important}.sourceHAT-TG,.sourceHAT-LMI{color:#a24545 !important;border-color:#a24545 !important;text-decoration-color:#a24545 !important}.sourceBGG{color:#469cb7 !important;border-color:#469cb7 !important;text-decoration-color:#469cb7 !important}.sourceTDCSR{color:#642e4b !important;border-color:#642e4b !important;text-decoration-color:#642e4b !important}.sourcePaBTSO{color:#b2b34e !important;border-color:#b2b34e !important;text-decoration-color:#b2b34e !important}.sourcePAitM,.sourceSatO,.sourceToFW,.sourceMPP{color:#a23087 !important;border-color:#a23087 !important;text-decoration-color:#a23087 !important}.sourceCoA{color:#a13a0f !important;border-color:#a13a0f !important;text-decoration-color:#a13a0f !important}.sourceHFFotM{color:#7b702c !important;border-color:#7b702c !important;text-decoration-color:#7b702c !important}.sourceBMT{color:#694165 !important;border-color:#694165 !important;text-decoration-color:#694165 !important}.sourceGHLoE{color:#c07e4e !important;border-color:#c07e4e !important;text-decoration-color:#c07e4e !important}.sourceDoDk{color:#825494 !important;border-color:#825494 !important;text-decoration-color:#825494 !important}.sp__school-A{color:#00b921}.sp__school-V{color:#bb0100}.sp__school-E{color:#b30083}.sp__school-I{color:#006dbd}.sp__school-D{color:#00adb3}.sp__school-N{color:#6c00cc}.sp__school-T{color:#ccbe00}.sp__school-C{color:#bd0044}.ve-source-marker{position:relative;font-size:80%;display:inline-block;margin-left:1px;align-self:start}.ve-source-marker--list{left:1px;margin-top:1px;margin-left:-20px;padding-left:20px;line-height:8px;height:8px;width:8px}.ve-source-marker--partnered{color:#00c797}.ve-source-marker--legacy{color:#777}@font-face{font-family:"Convergence";font-style:normal;font-weight:400;src:local("Convergence-Regular"),url("../fonts/Convergence-Regular.woff2") format("woff2")}@font-face{font-family:"Roboto";font-style:normal;font-weight:400;src:local("Roboto"),url("../fonts/Roboto-Regular.woff2") format("woff2")}@font-face{font-family:"Glyphicons Halflings";font-style:normal;font-weight:400;src:local("glyphicons-halflings-regular"),url("../fonts/glyphicons-halflings-regular.woff2") format("woff2")}@font-face{font-family:"Blambot Casual";src:local("Blambot-Casual"),url("../fonts/Blambot-Casual-Regular.woff2") format("woff2")}@keyframes kf-fade-out{from{opacity:1}to{opacity:0}}.linked-titles .rd__h--0 .entry-title-inner:hover::before{font-size:50%}.linked-titles .rd__h--1 .entry-title-inner:hover::before{font-size:55%}.linked-titles .rd__h--2 .entry-title-inner:hover::before{font-size:60%}.linked-titles .rd__h .entry-title-inner{cursor:copy}.linked-titles .rd__h .entry-title-inner:hover::before{content:" ๐Ÿ”—";color:rgba(0,0,0,.2);position:relative;float:left;width:14px;height:14px;right:20px;margin-right:-30px;font-size:85%}.night-mode .sourcePHB{color:#337ab7 !important;border-color:#337ab7 !important;text-decoration-color:#337ab7 !important}.night-mode .sourceSADS{color:#4f63f5 !important;border-color:#4f63f5 !important;text-decoration-color:#4f63f5 !important}.night-mode .sourcePSA{color:#eec276 !important;border-color:#eec276 !important;text-decoration-color:#eec276 !important}.night-mode .ve-source-marker--partnered{color:#27ac8c}@font-face{font-family:"Convergence";font-style:normal;font-weight:400;src:local("Convergence-Regular"),url("../fonts/Convergence-Regular.woff2") format("woff2")}@font-face{font-family:"Roboto";font-style:normal;font-weight:400;src:local("Roboto"),url("../fonts/Roboto-Regular.woff2") format("woff2")}@font-face{font-family:"Glyphicons Halflings";font-style:normal;font-weight:400;src:local("glyphicons-halflings-regular"),url("../fonts/glyphicons-halflings-regular.woff2") format("woff2")}@font-face{font-family:"Blambot Casual";src:local("Blambot-Casual"),url("../fonts/Blambot-Casual-Regular.woff2") format("woff2")}@keyframes kf-fade-out{from{opacity:1}to{opacity:0}}.linked-titles .rd__h--0 .entry-title-inner:hover::before{font-size:50%}.linked-titles .rd__h--1 .entry-title-inner:hover::before{font-size:55%}.linked-titles .rd__h--2 .entry-title-inner:hover::before{font-size:60%}.linked-titles .rd__h .entry-title-inner{cursor:copy}.linked-titles .rd__h .entry-title-inner:hover::before{content:" ๐Ÿ”—";color:rgba(0,0,0,.2);position:relative;float:left;width:14px;height:14px;right:20px;margin-right:-30px;font-size:85%}.lst__wrp-search-glass{position:absolute;top:0;bottom:2px;left:6px;opacity:.5}.lst__wrp-search-visible{position:absolute;top:0;right:6px;bottom:0;opacity:.5}.lst__caret--active{display:inline-block;width:0;height:0;vertical-align:middle;border-top:4px dashed;border-right:4px solid rgba(0,0,0,0);border-left:4px solid rgba(0,0,0,0);margin-left:2px}.lst__caret--reverse{transform:rotate(180deg)}input.lst__search{padding-left:23px}input.lst__search--no-border-h{border-radius:0;border-right:0}@font-face{font-family:"Convergence";font-style:normal;font-weight:400;src:local("Convergence-Regular"),url("../fonts/Convergence-Regular.woff2") format("woff2")}@font-face{font-family:"Roboto";font-style:normal;font-weight:400;src:local("Roboto"),url("../fonts/Roboto-Regular.woff2") format("woff2")}@font-face{font-family:"Glyphicons Halflings";font-style:normal;font-weight:400;src:local("glyphicons-halflings-regular"),url("../fonts/glyphicons-halflings-regular.woff2") format("woff2")}@font-face{font-family:"Blambot Casual";src:local("Blambot-Casual"),url("../fonts/Blambot-Casual-Regular.woff2") format("woff2")}@keyframes kf-fade-out{from{opacity:1}to{opacity:0}}.linked-titles .rd__h--0 .entry-title-inner:hover::before{font-size:50%}.linked-titles .rd__h--1 .entry-title-inner:hover::before{font-size:55%}.linked-titles .rd__h--2 .entry-title-inner:hover::before{font-size:60%}.linked-titles .rd__h .entry-title-inner{cursor:copy}.linked-titles .rd__h .entry-title-inner:hover::before{content:" ๐Ÿ”—";color:rgba(0,0,0,.2);position:relative;float:left;width:14px;height:14px;right:20px;margin-right:-30px;font-size:85%}.nav .dropdown-menu--top{margin-top:0;border-top-left-radius:0;border-top-right-radius:0}@media(max-width: 768px){.nav>li>a{border:1px solid #ccc}.night-mode .nav>li>a{border-color:#555}}.night-mode .nav>li:not(.active)>a{color:#bbb;background-color:#222;border:1px solid rgba(85,85,85,.6274509804);border-top:0}.night-mode .nav>li:not(.active)>a:focus,.night-mode .nav>li:not(.active)>a:hover{background-color:#272727;color:#fff}.night-mode .nav>li.active>a:focus,.night-mode .nav>li.active>a:hover{color:#fff}.night-mode .nav li.open>a,.night-mode .nav li.open>a:focus,.night-mode .nav li.open>a:hover{background-color:#272727;border-left:1px solid #337ab7;border-right:1px solid #337ab7;border-color:#337ab7}.night-mode .nav li.active.open>a,.night-mode .nav li.active.open>a:focus,.night-mode .nav li.active.open>a:hover{background-color:#333}@media(max-width: 1200px){.nav .caret--right{transform:none}.nav .dropdown-menu--side{top:100%;left:0}}.nav2-list__label{padding:0 20px}.nav2-list__disp-source{display:inline-block;height:15px;border-left:1px solid;position:relative;border-right:1px solid;top:2px;margin-right:7px;margin-left:4px}.nav2-accord__head{padding:3px 7px 3px 20px}.nav2-accord__head:focus,.nav2-accord__head:hover{background-color:#f5f5f5}.nav2-accord__head--active{background:#337ab7;color:#fff}.nav2-accord__head--active:focus,.nav2-accord__head--active:hover{background:#7398b7}.nav2-accord__body{padding:3px 0 3px 35px;display:flex;flex-direction:column}.nav2-accord__lnk-item{padding:3px 20px;color:#333}.nav2-accord__lnk-item:focus,.nav2-accord__lnk-item:hover{background-color:#f5f5f5;text-decoration:none}.nav2-accord__lnk-item--active{background:#337ab7;color:#fff}.nav2-accord__lnk-item--active:focus,.nav2-accord__lnk-item--active:hover{background:#7398b7;color:#fff}.night-mode .nav2-accord__head:focus,.night-mode .nav2-accord__head:hover{background-color:#383838;color:#fff}.night-mode .nav2-accord__head--active{color:#fff}.night-mode .nav2-accord__head--active:focus,.night-mode .nav2-accord__head--active:hover{background:#7398b7}.night-mode .nav2-accord__lnk-item{color:#bbb}.night-mode .nav2-accord__lnk-item:focus,.night-mode .nav2-accord__lnk-item:hover{background-color:#383838;color:#fff}.night-mode .nav2-accord__lnk-item--active{color:#fff}.night-mode .nav2-accord__lnk-item--active:focus,.night-mode .nav2-accord__lnk-item--active:hover{background:#7398b7}@font-face{font-family:"Convergence";font-style:normal;font-weight:400;src:local("Convergence-Regular"),url("../fonts/Convergence-Regular.woff2") format("woff2")}@font-face{font-family:"Roboto";font-style:normal;font-weight:400;src:local("Roboto"),url("../fonts/Roboto-Regular.woff2") format("woff2")}@font-face{font-family:"Glyphicons Halflings";font-style:normal;font-weight:400;src:local("glyphicons-halflings-regular"),url("../fonts/glyphicons-halflings-regular.woff2") format("woff2")}@font-face{font-family:"Blambot Casual";src:local("Blambot-Casual"),url("../fonts/Blambot-Casual-Regular.woff2") format("woff2")}@keyframes kf-fade-out{from{opacity:1}to{opacity:0}}.linked-titles .rd__h--0 .entry-title-inner:hover::before{font-size:50%}.linked-titles .rd__h--1 .entry-title-inner:hover::before{font-size:55%}.linked-titles .rd__h--2 .entry-title-inner:hover::before{font-size:60%}.linked-titles .rd__h .entry-title-inner{cursor:copy}.linked-titles .rd__h .entry-title-inner:hover::before{content:" ๐Ÿ”—";color:rgba(0,0,0,.2);position:relative;float:left;width:14px;height:14px;right:20px;margin-right:-30px;font-size:85%}.page__header{padding:0 15px 1px;box-shadow:0 1px 4px rgba(0,0,0,.475);color:#fff;background-color:#006bc4;min-height:0;flex-shrink:0}.page__title{margin-right:10px;display:inline}.page__title::after{color:#e0e0e0;content:"."}.page__title--home span{color:#e0e0e0}.page__subtitle{display:inline;font-style:italic;color:#d0d0d0}.page__btn-toggle-nav{margin-top:5px;text-align:center;width:6em;flex-shrink:0;height:32px;line-height:1;margin-right:2px}@media(min-width: 769px){.page__btn-toggle-nav{display:none}}.page__nav{position:relative;min-height:33px;flex-shrink:0}@media(max-width: 768px){.page__nav{width:100%;display:flex}}@media(max-width: 768px){.page__nav-inner{display:flex;margin-top:3px;flex-direction:column;width:calc(100% - 6em);flex-shrink:0}}.page__nav-date{margin-left:-16px;width:27px;color:#777}.page__wrp-download{box-shadow:0 6px 12px rgba(0,0,0,.175);position:fixed;z-index:2000;top:5px;min-height:40px;min-width:100px;max-width:850px;display:flex;width:90vw;right:0;left:0;margin:0 auto;padding:5px;justify-content:space-between;align-items:center;border:1px solid rgba(0,0,0,0);border-radius:4px;background:#fff}.page__wrp-download-bar{border:1px solid #2a6496;height:34px;border-radius:4px}.page__wrp-download-bar--error{border-color:#711617}.page__disp-download-progress-bar{position:absolute;top:0;bottom:0;left:0;background:#337ab7}.page__disp-download-progress-bar--error{background:#8a1a1b}.page__disp-download-progress-text{position:absolute;top:0;right:calc(50% - 30px);bottom:0;left:calc(50% - 30px);width:90px;text-shadow:1px 1px 0 #fff,-1px -1px 0 #fff,1px -1px 0 #fff,-1px 1px 0 #fff,3px 3px 5px #000}.active>.nav__link>.page__nav-date{color:#fff}.night-mode .page__wrp-download{background:#222}.night-mode .page__disp-download-progress-text{color:#333}@media(max-width: 768px){#navigation .page__nav-hidden-mobile{display:none;margin-left:0}}#legal-notice{box-shadow:0 6px 12px rgba(0,0,0,.175);position:fixed;z-index:10000;bottom:0;width:100vw;height:10em;display:flex;flex-direction:column;align-items:center;justify-content:space-evenly;font-weight:bold;padding:3rem;border:1px solid rgba(0,0,0,.15);background:#f8f8f8}.viewport-wrapper{position:absolute;top:0;right:0;bottom:0;left:0;display:flex;flex-flow:column nowrap;overflow:auto;height:100vh;width:100vw}@media(max-width: 991px){.viewport-wrapper{bottom:auto;height:initial;min-height:100vh}}.view-col-group--cancer{display:flex;flex-direction:column;position:relative;overflow-y:auto}.view-col-wrapper{display:flex;flex-direction:row;max-height:100%;height:100%;min-height:0}@media(max-width: 991px){.view-col-wrapper{flex-direction:column;max-height:none;height:initial;display:block}}.sidemenu{box-shadow:0 6px 12px rgba(0,0,0,.175);background:#fff;border:1px solid #ccc;position:fixed;z-index:60;top:0;bottom:0;left:-260px;width:250px;transition:left 51ms;cursor:default;display:flex;flex-direction:column;padding:4px 7px;overflow-y:auto}.sidemenu>*{flex-shrink:0}.sidemenu__toggle{box-shadow:0 6px 12px rgba(0,0,0,.175);background:#d3d3d3;position:absolute;z-index:60;top:46px;left:-7px;width:32px;height:32px;cursor:pointer;transition:left 51ms;display:flex;flex-direction:column;justify-content:space-around;padding:3px 4px}.sidemenu__hotzone{position:fixed;top:0;bottom:0;left:0;width:1px}.sidemenu__burger{background:#fff;height:3px;box-shadow:inset 0 0 1px 0 #888}.sidemenu__toggle:hover .sidemenu{left:0;transition:left 51ms}.sidemenu__toggle:hover .sidemenu--offset{left:-12px}.sidemenu__row__divider{background:rgba(204,204,204,.6274509804)}.sidemenu__row__divider--heavy{background:#aaa}.sidemenu__row__label{min-width:46px;flex-shrink:0}.sidemenu__row__label--cb-label{font-weight:initial;display:flex;margin-bottom:0;justify-content:space-between;align-items:center;width:100%}input[type=checkbox].sidemenu__row__label__cb{margin-right:3px;margin-left:7px}.night-mode .sidemenu{border-color:#555}.omni__wrp-output{width:100%;justify-content:flex-end}.omni__output{box-shadow:0 6px 12px rgba(0,0,0,.175);position:absolute;z-index:100;padding:.2em .7em;border-radius:.2em;border:1px solid rgba(0,0,0,.15);background:#fff}.omni__output--scrolled{position:fixed;top:42px;right:10px}@media only screen and (min-width: 320px){.omni__output{max-width:300px;min-width:270px}}@media only screen and (min-width: 481px){.omni__output{max-width:460px;min-width:430px}}@media only screen and (min-width: 769px){.omni__output{max-width:740px;min-width:500px}}@media only screen and (max-width: 768px){.omni__output{top:40px}}.omni__wrp-input{position:relative}@media only screen and (max-width: 768px){.omni__wrp-input{margin-top:2px;margin-left:0}}.omni__wrp-input--scrolled{position:fixed;z-index:100;top:5px;right:10px;width:85px;padding:2px 0;border-left:40px solid rgba(0,0,0,0)}.omni__wrp-input--scrolled .omni__input{min-width:initial;border-top-left-radius:4px;border-top-color:#ccc}.omni__wrp-input--scrolled .omni__submit{border-top-right-radius:4px;border-top-color:#ccc}.omni__wrp-input--scrolled input{padding:0;color:rgba(0,0,0,0)}.omni__wrp-input--scrolled:focus,.omni__wrp-input--scrolled:focus-within,.omni__wrp-input--scrolled:active,.omni__wrp-input--scrolled:hover{width:250px;border-left:0}.omni__wrp-input--scrolled:focus input,.omni__wrp-input--scrolled:focus-within input,.omni__wrp-input--scrolled:active input,.omni__wrp-input--scrolled:hover input{padding:6px 12px;color:inherit}.omni__input{height:32px;min-width:100px}@media(min-width: 992px){.omni__input{border-top-left-radius:0;border-top-color:rgba(0,0,0,0)}}.omni__btn-clear{top:9px;right:37px;opacity:.5}.omni__submit{height:32px;padding:3px 7px}@media(min-width: 992px){.omni__submit{border-top-right-radius:0;border-top-color:rgba(0,0,0,0)}}.omni__wrp-paginate{display:flex;justify-content:space-between}.omni__wrp-paginate>span{display:inline-block;user-select:none;padding:2px;font-size:1.1em;min-width:20px}.omni__paginate-ctrl{cursor:pointer}.omni__paginate-ctrl:hover{color:#337ab7}.omni__paginate-left{margin-right:auto}.omni__paginate-count{margin:0 auto}.omni__paginate-right{margin-left:auto}.omni__disp-srd{font-size:8.5px;align-self:start;top:2px;margin-right:1px;margin-left:2px}.omni__disp-source-marker{margin-left:2px}.omni__wrp-page{margin-left:4px;font-family:"Convergence",Arial,sans-serif;font-weight:100;font-size:94%}.omni__input:placeholder-shown+.omni__btn-clear{display:none}.omni__input:not(:focus):not(:focus-within):not(.omni__wrp-input--scrolled:active):not(:hover)+.omni__btn-clear{display:none}.btn-name-pronounce,.btn-stats-name{vertical-align:top;height:24px;width:24px}.name-pronounce-icon{line-height:16px}.name-pronounce{display:none}.hwin__top-border-icon{top:0;margin-left:auto;padding:2px;color:#f5f5f5;cursor:pointer;font-size:12px;width:18px;text-align:center}.hwin__top-border-icon--text{line-height:11px;font-weight:bolder;font-family:monospace}.hwin__top-border-icon:hover,.hwin__top-border-icon:active,.hwin__top-border-icon:visited{color:#e8e8e8;text-decoration:none}#tabs-right{margin-left:auto;display:flex}.rollbox-min{box-shadow:0 6px 12px rgba(0,0,0,.175);position:fixed;z-index:100;right:7px;bottom:0;color:rgba(255,255,255,.7);width:24px;height:calc(24px + env(safe-area-inset-bottom, 0)/2);cursor:pointer;user-select:none;border:0}.rollbox-min .glyphicon{position:absolute;top:5px;left:6px}.rollbox{box-shadow:0 6px 12px rgba(0,0,0,.175);position:fixed;width:260px;height:335px;z-index:110;border:1px solid rgba(0,0,0,.15);right:1em;bottom:0;flex-direction:column}.rollbox .ipt-roll{flex-shrink:0;overflow-x:auto;height:30px;width:100%}.rollbox .out-roll{overflow-y:auto;height:100%;display:flex;flex-direction:column-reverse;transform:translateZ(0)}.rollbox .head-roll{height:24px;flex-shrink:0;width:100%;box-shadow:0 0 3px rgba(0,0,0,.25);display:flex;flex-direction:row;justify-content:space-between;user-select:none;cursor:pointer}.rollbox .head-roll .hdr-roll{line-height:24px;padding:0 6px}.rollbox .out-roll .out-roll-wrp .out-roll-item:first-child{border-top-left-radius:6px;border-top-right-radius:6px}.rollbox .out-roll .out-roll-wrp .out-roll-item:last-child{border-bottom-left-radius:6px;border-bottom-right-radius:6px}.rollbox .out-roll .out-roll-item{position:relative;margin:1px 3px;padding:1px 3px;width:calc(100% - 6px);word-wrap:break-word;display:flex;align-items:center;justify-content:space-between}.rollbox .out-roll .out-roll-item--message{display:block}.rollbox .out-roll-item-button-wrp{display:none;position:absolute}.rollbox .out-roll-item:hover .out-roll-item-button-wrp{display:flex;right:3px}.rollbox .btn-copy-roll{padding:0 2px;line-height:1.4}.rollbox .out-roll .out-roll-item .roll-label{font-style:italic}.rollbox .out-roll .out-roll-item .roll{font-weight:bold}.rollbox .out-roll .out-roll-item .roll-min{color:#ff3100}.rollbox .out-roll .out-roll-item .roll-max{color:#00b400}.rollbox .out-roll .out-roll-id{width:100%;font-size:70%;padding:1px 3px}.rll__prompt-header{font-size:32px}.rll__dropped{text-decoration:red line-through}.rll__list{margin-bottom:0;padding-left:24px}.rll__exploded{color:#6f99b8}.rll__success{text-decoration:#209520 underline}.rll__min--muted{color:#d24c2d}.rll__max--muted{color:#209520}.rll__exploded{color:#6f99b8}.rll__min--muted{color:#d24c2d}.rll__max--muted{color:#209520}.wrp-stat-tab{width:100%;display:flex}.view-col{position:relative;margin:7px 7px 12px;flex:1;display:flex;flex-direction:column;height:calc(100% - 19px)}.view-col--wrp-book-contents{height:initial;position:relative;flex:1}#listcontainer.view-col{display:flex;flex-flow:column nowrap}#contentwrapper.view-col{overflow-x:hidden;margin-bottom:5px}#contentwrapper.view-col>*:not(.wrp-stats-table){flex-shrink:0}.wrp-stats-table{overflow-x:hidden;overflow-y:auto;border-top:1px solid #e69a28;border-bottom:1px solid #e69a28;transform:translateZ(0);flex-shrink:1}.wrp-stats-table--book{border-top-color:silver;border-bottom-color:silver}@media(max-width: 991px){.wrp-stats-table{overflow-y:initial}}.filtertools,#filtertools{font-size:.8em}.filtertools select,#filtertools select{margin:1px}.filtertools small:hover,#filtertools small:hover{cursor:pointer}#pointbuy input{margin:2px;text-align:right;width:3em}#pointbuy input[type=number]{appearance:textfield}#pointbuy input[type=checkbox]{width:initial;margin:initial}.list,.list-display-only{transform:translateZ(0);position:relative;padding-left:0;height:100%;overflow-y:auto;overflow-x:hidden;clear:both;font-size:.8em}@media(min-width: 992px){.list--stats,.list-display-only--stats{overflow-y:scroll;margin-right:-9px}}@media(max-width: 991px){.list,.list-display-only{max-height:40vh}}.list.rules,.list-display-only.rules{overflow-y:auto !important;padding-top:0}.lst--border{border-bottom:1px solid #ddd}.list-multi-selected{box-shadow:inset 0 0 0 5000px rgba(0,107,196,.3)}.list-multi-selected .lst--border{border-color:#6fa4d0}.list-multi-selected.lst__row--sublist{box-shadow:inset 0 0 0 5000px rgba(148,148,148,.2)}.list-multi-selected.lst__row--sublist .lst--border{border-color:#ccc}.row--blocklisted{display:none !important}.manbrew__source{overflow:hidden}.manbrew__search{border-bottom-left-radius:0;border-bottom-right-radius:0}.manbrew__filtertools button{border-top:0;border-top-left-radius:0;border-top-right-radius:0}.manbrew__filtertools .wrp-cb-all{border-bottom:1px solid #ccc;border-right:1px solid #ccc;line-height:14px;border-bottom-right-radius:3px;text-align:center;vertical-align:middle}.manbrew__wrp_btn_del_selected{text-align:right;padding-bottom:5px}.manbrew__current_brew{margin-bottom:5px}.manbrew__row{margin-right:0;margin-left:0;padding:4px 0}.manbrew__col--tall{line-height:30px}.manbrew__list{position:absolute;top:0;right:0;bottom:0;left:0;height:initial;overflow-y:initial;width:100%}.manbrew-row__icn-btn{top:2px}.manbrew-row__icn-btn--text{top:-1px}.night-mode .manbrew__filtertools .wrp-cb-all{border-color:#555}.rnd-name{position:relative;font-size:1.8em;font-family:"Times New Roman",serif;font-variant:small-caps;font-weight:500;padding-left:.2em !important}.rnd-name div.name-inner{display:flex;justify-content:space-between;align-items:flex-end}.stats{font-family:"Convergence",Arial,sans-serif;width:100%;font-size:12.6px;table-layout:fixed;overflow-wrap:break-word}.stats:last-child{margin-bottom:0}td,th{padding:1px .3em}.stats-name{font-size:unset;line-height:unset;color:#822000}.stats table{margin-bottom:5px;white-space:initial}.stats table.statsDataInset{margin:10px;width:calc(100% - 20px);border:1px solid rgba(0,0,0,.4);box-shadow:0 0 4px 0 #988d7c}.stats table caption{margin-left:5px;padding:0;font-weight:bold;font-size:1.1em}th.border{height:4px}.wrp-stats-table th.border{height:3px}th.border-thin{height:1px}td.divider div{background:#822000;height:2px;margin:6px 0}.stats span.name{font-weight:bold}.stats span.name{font-weight:bold}tr.text>td{padding-bottom:.7em}tr.text.compact>td{padding-bottom:0}tr.text.compact>td p:last-child{margin-bottom:0}div#lootoutput{height:100%;clear:both}.mon__btn-reset-cr,.mon__btn-scale-cr{padding:0 5px;font-size:10px}.mon__cr_slider_wrp{position:absolute;top:23px;left:0;background:#fff;border:1px solid #ccc;width:calc(100% - 30px);margin:0 10px;padding:5px 7px 0;border-radius:4px;box-shadow:0 0 3px 0 #000}.mon__cr_slider_wrp--compact{top:41px}.mon__wrp-size-type-align--token,.mon__wrp-avoid-token{max-width:calc(100% - 11rem)}.mon__sect-header-inner{display:block;margin-top:-0.3rem;margin-bottom:-0.3rem;font-weight:100;color:#822000;font-size:18px;line-height:23px;font-family:"Times New Roman",serif;font-variant:small-caps}.mon__sect-row-inner{padding-top:.5rem !important}.mon__sect-row-inner>*:last-of-type{margin-bottom:.5rem !important}.mon__stat-header-underline{border-bottom:1px solid #822000;vertical-align:bottom !important;padding-left:.2rem}.mon__wrp-token{display:block;position:absolute;z-index:10;top:0;right:.5rem;width:auto;max-width:11rem;height:auto;transition:opacity 34ms,max-width 34ms,right 34ms}.mon__wrp-token:hover{max-width:100%;right:0;opacity:1 !important;transition:opacity 34ms,max-width 34ms,right 34ms}.mon__wrp-token:hover .mon__btn-token-cycle{opacity:1;transition:opacity 34ms}.mon__wrp-token:hover .mon__wrp-token-footer{opacity:1;transition:opacity 34ms}.mon__token{width:100%;height:100%}.mon__btn-token-cycle{position:absolute;top:50%;bottom:50%;display:flex;align-items:center;justify-content:center;width:40px;height:40px;background:rgba(0,0,0,.475);cursor:pointer;color:#fff;border:1px solid rgba(204,204,204,.6274509804);opacity:0;transition:opacity 34ms}.mon__btn-token-cycle--left{left:0;border-top-left-radius:5px;border-bottom-left-radius:5px;border-right:0}.mon__btn-token-cycle--right{right:0;border-top-right-radius:5px;border-bottom-right-radius:5px;border-left:0}.mon__btn-token-cycle:hover{color:#ddd}.mon__wrp-token-footer{display:flex;position:absolute;height:22px;right:5px;bottom:0;left:5px;align-items:center;justify-content:center;opacity:0;transition:opacity 34ms}.mon__token-footer{background:rgba(0,0,0,.475);color:#fff;font-family:"Times New Roman",serif;font-variant:small-caps;font-size:16px;border-radius:5px;padding:1px 5px;border:1px solid rgba(204,204,204,.6274509804)}.night-mode .mon__cr_slider_wrp{border-color:#555}tr th.mon__name--token{padding-right:12rem}#crcalc input[type=number],#crcalc input[type=checkbox],#crcalc .inputwrap{text-align:right;width:6em}#crcalc input[type=checkbox]{width:auto}#crcalc input#hd{float:none;width:4em}#crcalc span#hdval{width:2.1em;text-align:center;display:inline-block}#crcalc input[type=number]#hd::-webkit-inner-spin-button,#crcalc input[type=number]#hd::-webkit-outer-spin-button{margin:0}#crcalc .explanation{font-weight:normal;width:26em}#crcalc input#hd:focus{border:1px solid initial}#msbcr{text-align:center;font-size:.8em;margin:0 auto}#msbcr th{text-align:center;padding:0 .5em;cursor:initial !important}#msbcr tr{cursor:pointer}#croutput{padding:.2em .7em;border-radius:7px}#instructions p{font-size:small}#expectedcr{text-align:center}img.token{position:absolute;z-index:10;top:0;right:.5rem;float:right;width:auto;max-width:11rem;height:auto;transition:opacity 34ms,max-width 34ms,right 34ms}img.token:hover{max-width:100%;right:0;opacity:1 !important;transition:opacity 34ms,max-width 34ms,right 34ms}.classes .stats{font-family:inherit}.stats p.subtrait{padding:0 1em;font-size:.9em}.init__wrp_conds{display:flex}.init__cond{width:7px;margin-right:3px;display:grid;grid-gap:3px;cursor:pointer}.init__cond:hover{box-shadow:0 0 5px 0 gray}.init__cond_bar{width:7px;height:100%}.initp__content{overflow-y:auto;overflow-x:hidden;height:100%;width:100%}.initp__wrp_active{display:flex;flex-direction:column;width:100%;height:100%}.initp__meta{font-size:1.6rem}.initp__header{width:100%;display:flex;justify-content:space-between;padding:0 3px;font-variant:small-caps;border-bottom:1px solid #ccc}.initp__h_name--compact{text-align:center}.initp__r_name{display:flex;justify-content:space-between}.initp__h_hp--compact{text-align:center}.initp__h_stat,.initp__r_stat{width:40px;text-align:center;flex-shrink:0;flex-grow:0}.initp__r_hp_pill{padding:2px 4px;border-radius:3px;color:#fff;text-align:center}.initp__h_score,.initp__r_score{flex:none;width:80px;text-align:center}.initp__h_score--compact,.initp__r_score--compact{width:40px}.initp__r_score{line-height:24px}.initp__r{width:100%;display:flex;justify-content:space-between;padding:2px 3px;border-bottom:1px solid rgba(204,204,204,.6274509804)}.initp__r:last-of-type{border-bottom:0}.initp__r:hover{background:rgba(0,0,0,.062745098)}.initp__r--active{background:rgba(207,229,255,.4705882353)}.initp__r--active:hover{background:rgba(191,213,239,.4705882353)}.night-mode .initp__header{border-color:#555}.night-mode .initp__r{border-color:rgba(85,85,85,.6274509804)}.night-mode .initp__r:hover{background:rgba(255,255,255,.0941176471)}.night-mode .initp__r--active:hover{background:rgba(147,186,232,.2196078431)}.lst__form-top{display:flex;flex-shrink:0}.lst__form-top>*{min-width:0}.lst__form-top>button{flex-shrink:0}.lst__form-top>*:first-child{border-bottom-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:0;border-right:0}.lst__form-top>*:last-child{border-bottom-right-radius:0;border-top-left-radius:0;border-bottom-left-radius:0}.lst__form-top>*:not(:first-child):not(:last-child){border-radius:0;border-right:0}.lst__search{padding-left:23px}.lst__list{margin-bottom:10px}.lst__row:hover{background:#f5f5f5}.lst__row--blocklisted{display:none !important}.lst__row-inner{line-height:14px;color:inherit;display:flex;align-items:center;overflow:hidden;padding:0 2px 1px;text-decoration:none}.lst__row-inner:hover,.lst__row-inner:focus{text-decoration:none}.lst__wrp-preview{font-size:12.6px;background:#fff}.lst__wrp-preview-inner{border-bottom:1px solid #ddd}.lst__vr-preview{top:7px;left:8px;height:calc(100% - 14px)}.lst__btn-toggle-expand{margin-bottom:-1px;line-height:15px}.lst__btn-toggle-expand:hover{background:rgba(0,0,0,.1254901961)}.lst__btn-collapse-all-previews{font-size:11.2px}*:first-child>input.lst__search--no-border-h{border-top-left-radius:4px}.stats-sub-header{font-style:italic;font-weight:bold}.stats-list-sub-header{font-style:italic;font-weight:bold;margin-left:5px}.list-entry-none{font-style:italic}.filter-sublist-item-wrapper{display:flex}.filter-sublist-item-text{margin-right:20px}input[type=checkbox].filter-checkbox{margin-left:auto;padding:0 10px}input[type=checkbox].readonly{pointer-events:none}.lst__wrp-cells{color:inherit;display:flex;align-items:center;overflow:hidden;padding:0 2px 1px;text-decoration:none}.lst__wrp-cells.bk__contents_header_link{padding:0}.lst__wrp-cells.bk__contents_show_all{height:16px}.lst__row--focusable:focus{box-shadow:inset 0 0 0 5000px rgba(0,107,196,.3)}.sublist{display:none;position:relative;padding:0 0 2px;flex-direction:column;flex-shrink:0;height:130px}.sublist .list{margin-bottom:3px;padding-top:3px}.sublist--visible{display:flex}.sublist--resizable{margin-bottom:3px;min-height:75px;max-height:80%}@media(max-width: 991px){.sublist--resizable{max-height:40vh;height:initial}}.sublist__wrp-cols{display:flex}.sublist__wrp-cols>*:last-child{flex-grow:1}.sublist__ele-resize{background-color:rgba(170,170,170,.2666666667);border:1px solid rgba(204,204,204,.4);position:absolute;bottom:0;height:1px;width:100%;cursor:ns-resize;user-select:none;line-height:1px;font-size:10px;text-align:center}.tview__row>td{min-width:100px}tr.trait .rd__b--3,tr.action .rd__b--3,tr.reaction .rd__b--3,tr.legendary .rd__b--3,tr.mythic .rd__b--3,tr.lairaction .rd__b--3,tr.regionaleffect .rd__b--3{margin-bottom:1rem}tr.lairaction p,tr.regionaleffect p{margin-bottom:5px}.stats--book-large{--sz-font-h0: 2.5em;--sz-font-h1: 1.9em;--sz-font-h2: 1.6em;--h-mb-p: 15px;--h-mb-p-inline: var(--h-mb-p);--h-mb-quote-line: 10px;--h-mb-li: 5px;--w-text-indent-inline-p: 0;border-radius:0;line-height:1.7;font-size:1em}.stats--book-large .rd__spc-inline-post{width:100%;height:var(--h-mb-p)}.stats--book-large .rd__spc-inline-post:last-child{height:0}.stats--book-large .rd__list-hang-notitle>.rd__li>.rd__p-list-item{text-indent:-1.1em}.stats--book-large .rd__p-cont-indent{text-indent:0;margin-top:5px}.book-view.view-col{flex:5}.bk-contents__sub_spacer--1{color:gray;display:inline-block;margin:0 4px}.book-contents .contents{height:initial;position:sticky;top:0;max-height:100vh}@media only screen and (min-width: 1600px){#listcontainer.book-contents{position:fixed;top:0;left:0;max-width:calc((100vw - 1170px)/2);margin:0;min-height:100vh}.book-contents .contents{position:relative}}.initial-message{color:#822000;font-family:"Times New Roman",serif;font-variant:small-caps;font-weight:500;text-align:center;line-height:2.3em}.initial-message--large{font-size:4vmin;color:initial}.book-view .initial-message{font-size:1.8em}.stats .initial-message,.bkmv .initial-message{font-size:1.4em}.f-all-wrapper{position:fixed;z-index:100;right:calc(50vw - 585px + 1.5em);bottom:10px;left:calc((100vw - 780px)/2 + 1.5em);padding:0 20px}@media(max-width: 1200px){.f-all-wrapper{right:calc(50vw - 485px + 1.5em);left:calc((100vw - 646.6666666667px)/2 + 1.5em)}}@media(max-width: 991px){.f-all-wrapper{right:calc((100vw - 750px)/2 + 1.5em);left:calc((100vw - 750px)/2 + 1.5em)}}@media only screen and (max-width: 768px){.f-all-wrapper{right:calc((100vw - 750px)/2 + 1.5em);left:calc((100vw - 750px)/2 + 1.5em)}}@media only screen and (max-width: 480px){.f-all-wrapper{right:3.5em;left:3.5em}}.f-all-wrapper>input{width:100%}.f-all-out{box-shadow:0 6px 12px rgba(0,0,0,.175);overflow-y:auto;max-height:400px;width:100%;border:1px solid rgba(0,0,0,.15);padding:.2em .7em;border-radius:.2em;display:none}.f-result{display:flex;justify-content:space-between;margin:0;padding:5px 0}.f-result>span{display:inline-block}.highlight{background-color:#ff0}header p.lead{color:#d3d3d3}.stats{background:#fdf1dc}@media only screen and (min-width: 1600px){#listcontainer.book-contents{box-shadow:0 6px 12px rgba(0,0,0,.175);background:#fff}}.shadow-big{box-shadow:0 6px 12px rgba(0,0,0,.175)}.night-mode .night__shadow-big{box-shadow:0 6px 12px rgba(0,0,0,.175)}.stats--book{box-shadow:0 6px 12px rgba(0,0,0,.175);font-family:Roboto,Helvetica,sans-serif;background:#fff}.stats--book ::selection{background:#242527;color:#fff;text-shadow:none}.bkmv{position:fixed;z-index:100;top:0;right:0;bottom:0;left:0;width:100vw;height:100vh;background:#fff}.bkmv__spacer-name{font-family:"Times New Roman",serif;font-variant:small-caps;text-transform:uppercase;font-weight:bold;height:20px;background:silver;font-size:12px;break-before:auto;break-after:auto;break-inside:avoid}.bkmv__wrp{column-count:6;column-gap:7px;break-inside:avoid-column}@media(max-width: 2160px){.bkmv__wrp{column-count:5}}@media(max-width: 1800px){.bkmv__wrp{column-count:4}}@media(max-width: 1440px){.bkmv__wrp{column-count:3}}@media(max-width: 1080px){.bkmv__wrp{column-count:2}}@media only screen and (max-width: 720px){.bkmv__wrp{column-count:1}}.bkmv__wrp-item{margin:0;width:100%;display:inline-block;border-radius:.2em;border:#ccc 1px solid}.bkmv__no-breaks{break-before:auto;break-after:auto;break-inside:avoid}.night-mode .bkmv__wrp-item{border-color:#555}.mode div.pnl-menu{background:#d3d3d3}.stripe-odd:nth-child(odd),.stripe-even:nth-child(even),.stripe-odd-table>tbody>tr:nth-child(odd),.stripe-even-table>tbody>tr:nth-child(even){background:rgba(192,192,192,.5019607843)}.stats .stripe-odd-table>tbody>tr:nth-child(odd),.stats .stripe-even-table>tbody>tr:nth-child(even){background:rgba(203,191,170,.5019607843)}.hwin .hoverborder,th.border{background:#e69a28}.hwin .hoverborder.hoverborder-book,.stats--book th.border{background:silver}.bklist__wrp-rows-inner{margin-left:6px}.bklist__row-chapter{margin-left:3rem}.bklist__row-section{margin-left:6rem}.bklist__vr-contents{left:6px;border-color:#ddd}.bk__stats--narrow{max-width:640px;margin:0 auto}.bk__contents-header{color:inherit;display:flex;align-items:center;overflow:hidden;margin-top:-1px;padding:1px 0 1px 5px;text-decoration:none;border-bottom:1px solid #ccc;border-top:1px solid #ccc;justify-content:space-between}.bk__nav-head-foot-item{min-width:75px}.bk__to-top{display:none}.bk__to-top--scrolled{display:flex;flex-direction:column;position:fixed;z-index:99;top:42px;right:10px;padding:2px 0}@media(max-width: 768px){.bk__to-top--scrolled{display:none}}.bk__overlay-loading{position:absolute;top:4px;right:0;bottom:4px;left:0;background:#fff;border-bottom:4px solid silver}.bk__wrp-btns-open-find{position:fixed;bottom:0;left:7px}.bk__head-chapter--active,.bk__head-section--active{background:#f5f5f5}.night-mode .bk__contents-header{border-color:#555}.night-mode .bk__overlay-loading{background:#222;border-bottom-color:#565656}.bks__wrp-bookshelf{align-items:stretch}.bks__wrp-bookshelf-item,.bks__wrp-bookshelf-item:hover{box-shadow:0 6px 12px rgba(0,0,0,.175);border:2px solid #ccc;color:#333;text-decoration-color:#333}.bks__wrp-bookshelf-item--blocklisted{display:none !important}.bks__bookshelf-item-name{min-height:40px;max-width:220px;font-weight:bold;flex-grow:1}.bks__bookshelf-image{width:300px;height:300px;object-fit:none}.night-mode .bks__wrp-bookshelf-item,.night-mode .bks__wrp-bookshelf-item:hover{background:#222;border-color:#555;color:#bbb;text-decoration-color:#bbb}.f-all-out{background:#fff}.life__output{background:#d3d3d3}.f-all-out>p:nth-child(odd){background:#f4f4f4}#msbcr tr:nth-child(even){background:#d3d3d3}#croutput{background:#d3d3d3}.hwin .hoverborder .window-title{color:#822000}.rollbox{background:#fff}.rollbox .ipt-roll{background:#fff;border-radius:0}.rollbox-min,.rollbox .head-roll{background:#d3d3d3}.rollbox-min:hover,.rollbox .head-roll:hover{background:#e3e3e3}.rollbox .out-roll .out-roll-item{background:rgba(176,176,176,.2078431373)}.rollbox .out-roll .out-roll-item .out-roll-item-code{font-family:"Courier New",monospace;background:#fff;border-radius:3px;padding:0 2px;cursor:pointer}.life__output-wrp-border{border:1px solid rgba(0,0,0,.15)}.toast{box-shadow:0 6px 12px rgba(0,0,0,.175);z-index:2000;right:0;bottom:200px;left:0;padding:5px 15px;min-height:40px;max-width:850px;display:flex;width:90vw;justify-content:space-between;align-items:center;border:1px solid rgba(0,0,0,0);border-radius:4px;opacity:1;transition:bottom 84ms}.toast--animate{bottom:0;margin-bottom:.5rem;transition:bottom 84ms}.toast--deleted{z-index:1999}.toast__container{position:fixed;z-index:2000;top:0;right:0;left:0;height:200px}.toast__wrp-control{margin:-5px -15px -5px 0;flex:0;display:flex;align-items:center;justify-content:center;align-self:stretch}.toast__btn-close{margin:0;height:100%;border-top-left-radius:0;border-bottom-left-radius:0;border-top:0;border-right:0;border-bottom:0;border-left:1px solid rgba(128,128,128,.2509803922);background:rgba(0,0,0,0)}.toast__btn-close:hover,.toast__btn-close:focus{background:rgba(128,128,128,.1254901961)}.toast--type-info{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.toast--type-danger{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.toast--type-warning{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.toast--type-success{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.night-mode .toast--type-info{color:#fff;background-color:rgba(32,52,74,.95);border-color:#5080b3}.night-mode .toast--type-danger{color:#fff;background-color:rgba(76,16,14,.95);border-color:#ce2a26}.night-mode .toast--type-warning{color:#fff;background-color:rgba(135,88,13,.95);border-color:#ecaa41}.night-mode .toast--type-success{color:#fff;background-color:rgba(0,82,44,.95);border-color:#00eb80}.cards__btn-choose-icon{width:26px;height:26px;padding:0}.cards__disp-btn-icon{width:24px;height:24px;background-repeat:no-repeat;background-size:24px 24px;filter:invert(1)}.cards__disp-typeahead-icon{width:24px;height:24px;background-repeat:no-repeat;background-size:24px 24px;display:inline-block}.cards-cfg__ipt-color{width:40px}.night-mode .cards__disp-btn-icon{filter:initial}.night-mode .cards__disp-typeahead-img{filter:invert(1)}.recipes__wrp-fluff .rd__wrp-image{margin-top:0}.recipes__wrp-fluff .rd__image{max-height:50vh}.form-control--error,.form-control--error[readonly],.form-control--error[disabled]{background-color:rgba(255,0,0,.0941176471) !important;border:1px solid #843534 !important}.form-control--error:focus,.form-control--error[readonly]:focus,.form-control--error[disabled]:focus{border-color:#843534 !important;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px #ce8483 !important}.form-control--warning,.form-control--warning[readonly],.form-control--warning[disabled]{background-color:rgba(255,170,0,.0941176471);border:1px solid #846334}.form-control--warning:focus,.form-control--warning[readonly]:focus,.form-control--warning[disabled]:focus{border-color:#846334;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px #ceaa83}.night-mode body{color:#bbb;background:#222 url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEYAAABGBAMAAACDAP+3AAAAGFBMVEUfHx8eHh4dHR0bGxshISEiIiIlJSUjIyM9IpsJAAAFjUlEQVR4AT3UuZLcOBaF4QuI2XJxboIhF/eQFe1WovoBAAqccpkaZpc5+4yrXa8/RGpx/lrIXPjFCYjTp9z8REqF4VYNWB3Av3zQJ6b6xBwlKB/9kRkCjXVwGH3ziK5UcjFHVkmgY6osiBsGDFfseqq2ZbTz7E00qBDpzOxnD7ToABeros1vM6MX0rBQaG1ith1A/HJkvkHxsPGJ82dP8vVCyWmbyPTaAfGzg40bgIdrv2f3pBVPycUcufx+BSUUWDuCZi6zBqdM50ElKYPODqtLDjc31rBb9CZ59lbN/JScuMxHLUBcGiy6QRH9zpwgZGhRj8qSydPVgNNVgbWqYX3HbM9K2rqTnKVmsmwKWzc1ffEd20+Zq3Ji65kl6TSjALNvzmJt4Pi2f1etytGJmy5erLAgbNY4bjykC3YCLIS3nSZMKgwRsBarWgjdeVzIEDzpTkoOUArTF4WFXYHwxY585sT0nmTYMxmXfs8fzwswfnam8TMU49bvqSRnyRPnqlno4tVQQiH2A9Za8tNTfXQ0lxbSxUaZna0uLlj9Q0XzD96CpsOZUftolINKBWJpAOoAJC0T6QqZnOtfvcfJFcDrD4Cuy5Hng316XrqzJ204HynyHwWed6i+XGF40Uw2T7Lc71HyssngEOrgONfBY7wvW0UZdVAma5xmSNjRp3xkvKJkW6aSg7PK4K0+mbKqYB0WYBgWwxCXiS74zBCVlEFpYQDEwjcA1qccb5yO6ZL8ozt/h3wHSCdWzLuqxU2ZZ9ev9MvRMbMvV9BQgN0qrFjlkzPQanI9nuaGCokVK2LV1Y2egyY1aFQGxjM9I7RBBAgyGEJtpKHP0lUySSeWCpyKHMT2pmM/vyP55u2Rw5lcSeabAfgiG5TPDX3uP3QvcoSipJXQByUCjS4C8VXqxEEZOJxzmJoyogFNJBRsCJs2XmoWWrWFqTsnbwtSn43gNFTTob9/SEpaPJNhUBKDGoZGCMINxvBv8vuKbb//lg/sK0wfPgBica/QsSk5F3KK4Ui6Yw+uv4+DWEOFbhdPOnbY5PLFpzrZMhakeqomY0Vz0TO+elQGTWdCk1IYFAOaoZg0IJQhT+YreXF+yia+O1cgtGufjXxQw28f85RPXfd15zv13ABoD15kB7FKJ/7pbHKP6+9TgNgkVj68NeV8Tp24f7OOndCgJzR3RNJBPNFReCmstMVqvjjzBoeK4GOFoBN32CPxu+4TwwBDa4DJTe/OU9c9ku7EGyfOVxh+fw9g/AATxPqKTEXJKEdCIBkB4iBUlO6MjUrWi6M5Kz31YAqFsYaCeB0KJC5d1+foo3LQWSfRaDrwdAQrMEC27yDZXJf7TlOJ2Bczr1di3OWvZB6XrvvqPuWJPDk9dAHgm7LvuZJTEdKqO3J3XgostArEnvkqgUznx3PX7cSzz1FXZyvakTA4XVVMbCPFPK1cFj66S0WoqQI1XG2uoU7CMPquO2VaUDJFQMdVgXKD2bpz6ufzzxXbxszHQ9fGO/F7A998yBQG6cShE+P+Pk7t1FwfF1QHN1Eui1VapRxCdj8tCtI1bog1Fo011Sx9u3o6c9bufI6wAT26Av9xJ+WWpTKbbBPp3K/1LbC4Vuhv396RCbJw4untjxVPndj+dIB9dVD8z2dylZ+6vMeJwbYChHJkvHV2J3fdHsJPASeHhrXq6QheXu1nBhUr5u6ryT0I13BFKD01ViZ/n3oaziRG7c6Ayg7g1LPeztNdT36ueMqcN4XGv3finjfv+7I/kMJ4d046MUanOA1QtMH1kLlfFasm99NiutSw63yNDeH4zeL1Uu8XKHNfcThPSSNwchGMbgUETScwkCcK77pH2jsgrAssvVyB8FLJ7GrmwyD8eVqsHoY/FwIv9T7lPu9+Yf8/9+w4nS1ma78AAAAASUVORK5CYII=") repeat scroll left top}.night-mode .bg-solid{background:#222}.night-mode .vr-0,.night-mode .vr-1,.night-mode .vr-2,.night-mode .vr-3,.night-mode .vr-4,.night-mode .vr-5{border-color:#555}.night-mode .vr-r{border-right-color:#555 !important}.night-mode .page__header{color:#bbb;background:#333}.night-mode .page__title--home span{color:#909090}.night-mode .page__title--home::after{color:#909090}.night-mode .page__title{color:#d0d0d0}.night-mode .page__nav-inner>li.active>a,.night-mode .page__nav-inner>li.active>a:focus,.night-mode .page__nav-inner>li.active>a:hover{background-color:#333;border-top:0;border-color:#555;border-left-color:rgba(0,0,0,0);border-right-color:rgba(0,0,0,0);color:#d0d0d0}.night-mode .text-muted{color:#959595 !important}.night-mode h1,.night-mode h2,.night-mode h3,.night-mode h4,.night-mode h5,.night-mode h6{color:#bbb}.night-mode .b-1p{border-color:#555 !important}.night-mode .bt-1p{border-top-color:#555 !important}.night-mode .br-1p{border-right-color:#555 !important}.night-mode .bb-1p{border-bottom-color:#555 !important}.night-mode .bb-1p-trans{border-bottom-color:rgba(85,85,85,.6274509804) !important}.night-mode .bl-1p{border-left-color:#555 !important}.night-mode pre{color:#bbb;background:#222;border-color:#555}.night-mode hr{border-color:#555}.night-mode #legal-notice{background:#222;color:#999}.night-mode a,.night-mode .roller{color:#7db6e8}.night-mode .hwin__top-border-icon{color:#bbb}.night-mode .hwin__top-border-icon:hover{color:#c8c8c8}.night-mode .text-muted a,.night-mode .text-muted .roller{color:#6e8eab}.night-mode .btn:hover{box-shadow:0 0 1px 1px #888}.night-mode .btn[disabled]:hover{box-shadow:initial}.night-mode .btn-default,.night-mode .btn-default:hover,.night-mode .btn-default:focus,.night-mode .btn-default:active{background-color:#222;color:#bbb;border-color:#555}.night-mode .btn-primary,.night-mode .btn-primary:hover,.night-mode .btn-primary:focus,.night-mode .btn-primary:active{background-color:#2a4e6c;color:#bbb}.night-mode .btn-danger,.night-mode .btn-danger:hover,.night-mode .btn-danger:focus,.night-mode .btn-danger:active{background-color:#7e3a38;color:#bbb}.night-mode .btn-danger:hover{box-shadow:0 0 1px 1px #d43f3a}.night-mode .btn-warning,.night-mode .btn-warning:hover,.night-mode .btn-warning:focus,.night-mode .btn-warning:active{background-color:#896838;color:#bbb}.night-mode .btn-info,.night-mode .btn-info:hover,.night-mode .btn-info:focus,.night-mode .btn-info:active{background-color:#2a697c;color:#bbb}.night-mode .btn-success,.night-mode .btn-success:hover,.night-mode .btn-success:focus,.night-mode .btn-success:active{background-color:#427442;color:#bbb}.night-mode .btn-default.active{background-color:#888;box-shadow:inset 0 3px 7px rgba(17,17,17,.9333333333);color:#222}.night-mode .btn-primary.active,.night-mode .btn-danger.active,.night-mode .btn-warning.active,.night-mode .btn-info.active,.night-mode .btn-success.active{box-shadow:inset 0 3px 7px rgba(17,17,17,.9333333333)}.night-mode .btn-nowrap{word-wrap:break-word;overflow-wrap:break-word}.night-mode dialog.dialog-modal,.night-mode .dropdown-menu{background:#222;color:#bbb;box-shadow:0 6px 12px rgba(0,0,0,.56)}.night-mode .dropdown-menu>li>a,.night-mode .dropdown-menu>li>span{color:#bbb}.night-mode .dropdown-menu>li>a:focus,.night-mode .dropdown-menu>li>a:hover,.night-mode .dropdown-menu>li>span:focus,.night-mode .dropdown-menu>li>span:hover{background-color:#383838;color:#fff}.night-mode .dropdown-menu>li.ctx-danger>a,.night-mode .dropdown-menu>li.ctx-danger>span{color:#fff;background-color:#7e3a38}.night-mode .dropdown-menu>li.ctx-danger>a:focus,.night-mode .dropdown-menu>li.ctx-danger>a:hover,.night-mode .dropdown-menu>li.ctx-danger>span:focus,.night-mode .dropdown-menu>li.ctx-danger>span:hover{color:#fff;background-color:#ac2925}.night-mode .dropdown-menu>li.active>a,.night-mode .dropdown-menu>li.active>span{color:#fff}.night-mode .dropdown-menu>li.disabled>a,.night-mode .dropdown-menu>li.disabled>span{color:#777}.night-mode .dropdown-menu>li.disabled>a:focus,.night-mode .dropdown-menu>li.disabled>a:hover,.night-mode .dropdown-menu>li.disabled>span:focus,.night-mode .dropdown-menu>li.disabled>span:hover{color:#777;background:rgba(0,0,0,0)}.night-mode .dropdown-menu .divider{background-color:#555}.night-mode select,.night-mode input{background-color:#222;color:#bbb}.night-mode select option{color:#bbb;background:#222}.night-mode .list .row{background:#222}.night-mode .table-striped>tbody>tr:nth-of-type(odd){background-color:#444}.night-mode .alert-info{color:#fff;background-color:rgba(55,90,127,.5);border-color:#5080b3}.night-mode .alert-info .alert-link{color:#5080b3}.night-mode .alert-danger{color:#fff;background-color:rgba(141,29,26,.5);border-color:#ce2a26}.night-mode .alert-danger .alert-link{color:#ce2a26}.night-mode .alert-warning{color:#fff;background-color:rgba(205,133,20,.5);border-color:#ecaa41}.night-mode .alert-warning .alert-link{color:#ecaa41}.night-mode .alert-success{color:#fff;background-color:rgba(0,158,86,.5);border-color:#00eb80}.night-mode .alert-success .alert-link{color:#00eb80}.night-mode .input-group-addon,.night-mode .form-control{background:#222;color:#bbb;border-color:#555}.night-mode .form-control[disabled]{background:rgba(14,14,14,.5333333333)}.night-mode .form-control--error,.night-mode .form-control--error[readonly],.night-mode .form-control--error[disabled]{background-color:#3e0000 !important;border:1px solid #843534 !important}.night-mode .form-control--error:focus,.night-mode .form-control--error[readonly]:focus,.night-mode .form-control--error[disabled]:focus{border-color:#843534 !important;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px #ce8483 !important}.night-mode .form-control--warning,.night-mode .form-control--warning[readonly],.night-mode .form-control--warning[disabled]{background-color:#483700;border-color:#846334}.night-mode .form-control--warning:focus,.night-mode .form-control--warning[readonly]:focus,.night-mode .form-control--warning[disabled]:focus{border-color:#846334;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px #ceaa83}.night-mode .omni__wrp-input--scrolled .omni__input{border-color:#555;background-color:#222;color:rgba(0,0,0,0)}.night-mode .omni__wrp-input--scrolled .omni__input:focus,.night-mode .omni__wrp-input--scrolled .omni__input:focus-within,.night-mode .omni__wrp-input--scrolled .omni__input:active,.night-mode .omni__wrp-input--scrolled .omni__input:hover{color:#bbb}.night-mode .omni__wrp-input--scrolled .omni__submit{border-color:#555}@media(min-width: 992px){.night-mode .omni__input{border-top-color:rgba(0,0,0,0)}}.night-mode .omni__input:focus{border-color:#66afe9}.night-mode .omni__submit{border-top-color:#555}@media(min-width: 992px){.night-mode .omni__submit{border-top-color:rgba(0,0,0,0)}}.night-mode .omni__submit.btn-default:active:focus,.night-mode .omni__submit.btn-default:active:hover,.night-mode .omni__submit.btn-default{background-color:#222;color:#bbb}.night-mode #pointbuy input[type=number]{border:1px solid #555;outline-offset:0;outline:none}.night-mode #pointbuy input.form-control--error[type=number]{border:1px solid red}.night-mode .stats{background:#222}.night-mode .lst__row{background:#222}.night-mode .lst__row:hover{background:#333}.night-mode .lst__row-inner{color:inherit}.night-mode .lst__wrp-preview{background:#222}.night-mode .lst__wrp-preview-inner{border-color:#444}.night-mode .lst__btn-toggle-expand:hover{background:rgba(255,255,255,.1882352941)}@media only screen and (min-width: 1600px){.night-mode #listcontainer.book-contents{background:#222;border-right:1px solid #404040}}.night-mode .bk__head-chapter--active,.night-mode .bk__head-section--active{background:#383838}.night-mode .bklist__wrp-rows-inner{background:#222}.night-mode .bklist__vr-contents{border-color:#444}.night-mode .hwin .hoverborder,.night-mode th.border,.night-mode .stats--book th.border{background:#565656}.night-mode .hwin__wrp-table{background:#222}.night-mode .wrp-stats-table{border-top:1px solid #565656;border-bottom:1px solid #565656}.night-mode .stats--book ::selection{color:#242527;background:#fff}.night-mode tr.text td{color:#bbb !important}.night-mode tr.text td{color:#bbb !important}.night-mode .mon__btn-token-cycle{color:#ddd;background:rgba(255,255,255,.15)}.night-mode .mon__btn-token-cycle:hover{color:#bbb}.night-mode .mon__token-footer{color:#ddd;background:#222}.night-mode tr.trait td,.night-mode tr.action td,.night-mode tr.reaction td,.night-mode tr.legendary td{color:#bbb !important}.night-mode .life__output{background:#222}.night-mode .f-all-wrapper>input,.night-mode .f-all-out,.night-mode .omni__output{background:#303030}.night-mode .f-all-out>p:nth-child(odd){background:#202020}.night-mode .omni__paginate-ctrl:hover{color:#999}.night-mode #msbcr tr:nth-child(even){background:rgba(0,0,0,.31)}.night-mode #croutput{background:rgba(0,0,0,.31)}.night-mode .stats-name{color:#d29a38}.night-mode .stats .divider div{background-color:#d29a38}.night-mode .stripe-odd-table>tbody>tr:nth-child(odd),.night-mode .stripe-even-table>tbody>tr:nth-child(even){background-color:rgba(170,170,170,.1333333333)}.night-mode #please-select-message.showing>td{color:#d29a38}.night-mode #actions td,.night-mode #reactions td,.night-mode #legendaries td,.night-mode #lairactions td,.night-mode #regionaleffects td{border-bottom-color:#d29a38;color:#d29a38}.night-mode .mon__stat-header-underline{border-bottom-color:#d29a38}.night-mode .mon__sect-header-inner{color:#d29a38}.night-mode .bkmv{background:#272727}.night-mode .bkmv__spacer-name{background-color:#565656}.night-mode .lst--border{border-color:#444}.night-mode .list-multi-selected .lst--border{border-color:#416482}.night-mode .list-multi-selected.lst__row--sublist .lst--border{border-color:#555}.night-mode #rulescontent caption{color:#bbb}.night-mode tr.trait td{color:#bbb !important}.night-mode ::-webkit-scrollbar-thumb{background:#475b6b}.night-mode .mon__cr_slider_wrp{background:#222;color:#bbb}.night-mode .hwin table.summary-noback th,.night-mode .hwin table.summary th{color:#bbb}.night-mode .hwin .hoverborder .window-title{color:#bbb}.night-mode .rollbox{background:#272727}.night-mode .rollbox .ipt-roll{background:#272727}.night-mode .rollbox-min,.night-mode .rollbox .head-roll{background:#101010}.night-mode .rollbox-min:hover,.night-mode .rollbox .head-roll:hover{background:#161616}.night-mode .rollbox .out-roll .out-roll-item{background:rgba(80,80,80,.4)}.night-mode .rollbox .out-roll .out-roll-item .out-roll-item-code{background:#555}.night-mode .life__output-wrp-border{border:1px solid rgba(255,255,255,.15)}.night-mode .hwin td div.border{background-color:#d29a38}.night-mode .initial-message{color:#d29a38}.night-mode .panel-content-textarea{background:#222}.night-mode .content-tab-bar{background:#222}.night-mode .highlight{color:#222;background-color:#cc0}.night-mode .sidemenu__row__divider{background:rgba(153,153,153,.5333333333)}.night-mode .sidemenu{background:#222}.night-mode .sidemenu__toggle{background:#444}.night-mode .sidemenu__burger{background:#222}.night-mode .initp__r--active{background:rgba(141,193,255,.1254901961)}.night-mode--alt body{background:#1c1c1c}.cancer__wrp-leaderboard{margin:0 auto;width:100%;display:flex;flex-direction:column;justify-content:center;flex-shrink:0;flex-grow:0;min-height:0;overflow:hidden}.cancer__wrp-leaderboard-inner{display:flex;width:100%;justify-content:center}.cancer__disp-cancer{width:100%;justify-content:center;font-size:9px;padding:2px 0;opacity:.6;display:none}.cancer__wrp-sidebar-rhs{position:fixed;z-index:1;top:160px;right:calc(50vw - 585px - 300px);width:300px;height:100%}.cancer__wrp-sidebar-rhs>*{margin-bottom:10px}.cancer__wrp-sidebar-rhs--single{height:calc(100% - 80px)}.cancer__wrp-sidebar-rhs--scrolling-page{position:absolute;top:150px}@media(max-width: 1800px){.cancer__wrp-sidebar-rhs{display:none}}.cancer__sidebar-rhs-inner{position:sticky}.cancer__sidebar-rhs-inner--top{top:10px}.cancer__sidebar-rhs-inner--bottom{top:620px}.cancer__footer-pad{height:100px}.cancer__wrp-mobile-1{display:flex;flex-direction:column}.cancer__sidebar-rhs-inner--scrolling-page .cancer__sidebar-rhs-inner--top{top:20px}.cancer__sidebar-rhs-inner--scrolling-page .cancer__sidebar-rhs-inner--bottom{top:630px}.night-mode .cancer__wrp-leaderboard{background:#333}.edge__body{overflow:hidden !important}.edge__overlay{background:darkred;position:fixed;z-index:99999;top:0;right:0;bottom:0;left:0;width:100vw;height:100vh;color:#fff;font-family:monospace}.edge__title{font-size:72px}.edge__btn-close{position:absolute;top:8px;right:8px;font-size:16px}.edge__link{color:#fff !important;text-decoration:underline}.TEST_LEADER{background:#f0f;user-select:none;color:#fff;width:728px;height:90px}.TEST_RHS_TOP{background:#f0f;user-select:none;color:#fff;width:300px;height:600px}.TEST_RHS_BOTTOM{background:lime;user-select:none;color:#fff;width:300px;height:250px}@media print{@page{margin:10mm 15mm}body{color:#000 !important;overflow:visible !important;background:none !important;font-size:10px !important}header,nav{display:none !important}strong,.bold{font-weight:600}a[href]::after{content:none !important}.help,.help--hover{text-decoration:none !important}.btn-reroll,.rollbox-min,.rollbox,.spacer-name{display:none !important}a,.roller{color:#000 !important}.stats .stats-source,.stats-source-abbreviation,.stats th{color:#000 !important}th.border,.wrp-stats-table th.border{background:#000 !important;height:1px !important}td.divider div{height:1px !important;background:#000 !important;margin:0 !important}.stats td,.stats th{padding:1px 2px !important}.stats--book-large .rd__b--3,.stats--book-large table,.stats--book-large p{margin:0 0 3px !important}.stats--book{box-shadow:none !important}#listcontainer,#stat-tabs,#float-token,.btn-name-pronounce,.btn-stats-name{display:none !important}.wrp-stats-table{border-top:0 !important;border-bottom:0 !important}#sticky-nav{display:none !important}#classtable table tr:nth-child(odd) td{background:#d3d3d3 !important}.cls-bkmv__wrp-tabs{display:none !important}.mon__btn-scale-cr,.mon__btn-reset-cr{display:none !important}.mon__name--token{padding-right:0 !important}.mon__stat-header-underline{border-bottom:1px solid #000 !important;color:#000 !important}.rd__b-inset{background:none !important;box-shadow:none !important;border-color:#000 !important}.rd__h-toggle{display:none !important}.rd__b-special,.rd__li{break-inside:avoid;page-break-inside:avoid}.bk__to-top,.bk__nav-head-foot-item{display:none !important}.bkmv-active>*:not(.bkmv){display:none !important}.bkmv-active .bkmv{position:relative;top:unset;right:unset;bottom:unset;left:unset;width:calc(100vw - 20px)}.bkmv-active .bkmv__no-breaks{break-before:unset !important;break-after:unset !important;break-inside:unset !important}.bkmv-active .bkmv__wrp--columns-1{column-count:1}.bkmv-active .bkmv__wrp--columns-2{column-count:2}.bkmv-active .bkmv th.border{border:0 !important;padding:0 !important}.bkmv-active .bkmv .pnl-menu{display:none}.stats--bkmv{break-before:auto !important;break-after:auto !important;break-inside:avoid !important}.stats--bkmv tr{break-inside:auto !important}.toast{display:none !important}.cancer__anchor{display:none !important}}@page{html.is-faux-print{margin:10mm 15mm}}html.is-faux-print body{color:#000 !important;overflow:visible !important;background:none !important;font-size:10px !important}html.is-faux-print header,html.is-faux-print nav{display:none !important}html.is-faux-print strong,html.is-faux-print .bold{font-weight:600}html.is-faux-print a[href]::after{content:none !important}html.is-faux-print .help,html.is-faux-print .help--hover{text-decoration:none !important}html.is-faux-print .btn-reroll,html.is-faux-print .rollbox-min,html.is-faux-print .rollbox,html.is-faux-print .spacer-name{display:none !important}html.is-faux-print a,html.is-faux-print .roller{color:#000 !important}html.is-faux-print .stats .stats-source,html.is-faux-print .stats-source-abbreviation,html.is-faux-print .stats th{color:#000 !important}html.is-faux-print th.border,html.is-faux-print .wrp-stats-table th.border{background:#000 !important;height:1px !important}html.is-faux-print td.divider div{height:1px !important;background:#000 !important;margin:0 !important}html.is-faux-print .stats td,html.is-faux-print .stats th{padding:1px 2px !important}html.is-faux-print .stats--book-large .rd__b--3,html.is-faux-print .stats--book-large table,html.is-faux-print .stats--book-large p{margin:0 0 3px !important}html.is-faux-print .stats--book{box-shadow:none !important}html.is-faux-print #listcontainer,html.is-faux-print #stat-tabs,html.is-faux-print #float-token,html.is-faux-print .btn-name-pronounce,html.is-faux-print .btn-stats-name{display:none !important}html.is-faux-print .wrp-stats-table{border-top:0 !important;border-bottom:0 !important}html.is-faux-print #sticky-nav{display:none !important}html.is-faux-print #classtable table tr:nth-child(odd) td{background:#d3d3d3 !important}html.is-faux-print .cls-bkmv__wrp-tabs{display:none !important}html.is-faux-print .mon__btn-scale-cr,html.is-faux-print .mon__btn-reset-cr{display:none !important}html.is-faux-print .mon__name--token{padding-right:0 !important}html.is-faux-print .mon__stat-header-underline{border-bottom:1px solid #000 !important;color:#000 !important}html.is-faux-print .rd__b-inset{background:none !important;box-shadow:none !important;border-color:#000 !important}html.is-faux-print .rd__h-toggle{display:none !important}html.is-faux-print .rd__b-special,html.is-faux-print .rd__li{break-inside:avoid;page-break-inside:avoid}html.is-faux-print .bk__to-top,html.is-faux-print .bk__nav-head-foot-item{display:none !important}html.is-faux-print .bkmv-active>*:not(.bkmv){display:none !important}html.is-faux-print .bkmv-active .bkmv{position:relative;top:unset;right:unset;bottom:unset;left:unset;width:calc(100vw - 20px)}html.is-faux-print .bkmv-active .bkmv__no-breaks{break-before:unset !important;break-after:unset !important;break-inside:unset !important}html.is-faux-print .bkmv-active .bkmv__wrp--columns-1{column-count:1}html.is-faux-print .bkmv-active .bkmv__wrp--columns-2{column-count:2}html.is-faux-print .bkmv-active .bkmv th.border{border:0 !important;padding:0 !important}html.is-faux-print .bkmv-active .bkmv .pnl-menu{display:none}html.is-faux-print .stats--bkmv{break-before:auto !important;break-after:auto !important;break-inside:avoid !important}html.is-faux-print .stats--bkmv tr{break-inside:auto !important}html.is-faux-print .toast{display:none !important}html.is-faux-print .cancer__anchor{display:none !important}@font-face{font-family:"Convergence";font-style:normal;font-weight:400;src:local("Convergence-Regular"),url("../fonts/Convergence-Regular.woff2") format("woff2")}@font-face{font-family:"Roboto";font-style:normal;font-weight:400;src:local("Roboto"),url("../fonts/Roboto-Regular.woff2") format("woff2")}@font-face{font-family:"Glyphicons Halflings";font-style:normal;font-weight:400;src:local("glyphicons-halflings-regular"),url("../fonts/glyphicons-halflings-regular.woff2") format("woff2")}@font-face{font-family:"Blambot Casual";src:local("Blambot-Casual"),url("../fonts/Blambot-Casual-Regular.woff2") format("woff2")}@keyframes kf-fade-out{from{opacity:1}to{opacity:0}}.linked-titles .rd__h--0 .entry-title-inner:hover::before{font-size:50%}.linked-titles .rd__h--1 .entry-title-inner:hover::before{font-size:55%}.linked-titles .rd__h--2 .entry-title-inner:hover::before{font-size:60%}.linked-titles .rd__h .entry-title-inner{cursor:copy}.linked-titles .rd__h .entry-title-inner:hover::before{content:" ๐Ÿ”—";color:rgba(0,0,0,.2);position:relative;float:left;width:14px;height:14px;right:20px;margin-right:-30px;font-size:85%}@media(max-width: 991px){.dropdown-menu-filter{max-height:525px}}.fltr__btn-close{min-width:100px}.fltr__minimal-hide{display:none}.fltr__no-items{display:none !important}.fltr__h{display:flex;justify-content:space-between;font-size:15px;align-items:center}@media only screen and (max-width: 768px){.fltr__h{flex-direction:column}.fltr__h--multi{flex-direction:initial}}@media only screen and (max-width: 768px){.fltr__h-text{align-self:flex-start}}@media only screen and (max-width: 768px){.fltr__h-wrp-btns-outer{width:100%;flex-direction:column;align-items:initial !important}.fltr__h-wrp-btns-outer>*{width:100%;margin:.25rem !important}}@media only screen and (max-width: 768px){.fltr__h-wrp-state-btns-outer{flex-direction:column}.fltr__h-wrp-state-btns-outer>*{width:100%}}.fltr__h-btn-mobile-settings{min-width:30px}.fltr__h-btn-logic{min-width:46px;font-weight:bold}.fltr__h-btn-logic.btn-xxs{min-width:34px}.fltr__h-btn-logic--blue{color:#337ab7}.fltr__h-btn-logic--blue:hover{color:#2a6496}.fltr__h-btn-logic--red{color:#8a1a1b}.fltr__h-btn-logic--red:hover{color:#711617}.fltr__h-btn--all,.fltr__h-btn--all:focus,.fltr__h-btn--all:hover{text-decoration:underline;text-decoration-color:#337ab7}.fltr__h-btn--clear,.fltr__h-btn--clear:focus,.fltr__h-btn--clear:hover{text-decoration:underline;text-decoration-color:#c3c3c3}.fltr__h-btn--none,.fltr__h-btn--none:focus,.fltr__h-btn--none:hover{text-decoration:underline;text-decoration-color:#8a1a1b}.fltr__summary_item{cursor:help;margin:0 3px;font-weight:bold;font-size:12px;line-height:12px}.fltr__summary_nest{display:flex;padding:2px 0;font-size:12px;align-items:center}.fltr__summary_item--include{color:#337ab7;text-shadow:0 0 1px #337ab7}.fltr__summary_item--exclude{color:#8a1a1b;text-shadow:0 0 1px #8a1a1b}.fltr__summary_item_spacer{margin:0 3px;padding-left:1px;cursor:default;background:rgba(204,204,204,.6274509804);min-height:12px}.fltr__btn_nest{margin:2px;padding:2px 6px;white-space:nowrap;text-align:center;font-size:10.5px;cursor:pointer;user-select:none;background:#f0f0f0;border:1px solid #ccc}.fltr__btn_nest:hover{background-color:#e6e6e6}.fltr__btn_nest--include{background:repeating-linear-gradient(135deg, #337ab7, #337ab7 11px, transparent 11px, transparent 22px)}.fltr__btn_nest--include:hover{background:repeating-linear-gradient(135deg, #2d6da3, #2d6da3 11px, transparent 11px, transparent 22px)}.fltr__btn_nest--include span{background:#fff;padding:1px 0}.fltr__btn_nest--include-all{background:#337ab7;color:#fff}.fltr__btn_nest--include-all:hover{background:#2d6da3}.fltr__btn_nest--exclude{background:repeating-linear-gradient(135deg, transparent, transparent 11px, #8a1a1b 11px, #8a1a1b 22px)}.fltr__btn_nest--exclude:hover{background:repeating-linear-gradient(135deg, transparent, transparent 11px, #751617 11px, #751617 22px)}.fltr__btn_nest--exclude span{background:#fff;padding:1px 0}.fltr__btn_nest--exclude-all{background:#8a1a1b;color:#fff}.fltr__btn_nest--exclude-all:hover{background:#751617}.fltr__btn_nest--both{background:repeating-linear-gradient(135deg, #337ab7, #337ab7 11px, #8a1a1b 11px, #8a1a1b 22px);color:#fff}.fltr__btn_nest--both:hover{background:repeating-linear-gradient(135deg, #2d6da3, #2d6da3 11px, #751617 11px, #751617 22px)}.fltr__container-pills{margin-right:-2px;margin-left:-2px}.fltr__dropdown-divider{border-bottom:#ccc 1px dotted;width:100%}@media only screen and (max-width: 768px){.fltr__dropdown-divider{box-shadow:inset 0 0 2px 2px #eee;height:7px;flex-shrink:0;border:0;background:#ccc;margin-top:.5rem;margin-bottom:.75rem !important}}.fltr__dropdown-divider--indented{opacity:.4;width:calc(100% - 80px);margin:0 auto}.fltr__dropdown-divider--sub{border-style:dashed;width:calc(100% - 2rem);border-color:rgba(204,204,204,.6274509804)}.fltr__pill{margin:2px;padding:2px 6px;background:#f0f0f0;white-space:nowrap;text-align:center;font-size:10.5px;cursor:pointer;user-select:none;border:1px solid #ccc;float:left}.fltr__pill:hover{background-color:#e6e6e6}.fltr__pill[state=yes]{background:#337ab7;color:#fff;border-color:#22527b}.fltr__pill[state=yes]:hover{background:#2d6da3}.fltr__pill[state=no]{background:#8a1a1b;color:#fff;border-color:#4a0e0e}.fltr__pill[state=no]:hover{background:#751617}.fltr__pill--ability-bonus{min-width:26px;border-right-width:0;margin:0;flex:1}.fltr__pill--ability-bonus:last-of-type{border-right-width:1px}.fltr__pill--muted{background-color:#dedede;color:#898989}.fltr__pill--muted[state=yes],.fltr__pill--muted[state=no]{color:#fff}.fltr__wrp-pills,.fltr__wrp-pills--sub{flex-wrap:wrap;margin-bottom:7px}.fltr__wrp-pills{display:block}.fltr__wrp-pills::after{content:"";clear:both;display:block}.fltr__wrp-pills--flex,.fltr__wrp-pills--sub{display:flex}.fltr__wrp-subs{display:block}.fltr__mini-view{border-left:#ccc 1px solid;border-right:#ccc 1px solid;background:linear-gradient(to top, #ccc, whitesmoke 1px);display:flex;flex-wrap:wrap;flex-shrink:0}.fltr__mini-view--no-sort-buttons{border-bottom:1px solid #ccc;background:#f5f5f5;border-bottom-left-radius:3px;border-bottom-right-radius:3px;min-height:3px}.fltr__mini-pill{margin:1px 2px;padding:1px 2px;white-space:nowrap;text-align:center;font-size:9.4px;border-radius:3px;cursor:pointer;user-select:none;display:none}.fltr__mini-pill:hover{text-decoration:red line-through}.fltr__mini-pill[state=yes]{background:#337ab7;color:#fff;display:block}.fltr__mini-pill--default-sel[state=yes]{background:#48637a}.fltr__mini-pill[state=no]{background:#822000;color:#fff;display:block}.fltr__mini-pill--default-desel[state=no]{background:#7a564f}.fltr__h-summary{position:relative;display:inline-block;vertical-align:middle;box-sizing:border-box;font-size:11px;line-height:22px;margin-left:auto}.fltr__h-summary-filtering{color:#333;text-shadow:0 0 1px #333}.fltr__h-btn-toggle-display{min-width:43px}.fltr__slider{width:100%}.fltr__range-inline-label{margin-left:15px;flex-shrink:0;min-width:75px;text-align:right;font-style:italic}.fltr__group-comb-toggle{font-style:italic;cursor:pointer;letter-spacing:-1px;user-select:none}.fltr__label-ability-score{width:80px}.fltr__hidden--inactive{display:none !important}.fltr__hidden--search{display:none !important}.fltr-search__wrp-search:focus .fltr-search__wrp-values,.fltr-search__wrp-search:focus-within .fltr-search__wrp-values,.fltr-search__wrp-search:focus-visible .fltr-search__wrp-values,.fltr-search__wrp-search:active .fltr-search__wrp-values{display:flex}.fltr-search__wrp-row:focus,.fltr-search__wrp-row:hover{background-color:#f5f5f5}.fltr-search__wrp-values{max-height:200px;background:#fff;border:1px solid #ccc;z-index:1;top:22px;right:0;left:0;display:none;flex-direction:column}.fltr-search__disp-name{font-size:10.5px}.fltr-search__btn-activate{width:16px;height:16px;border-radius:3px}.fltr-search__btn-activate--yes{background:#337ab7;color:#fff;border:1px solid #63a0d4}.fltr-search__btn-activate--yes:hover{background:#2d6da3}.fltr-search__btn-activate--no{background:#8a1a1b;color:#fff;border:1px solid #ca2628}.fltr-search__btn-activate--no:hover{background:#751617}.fltr-src__spc-pill{color:#777}.fltr-src__wrp-slider{background:#f0f0f0;border-radius:4px}.fltr-cls__tgl{width:16px;height:16px;padding:0;flex-shrink:0;flex-grow:0;display:inline-block;cursor:pointer;border:1px solid #ccc;border-radius:4px;outline:none;user-select:none;border-radius:7px}.fltr-cls__tgl:active{box-shadow:0 0 2px 0 rgba(0,0,0,.7333333333)}.fltr-cls__tgl.active{background:#666;border-color:#8c8c8c}.fltr-cls__tgl.active.disabled{background-color:#a6a6a6}.fltr-cls__tgl.disabled{cursor:default;box-shadow:none}.fltr__pill[state=yes]>.fltr-src__spc-pill{color:rgba(255,255,255,.6)}.fltr__pill[state=no]>.fltr-src__spc-pill{color:rgba(255,255,255,.6666666667)}@font-face{font-family:"Convergence";font-style:normal;font-weight:400;src:local("Convergence-Regular"),url("../fonts/Convergence-Regular.woff2") format("woff2")}@font-face{font-family:"Roboto";font-style:normal;font-weight:400;src:local("Roboto"),url("../fonts/Roboto-Regular.woff2") format("woff2")}@font-face{font-family:"Glyphicons Halflings";font-style:normal;font-weight:400;src:local("glyphicons-halflings-regular"),url("../fonts/glyphicons-halflings-regular.woff2") format("woff2")}@font-face{font-family:"Blambot Casual";src:local("Blambot-Casual"),url("../fonts/Blambot-Casual-Regular.woff2") format("woff2")}@keyframes kf-fade-out{from{opacity:1}to{opacity:0}}.linked-titles .rd__h--0 .entry-title-inner:hover::before{font-size:50%}.linked-titles .rd__h--1 .entry-title-inner:hover::before{font-size:55%}.linked-titles .rd__h--2 .entry-title-inner:hover::before{font-size:60%}.linked-titles .rd__h .entry-title-inner{cursor:copy}.linked-titles .rd__h .entry-title-inner:hover::before{content:" ๐Ÿ”—";color:rgba(0,0,0,.2);position:relative;float:left;width:14px;height:14px;right:20px;margin-right:-30px;font-size:85%}.night-mode .fltr__btn_nest{background:#222;border-color:#555}.night-mode .fltr__btn_nest:hover{background:#323232}.night-mode .fltr__btn_nest--include{background:repeating-linear-gradient(135deg, #337ab7, #337ab7 11px, transparent 11px, transparent 22px)}.night-mode .fltr__btn_nest--include:hover{background:repeating-linear-gradient(135deg, #2d6da3, #2d6da3 11px, transparent 11px, transparent 22px)}.night-mode .fltr__btn_nest--include span{background:#222}.night-mode .fltr__btn_nest--include-all{background:#337ab7}.night-mode .fltr__btn_nest--include-all:hover{background:#2d6da3}.night-mode .fltr__btn_nest--exclude{background:repeating-linear-gradient(135deg, transparent, transparent 11px, #8a1a1b 11px, #8a1a1b 22px)}.night-mode .fltr__btn_nest--exclude:hover{background:repeating-linear-gradient(135deg, transparent, transparent 11px, #751617 11px, #751617 22px)}.night-mode .fltr__btn_nest--exclude span{background:#222}.night-mode .fltr__btn_nest--exclude-all{background:#8a1a1b}.night-mode .fltr__btn_nest--exclude-all:hover{background:#751617}.night-mode .fltr__btn_nest--both{background:repeating-linear-gradient(135deg, #337ab7, #337ab7 11px, #8a1a1b 11px, #8a1a1b 22px)}.night-mode .fltr__btn_nest--both:hover{background:repeating-linear-gradient(135deg, #2d6da3, #2d6da3 11px, #751617 11px, #751617 22px)}.night-mode .fltr__dropdown-divider{border-color:#555}@media only screen and (max-width: 768px){.night-mode .fltr__dropdown-divider{box-shadow:inset 0 0 2px 2px #333;background:#555}}.night-mode .fltr__dropdown-divider--sub{border-color:rgba(85,85,85,.6274509804)}.night-mode .fltr__pill{border-color:#555}.night-mode .fltr__pill[state=ignore]{background:#222}.night-mode .fltr__pill[state=ignore]:hover{background:#323232}.night-mode .fltr__pill[state=yes]{border-color:#22527b}.night-mode .fltr__pill[state=no]{border-color:#4a0e0e}.night-mode .fltr__pill--muted{color:#656565}.night-mode .fltr__pill--muted[state=yes],.night-mode .fltr__pill--muted[state=no]{color:#fff}.night-mode .fltr__mini-view{background:#343434;border-color:#555;background:linear-gradient(to top, #555, #343434 1px)}.night-mode .fltr__h-btn-logic--blue{color:#337ab7}.night-mode .fltr__h-btn-logic--blue:hover{color:#7398b7}.night-mode .fltr__h-btn-logic--red{color:#8a1a1b}.night-mode .fltr__h-btn-logic--red:hover{color:#8a4b4b}.night-mode .fltr-search__wrp-row:focus,.night-mode .fltr-search__wrp-row:hover{background-color:#272727}.night-mode .fltr-search__wrp-values{background-color:#222;border-color:#555}.night-mode .fltr-src__wrp-slider{background:rgba(51,51,51,.6666666667)}.night-mode .fltr-cls__tgl{background:#222;border-color:#555}.night-mode .fltr-cls__tgl:active{box-shadow:0 0 3px 0 rgba(255,255,255,.7333333333)}.night-mode .fltr-cls__tgl.active{background:#555;border-color:#6f6f6f}.night-mode .fltr-cls__tgl.active.disabled{background-color:#888}.night-mode .fltr-cls__tgl.disabled{box-shadow:none}@font-face{font-family:"Convergence";font-style:normal;font-weight:400;src:local("Convergence-Regular"),url("../fonts/Convergence-Regular.woff2") format("woff2")}@font-face{font-family:"Roboto";font-style:normal;font-weight:400;src:local("Roboto"),url("../fonts/Roboto-Regular.woff2") format("woff2")}@font-face{font-family:"Glyphicons Halflings";font-style:normal;font-weight:400;src:local("glyphicons-halflings-regular"),url("../fonts/glyphicons-halflings-regular.woff2") format("woff2")}@font-face{font-family:"Blambot Casual";src:local("Blambot-Casual"),url("../fonts/Blambot-Casual-Regular.woff2") format("woff2")}@keyframes kf-fade-out{from{opacity:1}to{opacity:0}}.linked-titles .rd__h--0 .entry-title-inner:hover::before{font-size:50%}.linked-titles .rd__h--1 .entry-title-inner:hover::before{font-size:55%}.linked-titles .rd__h--2 .entry-title-inner:hover::before{font-size:60%}.linked-titles .rd__h .entry-title-inner{cursor:copy}.linked-titles .rd__h .entry-title-inner:hover::before{content:" ๐Ÿ”—";color:rgba(0,0,0,.2);position:relative;float:left;width:14px;height:14px;right:20px;margin-right:-30px;font-size:85%}.ui__btn-xxl-square{width:110px;height:110px}.ui__ipt-color{width:40px;padding:0}.ui__ipt-color::-webkit-color-swatch-wrapper{padding:3px}.ui__ipt-color::-webkit-color-swatch{border:1px solid #ccc}.ui-list__wrp{transform:translateZ(0);font-size:11.2px}.ui-list__wrp-preview{background:#fdf1dc;font-size:90%;border-top-left-radius:5px;margin-top:1px;margin-bottom:4px}.ui-list__btn-inline{cursor:pointer;color:#777}.ui-list__btn-inline:hover{background:rgba(0,0,0,.175);color:#373737}.ui-source__row{margin-left:calc(-96px - .5rem)}.ui-source__name{min-width:96px;white-space:nowrap;text-align:right}.ui-source__divider{height:1px;width:30px;background:#ccc;display:inline-block;margin:0 3px}.ui-modal__body-active{overflow-y:hidden !important}.ui-modal__row{margin-bottom:5px;display:flex;justify-content:space-between;align-items:center;font-weight:initial;min-height:30px}.ui-modal__row:first-of-type{margin-top:-1px}.ui-modal__row--cb{padding:0 3px;border-radius:3px}.ui-modal__row--cb:hover{background:#f5f5f5}.ui-modal__row--sel{padding:0 3px}.ui-modal__row>*{margin-right:5px}.ui-modal__row>*:last-child{margin-right:0}.ui-modal__header--border{border-bottom:1px solid rgba(204,204,204,.6274509804)}.ui-modal__header--fullscreen{box-shadow:0 3px 6px rgba(0,0,0,.175)}.ui-modal__footer{border-top:1px solid rgba(204,204,204,.6274509804)}.ui-modal__footer--fullscreen{box-shadow:0 3px 6px rgba(0,0,0,.175)}.ui-modal__overlay{position:fixed;z-index:1000;top:0;left:0;width:100vw;height:100vh;display:flex;justify-content:center;align-items:center;background-color:rgba(69,69,69,.5333333333)}.ui-modal__overlay-blind{background-color:#fff}.ui-modal__inner{position:relative;z-index:1001;top:initial;left:initial;margin:60px auto;padding:5px 10px;height:400px;float:none;min-width:600px;max-height:400px;min-height:400px;font-size:14px;background:#fff;border:1px solid #ccc;border-radius:4px;box-shadow:0 6px 12px rgba(0,0,0,.175)}@media(max-width: 767px){.ui-modal__inner{min-width:0}}@media(min-width: 768px){.ui-modal__inner{max-width:750px}}@media(min-width: 992px){.ui-modal__inner{max-width:970px}}@media(min-width: 1200px){.ui-modal__inner{max-width:1170px}}.ui-modal__inner--no-min-height{min-height:0;height:initial}.ui-modal__inner--no-min-height{min-width:0;width:initial}.ui-modal__inner--uncap-height{max-height:calc(100% - 120px);height:initial}.ui-modal__inner--uncap-width{max-width:calc(100% - 180px);width:initial}.ui-modal__inner--max-width-640p{max-width:640px}.ui-modal__inner--mode-fullscreen{max-height:0;height:100vh;flex-shrink:0;min-height:100vh;border-radius:0;box-shadow:none;border:0}.ui-modal__scroller{height:100%;width:100%;min-height:0;overflow-y:auto}.ui-search__wrp-output{position:relative;height:100%;width:100%;display:flex;flex-direction:column}.ui-search__wrp-controls{width:100%;display:flex;z-index:900}.ui-search__wrp-controls--in-tabs{margin-top:-1px}.ui-search__wrp-results{position:relative;padding:3px;transform:translateZ(0);height:100%;overflow-y:auto;overflow-x:hidden;font-size:11.2px}.ui-search__row{cursor:pointer;font-weight:bold;padding:1px 2px;display:flex;justify-content:space-between;border-bottom:1px solid #ccc}.ui-search__row:hover{background:#d3d3d3}.ui-search__row:focus{box-shadow:inset 0 0 0 5000px rgba(0,107,196,.3)}.ui-search__sel-category{border-radius:0;max-width:180px;flex-shrink:0;border-right:0}.ui-search__ipt-search{border-radius:0;width:100%}.ui-search__ipt-search-sub-ipt[type=radio]{display:inline-block;margin:0 3px 0 0}.ui-search__ipt-search-sub-ipt-custom{max-width:30px;border-radius:0;border-left:0;margin-right:-1px;border-right-color:#e0e0e0;border-left-color:#e0e0e0;padding-left:0}.ui-search__ipt-search-sub-ipt-custom[type=number]::-webkit-inner-spin-button{margin:0;-webkit-appearance:none}.ui-search__ipt-search-sub-wrp{flex-shrink:0;margin-bottom:0;padding:5px;font-weight:normal;border:1px solid #ccc;height:34px;border-left:0}.ui-search__ipt-search-sub-lbl{display:flex;align-items:center;height:100%}.ui-search__ipt-search-sub-lbl:not(:last-child){margin-right:7px}.ui-search__message{font-size:1.4rem;width:100%;height:100%;display:flex;justify-content:center;align-items:center;font-family:"Times New Roman",serif;font-variant:small-caps;font-weight:500}.ui-tab__btn-tab-head{display:inline-block;padding:2px 4px 0;border-bottom-right-radius:0;border-bottom-left-radius:0;cursor:pointer;user-select:none;border-bottom:0}.ui-tab__btn-tab-head.active{background-color:#e6e6e6;border-color:#adadad}.ui-tab__wrp-tab-body{width:100%;height:100%;overflow-y:auto;overflow-x:hidden}.ui-tab__wrp-tab-body--border{padding:3px 0}.ui-tab__wrp-tab-body--background{background:#fff;border:1px solid rgba(204,204,204,.6274509804);border-top:0;border-bottom-color:rgba(204,204,204,.4)}.ui-tab__wrp-tab-heads--border{border-bottom:1px solid #ccc}.ui-tab-side__disp-active-tab-name{margin-left:126px;font-size:28px}@media only screen and (max-width: 1200px){.ui-tab-side__disp-active-tab-name{margin-left:39px}}.ui-tab-side__btn-tab{width:120px}@media only screen and (max-width: 1200px){.ui-tab-side__btn-tab{width:33px;height:30px}}.ui-tab-side__icon-tab{min-width:15px;min-height:12px}.ui-tab-side__wrp-tab{background:#fff;border:1px solid rgba(204,204,204,.6274509804);border-bottom:0}.ui-tab-side__wrp-tab--single{border:0}.ui-prof__btn-cycle{width:16px;height:16px;padding:0;flex-shrink:0;flex-grow:0;display:inline-block;cursor:pointer;border:1px solid #ccc;border-radius:4px;outline:none;user-select:none}.ui-prof__btn-cycle:active{box-shadow:0 0 2px 0 rgba(0,0,0,.7333333333)}.ui-prof__btn-cycle.active{background:#666;border-color:#8c8c8c}.ui-prof__btn-cycle.active.disabled{background-color:#a6a6a6}.ui-prof__btn-cycle.disabled{cursor:default;box-shadow:none}.ui-prof__btn-cycle[data-state="0"]{background:#fff}.ui-prof__btn-cycle[data-state="1"]{background:#666;border-color:#8c8c8c}.ui-prof__btn-cycle[data-state="2"]{background:#666;border-color:#8c8c8c;display:flex;line-height:14px;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-style:normal;font-variant:normal;text-rendering:auto;font-family:"Font Awesome 5 Pro";font-weight:900;color:#fff;font-size:12px}.ui-prof__btn-cycle[data-state="2"]::before{content:"๏€…"}.ui-prof__btn-cycle[data-state="3"]{background:repeating-linear-gradient(135deg, white, white 10px, #666 10px, #666 20px);border-color:#8c8c8c}.ui-dir__face{position:relative;width:92px;height:92px;border-radius:46px;background:#f0f0f0;border:1px solid #ccc;user-select:none;cursor:grab}.ui-dir__arm{width:1px;height:40px;background:#333;position:absolute;top:46px;left:46px;transform:rotate(180deg);transform-origin:top;pointer-events:none;user-select:none;box-shadow:0 0 2px 0 rgba(0,0,0,.75)}.ui-icn__wrp-icon{font-size:24px}.ui-drag__wrp-drag-block{position:absolute;top:0;right:0;bottom:0;left:0}.ui-drag__wrp-drag-dummy--highlight{background:rgba(207,229,255,.4705882353)}.ui-drag__wrp-drag-dummy--lowlight{background:rgba(0,0,0,0)}.ui-drag__patch{cursor:move;user-select:none;display:flex;flex-shrink:0;padding:5px 3px;width:14px;font-size:14px}.ui-drag__dummy-patch{width:14px}.ui-drag__patch-col{display:flex;flex-direction:column;flex-shrink:0}.ui-drag__patch-col>div{line-height:4px;text-align:center}.ui-tip__parent{cursor:help;position:relative}.ui-tip__child{box-shadow:0 6px 12px rgba(0,0,0,.175);display:none;position:absolute;border:1px solid #ccc;background:#fff;border-radius:3px;z-index:1;top:calc(100% + 5px);padding:5px;opacity:0;transition:opacity 84ms ease-in-out;pointer-events:none}.ui-tip__parent:hover .ui-tip__child{display:flex;opacity:1}.ui-ctx__wrp{box-shadow:0 6px 12px rgba(0,0,0,.475);z-index:1100;font-size:14px;background:#fff;border:1px solid rgba(204,204,204,.6274509804);border-top-color:#ccc}.ui-ctx__divider{height:1px;width:100%;background:#ccc}.ui-ctx__row{min-width:160px}.ui-ctx__btn{cursor:pointer}.ui-ctx__btn:hover{background:#f5f5f5}.ui-ctx__btn.disabled,.ui-ctx__btn.disabled:hover{cursor:default;background:#fff}.ui-pick__btn-add{font-weight:bold;padding:1px 2px;line-height:8px;font-size:18px;display:flex;height:16px}.ui-pick__btn-add--sub{line-height:11px;height:14px;font-size:16px;border-radius:0;padding:0 1px;font-weight:bold}.ui-pick__btn-remove{width:10px;line-height:20px;padding:0;border-radius:0;font-size:12px;flex-shrink:0;flex-grow:0;cursor:pointer;font-style:initial}.ui-pick__btn-remove--sub{height:18px;line-height:16px}.ui-pick__pill{align-items:stretch}.ui-pick__disp-text{border:1px solid #ccc;border-right:0}.fa--btn-sm{position:relative;top:1px;font-size:15px}.fa--btn-xs{position:relative;font-size:12px}.fa--btn-xs::before{width:12px;height:14px;display:inline-block;text-align:center}.fa--btn-xs.fa-dice{left:-2px}.clp__wrp-temp{position:fixed;top:-10000px;left:-10000px;width:1px;height:1px}.clp__disp-copied{position:fixed;white-space:nowrap;width:auto;transform:translateX(-50%);pointer-events:none;user-select:none;height:24px;font-size:12px;z-index:2000;background:radial-gradient(ellipse at center, white 0%, white 35%, transparent 75%, transparent 100%)}.ui-ideco__ipt--left{padding-left:22px !important}.ui-ideco__ipt--right{padding-right:22px !important}.ui-ideco__wrp{position:absolute;top:0;bottom:0;opacity:.5;justify-content:center}.ui-ideco__wrp>.glyphicon{top:0}.ui-ideco__wrp--left{left:5px}.ui-ideco__wrp--right{right:5px}.ui-ideco__btn-ticker{transition:opacity 34ms;opacity:0;padding:0;width:14px;height:10px;border:0;font-size:14px;line-height:10px;border-radius:0;background:rgba(0,0,0,.2);color:#333}.ui-ideco__btn-ticker:hover,.ui-ideco__btn-ticker:active,.ui-ideco__btn-ticker:focus,.ui-ideco__btn-ticker:active:focus{box-shadow:none;outline:none}.ui-ideco__btn-ticker:hover{background:rgba(0,0,0,.3333333333);color:#333}.ui-ideco__btn-ticker:active,.ui-ideco__btn-ticker:focus,.ui-ideco__btn-ticker:active:focus{background:rgba(0,0,0,.4666666667);color:#333}.ui-ideco__ipt:hover+.ui-ideco__wrp .ui-ideco__btn-ticker,.ui-ideco__wrp:hover .ui-ideco__btn-ticker{transition:opacity 34ms;opacity:1}.ui-sel2__ipt-search{top:0;right:0;left:0;opacity:0;background:rgba(0,0,0,0)}.ui-sel2__ipt-display{padding-right:20px}.ui-sel2__wrp:focus>.ui-sel2__ipt-search,.ui-sel2__wrp:focus-within>.ui-sel2__ipt-search{opacity:1}.ui-sel2__wrp:focus>.ui-sel2__ipt-display,.ui-sel2__wrp:focus-within>.ui-sel2__ipt-display{text-align:right;color:#777;font-weight:bold}.ui-sel2__wrp:focus>.ui-sel2__wrp-options,.ui-sel2__wrp:focus-within>.ui-sel2__wrp-options{display:flex}.ui-sel2__wrp-options{z-index:1;top:22px;right:0;left:0;display:none;flex-direction:column;background:#fff;border:1px solid #ccc;border-top:0;max-height:200px}.ui-sel2__wrp-options:hover,.ui-sel2__wrp-options:active,.ui-sel2__wrp-options:focus,.ui-sel2__wrp-options:focus-within{display:flex}.ui-sel2__disp-option.active,.ui-sel2__disp-option:focus,.ui-sel2__disp-option:hover{background:#f5f5f5}.ui-sel2__disp-option:focus.active,.ui-sel2__disp-option:hover.active{background:#dcdcdc}.ui-sel2__disp-arrow{top:4px;right:4px;bottom:0;font-size:12px}.ui-slidr__wrp{font-size:14px}.ui-slidr__thumb{width:14px;height:18px;top:-5px;background:#f5f5f5;border:1px solid #ccc;border-radius:2px}.ui-slidr__thumb--hover,.ui-slidr__thumb:hover{background:#dcdcdc;border-color:#b3b3b3}.ui-slidr__wrp-track{padding-top:6px;padding-bottom:7px}.ui-slidr__track-outer{border:1px solid #ccc;height:10px;border-radius:3px}.ui-slidr__track-inner{background:#eee}.ui-slidr__disp-value{width:80px;height:26px;border-radius:4px}.ui-slidr__disp-value--visible{border:1px solid #ccc;background:#fff}.ui-slidr__disp-value--left{margin-right:15px}.ui-slidr__disp-value--right{margin-left:15px}.ui-slidr__wrp-bottom{height:3em}.ui-slidr__wrp-pips{padding-top:6px}.ui-slidr__pip{width:1px;height:4px;background:#ccc}.ui-slidr__pip--major{height:6px;background:#a6a6a6}.ui-slidr__pip-label{top:0;width:24px;height:20px;padding-top:20px}@font-face{font-family:"Convergence";font-style:normal;font-weight:400;src:local("Convergence-Regular"),url("../fonts/Convergence-Regular.woff2") format("woff2")}@font-face{font-family:"Roboto";font-style:normal;font-weight:400;src:local("Roboto"),url("../fonts/Roboto-Regular.woff2") format("woff2")}@font-face{font-family:"Glyphicons Halflings";font-style:normal;font-weight:400;src:local("glyphicons-halflings-regular"),url("../fonts/glyphicons-halflings-regular.woff2") format("woff2")}@font-face{font-family:"Blambot Casual";src:local("Blambot-Casual"),url("../fonts/Blambot-Casual-Regular.woff2") format("woff2")}@keyframes kf-fade-out{from{opacity:1}to{opacity:0}}.linked-titles .rd__h--0 .entry-title-inner:hover::before{font-size:50%}.linked-titles .rd__h--1 .entry-title-inner:hover::before{font-size:55%}.linked-titles .rd__h--2 .entry-title-inner:hover::before{font-size:60%}.linked-titles .rd__h .entry-title-inner{cursor:copy}.linked-titles .rd__h .entry-title-inner:hover::before{content:" ๐Ÿ”—";color:rgba(0,0,0,.2);position:relative;float:left;width:14px;height:14px;right:20px;margin-right:-30px;font-size:85%}.night-mode .ui__ipt-color::-webkit-color-swatch{border:0}.night-mode .ui-list__wrp-preview{background:#222}.night-mode .ui-list__btn-inline{color:#bbb}.night-mode .ui-list__btn-inline:hover{color:#d5d5d5;background:rgba(255,255,255,.1882352941)}.night-mode .ui-source__divider{background:#555}.night-mode .ui-modal__header--border{border-color:rgba(85,85,85,.6274509804)}.night-mode .ui-modal__footer{border-color:rgba(85,85,85,.6274509804)}.night-mode .ui-modal__overlay-blind{background-color:#222}.night-mode .ui-modal__inner{background:#222;box-shadow:0 6px 12px rgba(0,0,0,.56);border-color:rgba(85,85,85,.6274509804)}.night-mode .ui-modal__inner--mode-fullscreen{box-shadow:none}.night-mode .ui-modal__row--cb:hover{background:#383838}.night-mode .ui-search__row{border-color:#555}.night-mode .ui-search__row:hover{background:#333}.night-mode .ui-search__ipt-search-sub-wrp{border-color:#555}.night-mode .ui-tab__btn-tab-head--active,.night-mode .ui-tab__btn-tab-head--active:focus,.night-mode .ui-tab__btn-tab-head--active:hover,.night-mode .ui-tab__btn-tab-head--active:active{background-color:rgba(255,255,255,.2509803922)}.night-mode .ui-tab__wrp-tab-body--background{background:#222;border-color:rgba(85,85,85,.6274509804);border-bottom-color:rgba(47,47,47,.6274509804)}.night-mode .ui-tab__wrp-tab-heads--border{border-color:#555;border-width:2px}.night-mode .ui-tab-side__wrp-tab{background:#222;border-color:rgba(85,85,85,.6274509804)}.night-mode .ui-prof__btn-cycle{border-color:#555}.night-mode .ui-prof__btn-cycle:active{box-shadow:0 0 3px 0 rgba(255,255,255,.7333333333)}.night-mode .ui-prof__btn-cycle[data-state="0"]{background:#222}.night-mode .ui-prof__btn-cycle[data-state="1"]{background:#555;border-color:#6f6f6f}.night-mode .ui-prof__btn-cycle[data-state="2"]{background:#555;border-color:#6f6f6f}.night-mode .ui-prof__btn-cycle[data-state="2"]::before{color:#222}.night-mode .ui-prof__btn-cycle[data-state="3"]{background:repeating-linear-gradient(135deg, #222, #222 10px, #555 10px, #555 20px);border-color:#6f6f6f}.night-mode .ui-dir__face{background:#222;border-color:#555}.night-mode .ui-dir__arm{background:#bbb;box-shadow:none}.night-mode .ui-tip__child{border-color:#555;background:#222}.night-mode .ui-ctx__wrp{background:#222;border:1px solid rgba(85,85,85,.6274509804);border-top-color:#555}.night-mode .ui-ctx__divider{background:#555}.night-mode .ui-ctx__btn:hover{background:#383838;color:#fff}.night-mode .ui-ctx__btn.disabled,.night-mode .ui-ctx__btn.disabled:hover{background:#222;color:#bbb}.night-mode .ui-pick__disp-text{border-color:#555}.night-mode .clp__disp-copied{background:radial-gradient(ellipse at center, #222 0%, #222 35%, transparent 75%, transparent 100%)}.night-mode .ui-ideco__btn-ticker{background:#555;color:#fff}.night-mode .ui-ideco__btn-ticker:hover,.night-mode .ui-ideco__btn-ticker:active,.night-mode .ui-ideco__btn-ticker:focus,.night-mode .ui-ideco__btn-ticker:active:focus{box-shadow:none;outline:none}.night-mode .ui-ideco__btn-ticker:hover{background:#484848;color:#fff}.night-mode .ui-ideco__btn-ticker:active,.night-mode .ui-ideco__btn-ticker:focus,.night-mode .ui-ideco__btn-ticker:active:focus{background:#3c3c3c;color:#fff}.night-mode .ui-sel2__ipt-search{background:rgba(0,0,0,0)}.night-mode .ui-sel2__wrp-options{background-color:#222;border-color:#555}.night-mode .ui-sel2__disp-option.active,.night-mode .ui-sel2__disp-option:focus,.night-mode .ui-sel2__disp-option:hover{background:#383838}.night-mode .ui-sel2__disp-option:focus.active,.night-mode .ui-sel2__disp-option:hover.active{background:#525252}.night-mode .ui-slidr__thumb{background:rgba(204,204,204,.6274509804);border-color:#bbb}.night-mode .ui-slidr__thumb--hover,.night-mode .ui-slidr__thumb:hover{background:rgba(230,230,230,.6274509804);border-color:#bbb}.night-mode .ui-slidr__track-outer{border-color:#555}.night-mode .ui-slidr__track-inner{background:rgba(85,85,85,.6274509804)}.night-mode .ui-slidr__disp-value--visible{border-color:#555;background:#222}.night-mode .ui-slidr__pip{background:#bbb}@font-face{font-family:"Convergence";font-style:normal;font-weight:400;src:local("Convergence-Regular"),url("../fonts/Convergence-Regular.woff2") format("woff2")}@font-face{font-family:"Roboto";font-style:normal;font-weight:400;src:local("Roboto"),url("../fonts/Roboto-Regular.woff2") format("woff2")}@font-face{font-family:"Glyphicons Halflings";font-style:normal;font-weight:400;src:local("glyphicons-halflings-regular"),url("../fonts/glyphicons-halflings-regular.woff2") format("woff2")}@font-face{font-family:"Blambot Casual";src:local("Blambot-Casual"),url("../fonts/Blambot-Casual-Regular.woff2") format("woff2")}@keyframes kf-fade-out{from{opacity:1}to{opacity:0}}.linked-titles .rd__h--0 .entry-title-inner:hover::before{font-size:50%}.linked-titles .rd__h--1 .entry-title-inner:hover::before{font-size:55%}.linked-titles .rd__h--2 .entry-title-inner:hover::before{font-size:60%}.linked-titles .rd__h .entry-title-inner{cursor:copy}.linked-titles .rd__h .entry-title-inner:hover::before{content:" ๐Ÿ”—";color:rgba(0,0,0,.2);position:relative;float:left;width:14px;height:14px;right:20px;margin-right:-30px;font-size:85%}@media print{.no-print{display:none !important}.print__ve-block{display:block !important}.print__h-initial{height:initial !important}.print__overflow-visible{overflow:visible !important}.print__my-2{margin-top:.5rem !important;margin-bottom:.5rem !important}}html.is-faux-print .no-print{display:none !important}html.is-faux-print .print__ve-block{display:block !important}html.is-faux-print .print__h-initial{height:initial !important}html.is-faux-print .print__overflow-visible{overflow:visible !important}html.is-faux-print .print__my-2{margin-top:.5rem !important;margin-bottom:.5rem !important}/*# sourceMappingURL=main.css.map */ diff --git a/charbuilder/5etools-js/parser.js b/charbuilder/5etools-js/parser.js new file mode 100644 index 0000000..df9fab6 --- /dev/null +++ b/charbuilder/5etools-js/parser.js @@ -0,0 +1,3697 @@ +"use strict"; + +// PARSING ============================================================================================================= +globalThis.Parser = {}; + +Parser._parse_aToB = function (abMap, a, fallback) { + if (a === undefined || a === null) throw new TypeError("undefined or null object passed to parser"); + if (typeof a === "string") a = a.trim(); + if (abMap[a] !== undefined) return abMap[a]; + return fallback !== undefined ? fallback : a; +}; + +Parser._parse_bToA = function (abMap, b, fallback) { + if (b === undefined || b === null) throw new TypeError("undefined or null object passed to parser"); + if (typeof b === "string") b = b.trim(); + for (const v in abMap) { + if (!abMap.hasOwnProperty(v)) continue; + if (abMap[v] === b) return v; + } + return fallback !== undefined ? fallback : b; +}; + +Parser.attrChooseToFull = function (attList) { + if (attList.length === 1) return `${Parser.attAbvToFull(attList[0])} modifier`; + else { + const attsTemp = []; + for (let i = 0; i < attList.length; ++i) { + attsTemp.push(Parser.attAbvToFull(attList[i])); + } + return `${attsTemp.join(" or ")} modifier (your choice)`; + } +}; + +Parser.numberToText = function (number) { + if (number == null) throw new TypeError(`undefined or null object passed to parser`); + if (Math.abs(number) >= 100) return `${number}`; + + return `${number < 0 ? "negative " : ""}${Parser.numberToText._getPositiveNumberAsText(Math.abs(number))}`; +}; + +Parser.numberToText._getPositiveNumberAsText = num => { + const [preDotRaw, postDotRaw] = `${num}`.split("."); + + if (!postDotRaw) return Parser.numberToText._getPositiveIntegerAsText(num); + + let preDot = preDotRaw === "0" ? "" : `${Parser.numberToText._getPositiveIntegerAsText(Math.trunc(num))} and `; + + // See also: `Parser.numberToVulgar` + switch (postDotRaw) { + case "125": return `${preDot}one-eighth`; + case "2": return `${preDot}one-fifth`; + case "25": return `${preDot}one-quarter`; + case "375": return `${preDot}three-eighths`; + case "4": return `${preDot}two-fifths`; + case "5": return `${preDot}one-half`; + case "6": return `${preDot}three-fifths`; + case "625": return `${preDot}five-eighths`; + case "75": return `${preDot}three-quarters`; + case "8": return `${preDot}four-fifths`; + case "875": return `${preDot}seven-eighths`; + + default: { + // Handle recursive + const asNum = Number(`0.${postDotRaw}`); + + if (asNum.toFixed(2) === (1 / 3).toFixed(2)) return `${preDot}one-third`; + if (asNum.toFixed(2) === (2 / 3).toFixed(2)) return `${preDot}two-thirds`; + + if (asNum.toFixed(2) === (1 / 6).toFixed(2)) return `${preDot}one-sixth`; + if (asNum.toFixed(2) === (5 / 6).toFixed(2)) return `${preDot}five-sixths`; + } + } +}; + +Parser.numberToText._getPositiveIntegerAsText = num => { + switch (num) { + case 0: return "zero"; + case 1: return "one"; + case 2: return "two"; + case 3: return "three"; + case 4: return "four"; + case 5: return "five"; + case 6: return "six"; + case 7: return "seven"; + case 8: return "eight"; + case 9: return "nine"; + case 10: return "ten"; + case 11: return "eleven"; + case 12: return "twelve"; + case 13: return "thirteen"; + case 14: return "fourteen"; + case 15: return "fifteen"; + case 16: return "sixteen"; + case 17: return "seventeen"; + case 18: return "eighteen"; + case 19: return "nineteen"; + case 20: return "twenty"; + case 30: return "thirty"; + case 40: return "forty"; + case 50: return "fifty"; + case 60: return "sixty"; + case 70: return "seventy"; + case 80: return "eighty"; + case 90: return "ninety"; + default: { + const str = String(num); + return `${Parser.numberToText._getPositiveIntegerAsText(Number(`${str[0]}0`))}-${Parser.numberToText._getPositiveIntegerAsText(Number(str[1]))}`; + } + } +}; + +Parser.textToNumber = function (str) { + str = str.trim().toLowerCase(); + if (!isNaN(str)) return Number(str); + switch (str) { + case "zero": return 0; + case "one": case "a": case "an": return 1; + case "two": case "double": return 2; + case "three": case "triple": return 3; + case "four": case "quadruple": return 4; + case "five": return 5; + case "six": return 6; + case "seven": return 7; + case "eight": return 8; + case "nine": return 9; + case "ten": return 10; + case "eleven": return 11; + case "twelve": return 12; + case "thirteen": return 13; + case "fourteen": return 14; + case "fifteen": return 15; + case "sixteen": return 16; + case "seventeen": return 17; + case "eighteen": return 18; + case "nineteen": return 19; + case "twenty": return 20; + case "thirty": return 30; + case "forty": return 40; + case "fifty": return 50; + case "sixty": return 60; + case "seventy": return 70; + case "eighty": return 80; + case "ninety": return 90; + } + return NaN; +}; + +Parser.numberToVulgar = function (number, {isFallbackOnFractional = true} = {}) { + const isNeg = number < 0; + const spl = `${number}`.replace(/^-/, "").split("."); + if (spl.length === 1) return number; + + let preDot = spl[0] === "0" ? "" : spl[0]; + if (isNeg) preDot = `-${preDot}`; + + // See also: `Parser.numberToText._getPositiveNumberAsText` + switch (spl[1]) { + case "125": return `${preDot}โ…›`; + case "2": return `${preDot}โ…•`; + case "25": return `${preDot}ยผ`; + case "375": return `${preDot}โ…œ`; + case "4": return `${preDot}โ…–`; + case "5": return `${preDot}ยฝ`; + case "6": return `${preDot}โ…—`; + case "625": return `${preDot}โ…`; + case "75": return `${preDot}ยพ`; + case "8": return `${preDot}โ…˜`; + case "875": return `${preDot}โ…ž`; + + default: { + // Handle recursive + const asNum = Number(`0.${spl[1]}`); + + if (asNum.toFixed(2) === (1 / 3).toFixed(2)) return `${preDot}โ…“`; + if (asNum.toFixed(2) === (2 / 3).toFixed(2)) return `${preDot}โ…”`; + + if (asNum.toFixed(2) === (1 / 6).toFixed(2)) return `${preDot}โ…™`; + if (asNum.toFixed(2) === (5 / 6).toFixed(2)) return `${preDot}โ…š`; + } + } + + return isFallbackOnFractional ? Parser.numberToFractional(number) : null; +}; + +Parser.vulgarToNumber = function (str) { + const [, leading = "0", vulgar = ""] = /^(\d+)?([โ…›ยผโ…œยฝโ…ยพโ…žโ…“โ…”โ…™โ…š])?$/.exec(str) || []; + let out = Number(leading); + switch (vulgar) { + case "โ…›": out += 0.125; break; + case "ยผ": out += 0.25; break; + case "โ…œ": out += 0.375; break; + case "ยฝ": out += 0.5; break; + case "โ…": out += 0.625; break; + case "ยพ": out += 0.75; break; + case "โ…ž": out += 0.875; break; + case "โ…“": out += 1 / 3; break; + case "โ…”": out += 2 / 3; break; + case "โ…™": out += 1 / 6; break; + case "โ…š": out += 5 / 6; break; + case "": break; + default: throw new Error(`Unhandled vulgar part "${vulgar}"`); + } + return out; +}; + +Parser.numberToSuperscript = function (number) { + return `${number}`.split("").map(c => isNaN(c) ? c : Parser._NUMBERS_SUPERSCRIPT[Number(c)]).join(""); +}; +Parser._NUMBERS_SUPERSCRIPT = "โฐยนยฒยณโดโตโถโทโธโน"; + +Parser.numberToSubscript = function (number) { + return `${number}`.split("").map(c => isNaN(c) ? c : Parser._NUMBERS_SUBSCRIPT[Number(c)]).join(""); +}; +Parser._NUMBERS_SUBSCRIPT = "โ‚€โ‚โ‚‚โ‚ƒโ‚„โ‚…โ‚†โ‚‡โ‚ˆโ‚‰"; + +Parser._greatestCommonDivisor = function (a, b) { + if (b < Number.EPSILON) return a; + return Parser._greatestCommonDivisor(b, Math.floor(a % b)); +}; +Parser.numberToFractional = function (number) { + const len = number.toString().length - 2; + let denominator = 10 ** len; + let numerator = number * denominator; + const divisor = Parser._greatestCommonDivisor(numerator, denominator); + numerator = Math.floor(numerator / divisor); + denominator = Math.floor(denominator / divisor); + + return denominator === 1 ? String(numerator) : `${Math.floor(numerator)}/${Math.floor(denominator)}`; +}; + +Parser.ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + +Parser.attAbvToFull = function (abv) { + return Parser._parse_aToB(Parser.ATB_ABV_TO_FULL, abv); +}; + +Parser.attFullToAbv = function (full) { + return Parser._parse_bToA(Parser.ATB_ABV_TO_FULL, full); +}; + +Parser.sizeAbvToFull = function (abv) { + return Parser._parse_aToB(Parser.SIZE_ABV_TO_FULL, abv); +}; + +Parser.getAbilityModNumber = function (abilityScore) { + return Math.floor((abilityScore - 10) / 2); +}; + +Parser.getAbilityModifier = function (abilityScore) { + let modifier = Parser.getAbilityModNumber(abilityScore); + if (modifier >= 0) modifier = `+${modifier}`; + return `${modifier}`; +}; + +Parser.getSpeedString = (ent, {isMetric = false, isSkipZeroWalk = false} = {}) => { + if (ent.speed == null) return "\u2014"; + + const unit = isMetric ? Parser.metric.getMetricUnit({originalUnit: "ft.", isShortForm: true}) : "ft."; + if (typeof ent.speed === "object") { + const stack = []; + let joiner = ", "; + + Parser.SPEED_MODES + .filter(mode => !ent.speed.hidden?.includes(mode)) + .forEach(mode => Parser._getSpeedString_addSpeedMode({ent, prop: mode, stack, isMetric, isSkipZeroWalk, unit})); + + if (ent.speed.choose && !ent.speed.hidden?.includes("choose")) { + joiner = "; "; + stack.push(`${ent.speed.choose.from.sort().joinConjunct(", ", " or ")} ${ent.speed.choose.amount} ${unit}${ent.speed.choose.note ? ` ${ent.speed.choose.note}` : ""}`); + } + + return stack.join(joiner) + (ent.speed.note ? ` ${ent.speed.note}` : ""); + } + + return (isMetric ? Parser.metric.getMetricNumber({originalValue: ent.speed, originalUnit: Parser.UNT_FEET}) : ent.speed) + + (ent.speed === "Varies" ? "" : ` ${unit} `); +}; +Parser._getSpeedString_addSpeedMode = ({ent, prop, stack, isMetric, isSkipZeroWalk, unit}) => { + if (ent.speed[prop] || (!isSkipZeroWalk && prop === "walk")) Parser._getSpeedString_addSpeed({prop, speed: ent.speed[prop] || 0, isMetric, unit, stack}); + if (ent.speed.alternate && ent.speed.alternate[prop]) ent.speed.alternate[prop].forEach(speed => Parser._getSpeedString_addSpeed({prop, speed, isMetric, unit, stack})); +}; +Parser._getSpeedString_addSpeed = ({prop, speed, isMetric, unit, stack}) => { + const ptName = prop === "walk" ? "" : `${prop} `; + const ptValue = Parser._getSpeedString_getVal({prop, speed, isMetric}); + const ptUnit = speed === true ? "" : ` ${unit}`; + const ptCondition = Parser._getSpeedString_getCondition({speed}); + stack.push([ptName, ptValue, ptUnit, ptCondition].join("")); +}; +Parser._getSpeedString_getVal = ({prop, speed, isMetric}) => { + if (speed === true && prop !== "walk") return "equal to your walking speed"; + + const num = speed === true + ? 0 + : speed.number != null ? speed.number : speed; + + return isMetric ? Parser.metric.getMetricNumber({originalValue: num, originalUnit: Parser.UNT_FEET}) : num; +}; +Parser._getSpeedString_getCondition = ({speed}) => speed.condition ? ` ${Renderer.get().render(speed.condition)}` : ""; + +Parser.SPEED_MODES = ["walk", "burrow", "climb", "fly", "swim"]; + +Parser.SPEED_TO_PROGRESSIVE = { + "walk": "walking", + "burrow": "burrowing", + "climb": "climbing", + "fly": "flying", + "swim": "swimming", +}; + +Parser.speedToProgressive = function (prop) { + return Parser._parse_aToB(Parser.SPEED_TO_PROGRESSIVE, prop); +}; + +Parser._addCommas = function (intNum) { + return `${intNum}`.replace(/(\d)(?=(\d{3})+$)/g, "$1,"); +}; + +Parser.raceCreatureTypesToFull = function (creatureTypes) { + const hasSubOptions = creatureTypes.some(it => it.choose); + return creatureTypes + .map(it => { + if (!it.choose) return Parser.monTypeToFullObj(it).asText; + return [...it.choose] + .sort(SortUtil.ascSortLower) + .map(sub => Parser.monTypeToFullObj(sub).asText) + .joinConjunct(", ", " or "); + }) + .joinConjunct(hasSubOptions ? "; " : ", ", " and "); +}; + +Parser.crToXp = function (cr, {isDouble = false} = {}) { + if (cr != null && cr.xp) return Parser._addCommas(`${isDouble ? cr.xp * 2 : cr.xp}`); + + const toConvert = cr ? (cr.cr || cr) : null; + if (toConvert === "Unknown" || toConvert == null || !Parser.XP_CHART_ALT[toConvert]) return "Unknown"; + // CR 0 creatures can be 0 or 10 XP, but 10 XP is used in almost every case. + // Exceptions, such as MM's Frog and Sea Horse, have their XP set to 0 on the creature + if (toConvert === "0") return "10"; + const xp = Parser.XP_CHART_ALT[toConvert]; + return Parser._addCommas(`${isDouble ? 2 * xp : xp}`); +}; + +Parser.crToXpNumber = function (cr) { + if (cr != null && cr.xp) return cr.xp; + const toConvert = cr ? (cr.cr || cr) : cr; + if (toConvert === "Unknown" || toConvert == null) return null; + return Parser.XP_CHART_ALT[toConvert] ?? null; +}; + +Parser.LEVEL_TO_XP_EASY = [0, 25, 50, 75, 125, 250, 300, 350, 450, 550, 600, 800, 1000, 1100, 1250, 1400, 1600, 2000, 2100, 2400, 2800]; +Parser.LEVEL_TO_XP_MEDIUM = [0, 50, 100, 150, 250, 500, 600, 750, 900, 1100, 1200, 1600, 2000, 2200, 2500, 2800, 3200, 3900, 4100, 4900, 5700]; +Parser.LEVEL_TO_XP_HARD = [0, 75, 150, 225, 375, 750, 900, 1100, 1400, 1600, 1900, 2400, 3000, 3400, 3800, 4300, 4800, 5900, 6300, 7300, 8500]; +Parser.LEVEL_TO_XP_DEADLY = [0, 100, 200, 400, 500, 1100, 1400, 1700, 2100, 2400, 2800, 3600, 4500, 5100, 5700, 6400, 7200, 8800, 9500, 10900, 12700]; +Parser.LEVEL_TO_XP_DAILY = [0, 300, 600, 1200, 1700, 3500, 4000, 5000, 6000, 7500, 9000, 10500, 11500, 13500, 15000, 18000, 20000, 25000, 27000, 30000, 40000]; + +Parser.LEVEL_XP_REQUIRED = [0, 300, 900, 2700, 6500, 14000, 23000, 34000, 48000, 64000, 85000, 100000, 120000, 140000, 165000, 195000, 225000, 265000, 305000, 355000]; + +Parser.CRS = ["0", "1/8", "1/4", "1/2", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30"]; + +Parser.levelToXpThreshold = function (level) { + return [Parser.LEVEL_TO_XP_EASY[level], Parser.LEVEL_TO_XP_MEDIUM[level], Parser.LEVEL_TO_XP_HARD[level], Parser.LEVEL_TO_XP_DEADLY[level]]; +}; + +Parser.isValidCr = function (cr) { + return Parser.CRS.includes(cr); +}; + +Parser.crToNumber = function (cr, opts = {}) { + const {isDefaultNull = false} = opts; + + if (cr === "Unknown" || cr === "\u2014" || cr == null) return isDefaultNull ? null : VeCt.CR_UNKNOWN; + if (cr.cr) return Parser.crToNumber(cr.cr, opts); + + const parts = cr.trim().split("/"); + if (!parts.length || parts.length >= 3) return isDefaultNull ? null : VeCt.CR_CUSTOM; + if (isNaN(parts[0])) return isDefaultNull ? null : VeCt.CR_CUSTOM; + + if (parts.length === 2) { + if (isNaN(Number(parts[1]))) return isDefaultNull ? null : VeCt.CR_CUSTOM; + return Number(parts[0]) / Number(parts[1]); + } + + return Number(parts[0]); +}; + +Parser.numberToCr = function (number, safe) { + // avoid dying if already-converted number is passed in + if (safe && typeof number === "string" && Parser.CRS.includes(number)) return number; + + if (number == null) return "Unknown"; + + return Parser.numberToFractional(number); +}; + +Parser.crToPb = function (cr) { + if (cr === "Unknown" || cr == null) return 0; + cr = cr.cr || cr; + if (Parser.crToNumber(cr) < 5) return 2; + return Math.ceil(cr / 4) + 1; +}; + +Parser.levelToPb = function (level) { + if (!level) return 2; + return Math.ceil(level / 4) + 1; +}; + +Parser.SKILL_TO_ATB_ABV = { + "athletics": "str", + "acrobatics": "dex", + "sleight of hand": "dex", + "stealth": "dex", + "arcana": "int", + "history": "int", + "investigation": "int", + "nature": "int", + "religion": "int", + "animal handling": "wis", + "insight": "wis", + "medicine": "wis", + "perception": "wis", + "survival": "wis", + "deception": "cha", + "intimidation": "cha", + "performance": "cha", + "persuasion": "cha", +}; + +Parser.skillToAbilityAbv = function (skill) { + return Parser._parse_aToB(Parser.SKILL_TO_ATB_ABV, skill); +}; + +Parser.SKILL_TO_SHORT = { + "athletics": "ath", + "acrobatics": "acro", + "sleight of hand": "soh", + "stealth": "slth", + "arcana": "arc", + "history": "hist", + "investigation": "invn", + "nature": "natr", + "religion": "reli", + "animal handling": "hndl", + "insight": "ins", + "medicine": "med", + "perception": "perp", + "survival": "surv", + "deception": "decp", + "intimidation": "intm", + "performance": "perf", + "persuasion": "pers", +}; + +Parser.skillToShort = function (skill) { + return Parser._parse_aToB(Parser.SKILL_TO_SHORT, skill); +}; + +Parser.LANGUAGES_STANDARD = [ + "Common", + "Dwarvish", + "Elvish", + "Giant", + "Gnomish", + "Goblin", + "Halfling", + "Orc", +]; + +Parser.LANGUAGES_EXOTIC = [ + "Abyssal", + "Aquan", + "Auran", + "Celestial", + "Draconic", + "Deep Speech", + "Ignan", + "Infernal", + "Primordial", + "Sylvan", + "Terran", + "Undercommon", +]; + +Parser.LANGUAGES_SECRET = [ + "Druidic", + "Thieves' cant", +]; + +Parser.LANGUAGES_ALL = [ + ...Parser.LANGUAGES_STANDARD, + ...Parser.LANGUAGES_EXOTIC, + ...Parser.LANGUAGES_SECRET, +].sort(); + +Parser.acToFull = function (ac, renderer) { + if (typeof ac === "string") return ac; // handle classic format + + renderer = renderer || Renderer.get(); + + let stack = ""; + let inBraces = false; + for (let i = 0; i < ac.length; ++i) { + const cur = ac[i]; + const nxt = ac[i + 1]; + + if (cur.special != null) { + if (inBraces) inBraces = false; + + stack += cur.special; + } else if (cur.ac) { + const isNxtBraces = nxt && nxt.braces; + + if (!inBraces && cur.braces) { + stack += "("; + inBraces = true; + } + + stack += cur.ac; + + if (cur.from) { + // always brace nested braces + if (cur.braces) { + stack += " ("; + } else { + stack += inBraces ? "; " : " ("; + } + + inBraces = true; + + stack += cur.from.map(it => renderer.render(it)).join(", "); + + if (cur.braces) { + stack += ")"; + } else if (!isNxtBraces) { + stack += ")"; + inBraces = false; + } + } + + if (cur.condition) stack += ` ${renderer.render(cur.condition)}`; + + if (inBraces && !isNxtBraces) { + stack += ")"; + inBraces = false; + } + } else { + stack += cur; + } + + if (nxt) { + if (nxt.braces) { + stack += inBraces ? "; " : " ("; + inBraces = true; + } else stack += ", "; + } + } + if (inBraces) stack += ")"; + + return stack.trim(); +}; + +Parser.MONSTER_COUNT_TO_XP_MULTIPLIER = [1, 1.5, 2, 2, 2, 2, 2.5, 2.5, 2.5, 2.5, 3, 3, 3, 3, 4]; +Parser.numMonstersToXpMult = function (num, playerCount = 3) { + const baseVal = (() => { + if (num >= Parser.MONSTER_COUNT_TO_XP_MULTIPLIER.length) return 4; + return Parser.MONSTER_COUNT_TO_XP_MULTIPLIER[num - 1]; + })(); + + if (playerCount < 3) return baseVal >= 3 ? baseVal + 1 : baseVal + 0.5; + else if (playerCount > 5) { + return baseVal === 4 ? 3 : baseVal - 0.5; + } else return baseVal; +}; + +Parser.armorFullToAbv = function (armor) { + return Parser._parse_bToA(Parser.ARMOR_ABV_TO_FULL, armor); +}; + +Parser.weaponFullToAbv = function (weapon) { + return Parser._parse_bToA(Parser.WEAPON_ABV_TO_FULL, weapon); +}; + +Parser._getSourceStringFromSource = function (source) { + if (source && source.source) return source.source; + return source; +}; +Parser._buildSourceCache = function (dict) { + const out = {}; + Object.entries(dict).forEach(([k, v]) => out[k.toLowerCase()] = v); + return out; +}; +Parser._sourceJsonCache = null; +Parser.hasSourceJson = function (source) { + Parser._sourceJsonCache = Parser._sourceJsonCache || Parser._buildSourceCache(Object.keys(Parser.SOURCE_JSON_TO_FULL).mergeMap(k => ({[k]: k}))); + return !!Parser._sourceJsonCache[source.toLowerCase()]; +}; +Parser._sourceFullCache = null; +Parser.hasSourceFull = function (source) { + Parser._sourceFullCache = Parser._sourceFullCache || Parser._buildSourceCache(Parser.SOURCE_JSON_TO_FULL); + return !!Parser._sourceFullCache[source.toLowerCase()]; +}; +Parser._sourceAbvCache = null; +Parser.hasSourceAbv = function (source) { + Parser._sourceAbvCache = Parser._sourceAbvCache || Parser._buildSourceCache(Parser.SOURCE_JSON_TO_ABV); + return !!Parser._sourceAbvCache[source.toLowerCase()]; +}; +Parser._sourceDateCache = null; +Parser.hasSourceDate = function (source) { + Parser._sourceDateCache = Parser._sourceDateCache || Parser._buildSourceCache(Parser.SOURCE_JSON_TO_DATE); + return !!Parser._sourceDateCache[source.toLowerCase()]; +}; +Parser.sourceJsonToJson = function (source) { + source = Parser._getSourceStringFromSource(source); + if (Parser.hasSourceJson(source)) return Parser._sourceJsonCache[source.toLowerCase()]; + if (typeof PrereleaseUtil !== "undefined" && PrereleaseUtil.hasSourceJson(source)) return PrereleaseUtil.sourceJsonToSource(source).json; + if (typeof BrewUtil2 !== "undefined" && BrewUtil2.hasSourceJson(source)) return BrewUtil2.sourceJsonToSource(source).json; + return source; +}; +Parser.sourceJsonToFull = function (source) { + source = Parser._getSourceStringFromSource(source); + if (Parser.hasSourceFull(source)) return Parser._sourceFullCache[source.toLowerCase()].replace(/'/g, "\u2019"); + if (typeof PrereleaseUtil !== "undefined" && PrereleaseUtil.hasSourceJson(source)) return PrereleaseUtil.sourceJsonToFull(source).replace(/'/g, "\u2019"); + if (typeof BrewUtil2 !== "undefined" && BrewUtil2.hasSourceJson(source)) return BrewUtil2.sourceJsonToFull(source).replace(/'/g, "\u2019"); + return Parser._parse_aToB(Parser.SOURCE_JSON_TO_FULL, source).replace(/'/g, "\u2019"); +}; +Parser.sourceJsonToFullCompactPrefix = function (source) { + return Parser.sourceJsonToFull(source) + .replace(Parser.UA_PREFIX, Parser.UA_PREFIX_SHORT) + .replace(/^Unearthed Arcana (\d+): /, "UA$1: ") + .replace(Parser.AL_PREFIX, Parser.AL_PREFIX_SHORT) + .replace(Parser.PS_PREFIX, Parser.PS_PREFIX_SHORT); +}; +Parser.sourceJsonToAbv = function (source) { + source = Parser._getSourceStringFromSource(source); + if (Parser.hasSourceAbv(source)) return Parser._sourceAbvCache[source.toLowerCase()]; + if (typeof PrereleaseUtil !== "undefined" && PrereleaseUtil.hasSourceJson(source)) return PrereleaseUtil.sourceJsonToAbv(source); + if (typeof BrewUtil2 !== "undefined" && BrewUtil2.hasSourceJson(source)) return BrewUtil2.sourceJsonToAbv(source); + return Parser._parse_aToB(Parser.SOURCE_JSON_TO_ABV, source); +}; +Parser.sourceJsonToDate = function (source) { + source = Parser._getSourceStringFromSource(source); + if (Parser.hasSourceDate(source)) return Parser._sourceDateCache[source.toLowerCase()]; + if (typeof PrereleaseUtil !== "undefined" && PrereleaseUtil.hasSourceJson(source)) return PrereleaseUtil.sourceJsonToDate(source); + if (typeof BrewUtil2 !== "undefined" && BrewUtil2.hasSourceJson(source)) return BrewUtil2.sourceJsonToDate(source); + return Parser._parse_aToB(Parser.SOURCE_JSON_TO_DATE, source, null); +}; + +Parser.sourceJsonToColor = function (source) { + return `source${Parser.sourceJsonToAbv(source)}`; +}; + +Parser.sourceJsonToStyle = function (source) { + source = Parser._getSourceStringFromSource(source); + if (Parser.hasSourceJson(source)) return ""; + if (typeof PrereleaseUtil !== "undefined" && PrereleaseUtil.hasSourceJson(source)) return PrereleaseUtil.sourceJsonToStyle(source); + if (typeof BrewUtil2 !== "undefined" && BrewUtil2.hasSourceJson(source)) return BrewUtil2.sourceJsonToStyle(source); + return ""; +}; + +Parser.sourceJsonToStylePart = function (source) { + source = Parser._getSourceStringFromSource(source); + if (Parser.hasSourceJson(source)) return ""; + if (typeof PrereleaseUtil !== "undefined" && PrereleaseUtil.hasSourceJson(source)) return PrereleaseUtil.sourceJsonToStylePart(source); + if (typeof BrewUtil2 !== "undefined" && BrewUtil2.hasSourceJson(source)) return BrewUtil2.sourceJsonToStylePart(source); + return ""; +}; + +Parser.sourceJsonToMarkerHtml = function (source, {isList = true, additionalStyles = ""} = {}) { + source = Parser._getSourceStringFromSource(source); + // TODO(Future) consider enabling this + // if (SourceUtil.isPartneredSourceWotc(source)) return `${isList ? "" : "["}โœฆ${isList ? "" : "]"}`; + if (SourceUtil.isLegacySourceWotc(source)) return `${isList ? "" : "["}สŸ${isList ? "" : "]"}`; + return ""; +}; + +Parser.stringToSlug = function (str) { + return str.trim().toLowerCase().toAscii().replace(/[^\w ]+/g, "").replace(/ +/g, "-"); +}; + +Parser.stringToCasedSlug = function (str) { + return str.toAscii().replace(/[^\w ]+/g, "").replace(/ +/g, "-"); +}; + +Parser.ITEM_SPELLCASTING_FOCUS_CLASSES = ["Artificer", "Bard", "Cleric", "Druid", "Paladin", "Ranger", "Sorcerer", "Warlock", "Wizard"]; + +Parser.itemValueToFull = function (item, opts = {isShortForm: false, isSmallUnits: false}) { + return Parser._moneyToFull(item, "value", "valueMult", opts); +}; + +Parser.itemValueToFullMultiCurrency = function (item, opts = {isShortForm: false, isSmallUnits: false}) { + return Parser._moneyToFullMultiCurrency(item, "value", "valueMult", opts); +}; + +Parser.itemVehicleCostsToFull = function (item, isShortForm) { + return { + travelCostFull: Parser._moneyToFull(item, "travelCost", "travelCostMult", {isShortForm}), + shippingCostFull: Parser._moneyToFull(item, "shippingCost", "shippingCostMult", {isShortForm}), + }; +}; + +Parser.spellComponentCostToFull = function (item, isShortForm) { + return Parser._moneyToFull(item, "cost", "costMult", {isShortForm}); +}; + +Parser.vehicleCostToFull = function (item, isShortForm) { + return Parser._moneyToFull(item, "cost", "costMult", {isShortForm}); +}; + +Parser._moneyToFull = function (it, prop, propMult, opts = {isShortForm: false, isSmallUnits: false}) { + if (it[prop] == null && it[propMult] == null) return ""; + if (it[prop] != null) { + const {coin, mult} = Parser.getCurrencyAndMultiplier(it[prop], it.currencyConversion); + return `${(it[prop] * mult).toLocaleString(undefined, {maximumFractionDigits: 5})}${opts.isSmallUnits ? `${coin}` : ` ${coin}`}`; + } else if (it[propMult] != null) return opts.isShortForm ? `ร—${it[propMult]}` : `base value ร—${it[propMult]}`; + return ""; +}; + +Parser._moneyToFullMultiCurrency = function (it, prop, propMult, {isShortForm, multiplier} = {}) { + if (it[prop]) { + const conversionTable = Parser.getCurrencyConversionTable(it.currencyConversion); + + const simplified = it.currencyConversion + ? CurrencyUtil.doSimplifyCoins( + { + // Assume the e.g. item's value is in the lowest available denomination + [conversionTable[0]?.coin || "cp"]: it[prop] * (multiplier ?? conversionTable[0]?.mult ?? 1), + }, + { + currencyConversionId: it.currencyConversion, + }, + ) + : CurrencyUtil.doSimplifyCoins({ + cp: it[prop] * (multiplier ?? 1), + }); + + return [...conversionTable] + .reverse() + .filter(meta => simplified[meta.coin]) + .map(meta => `${simplified[meta.coin].toLocaleString(undefined, {maximumFractionDigits: 5})} ${meta.coin}`) + .join(", "); + } + + if (it[propMult]) return isShortForm ? `ร—${it[propMult]}` : `base value ร—${it[propMult]}`; + + return ""; +}; + +Parser.DEFAULT_CURRENCY_CONVERSION_TABLE = [ + { + coin: "cp", + mult: 1, + }, + { + coin: "sp", + mult: 0.1, + }, + { + coin: "gp", + mult: 0.01, + isFallback: true, + }, +]; +Parser.FULL_CURRENCY_CONVERSION_TABLE = [ + { + coin: "cp", + mult: 1, + }, + { + coin: "sp", + mult: 0.1, + }, + { + coin: "ep", + mult: 0.02, + }, + { + coin: "gp", + mult: 0.01, + isFallback: true, + }, + { + coin: "pp", + mult: 0.001, + }, +]; +Parser.getCurrencyConversionTable = function (currencyConversionId) { + const fromPrerelease = currencyConversionId ? PrereleaseUtil.getMetaLookup("currencyConversions")?.[currencyConversionId] : null; + const fromBrew = currencyConversionId ? BrewUtil2.getMetaLookup("currencyConversions")?.[currencyConversionId] : null; + const conversionTable = fromPrerelease?.length + ? fromPrerelease + : fromBrew?.length + ? fromBrew + : Parser.DEFAULT_CURRENCY_CONVERSION_TABLE; + if (conversionTable !== Parser.DEFAULT_CURRENCY_CONVERSION_TABLE) conversionTable.sort((a, b) => SortUtil.ascSort(b.mult, a.mult)); + return conversionTable; +}; +Parser.getCurrencyAndMultiplier = function (value, currencyConversionId) { + const conversionTable = Parser.getCurrencyConversionTable(currencyConversionId); + + if (!value) return conversionTable.find(it => it.isFallback) || conversionTable[0]; + if (conversionTable.length === 1) return conversionTable[0]; + if (!Number.isInteger(value) && value < conversionTable[0].mult) return conversionTable[0]; + + for (let i = conversionTable.length - 1; i >= 0; --i) { + if (Number.isInteger(value * conversionTable[i].mult)) return conversionTable[i]; + } + + return conversionTable.last(); +}; + +Parser.COIN_ABVS = ["cp", "sp", "ep", "gp", "pp"]; +Parser.COIN_ABV_TO_FULL = { + "cp": "copper pieces", + "sp": "silver pieces", + "ep": "electrum pieces", + "gp": "gold pieces", + "pp": "platinum pieces", +}; +Parser.COIN_CONVERSIONS = [1, 10, 50, 100, 1000]; + +Parser.coinAbvToFull = function (coin) { + return Parser._parse_aToB(Parser.COIN_ABV_TO_FULL, coin); +}; + +/** + * @param currency Object of the form `{pp: , gp: , ... }`. + * @param isDisplayEmpty If "empty" values (i.e., those which are 0) should be displayed. + */ +Parser.getDisplayCurrency = function (currency, {isDisplayEmpty = false} = {}) { + return [...Parser.COIN_ABVS] + .reverse() + .filter(abv => isDisplayEmpty ? currency[abv] != null : currency[abv]) + .map(abv => `${currency[abv].toLocaleString()} ${abv}`) + .join(", "); +}; + +Parser.itemWeightToFull = function (item, isShortForm) { + if (item.weight) { + // Handle pure integers + if (Math.round(item.weight) === item.weight) return `${item.weight} lb.${(item.weightNote ? ` ${item.weightNote}` : "")}`; + + const integerPart = Math.floor(item.weight); + + // Attempt to render the amount as (a number +) a vulgar + const vulgarGlyph = Parser.numberToVulgar(item.weight - integerPart, {isFallbackOnFractional: false}); + if (vulgarGlyph) return `${integerPart || ""}${vulgarGlyph} lb.${(item.weightNote ? ` ${item.weightNote}` : "")}`; + + // Fall back on decimal pounds or ounces + return `${(item.weight < 1 ? item.weight * 16 : item.weight).toLocaleString(undefined, {maximumFractionDigits: 5})} ${item.weight < 1 ? "oz" : "lb"}.${(item.weightNote ? ` ${item.weightNote}` : "")}`; + } + if (item.weightMult) return isShortForm ? `ร—${item.weightMult}` : `base weight ร—${item.weightMult}`; + return ""; +}; + +Parser.ITEM_RECHARGE_TO_FULL = { + round: "Every Round", + restShort: "Short Rest", + restLong: "Long Rest", + dawn: "Dawn", + dusk: "Dusk", + midnight: "Midnight", + week: "Week", + month: "Month", + year: "Year", + decade: "Decade", + century: "Century", + special: "Special", +}; +Parser.itemRechargeToFull = function (recharge) { + return Parser._parse_aToB(Parser.ITEM_RECHARGE_TO_FULL, recharge); +}; + +Parser.ITEM_MISC_TAG_TO_FULL = { + "CF/W": "Creates Food/Water", + "TT": "Trinket Table", +}; +Parser.itemMiscTagToFull = function (type) { + return Parser._parse_aToB(Parser.ITEM_MISC_TAG_TO_FULL, type); +}; + +Parser._decimalSeparator = (0.1).toLocaleString().substring(1, 2); +Parser._numberCleanRegexp = Parser._decimalSeparator === "." ? new RegExp(/[\s,]*/g, "g") : new RegExp(/[\s.]*/g, "g"); +Parser._costSplitRegexp = Parser._decimalSeparator === "." ? new RegExp(/(\d+(\.\d+)?)([csegp]p)/) : new RegExp(/(\d+(,\d+)?)([csegp]p)/); + +/** input e.g. "25 gp", "1,000pp" */ +Parser.coinValueToNumber = function (value) { + if (!value) return 0; + // handle oddities + if (value === "Varies") return 0; + + value = value + .replace(/\s*/, "") + .replace(Parser._numberCleanRegexp, "") + .toLowerCase(); + const m = Parser._costSplitRegexp.exec(value); + if (!m) throw new Error(`Badly formatted value "${value}"`); + const ixCoin = Parser.COIN_ABVS.indexOf(m[3]); + if (!~ixCoin) throw new Error(`Unknown coin type "${m[3]}"`); + return Number(m[1]) * Parser.COIN_CONVERSIONS[ixCoin]; +}; + +Parser.weightValueToNumber = function (value) { + if (!value) return 0; + + if (Number(value)) return Number(value); + else throw new Error(`Badly formatted value ${value}`); +}; + +Parser.dmgTypeToFull = function (dmgType) { + return Parser._parse_aToB(Parser.DMGTYPE_JSON_TO_FULL, dmgType); +}; + +Parser.skillProficienciesToFull = function (skillProficiencies) { + function renderSingle (skProf) { + if (skProf.any) { + skProf = MiscUtil.copyFast(skProf); + skProf.choose = {"from": Object.keys(Parser.SKILL_TO_ATB_ABV), "count": skProf.any}; + delete skProf.any; + } + + const keys = Object.keys(skProf).sort(SortUtil.ascSortLower); + + const ixChoose = keys.indexOf("choose"); + if (~ixChoose) keys.splice(ixChoose, 1); + + const baseStack = []; + keys.filter(k => skProf[k]).forEach(k => baseStack.push(Renderer.get().render(`{@skill ${k.toTitleCase()}}`))); + + const chooseStack = []; + if (~ixChoose) { + const chObj = skProf.choose; + if (chObj.from.length === 18) { + chooseStack.push(`choose any ${!chObj.count || chObj.count === 1 ? "skill" : chObj.count}`); + } else { + chooseStack.push(`choose ${chObj.count || 1} from ${chObj.from.map(it => Renderer.get().render(`{@skill ${it.toTitleCase()}}`)).joinConjunct(", ", " and ")}`); + } + } + + const base = baseStack.joinConjunct(", ", " and "); + const choose = chooseStack.join(""); // this should currently only ever be 1-length + + if (baseStack.length && chooseStack.length) return `${base}; and ${choose}`; + else if (baseStack.length) return base; + else if (chooseStack.length) return choose; + } + + return skillProficiencies.map(renderSingle).join(" or "); +}; + +// sp-prefix functions are for parsing spell data, and shared with the roll20 script +Parser.spSchoolAndSubschoolsAbvsToFull = function (school, subschools) { + if (!subschools || !subschools.length) return Parser.spSchoolAbvToFull(school); + else return `${Parser.spSchoolAbvToFull(school)} (${subschools.map(sub => Parser.spSchoolAbvToFull(sub)).join(", ")})`; +}; + +Parser.spSchoolAbvToFull = function (schoolOrSubschool) { + const out = Parser._parse_aToB(Parser.SP_SCHOOL_ABV_TO_FULL, schoolOrSubschool); + if (Parser.SP_SCHOOL_ABV_TO_FULL[schoolOrSubschool]) return out; + if (PrereleaseUtil.getMetaLookup("spellSchools")?.[schoolOrSubschool]) return PrereleaseUtil.getMetaLookup("spellSchools")?.[schoolOrSubschool].full; + if (BrewUtil2.getMetaLookup("spellSchools")?.[schoolOrSubschool]) return BrewUtil2.getMetaLookup("spellSchools")?.[schoolOrSubschool].full; + return out; +}; + +Parser.spSchoolAndSubschoolsAbvsShort = function (school, subschools) { + if (!subschools || !subschools.length) return Parser.spSchoolAbvToShort(school); + else return `${Parser.spSchoolAbvToShort(school)} (${subschools.map(sub => Parser.spSchoolAbvToShort(sub)).join(", ")})`; +}; + +Parser.spSchoolAbvToShort = function (school) { + const out = Parser._parse_aToB(Parser.SP_SCHOOL_ABV_TO_SHORT, school); + if (Parser.SP_SCHOOL_ABV_TO_SHORT[school]) return out; + if (PrereleaseUtil.getMetaLookup("spellSchools")?.[school]) return PrereleaseUtil.getMetaLookup("spellSchools")?.[school].short; + if (BrewUtil2.getMetaLookup("spellSchools")?.[school]) return BrewUtil2.getMetaLookup("spellSchools")?.[school].short; + if (out.length <= 4) return out; + return `${out.slice(0, 3)}.`; +}; + +Parser.spSchoolAbvToStyle = function (school) { // For prerelease/homebrew + const stylePart = Parser.spSchoolAbvToStylePart(school); + if (!stylePart) return stylePart; + return `style="${stylePart}"`; +}; + +Parser.spSchoolAbvToStylePart = function (school) { // For prerelease/homebrew + return Parser._spSchoolAbvToStylePart_prereleaseBrew({school, brewUtil: PrereleaseUtil}) + || Parser._spSchoolAbvToStylePart_prereleaseBrew({school, brewUtil: BrewUtil2}) + || ""; +}; + +Parser._spSchoolAbvToStylePart_prereleaseBrew = function ({school, brewUtil}) { + const rawColor = brewUtil.getMetaLookup("spellSchools")?.[school]?.color; + if (!rawColor || !rawColor.trim()) return ""; + const validColor = BrewUtilShared.getValidColor(rawColor); + if (validColor.length) return `color: #${validColor};`; +}; + +Parser.getOrdinalForm = function (i) { + i = Number(i); + if (isNaN(i)) return ""; + const j = i % 10; const k = i % 100; + if (j === 1 && k !== 11) return `${i}st`; + if (j === 2 && k !== 12) return `${i}nd`; + if (j === 3 && k !== 13) return `${i}rd`; + return `${i}th`; +}; + +Parser.spLevelToFull = function (level) { + if (level === 0) return "Cantrip"; + else return Parser.getOrdinalForm(level); +}; + +Parser.getArticle = function (str) { + str = `${str}`; + str = str.replace(/\d+/g, (...m) => Parser.numberToText(m[0])); + return /^[aeiou]/i.test(str) ? "an" : "a"; +}; + +Parser.spLevelToFullLevelText = function (level, {isDash = false, isPluralCantrips = true} = {}) { + return `${Parser.spLevelToFull(level)}${(level === 0 ? (isPluralCantrips ? "s" : "") : `${isDash ? "-" : " "}level`)}`; +}; + +Parser.spLevelToSpellPoints = function (lvl) { + lvl = Number(lvl); + if (isNaN(lvl) || lvl === 0) return 0; + return Math.ceil(1.34 * lvl); +}; + +Parser.spMetaToArr = function (meta) { + if (!meta) return []; + return Object.entries(meta) + .filter(([_, v]) => v) + .sort(SortUtil.ascSort) + .map(([k]) => k); +}; + +Parser.spMetaToFull = function (meta) { + if (!meta) return ""; + const metaTags = Parser.spMetaToArr(meta); + if (metaTags.length) return ` (${metaTags.join(", ")})`; + return ""; +}; + +Parser.spLevelSchoolMetaToFull = function (level, school, meta, subschools) { + const levelPart = level === 0 ? Parser.spLevelToFull(level).toLowerCase() : `${Parser.spLevelToFull(level)}-level`; + const levelSchoolStr = level === 0 ? `${Parser.spSchoolAbvToFull(school)} ${levelPart}` : `${levelPart} ${Parser.spSchoolAbvToFull(school).toLowerCase()}`; + + const metaArr = Parser.spMetaToArr(meta); + if (metaArr.length || (subschools && subschools.length)) { + const metaAndSubschoolPart = [ + (subschools || []).map(sub => Parser.spSchoolAbvToFull(sub)).join(", "), + metaArr.join(", "), + ].filter(Boolean).join("; ").toLowerCase(); + return `${levelSchoolStr} (${metaAndSubschoolPart})`; + } + return levelSchoolStr; +}; + +Parser.spTimeListToFull = function (times, isStripTags) { + return times.map(t => `${Parser.getTimeToFull(t)}${t.condition ? `, ${isStripTags ? Renderer.stripTags(t.condition) : Renderer.get().render(t.condition)}` : ""}`).join(" or "); +}; + +Parser.getTimeToFull = function (time) { + return `${time.number ? `${time.number} ` : ""}${time.unit === "bonus" ? "bonus action" : time.unit}${time.number > 1 ? "s" : ""}`; +}; + +Parser.getMinutesToFull = function (mins, {isShort = false} = {}) { + const days = Math.floor(mins / (24 * 60)); + mins = mins % (24 * 60); + + const hours = Math.floor(mins / 60); + mins = mins % 60; + + return [ + days ? `${days} ${isShort ? `d` : `day${days > 1 ? "s" : ""}`}` : null, + hours ? `${hours} ${isShort ? `h` : `hour${hours > 1 ? "s" : ""}`}` : null, + mins ? `${mins} ${isShort ? `m` : `minute${mins > 1 ? "s" : ""}`}` : null, + ].filter(Boolean) + .join(" "); +}; + +Parser.RNG_SPECIAL = "special"; +Parser.RNG_POINT = "point"; +Parser.RNG_LINE = "line"; +Parser.RNG_CUBE = "cube"; +Parser.RNG_CONE = "cone"; +Parser.RNG_RADIUS = "radius"; +Parser.RNG_SPHERE = "sphere"; +Parser.RNG_HEMISPHERE = "hemisphere"; +Parser.RNG_CYLINDER = "cylinder"; // homebrew only +Parser.RNG_SELF = "self"; +Parser.RNG_SIGHT = "sight"; +Parser.RNG_UNLIMITED = "unlimited"; +Parser.RNG_UNLIMITED_SAME_PLANE = "plane"; +Parser.RNG_TOUCH = "touch"; +Parser.SP_RANGE_TYPE_TO_FULL = { + [Parser.RNG_SPECIAL]: "Special", + [Parser.RNG_POINT]: "Point", + [Parser.RNG_LINE]: "Line", + [Parser.RNG_CUBE]: "Cube", + [Parser.RNG_CONE]: "Cone", + [Parser.RNG_RADIUS]: "Radius", + [Parser.RNG_SPHERE]: "Sphere", + [Parser.RNG_HEMISPHERE]: "Hemisphere", + [Parser.RNG_CYLINDER]: "Cylinder", + [Parser.RNG_SELF]: "Self", + [Parser.RNG_SIGHT]: "Sight", + [Parser.RNG_UNLIMITED]: "Unlimited", + [Parser.RNG_UNLIMITED_SAME_PLANE]: "Unlimited on the same plane", + [Parser.RNG_TOUCH]: "Touch", +}; + +Parser.spRangeTypeToFull = function (range) { + return Parser._parse_aToB(Parser.SP_RANGE_TYPE_TO_FULL, range); +}; + +Parser.UNT_FEET = "feet"; +Parser.UNT_YARDS = "yards"; +Parser.UNT_MILES = "miles"; +Parser.SP_DIST_TYPE_TO_FULL = { + [Parser.UNT_FEET]: "Feet", + [Parser.UNT_YARDS]: "Yards", + [Parser.UNT_MILES]: "Miles", + [Parser.RNG_SELF]: Parser.SP_RANGE_TYPE_TO_FULL[Parser.RNG_SELF], + [Parser.RNG_TOUCH]: Parser.SP_RANGE_TYPE_TO_FULL[Parser.RNG_TOUCH], + [Parser.RNG_SIGHT]: Parser.SP_RANGE_TYPE_TO_FULL[Parser.RNG_SIGHT], + [Parser.RNG_UNLIMITED]: Parser.SP_RANGE_TYPE_TO_FULL[Parser.RNG_UNLIMITED], + [Parser.RNG_UNLIMITED_SAME_PLANE]: Parser.SP_RANGE_TYPE_TO_FULL[Parser.RNG_UNLIMITED_SAME_PLANE], +}; + +Parser.spDistanceTypeToFull = function (range) { + return Parser._parse_aToB(Parser.SP_DIST_TYPE_TO_FULL, range); +}; + +Parser.SP_RANGE_TO_ICON = { + [Parser.RNG_SPECIAL]: "fa-star", + [Parser.RNG_POINT]: "", + [Parser.RNG_LINE]: "fa-grip-lines-vertical", + [Parser.RNG_CUBE]: "fa-cube", + [Parser.RNG_CONE]: "fa-traffic-cone", + [Parser.RNG_RADIUS]: "fa-hockey-puck", + [Parser.RNG_SPHERE]: "fa-globe", + [Parser.RNG_HEMISPHERE]: "fa-globe", + [Parser.RNG_CYLINDER]: "fa-database", + [Parser.RNG_SELF]: "fa-street-view", + [Parser.RNG_SIGHT]: "fa-eye", + [Parser.RNG_UNLIMITED_SAME_PLANE]: "fa-globe-americas", + [Parser.RNG_UNLIMITED]: "fa-infinity", + [Parser.RNG_TOUCH]: "fa-hand-paper", +}; + +Parser.spRangeTypeToIcon = function (range) { + return Parser._parse_aToB(Parser.SP_RANGE_TO_ICON, range); +}; + +Parser.spRangeToShortHtml = function (range) { + switch (range.type) { + case Parser.RNG_SPECIAL: return ``; + case Parser.RNG_POINT: return Parser.spRangeToShortHtml._renderPoint(range); + case Parser.RNG_LINE: + case Parser.RNG_CUBE: + case Parser.RNG_CONE: + case Parser.RNG_RADIUS: + case Parser.RNG_SPHERE: + case Parser.RNG_HEMISPHERE: + case Parser.RNG_CYLINDER: + return Parser.spRangeToShortHtml._renderArea(range); + } +}; +Parser.spRangeToShortHtml._renderPoint = function (range) { + const dist = range.distance; + switch (dist.type) { + case Parser.RNG_SELF: + case Parser.RNG_SIGHT: + case Parser.RNG_UNLIMITED: + case Parser.RNG_UNLIMITED_SAME_PLANE: + case Parser.RNG_SPECIAL: + case Parser.RNG_TOUCH: return ``; + case Parser.UNT_FEET: + case Parser.UNT_YARDS: + case Parser.UNT_MILES: + default: + return `${dist.amount} ${Parser.getSingletonUnit(dist.type, true)}`; + } +}; +Parser.spRangeToShortHtml._renderArea = function (range) { + const size = range.distance; + return ` ${size.amount}-${Parser.getSingletonUnit(size.type, true)} ${Parser.spRangeToShortHtml._getAreaStyleString(range)}`; +}; +Parser.spRangeToShortHtml._getAreaStyleString = function (range) { + return ``; +}; + +Parser.spRangeToFull = function (range) { + switch (range.type) { + case Parser.RNG_SPECIAL: return Parser.spRangeTypeToFull(range.type); + case Parser.RNG_POINT: return Parser.spRangeToFull._renderPoint(range); + case Parser.RNG_LINE: + case Parser.RNG_CUBE: + case Parser.RNG_CONE: + case Parser.RNG_RADIUS: + case Parser.RNG_SPHERE: + case Parser.RNG_HEMISPHERE: + case Parser.RNG_CYLINDER: + return Parser.spRangeToFull._renderArea(range); + } +}; +Parser.spRangeToFull._renderPoint = function (range) { + const dist = range.distance; + switch (dist.type) { + case Parser.RNG_SELF: + case Parser.RNG_SIGHT: + case Parser.RNG_UNLIMITED: + case Parser.RNG_UNLIMITED_SAME_PLANE: + case Parser.RNG_SPECIAL: + case Parser.RNG_TOUCH: return Parser.spRangeTypeToFull(dist.type); + case Parser.UNT_FEET: + case Parser.UNT_YARDS: + case Parser.UNT_MILES: + default: + return `${dist.amount} ${dist.amount === 1 ? Parser.getSingletonUnit(dist.type) : dist.type}`; + } +}; +Parser.spRangeToFull._renderArea = function (range) { + const size = range.distance; + return `Self (${size.amount}-${Parser.getSingletonUnit(size.type)}${Parser.spRangeToFull._getAreaStyleString(range)}${range.type === Parser.RNG_CYLINDER ? `${size.amountSecondary != null && size.typeSecondary != null ? `, ${size.amountSecondary}-${Parser.getSingletonUnit(size.typeSecondary)}-high` : ""} cylinder` : ""})`; +}; +Parser.spRangeToFull._getAreaStyleString = function (range) { + switch (range.type) { + case Parser.RNG_SPHERE: return " radius"; + case Parser.RNG_HEMISPHERE: return `-radius ${range.type}`; + case Parser.RNG_CYLINDER: return "-radius"; + default: return ` ${range.type}`; + } +}; + +Parser.getSingletonUnit = function (unit, isShort) { + switch (unit) { + case Parser.UNT_FEET: + return isShort ? "ft." : "foot"; + case Parser.UNT_YARDS: + return isShort ? "yd." : "yard"; + case Parser.UNT_MILES: + return isShort ? "mi." : "mile"; + default: { + const fromPrerelease = Parser._getSingletonUnit_prereleaseBrew({unit, isShort, brewUtil: PrereleaseUtil}); + if (fromPrerelease) return fromPrerelease; + + const fromBrew = Parser._getSingletonUnit_prereleaseBrew({unit, isShort, brewUtil: BrewUtil2}); + if (fromBrew) return fromBrew; + + if (unit.charAt(unit.length - 1) === "s") return unit.slice(0, -1); + return unit; + } + } +}; + +Parser._getSingletonUnit_prereleaseBrew = function ({unit, isShort, brewUtil}) { + const fromBrew = brewUtil.getMetaLookup("spellDistanceUnits")?.[unit]?.["singular"]; + if (fromBrew) return fromBrew; +}; + +Parser.RANGE_TYPES = [ + {type: Parser.RNG_POINT, hasDistance: true, isRequireAmount: false}, + + {type: Parser.RNG_LINE, hasDistance: true, isRequireAmount: true}, + {type: Parser.RNG_CUBE, hasDistance: true, isRequireAmount: true}, + {type: Parser.RNG_CONE, hasDistance: true, isRequireAmount: true}, + {type: Parser.RNG_RADIUS, hasDistance: true, isRequireAmount: true}, + {type: Parser.RNG_SPHERE, hasDistance: true, isRequireAmount: true}, + {type: Parser.RNG_HEMISPHERE, hasDistance: true, isRequireAmount: true}, + {type: Parser.RNG_CYLINDER, hasDistance: true, isRequireAmount: true}, + + {type: Parser.RNG_SPECIAL, hasDistance: false, isRequireAmount: false}, +]; + +Parser.DIST_TYPES = [ + {type: Parser.RNG_SELF, hasAmount: false}, + {type: Parser.RNG_TOUCH, hasAmount: false}, + + {type: Parser.UNT_FEET, hasAmount: true}, + {type: Parser.UNT_YARDS, hasAmount: true}, + {type: Parser.UNT_MILES, hasAmount: true}, + + {type: Parser.RNG_SIGHT, hasAmount: false}, + {type: Parser.RNG_UNLIMITED_SAME_PLANE, hasAmount: false}, + {type: Parser.RNG_UNLIMITED, hasAmount: false}, +]; + +Parser.spComponentsToFull = function (comp, level, {isPlainText = false} = {}) { + if (!comp) return "None"; + const out = []; + if (comp.v) out.push("V"); + if (comp.s) out.push("S"); + if (comp.m != null) { + const fnRender = isPlainText ? Renderer.stripTags.bind(Renderer) : Renderer.get().render.bind(Renderer.get()); + out.push(`M${comp.m !== true ? ` (${fnRender(comp.m.text != null ? comp.m.text : comp.m)})` : ""}`); + } + if (comp.r) out.push(`R (${level} gp)`); + return out.join(", ") || "None"; +}; + +Parser.SP_END_TYPE_TO_FULL = { + "dispel": "dispelled", + "trigger": "triggered", + "discharge": "discharged", +}; +Parser.spEndTypeToFull = function (type) { + return Parser._parse_aToB(Parser.SP_END_TYPE_TO_FULL, type); +}; + +Parser.spDurationToFull = function (dur) { + let hasSubOr = false; + const outParts = dur.map(d => { + switch (d.type) { + case "special": + return "Special"; + case "instant": + return `Instantaneous${d.condition ? ` (${d.condition})` : ""}`; + case "timed": + return `${d.concentration ? "Concentration, " : ""}${d.concentration ? "u" : d.duration.upTo ? "U" : ""}${d.concentration || d.duration.upTo ? "p to " : ""}${d.duration.amount} ${d.duration.amount === 1 ? d.duration.type : `${d.duration.type}s`}`; + case "permanent": { + if (d.ends) { + const endsToJoin = d.ends.map(m => Parser.spEndTypeToFull(m)); + hasSubOr = hasSubOr || endsToJoin.length > 1; + return `Until ${endsToJoin.joinConjunct(", ", " or ")}`; + } else { + return "Permanent"; + } + } + } + }); + return `${outParts.joinConjunct(hasSubOr ? "; " : ", ", " or ")}${dur.length > 1 ? " (see below)" : ""}`; +}; + +Parser.DURATION_TYPES = [ + {type: "instant", full: "Instantaneous"}, + {type: "timed", hasAmount: true}, + {type: "permanent", hasEnds: true}, + {type: "special"}, +]; + +Parser.DURATION_AMOUNT_TYPES = [ + "turn", + "round", + "minute", + "hour", + "day", + "week", + "month", + "year", +]; + +Parser.spClassesToFull = function (sp, {isTextOnly = false, subclassLookup = {}} = {}) { + const fromSubclassList = Renderer.spell.getCombinedClasses(sp, "fromSubclass"); + const fromSubclasses = Parser.spSubclassesToFull(fromSubclassList, {isTextOnly, subclassLookup}); + const fromClassList = Renderer.spell.getCombinedClasses(sp, "fromClassList"); + return `${Parser.spMainClassesToFull(fromClassList, {isTextOnly})}${fromSubclasses ? `, ${fromSubclasses}` : ""}`; +}; + +Parser.spMainClassesToFull = function (fromClassList, {isTextOnly = false} = {}) { + return fromClassList + .map(c => ({hash: UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_CLASSES](c), c})) + .filter(it => !ExcludeUtil.isInitialised || !ExcludeUtil.isExcluded(it.hash, "class", it.c.source)) + .sort((a, b) => SortUtil.ascSort(a.c.name, b.c.name)) + .map(it => { + if (isTextOnly) return it.c.name; + + return `${Renderer.get().render(`{@class ${it.c.name}|${it.c.source}}`)}`; + }) + .join(", ") || ""; +}; + +Parser.spSubclassesToFull = function (fromSubclassList, {isTextOnly = false, subclassLookup = {}} = {}) { + return fromSubclassList + .filter(mt => { + if (!ExcludeUtil.isInitialised) return true; + const excludeClass = ExcludeUtil.isExcluded(UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_CLASSES](mt.class), "class", mt.class.source); + if (excludeClass) return false; + + return !ExcludeUtil.isExcluded( + UrlUtil.URL_TO_HASH_BUILDER["subclass"]({ + shortName: mt.subclass.name, + source: mt.subclass.source, + className: mt.class.name, + classSource: mt.class.source, + }), + "subclass", + mt.subclass.source, + {isNoCount: true}, + ); + }) + .sort((a, b) => { + const byName = SortUtil.ascSort(a.class.name, b.class.name); + return byName || SortUtil.ascSort(a.subclass.name, b.subclass.name); + }) + .map(c => Parser._spSubclassItem({fromSubclass: c, isTextOnly})) + .join(", ") || ""; +}; + +Parser._spSubclassItem = function ({fromSubclass, isTextOnly}) { + const c = fromSubclass.class; + const sc = fromSubclass.subclass; + const text = `${sc.shortName}${sc.subSubclass ? ` (${sc.subSubclass})` : ""}`; + if (isTextOnly) return text; + + const classPart = `${Renderer.get().render(`{@class ${c.name}|${c.source}}`)}`; + + return `${Renderer.get().render(`{@class ${c.name}|${c.source}|${text}|${sc.shortName}|${sc.source}}`)} ${classPart}`; +}; + +Parser.SPELL_ATTACK_TYPE_TO_FULL = {}; +Parser.SPELL_ATTACK_TYPE_TO_FULL["M"] = "Melee"; +Parser.SPELL_ATTACK_TYPE_TO_FULL["R"] = "Ranged"; +Parser.SPELL_ATTACK_TYPE_TO_FULL["O"] = "Other/Unknown"; + +Parser.spAttackTypeToFull = function (type) { + return Parser._parse_aToB(Parser.SPELL_ATTACK_TYPE_TO_FULL, type); +}; + +Parser.SPELL_AREA_TYPE_TO_FULL = { + ST: "Single Target", + MT: "Multiple Targets", + C: "Cube", + N: "Cone", + Y: "Cylinder", + S: "Sphere", + R: "Circle", + Q: "Square", + L: "Line", + H: "Hemisphere", + W: "Wall", +}; +Parser.spAreaTypeToFull = function (type) { + return Parser._parse_aToB(Parser.SPELL_AREA_TYPE_TO_FULL, type); +}; + +Parser.SP_MISC_TAG_TO_FULL = { + HL: "Healing", + THP: "Grants Temporary Hit Points", + SGT: "Requires Sight", + PRM: "Permanent Effects", + SCL: "Scaling Effects", + SMN: "Summons Creature", + MAC: "Modifies AC", + TP: "Teleportation", + FMV: "Forced Movement", + RO: "Rollable Effects", + LGTS: "Creates Sunlight", + LGT: "Creates Light", + UBA: "Uses Bonus Action", + PS: "Plane Shifting", + OBS: "Obscures Vision", + DFT: "Difficult Terrain", + AAD: "Additional Attack Damage", + OBJ: "Affects Objects", + ADV: "Grants Advantage", +}; +Parser.spMiscTagToFull = function (type) { + return Parser._parse_aToB(Parser.SP_MISC_TAG_TO_FULL, type); +}; + +Parser.SP_CASTER_PROGRESSION_TO_FULL = { + full: "Full", + "1/2": "Half", + "1/3": "One-Third", + "pact": "Pact Magic", +}; +Parser.spCasterProgressionToFull = function (type) { + return Parser._parse_aToB(Parser.SP_CASTER_PROGRESSION_TO_FULL, type); +}; + +// mon-prefix functions are for parsing monster data, and shared with the roll20 script +Parser.monTypeToFullObj = function (type) { + const out = { + types: [], + tags: [], + asText: "", + asTextShort: "", + + typeSidekick: null, + tagsSidekick: [], + asTextSidekick: null, + }; + if (type == null) return out; + + // handles e.g. "fey" + if (typeof type === "string") { + out.types = [type]; + out.asText = type.toTitleCase(); + out.asTextShort = out.asText; + return out; + } + + if (type.type?.choose) { + out.types = type.type.choose; + } else { + out.types = [type.type]; + } + + if (type.swarmSize) { + out.tags.push("swarm"); + out.asText = `swarm of ${Parser.sizeAbvToFull(type.swarmSize)} ${out.types.map(typ => Parser.monTypeToPlural(typ).toTitleCase()).joinConjunct(", ", " or ")}`; + out.asTextShort = out.asText; + out.swarmSize = type.swarmSize; + } else { + out.asText = out.types.map(typ => typ.toTitleCase()).joinConjunct(", ", " or "); + out.asTextShort = out.asText; + } + + const tagMetas = Parser.monTypeToFullObj._getTagMetas(type.tags); + if (tagMetas.length) { + out.tags.push(...tagMetas.map(({filterTag}) => filterTag)); + const ptTags = ` (${tagMetas.map(({displayTag}) => displayTag).join(", ")})`; + out.asText += ptTags; + out.asTextShort += ptTags; + } + + if (type.note) out.asText += ` ${type.note}`; + + // region Sidekick + if (type.sidekickType) { + out.typeSidekick = type.sidekickType; + if (!type.sidekickHidden) out.asTextSidekick = `${type.sidekickType}`; + + const tagMetas = Parser.monTypeToFullObj._getTagMetas(type.sidekickTags); + if (tagMetas.length) { + out.tagsSidekick.push(...tagMetas.map(({filterTag}) => filterTag)); + if (!type.sidekickHidden) out.asTextSidekick += ` (${tagMetas.map(({displayTag}) => displayTag).join(", ")})`; + } + } + // endregion + + return out; +}; + +Parser.monTypeToFullObj._getTagMetas = (tags) => { + return tags + ? tags.map(tag => { + if (typeof tag === "string") { // handles e.g. "Fiend (Devil)" + return { + filterTag: tag.toLowerCase(), + displayTag: tag.toTitleCase(), + }; + } else { // handles e.g. "Humanoid (Chondathan Human)" + return { + filterTag: tag.tag.toLowerCase(), + displayTag: `${tag.prefix} ${tag.tag}`.toTitleCase(), + }; + } + }) + : []; +}; + +Parser.monTypeToPlural = function (type) { + return Parser._parse_aToB(Parser.MON_TYPE_TO_PLURAL, type); +}; + +Parser.monTypeFromPlural = function (type) { + return Parser._parse_bToA(Parser.MON_TYPE_TO_PLURAL, type); +}; + +Parser.monCrToFull = function (cr, {xp = null, isMythic = false} = {}) { + if (cr == null) return ""; + + if (typeof cr === "string") { + if (Parser.crToNumber(cr) >= VeCt.CR_CUSTOM) return `${cr}${xp != null ? ` (${xp} XP)` : ""}`; + + xp = xp != null ? Parser._addCommas(xp) : Parser.crToXp(cr); + return `${cr} (${xp} XP${isMythic ? `, or ${Parser.crToXp(cr, {isDouble: true})} XP as a mythic encounter` : ""})`; + } else { + const stack = [Parser.monCrToFull(cr.cr, {xp: cr.xp, isMythic})]; + if (cr.lair) stack.push(`${Parser.monCrToFull(cr.lair)} when encountered in lair`); + if (cr.coven) stack.push(`${Parser.monCrToFull(cr.coven)} when part of a coven`); + return stack.joinConjunct(", ", " or "); + } +}; + +Parser.getFullImmRes = function (toParse) { + if (!toParse?.length) return ""; + + let maxDepth = 0; + + function toString (it, depth = 0) { + maxDepth = Math.max(maxDepth, depth); + if (typeof it === "string") { + return it; + } else if (it.special) { + return it.special; + } else { + const stack = []; + + if (it.preNote) stack.push(it.preNote); + + const prop = it.immune ? "immune" : it.resist ? "resist" : it.vulnerable ? "vulnerable" : null; + if (prop) { + const toJoin = it[prop].length === Parser.DMG_TYPES.length && CollectionUtil.deepEquals(Parser.DMG_TYPES, it[prop]) + ? ["all damage"] + : it[prop].map(nxt => toString(nxt, depth + 1)); + stack.push(depth ? toJoin.join(maxDepth ? "; " : ", ") : toJoin.joinConjunct(", ", " and ")); + } + + if (it.note) stack.push(it.note); + + return stack.join(" "); + } + } + + const arr = toParse.map(it => toString(it)); + + if (arr.length <= 1) return arr.join(""); + + let out = ""; + for (let i = 0; i < arr.length - 1; ++i) { + const it = arr[i]; + const nxt = arr[i + 1]; + + const orig = toParse[i]; + const origNxt = toParse[i + 1]; + + out += it; + out += (it.includes(",") || nxt.includes(",") || (orig && orig.cond) || (origNxt && origNxt.cond)) ? "; " : ", "; + } + out += arr.last(); + return out; +}; + +Parser.getFullCondImm = function (condImm, {isPlainText = false, isEntry = false} = {}) { + if (isPlainText && isEntry) throw new Error(`Options "isPlainText" and "isEntry" are mutually exclusive!`); + + if (!condImm?.length) return ""; + + const render = condition => { + if (isPlainText) return condition; + const ent = `{@condition ${condition}}`; + if (isEntry) return ent; + return Renderer.get().render(ent); + }; + + return condImm + .map(it => { + if (it.special) return it.special; + if (it.conditionImmune) return `${it.preNote ? `${it.preNote} ` : ""}${it.conditionImmune.map(render).join(", ")}${it.note ? ` ${it.note}` : ""}`; + return render(it); + }) + .sort(SortUtil.ascSortLower).join(", "); +}; + +Parser.MON_SENSE_TAG_TO_FULL = { + "B": "blindsight", + "D": "darkvision", + "SD": "superior darkvision", + "T": "tremorsense", + "U": "truesight", +}; +Parser.monSenseTagToFull = function (tag) { + return Parser._parse_aToB(Parser.MON_SENSE_TAG_TO_FULL, tag); +}; + +Parser.MON_SPELLCASTING_TAG_TO_FULL = { + "P": "Psionics", + "I": "Innate", + "F": "Form Only", + "S": "Shared", + "O": "Other", + "CA": "Class, Artificer", + "CB": "Class, Bard", + "CC": "Class, Cleric", + "CD": "Class, Druid", + "CP": "Class, Paladin", + "CR": "Class, Ranger", + "CS": "Class, Sorcerer", + "CL": "Class, Warlock", + "CW": "Class, Wizard", +}; +Parser.monSpellcastingTagToFull = function (tag) { + return Parser._parse_aToB(Parser.MON_SPELLCASTING_TAG_TO_FULL, tag); +}; + +Parser.MON_MISC_TAG_TO_FULL = { + "AOE": "Has Areas of Effect", + "HPR": "Has HP Reduction", + "MW": "Has Weapon Attacks, Melee", + "RW": "Has Weapon Attacks, Ranged", + "MLW": "Has Melee Weapons", + "RNG": "Has Ranged Weapons", + "RCH": "Has Reach Attacks", + "THW": "Has Thrown Weapons", +}; +Parser.monMiscTagToFull = function (tag) { + return Parser._parse_aToB(Parser.MON_MISC_TAG_TO_FULL, tag); +}; + +Parser.MON_LANGUAGE_TAG_TO_FULL = { + "AB": "Abyssal", + "AQ": "Aquan", + "AU": "Auran", + "C": "Common", + "CE": "Celestial", + "CS": "Can't Speak Known Languages", + "D": "Dwarvish", + "DR": "Draconic", + "DS": "Deep Speech", + "DU": "Druidic", + "E": "Elvish", + "G": "Gnomish", + "GI": "Giant", + "GO": "Goblin", + "GTH": "Gith", + "H": "Halfling", + "I": "Infernal", + "IG": "Ignan", + "LF": "Languages Known in Life", + "O": "Orc", + "OTH": "Other", + "P": "Primordial", + "S": "Sylvan", + "T": "Terran", + "TC": "Thieves' cant", + "TP": "Telepathy", + "U": "Undercommon", + "X": "Any (Choose)", + "XX": "All", +}; +Parser.monLanguageTagToFull = function (tag) { + return Parser._parse_aToB(Parser.MON_LANGUAGE_TAG_TO_FULL, tag); +}; + +Parser.ENVIRONMENTS = ["arctic", "coastal", "desert", "forest", "grassland", "hill", "mountain", "swamp", "underdark", "underwater", "urban"]; + +// psi-prefix functions are for parsing psionic data, and shared with the roll20 script +Parser.PSI_ABV_TYPE_TALENT = "T"; +Parser.PSI_ABV_TYPE_DISCIPLINE = "D"; +Parser.PSI_ORDER_NONE = "None"; +Parser.psiTypeToFull = type => Parser.psiTypeToMeta(type).full; + +Parser.psiTypeToMeta = type => { + let out = {}; + if (type === Parser.PSI_ABV_TYPE_TALENT) out = {hasOrder: false, full: "Talent"}; + else if (type === Parser.PSI_ABV_TYPE_DISCIPLINE) out = {hasOrder: true, full: "Discipline"}; + else if (PrereleaseUtil.getMetaLookup("psionicTypes")?.[type]) out = MiscUtil.copyFast(PrereleaseUtil.getMetaLookup("psionicTypes")[type]); + else if (BrewUtil2.getMetaLookup("psionicTypes")?.[type]) out = MiscUtil.copyFast(BrewUtil2.getMetaLookup("psionicTypes")[type]); + out.full = out.full || "Unknown"; + out.short = out.short || out.full; + return out; +}; + +Parser.psiOrderToFull = (order) => { + return order === undefined ? Parser.PSI_ORDER_NONE : order; +}; + +Parser.prereqSpellToFull = function (spell, {isTextOnly = false} = {}) { + if (spell) { + const [text, suffix] = spell.split("#"); + if (!suffix) return isTextOnly ? spell : Renderer.get().render(`{@spell ${spell}}`); + else if (suffix === "c") return (isTextOnly ? Renderer.stripTags : Renderer.get().render.bind(Renderer.get()))(`{@spell ${text}} cantrip`); + else if (suffix === "x") return (isTextOnly ? Renderer.stripTags : Renderer.get().render.bind(Renderer.get()))("{@spell hex} spell or a warlock feature that curses"); + } else return VeCt.STR_NONE; +}; + +Parser.prereqPactToFull = function (pact) { + if (pact === "Chain") return "Pact of the Chain"; + if (pact === "Tome") return "Pact of the Tome"; + if (pact === "Blade") return "Pact of the Blade"; + if (pact === "Talisman") return "Pact of the Talisman"; + return pact; +}; + +Parser.prereqPatronToShort = function (patron) { + if (patron === "Any") return patron; + const mThe = /^The (.*?)$/.exec(patron); + if (mThe) return mThe[1]; + return patron; +}; + +// NOTE: These need to be reflected in omnidexer.js to be indexed +Parser.OPT_FEATURE_TYPE_TO_FULL = { + AI: "Artificer Infusion", + ED: "Elemental Discipline", + EI: "Eldritch Invocation", + MM: "Metamagic", + "MV": "Maneuver", + "MV:B": "Maneuver, Battle Master", + "MV:C2-UA": "Maneuver, Cavalier V2 (UA)", + "AS:V1-UA": "Arcane Shot, V1 (UA)", + "AS:V2-UA": "Arcane Shot, V2 (UA)", + "AS": "Arcane Shot", + OTH: "Other", + "FS:F": "Fighting Style; Fighter", + "FS:B": "Fighting Style; Bard", + "FS:P": "Fighting Style; Paladin", + "FS:R": "Fighting Style; Ranger", + "PB": "Pact Boon", + "OR": "Onomancy Resonant", + "RN": "Rune Knight Rune", + "AF": "Alchemical Formula", +}; + +Parser.optFeatureTypeToFull = function (type) { + if (Parser.OPT_FEATURE_TYPE_TO_FULL[type]) return Parser.OPT_FEATURE_TYPE_TO_FULL[type]; + if (PrereleaseUtil.getMetaLookup("optionalFeatureTypes")?.[type]) return PrereleaseUtil.getMetaLookup("optionalFeatureTypes")[type]; + if (BrewUtil2.getMetaLookup("optionalFeatureTypes")?.[type]) return BrewUtil2.getMetaLookup("optionalFeatureTypes")[type]; + return type; +}; + +Parser.CHAR_OPTIONAL_FEATURE_TYPE_TO_FULL = { + "SG": "Supernatural Gift", + "OF": "Optional Feature", + "DG": "Dark Gift", + "RF:B": "Replacement Feature: Background", + "CS": "Character Secret", // Specific to IDRotF (rules on page 14) +}; + +Parser.charCreationOptionTypeToFull = function (type) { + if (Parser.CHAR_OPTIONAL_FEATURE_TYPE_TO_FULL[type]) return Parser.CHAR_OPTIONAL_FEATURE_TYPE_TO_FULL[type]; + if (PrereleaseUtil.getMetaLookup("charOption")?.[type]) return PrereleaseUtil.getMetaLookup("charOption")[type]; + if (BrewUtil2.getMetaLookup("charOption")?.[type]) return BrewUtil2.getMetaLookup("charOption")[type]; + return type; +}; + +Parser.alignmentAbvToFull = function (alignment) { + if (!alignment) return null; // used in sidekicks + if (typeof alignment === "object") { + if (alignment.special != null) { + // use in MTF Sacred Statue + return alignment.special; + } else { + // e.g. `{alignment: ["N", "G"], chance: 50}` or `{alignment: ["N", "G"]}` + return `${alignment.alignment.map(a => Parser.alignmentAbvToFull(a)).join(" ")}${alignment.chance ? ` (${alignment.chance}%)` : ""}${alignment.note ? ` (${alignment.note})` : ""}`; + } + } else { + alignment = alignment.toUpperCase(); + switch (alignment) { + case "L": + return "lawful"; + case "N": + return "neutral"; + case "NX": + return "neutral (law/chaos axis)"; + case "NY": + return "neutral (good/evil axis)"; + case "C": + return "chaotic"; + case "G": + return "good"; + case "E": + return "evil"; + // "special" values + case "U": + return "unaligned"; + case "A": + return "any alignment"; + } + return alignment; + } +}; + +Parser.alignmentListToFull = function (alignList) { + if (!alignList) return ""; + if (alignList.some(it => typeof it !== "string")) { + if (alignList.some(it => typeof it === "string")) throw new Error(`Mixed alignment types: ${JSON.stringify(alignList)}`); + // filter out any nonexistent alignments, as we don't care about "alignment does not exist" if there are other alignments + alignList = alignList.filter(it => it.alignment === undefined || it.alignment != null); + return alignList.map(it => it.special != null || it.chance != null || it.note != null ? Parser.alignmentAbvToFull(it) : Parser.alignmentListToFull(it.alignment)).join(" or "); + } else { + // assume all single-length arrays can be simply parsed + if (alignList.length === 1) return Parser.alignmentAbvToFull(alignList[0]); + // a pair of abv's, e.g. "L" "G" + if (alignList.length === 2) { + return alignList.map(a => Parser.alignmentAbvToFull(a)).join(" "); + } + if (alignList.length === 3) { + if (alignList.includes("NX") && alignList.includes("NY") && alignList.includes("N")) return "any neutral alignment"; + } + // longer arrays should have a custom mapping + if (alignList.length === 5) { + if (!alignList.includes("G")) return "any non-good alignment"; + if (!alignList.includes("E")) return "any non-evil alignment"; + if (!alignList.includes("L")) return "any non-lawful alignment"; + if (!alignList.includes("C")) return "any non-chaotic alignment"; + } + if (alignList.length === 4) { + if (!alignList.includes("L") && !alignList.includes("NX")) return "any chaotic alignment"; + if (!alignList.includes("G") && !alignList.includes("NY")) return "any evil alignment"; + if (!alignList.includes("C") && !alignList.includes("NX")) return "any lawful alignment"; + if (!alignList.includes("E") && !alignList.includes("NY")) return "any good alignment"; + } + throw new Error(`Unmapped alignment: ${JSON.stringify(alignList)}`); + } +}; + +Parser.weightToFull = function (lbs, isSmallUnit) { + const tons = Math.floor(lbs / 2000); + lbs = lbs - (2000 * tons); + return [ + tons ? `${tons}${isSmallUnit ? `` : " "}ton${tons === 1 ? "" : "s"}${isSmallUnit ? `` : ""}` : null, + lbs ? `${lbs}${isSmallUnit ? `` : " "}lb.${isSmallUnit ? `` : ""}` : null, + ].filter(Boolean).join(", "); +}; + +Parser.RARITIES = ["common", "uncommon", "rare", "very rare", "legendary", "artifact"]; +Parser.ITEM_RARITIES = ["none", ...Parser.RARITIES, "varies", "unknown", "unknown (magic)", "other"]; + +Parser.CAT_ID_CREATURE = 1; +Parser.CAT_ID_SPELL = 2; +Parser.CAT_ID_BACKGROUND = 3; +Parser.CAT_ID_ITEM = 4; +Parser.CAT_ID_CLASS = 5; +Parser.CAT_ID_CONDITION = 6; +Parser.CAT_ID_FEAT = 7; +Parser.CAT_ID_ELDRITCH_INVOCATION = 8; +Parser.CAT_ID_PSIONIC = 9; +Parser.CAT_ID_RACE = 10; +Parser.CAT_ID_OTHER_REWARD = 11; +Parser.CAT_ID_VARIANT_OPTIONAL_RULE = 12; +Parser.CAT_ID_ADVENTURE = 13; +Parser.CAT_ID_DEITY = 14; +Parser.CAT_ID_OBJECT = 15; +Parser.CAT_ID_TRAP = 16; +Parser.CAT_ID_HAZARD = 17; +Parser.CAT_ID_QUICKREF = 18; +Parser.CAT_ID_CULT = 19; +Parser.CAT_ID_BOON = 20; +Parser.CAT_ID_DISEASE = 21; +Parser.CAT_ID_METAMAGIC = 22; +Parser.CAT_ID_MANEUVER_BATTLEMASTER = 23; +Parser.CAT_ID_TABLE = 24; +Parser.CAT_ID_TABLE_GROUP = 25; +Parser.CAT_ID_MANEUVER_CAVALIER = 26; +Parser.CAT_ID_ARCANE_SHOT = 27; +Parser.CAT_ID_OPTIONAL_FEATURE_OTHER = 28; +Parser.CAT_ID_FIGHTING_STYLE = 29; +Parser.CAT_ID_CLASS_FEATURE = 30; +Parser.CAT_ID_VEHICLE = 31; +Parser.CAT_ID_PACT_BOON = 32; +Parser.CAT_ID_ELEMENTAL_DISCIPLINE = 33; +Parser.CAT_ID_ARTIFICER_INFUSION = 34; +Parser.CAT_ID_SHIP_UPGRADE = 35; +Parser.CAT_ID_INFERNAL_WAR_MACHINE_UPGRADE = 36; +Parser.CAT_ID_ONOMANCY_RESONANT = 37; +Parser.CAT_ID_RUNE_KNIGHT_RUNE = 37; +Parser.CAT_ID_ALCHEMICAL_FORMULA = 38; +Parser.CAT_ID_MANEUVER = 39; +Parser.CAT_ID_SUBCLASS = 40; +Parser.CAT_ID_SUBCLASS_FEATURE = 41; +Parser.CAT_ID_ACTION = 42; +Parser.CAT_ID_LANGUAGE = 43; +Parser.CAT_ID_BOOK = 44; +Parser.CAT_ID_PAGE = 45; +Parser.CAT_ID_LEGENDARY_GROUP = 46; +Parser.CAT_ID_CHAR_CREATION_OPTIONS = 47; +Parser.CAT_ID_RECIPES = 48; +Parser.CAT_ID_STATUS = 49; +Parser.CAT_ID_SKILLS = 50; +Parser.CAT_ID_SENSES = 51; +Parser.CAT_ID_DECK = 52; +Parser.CAT_ID_CARD = 53; + +Parser.CAT_ID_TO_FULL = {}; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_CREATURE] = "Bestiary"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_SPELL] = "Spell"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_BACKGROUND] = "Background"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_ITEM] = "Item"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_CLASS] = "Class"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_CONDITION] = "Condition"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_FEAT] = "Feat"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_ELDRITCH_INVOCATION] = "Eldritch Invocation"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_PSIONIC] = "Psionic"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_RACE] = "Race"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_OTHER_REWARD] = "Other Reward"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_VARIANT_OPTIONAL_RULE] = "Variant/Optional Rule"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_ADVENTURE] = "Adventure"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_DEITY] = "Deity"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_OBJECT] = "Object"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_TRAP] = "Trap"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_HAZARD] = "Hazard"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_QUICKREF] = "Quick Reference"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_CULT] = "Cult"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_BOON] = "Boon"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_DISEASE] = "Disease"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_METAMAGIC] = "Metamagic"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_MANEUVER_BATTLEMASTER] = "Maneuver; Battlemaster"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_TABLE] = "Table"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_TABLE_GROUP] = "Table"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_MANEUVER_CAVALIER] = "Maneuver; Cavalier"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_ARCANE_SHOT] = "Arcane Shot"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_OPTIONAL_FEATURE_OTHER] = "Optional Feature"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_FIGHTING_STYLE] = "Fighting Style"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_CLASS_FEATURE] = "Class Feature"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_VEHICLE] = "Vehicle"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_PACT_BOON] = "Pact Boon"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_ELEMENTAL_DISCIPLINE] = "Elemental Discipline"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_ARTIFICER_INFUSION] = "Infusion"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_SHIP_UPGRADE] = "Ship Upgrade"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_INFERNAL_WAR_MACHINE_UPGRADE] = "Infernal War Machine Upgrade"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_ONOMANCY_RESONANT] = "Onomancy Resonant"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_RUNE_KNIGHT_RUNE] = "Rune Knight Rune"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_ALCHEMICAL_FORMULA] = "Alchemical Formula"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_MANEUVER] = "Maneuver"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_SUBCLASS] = "Subclass"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_SUBCLASS_FEATURE] = "Subclass Feature"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_ACTION] = "Action"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_LANGUAGE] = "Language"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_BOOK] = "Book"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_PAGE] = "Page"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_LEGENDARY_GROUP] = "Legendary Group"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_CHAR_CREATION_OPTIONS] = "Character Creation Option"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_RECIPES] = "Recipe"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_STATUS] = "Status"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_DECK] = "Deck"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_CARD] = "Card"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_SKILLS] = "Skill"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_SENSES] = "Sense"; + +Parser.pageCategoryToFull = function (catId) { + return Parser._parse_aToB(Parser.CAT_ID_TO_FULL, catId); +}; + +Parser.CAT_ID_TO_PROP = {}; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_CREATURE] = "monster"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_SPELL] = "spell"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_BACKGROUND] = "background"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_ITEM] = "item"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_CLASS] = "class"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_CONDITION] = "condition"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_FEAT] = "feat"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_PSIONIC] = "psionic"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_RACE] = "race"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_OTHER_REWARD] = "reward"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_VARIANT_OPTIONAL_RULE] = "variantrule"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_ADVENTURE] = "adventure"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_DEITY] = "deity"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_OBJECT] = "object"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_TRAP] = "trap"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_HAZARD] = "hazard"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_CULT] = "cult"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_BOON] = "boon"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_DISEASE] = "condition"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_TABLE] = "table"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_TABLE_GROUP] = "tableGroup"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_VEHICLE] = "vehicle"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_ELDRITCH_INVOCATION] = "optionalfeature"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_MANEUVER_CAVALIER] = "optionalfeature"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_ARCANE_SHOT] = "optionalfeature"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_OPTIONAL_FEATURE_OTHER] = "optionalfeature"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_FIGHTING_STYLE] = "optionalfeature"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_METAMAGIC] = "optionalfeature"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_MANEUVER_BATTLEMASTER] = "optionalfeature"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_PACT_BOON] = "optionalfeature"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_ELEMENTAL_DISCIPLINE] = "optionalfeature"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_ARTIFICER_INFUSION] = "optionalfeature"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_SHIP_UPGRADE] = "vehicleUpgrade"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_INFERNAL_WAR_MACHINE_UPGRADE] = "vehicleUpgrade"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_ONOMANCY_RESONANT] = "optionalfeature"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_RUNE_KNIGHT_RUNE] = "optionalfeature"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_ALCHEMICAL_FORMULA] = "optionalfeature"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_MANEUVER] = "optionalfeature"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_QUICKREF] = null; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_CLASS_FEATURE] = "classFeature"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_SUBCLASS] = "subclass"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_SUBCLASS_FEATURE] = "subclassFeature"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_ACTION] = "action"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_LANGUAGE] = "language"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_BOOK] = "book"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_PAGE] = null; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_LEGENDARY_GROUP] = "legendaryGroup"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_CHAR_CREATION_OPTIONS] = "charoption"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_RECIPES] = "recipe"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_STATUS] = "status"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_DECK] = "deck"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_CARD] = "card"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_SKILLS] = "skill"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_SENSES] = "sense"; + +Parser.pageCategoryToProp = function (catId) { + return Parser._parse_aToB(Parser.CAT_ID_TO_PROP, catId); +}; + +Parser.ABIL_ABVS = ["str", "dex", "con", "int", "wis", "cha"]; + +Parser.spClassesToCurrentAndLegacy = function (fromClassList) { + const current = []; + const legacy = []; + fromClassList.forEach(cls => { + if ((cls.name === "Artificer" && cls.source === "UAArtificer") || (cls.name === "Artificer (Revisited)" && cls.source === "UAArtificerRevisited")) legacy.push(cls); + else current.push(cls); + }); + return [current, legacy]; +}; + +/** + * Build a pair of strings; one with all current subclasses, one with all legacy subclasses + * + * @param sp a spell + * @param subclassLookup Data loaded from `generated/gendata-subclass-lookup.json`. Of the form: `{PHB: {Barbarian: {PHB: {Berserker: "Path of the Berserker"}}}}` + * @returns {*[]} A two-element array. First item is a string of all the current subclasses, second item a string of + * all the legacy/superseded subclasses + */ +Parser.spSubclassesToCurrentAndLegacyFull = function (sp, subclassLookup) { + return Parser._spSubclassesToCurrentAndLegacyFull({sp, subclassLookup, prop: "fromSubclass"}); +}; + +Parser.spVariantSubclassesToCurrentAndLegacyFull = function (sp, subclassLookup) { + return Parser._spSubclassesToCurrentAndLegacyFull({sp, subclassLookup, prop: "fromSubclassVariant"}); +}; + +Parser._spSubclassesToCurrentAndLegacyFull = ({sp, subclassLookup, prop}) => { + const fromSubclass = Renderer.spell.getCombinedClasses(sp, prop); + if (!fromSubclass.length) return ["", ""]; + + const current = []; + const legacy = []; + const curNames = new Set(); + const toCheck = []; + fromSubclass + .filter(c => { + const excludeClass = ExcludeUtil.isExcluded( + UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_CLASSES]({name: c.class.name, source: c.class.source}), + "class", + c.class.source, + {isNoCount: true}, + ); + if (excludeClass) return false; + + const excludeSubclass = ExcludeUtil.isExcluded( + UrlUtil.URL_TO_HASH_BUILDER["subclass"]({ + shortName: c.subclass.shortName, + source: c.subclass.source, + className: c.class.name, + classSource: c.class.source, + }), + "subclass", + c.subclass.source, + {isNoCount: true}, + ); + if (excludeSubclass) return false; + + return !Renderer.spell.isExcludedSubclassVariantSource({classDefinedInSource: c.class.definedInSource}); + }) + .sort((a, b) => { + const byName = SortUtil.ascSort(a.subclass.name, b.subclass.name); + return byName || SortUtil.ascSort(a.class.name, b.class.name); + }) + .forEach(c => { + const nm = c.subclass.name; + const src = c.subclass.source; + + const toAdd = Parser._spSubclassItem({fromSubclass: c, isTextOnly: false}); + + const fromLookup = MiscUtil.get( + subclassLookup, + c.class.source, + c.class.name, + c.subclass.source, + c.subclass.name, + ); + + if (fromLookup && fromLookup.isReprinted) { + legacy.push(toAdd); + } else if (SourceUtil.isNonstandardSource(src)) { + const cleanName = Parser._spSubclassesToCurrentAndLegacyFull.mapClassShortNameToMostRecent( + nm.split("(")[0].trim().split(/v\d+/)[0].trim(), + ); + toCheck.push({"name": cleanName, "ele": toAdd}); + } else { + current.push(toAdd); + curNames.add(nm); + } + }); + + toCheck.forEach(n => { + if (curNames.has(n.name)) { + legacy.push(n.ele); + } else { + current.push(n.ele); + } + }); + + return [current.join(", "), legacy.join(", ")]; +}; + +/** + * Get the most recent iteration of a subclass name. + */ +Parser._spSubclassesToCurrentAndLegacyFull.mapClassShortNameToMostRecent = (shortName) => { + switch (shortName) { + case "Favored Soul": return "Divine Soul"; + case "Undying Light": return "Celestial"; + case "Deep Stalker": return "Gloom Stalker"; + } + return shortName; +}; + +Parser.spVariantClassesToCurrentAndLegacy = function (fromVariantClassList) { + const current = []; + const legacy = []; + fromVariantClassList.forEach(cls => { + if (SourceUtil.isPrereleaseSource(cls.definedInSource)) legacy.push(cls); + else current.push(cls); + }); + return [current, legacy]; +}; + +Parser.attackTypeToFull = function (attackType) { + return Parser._parse_aToB(Parser.ATK_TYPE_TO_FULL, attackType); +}; + +Parser.trapHazTypeToFull = function (type) { + return Parser._parse_aToB(Parser.TRAP_HAZARD_TYPE_TO_FULL, type); +}; + +Parser.TRAP_HAZARD_TYPE_TO_FULL = { + MECH: "Mechanical Trap", + MAG: "Magical Trap", + SMPL: "Simple Trap", + CMPX: "Complex Trap", + HAZ: "Hazard", + WTH: "Weather", + ENV: "Environmental Hazard", + WLD: "Wilderness Hazard", + GEN: "Generic", + EST: "Eldritch Storm", +}; + +Parser.tierToFullLevel = function (tier) { + return Parser._parse_aToB(Parser.TIER_TO_FULL_LEVEL, tier); +}; + +Parser.TIER_TO_FULL_LEVEL = {}; +Parser.TIER_TO_FULL_LEVEL[1] = "1st\u20134th Level"; +Parser.TIER_TO_FULL_LEVEL[2] = "5th\u201310th Level"; +Parser.TIER_TO_FULL_LEVEL[3] = "11th\u201316th Level"; +Parser.TIER_TO_FULL_LEVEL[4] = "17th\u201320th Level"; + +Parser.trapInitToFull = function (init) { + return Parser._parse_aToB(Parser.TRAP_INIT_TO_FULL, init); +}; + +Parser.TRAP_INIT_TO_FULL = {}; +Parser.TRAP_INIT_TO_FULL[1] = "initiative count 10"; +Parser.TRAP_INIT_TO_FULL[2] = "initiative count 20"; +Parser.TRAP_INIT_TO_FULL[3] = "initiative count 20 and initiative count 10"; + +Parser.ATK_TYPE_TO_FULL = {}; +Parser.ATK_TYPE_TO_FULL["MW"] = "Melee Weapon Attack"; +Parser.ATK_TYPE_TO_FULL["RW"] = "Ranged Weapon Attack"; + +Parser.bookOrdinalToAbv = (ordinal, preNoSuff) => { + if (ordinal === undefined) return ""; + switch (ordinal.type) { + case "part": return `${preNoSuff ? " " : ""}Part ${ordinal.identifier}${preNoSuff ? "" : " \u2014 "}`; + case "chapter": return `${preNoSuff ? " " : ""}Ch. ${ordinal.identifier}${preNoSuff ? "" : ": "}`; + case "episode": return `${preNoSuff ? " " : ""}Ep. ${ordinal.identifier}${preNoSuff ? "" : ": "}`; + case "appendix": return `${preNoSuff ? " " : ""}App.${ordinal.identifier != null ? ` ${ordinal.identifier}` : ""}${preNoSuff ? "" : ": "}`; + case "level": return `${preNoSuff ? " " : ""}Level ${ordinal.identifier}${preNoSuff ? "" : ": "}`; + default: throw new Error(`Unhandled ordinal type "${ordinal.type}"`); + } +}; + +Parser.IMAGE_TYPE_TO_FULL = { + "map": "Map", + "mapPlayer": "Map (Player)", +}; +Parser.imageTypeToFull = function (imageType) { + return Parser._parse_aToB(Parser.IMAGE_TYPE_TO_FULL, imageType, "Other"); +}; + +Parser.nameToTokenName = function (name) { + return name + .toAscii() + .replace(/"/g, ""); +}; + +Parser.bytesToHumanReadable = function (bytes, {fixedDigits = 2} = {}) { + if (bytes == null) return ""; + if (!bytes) return "0 B"; + const e = Math.floor(Math.log(bytes) / Math.log(1024)); + return `${(bytes / Math.pow(1024, e)).toFixed(fixedDigits)} ${`\u200bKMGTP`.charAt(e)}B`; +}; + +Parser.SKL_ABV_ABJ = "A"; +Parser.SKL_ABV_EVO = "V"; +Parser.SKL_ABV_ENC = "E"; +Parser.SKL_ABV_ILL = "I"; +Parser.SKL_ABV_DIV = "D"; +Parser.SKL_ABV_NEC = "N"; +Parser.SKL_ABV_TRA = "T"; +Parser.SKL_ABV_CON = "C"; +Parser.SKL_ABV_PSI = "P"; +Parser.SKL_ABVS = [ + Parser.SKL_ABV_ABJ, + Parser.SKL_ABV_CON, + Parser.SKL_ABV_DIV, + Parser.SKL_ABV_ENC, + Parser.SKL_ABV_EVO, + Parser.SKL_ABV_ILL, + Parser.SKL_ABV_NEC, + Parser.SKL_ABV_PSI, + Parser.SKL_ABV_TRA, +]; + +Parser.SP_TM_ACTION = "action"; +Parser.SP_TM_B_ACTION = "bonus"; +Parser.SP_TM_REACTION = "reaction"; +Parser.SP_TM_ROUND = "round"; +Parser.SP_TM_MINS = "minute"; +Parser.SP_TM_HRS = "hour"; +Parser.SP_TIME_SINGLETONS = [Parser.SP_TM_ACTION, Parser.SP_TM_B_ACTION, Parser.SP_TM_REACTION, Parser.SP_TM_ROUND]; +Parser.SP_TIME_TO_FULL = { + [Parser.SP_TM_ACTION]: "Action", + [Parser.SP_TM_B_ACTION]: "Bonus Action", + [Parser.SP_TM_REACTION]: "Reaction", + [Parser.SP_TM_ROUND]: "Rounds", + [Parser.SP_TM_MINS]: "Minutes", + [Parser.SP_TM_HRS]: "Hours", +}; +Parser.spTimeUnitToFull = function (timeUnit) { + return Parser._parse_aToB(Parser.SP_TIME_TO_FULL, timeUnit); +}; + +Parser.SP_TIME_TO_SHORT = { + [Parser.SP_TM_ROUND]: "Rnd.", + [Parser.SP_TM_MINS]: "Min.", + [Parser.SP_TM_HRS]: "Hr.", +}; +Parser.spTimeUnitToShort = function (timeUnit) { + return Parser._parse_aToB(Parser.SP_TIME_TO_SHORT, timeUnit); +}; + +Parser.SP_TIME_TO_ABV = { + [Parser.SP_TM_ACTION]: "A", + [Parser.SP_TM_B_ACTION]: "BA", + [Parser.SP_TM_REACTION]: "R", + [Parser.SP_TM_ROUND]: "rnd", + [Parser.SP_TM_MINS]: "min", + [Parser.SP_TM_HRS]: "hr", +}; +Parser.spTimeUnitToAbv = function (timeUnit) { + return Parser._parse_aToB(Parser.SP_TIME_TO_ABV, timeUnit); +}; + +Parser.spTimeToShort = function (time, isHtml) { + if (!time) return ""; + return (time.number === 1 && Parser.SP_TIME_SINGLETONS.includes(time.unit)) + ? `${Parser.spTimeUnitToAbv(time.unit).uppercaseFirst()}${time.condition ? "*" : ""}` + : `${time.number} ${isHtml ? `` : ""}${Parser.spTimeUnitToAbv(time.unit)}${isHtml ? `` : ""}${time.condition ? "*" : ""}`; +}; + +Parser.SKL_ABJ = "Abjuration"; +Parser.SKL_EVO = "Evocation"; +Parser.SKL_ENC = "Enchantment"; +Parser.SKL_ILL = "Illusion"; +Parser.SKL_DIV = "Divination"; +Parser.SKL_NEC = "Necromancy"; +Parser.SKL_TRA = "Transmutation"; +Parser.SKL_CON = "Conjuration"; +Parser.SKL_PSI = "Psionic"; + +Parser.SP_SCHOOL_ABV_TO_FULL = {}; +Parser.SP_SCHOOL_ABV_TO_FULL[Parser.SKL_ABV_ABJ] = Parser.SKL_ABJ; +Parser.SP_SCHOOL_ABV_TO_FULL[Parser.SKL_ABV_EVO] = Parser.SKL_EVO; +Parser.SP_SCHOOL_ABV_TO_FULL[Parser.SKL_ABV_ENC] = Parser.SKL_ENC; +Parser.SP_SCHOOL_ABV_TO_FULL[Parser.SKL_ABV_ILL] = Parser.SKL_ILL; +Parser.SP_SCHOOL_ABV_TO_FULL[Parser.SKL_ABV_DIV] = Parser.SKL_DIV; +Parser.SP_SCHOOL_ABV_TO_FULL[Parser.SKL_ABV_NEC] = Parser.SKL_NEC; +Parser.SP_SCHOOL_ABV_TO_FULL[Parser.SKL_ABV_TRA] = Parser.SKL_TRA; +Parser.SP_SCHOOL_ABV_TO_FULL[Parser.SKL_ABV_CON] = Parser.SKL_CON; +Parser.SP_SCHOOL_ABV_TO_FULL[Parser.SKL_ABV_PSI] = Parser.SKL_PSI; + +Parser.SP_SCHOOL_ABV_TO_SHORT = {}; +Parser.SP_SCHOOL_ABV_TO_SHORT[Parser.SKL_ABV_ABJ] = "Abj."; +Parser.SP_SCHOOL_ABV_TO_SHORT[Parser.SKL_ABV_EVO] = "Evoc."; +Parser.SP_SCHOOL_ABV_TO_SHORT[Parser.SKL_ABV_ENC] = "Ench."; +Parser.SP_SCHOOL_ABV_TO_SHORT[Parser.SKL_ABV_ILL] = "Illu."; +Parser.SP_SCHOOL_ABV_TO_SHORT[Parser.SKL_ABV_DIV] = "Divin."; +Parser.SP_SCHOOL_ABV_TO_SHORT[Parser.SKL_ABV_NEC] = "Necro."; +Parser.SP_SCHOOL_ABV_TO_SHORT[Parser.SKL_ABV_TRA] = "Trans."; +Parser.SP_SCHOOL_ABV_TO_SHORT[Parser.SKL_ABV_CON] = "Conj."; +Parser.SP_SCHOOL_ABV_TO_SHORT[Parser.SKL_ABV_PSI] = "Psi."; + +Parser.ATB_ABV_TO_FULL = { + "str": "Strength", + "dex": "Dexterity", + "con": "Constitution", + "int": "Intelligence", + "wis": "Wisdom", + "cha": "Charisma", +}; + +Parser.TP_ABERRATION = "aberration"; +Parser.TP_BEAST = "beast"; +Parser.TP_CELESTIAL = "celestial"; +Parser.TP_CONSTRUCT = "construct"; +Parser.TP_DRAGON = "dragon"; +Parser.TP_ELEMENTAL = "elemental"; +Parser.TP_FEY = "fey"; +Parser.TP_FIEND = "fiend"; +Parser.TP_GIANT = "giant"; +Parser.TP_HUMANOID = "humanoid"; +Parser.TP_MONSTROSITY = "monstrosity"; +Parser.TP_OOZE = "ooze"; +Parser.TP_PLANT = "plant"; +Parser.TP_UNDEAD = "undead"; +Parser.MON_TYPES = [Parser.TP_ABERRATION, Parser.TP_BEAST, Parser.TP_CELESTIAL, Parser.TP_CONSTRUCT, Parser.TP_DRAGON, Parser.TP_ELEMENTAL, Parser.TP_FEY, Parser.TP_FIEND, Parser.TP_GIANT, Parser.TP_HUMANOID, Parser.TP_MONSTROSITY, Parser.TP_OOZE, Parser.TP_PLANT, Parser.TP_UNDEAD]; +Parser.MON_TYPE_TO_PLURAL = {}; +Parser.MON_TYPE_TO_PLURAL[Parser.TP_ABERRATION] = "aberrations"; +Parser.MON_TYPE_TO_PLURAL[Parser.TP_BEAST] = "beasts"; +Parser.MON_TYPE_TO_PLURAL[Parser.TP_CELESTIAL] = "celestials"; +Parser.MON_TYPE_TO_PLURAL[Parser.TP_CONSTRUCT] = "constructs"; +Parser.MON_TYPE_TO_PLURAL[Parser.TP_DRAGON] = "dragons"; +Parser.MON_TYPE_TO_PLURAL[Parser.TP_ELEMENTAL] = "elementals"; +Parser.MON_TYPE_TO_PLURAL[Parser.TP_FEY] = "fey"; +Parser.MON_TYPE_TO_PLURAL[Parser.TP_FIEND] = "fiends"; +Parser.MON_TYPE_TO_PLURAL[Parser.TP_GIANT] = "giants"; +Parser.MON_TYPE_TO_PLURAL[Parser.TP_HUMANOID] = "humanoids"; +Parser.MON_TYPE_TO_PLURAL[Parser.TP_MONSTROSITY] = "monstrosities"; +Parser.MON_TYPE_TO_PLURAL[Parser.TP_OOZE] = "oozes"; +Parser.MON_TYPE_TO_PLURAL[Parser.TP_PLANT] = "plants"; +Parser.MON_TYPE_TO_PLURAL[Parser.TP_UNDEAD] = "undead"; + +Parser.SZ_FINE = "F"; +Parser.SZ_DIMINUTIVE = "D"; +Parser.SZ_TINY = "T"; +Parser.SZ_SMALL = "S"; +Parser.SZ_MEDIUM = "M"; +Parser.SZ_LARGE = "L"; +Parser.SZ_HUGE = "H"; +Parser.SZ_GARGANTUAN = "G"; +Parser.SZ_COLOSSAL = "C"; +Parser.SZ_VARIES = "V"; +Parser.SIZE_ABVS = [Parser.SZ_TINY, Parser.SZ_SMALL, Parser.SZ_MEDIUM, Parser.SZ_LARGE, Parser.SZ_HUGE, Parser.SZ_GARGANTUAN, Parser.SZ_VARIES]; +Parser.SIZE_ABV_TO_FULL = {}; +Parser.SIZE_ABV_TO_FULL[Parser.SZ_FINE] = "Fine"; +Parser.SIZE_ABV_TO_FULL[Parser.SZ_DIMINUTIVE] = "Diminutive"; +Parser.SIZE_ABV_TO_FULL[Parser.SZ_TINY] = "Tiny"; +Parser.SIZE_ABV_TO_FULL[Parser.SZ_SMALL] = "Small"; +Parser.SIZE_ABV_TO_FULL[Parser.SZ_MEDIUM] = "Medium"; +Parser.SIZE_ABV_TO_FULL[Parser.SZ_LARGE] = "Large"; +Parser.SIZE_ABV_TO_FULL[Parser.SZ_HUGE] = "Huge"; +Parser.SIZE_ABV_TO_FULL[Parser.SZ_GARGANTUAN] = "Gargantuan"; +Parser.SIZE_ABV_TO_FULL[Parser.SZ_COLOSSAL] = "Colossal"; +Parser.SIZE_ABV_TO_FULL[Parser.SZ_VARIES] = "Varies"; + +Parser.XP_CHART_ALT = { + "0": 10, + "1/8": 25, + "1/4": 50, + "1/2": 100, + "1": 200, + "2": 450, + "3": 700, + "4": 1100, + "5": 1800, + "6": 2300, + "7": 2900, + "8": 3900, + "9": 5000, + "10": 5900, + "11": 7200, + "12": 8400, + "13": 10000, + "14": 11500, + "15": 13000, + "16": 15000, + "17": 18000, + "18": 20000, + "19": 22000, + "20": 25000, + "21": 33000, + "22": 41000, + "23": 50000, + "24": 62000, + "25": 75000, + "26": 90000, + "27": 105000, + "28": 120000, + "29": 135000, + "30": 155000, +}; + +Parser.ARMOR_ABV_TO_FULL = { + "l.": "light", + "m.": "medium", + "h.": "heavy", +}; + +Parser.WEAPON_ABV_TO_FULL = { + "s.": "simple", + "m.": "martial", +}; + +Parser.CONDITION_TO_COLOR = { + "Blinded": "#525252", + "Charmed": "#f01789", + "Deafened": "#ababab", + "Exhausted": "#947a47", + "Frightened": "#c9ca18", + "Grappled": "#8784a0", + "Incapacitated": "#3165a0", + "Invisible": "#7ad2d6", + "Paralyzed": "#c00900", + "Petrified": "#a0a0a0", + "Poisoned": "#4dc200", + "Prone": "#5e60a0", + "Restrained": "#d98000", + "Stunned": "#a23bcb", + "Unconscious": "#3a40ad", + + "Concentration": "#009f7a", +}; + +Parser.RULE_TYPE_TO_FULL = { + "O": "Optional", + "P": "Prerelease", + "V": "Variant", + "VO": "Variant Optional", + "VV": "Variant Variant", + "U": "Unknown", +}; + +Parser.ruleTypeToFull = function (ruleType) { + return Parser._parse_aToB(Parser.RULE_TYPE_TO_FULL, ruleType); +}; + +Parser.VEHICLE_TYPE_TO_FULL = { + "SHIP": "Ship", + "SPELLJAMMER": "Spelljammer Ship", + "INFWAR": "Infernal War Machine", + "CREATURE": "Creature", + "OBJECT": "Object", + "SHP:H": "Ship Upgrade, Hull", + "SHP:M": "Ship Upgrade, Movement", + "SHP:W": "Ship Upgrade, Weapon", + "SHP:F": "Ship Upgrade, Figurehead", + "SHP:O": "Ship Upgrade, Miscellaneous", + "IWM:W": "Infernal War Machine Variant, Weapon", + "IWM:A": "Infernal War Machine Upgrade, Armor", + "IWM:G": "Infernal War Machine Upgrade, Gadget", +}; + +Parser.vehicleTypeToFull = function (vehicleType) { + return Parser._parse_aToB(Parser.VEHICLE_TYPE_TO_FULL, vehicleType); +}; + +// SOURCES ============================================================================================================= + +Parser.SRC_5ETOOLS_TMP = "Parser.SRC_5ETOOLS_TMP"; // Temp source, used as a placeholder value + +Parser.SRC_CoS = "CoS"; +Parser.SRC_DMG = "DMG"; +Parser.SRC_EEPC = "EEPC"; +Parser.SRC_EET = "EET"; +Parser.SRC_HotDQ = "HotDQ"; +Parser.SRC_LMoP = "LMoP"; +Parser.SRC_MM = "MM"; +Parser.SRC_OotA = "OotA"; +Parser.SRC_PHB = "PHB"; +Parser.SRC_PotA = "PotA"; +Parser.SRC_RoT = "RoT"; +Parser.SRC_RoTOS = "RoTOS"; +Parser.SRC_SCAG = "SCAG"; +Parser.SRC_SKT = "SKT"; +Parser.SRC_ToA = "ToA"; +Parser.SRC_TLK = "TLK"; +Parser.SRC_ToD = "ToD"; +Parser.SRC_TTP = "TTP"; +Parser.SRC_TYP = "TftYP"; +Parser.SRC_TYP_AtG = "TftYP-AtG"; +Parser.SRC_TYP_DiT = "TftYP-DiT"; +Parser.SRC_TYP_TFoF = "TftYP-TFoF"; +Parser.SRC_TYP_THSoT = "TftYP-THSoT"; +Parser.SRC_TYP_TSC = "TftYP-TSC"; +Parser.SRC_TYP_ToH = "TftYP-ToH"; +Parser.SRC_TYP_WPM = "TftYP-WPM"; +Parser.SRC_VGM = "VGM"; +Parser.SRC_XGE = "XGE"; +Parser.SRC_OGA = "OGA"; +Parser.SRC_MTF = "MTF"; +Parser.SRC_WDH = "WDH"; +Parser.SRC_WDMM = "WDMM"; +Parser.SRC_GGR = "GGR"; +Parser.SRC_KKW = "KKW"; +Parser.SRC_LLK = "LLK"; +Parser.SRC_AZfyT = "AZfyT"; +Parser.SRC_GoS = "GoS"; +Parser.SRC_AI = "AI"; +Parser.SRC_OoW = "OoW"; +Parser.SRC_ESK = "ESK"; +Parser.SRC_DIP = "DIP"; +Parser.SRC_HftT = "HftT"; +Parser.SRC_DC = "DC"; +Parser.SRC_SLW = "SLW"; +Parser.SRC_SDW = "SDW"; +Parser.SRC_BGDIA = "BGDIA"; +Parser.SRC_LR = "LR"; +Parser.SRC_AL = "AL"; +Parser.SRC_SAC = "SAC"; +Parser.SRC_ERLW = "ERLW"; +Parser.SRC_EFR = "EFR"; +Parser.SRC_RMBRE = "RMBRE"; +Parser.SRC_RMR = "RMR"; +Parser.SRC_MFF = "MFF"; +Parser.SRC_AWM = "AWM"; +Parser.SRC_IMR = "IMR"; +Parser.SRC_SADS = "SADS"; +Parser.SRC_EGW = "EGW"; +Parser.SRC_EGW_ToR = "ToR"; +Parser.SRC_EGW_DD = "DD"; +Parser.SRC_EGW_FS = "FS"; +Parser.SRC_EGW_US = "US"; +Parser.SRC_MOT = "MOT"; +Parser.SRC_IDRotF = "IDRotF"; +Parser.SRC_TCE = "TCE"; +Parser.SRC_VRGR = "VRGR"; +Parser.SRC_HoL = "HoL"; +Parser.SRC_XMtS = "XMtS"; +Parser.SRC_RtG = "RtG"; +Parser.SRC_AitFR = "AitFR"; +Parser.SRC_AitFR_ISF = "AitFR-ISF"; +Parser.SRC_AitFR_THP = "AitFR-THP"; +Parser.SRC_AitFR_AVT = "AitFR-AVT"; +Parser.SRC_AitFR_DN = "AitFR-DN"; +Parser.SRC_AitFR_FCD = "AitFR-FCD"; +Parser.SRC_WBtW = "WBtW"; +Parser.SRC_DoD = "DoD"; +Parser.SRC_MaBJoV = "MaBJoV"; +Parser.SRC_FTD = "FTD"; +Parser.SRC_SCC = "SCC"; +Parser.SRC_SCC_CK = "SCC-CK"; +Parser.SRC_SCC_HfMT = "SCC-HfMT"; +Parser.SRC_SCC_TMM = "SCC-TMM"; +Parser.SRC_SCC_ARiR = "SCC-ARiR"; +Parser.SRC_MPMM = "MPMM"; +Parser.SRC_CRCotN = "CRCotN"; +Parser.SRC_JttRC = "JttRC"; +Parser.SRC_SAiS = "SAiS"; +Parser.SRC_AAG = "AAG"; +Parser.SRC_BAM = "BAM"; +Parser.SRC_LoX = "LoX"; +Parser.SRC_DoSI = "DoSI"; +Parser.SRC_DSotDQ = "DSotDQ"; +Parser.SRC_KftGV = "KftGV"; +Parser.SRC_BGG = "BGG"; +Parser.SRC_TDCSR = "TDCSR"; +Parser.SRC_PaBTSO = "PaBTSO"; +Parser.SRC_PAitM = "PAitM"; +Parser.SRC_SatO = "SatO"; +Parser.SRC_ToFW = "ToFW"; +Parser.SRC_MPP = "MPP"; +Parser.SRC_BMT = "BMT"; +Parser.SRC_GHLoE = "GHLoE"; +Parser.SRC_DoDk = "DoDk"; +Parser.SRC_SCREEN = "Screen"; +Parser.SRC_SCREEN_WILDERNESS_KIT = "ScreenWildernessKit"; +Parser.SRC_SCREEN_DUNGEON_KIT = "ScreenDungeonKit"; +Parser.SRC_SCREEN_SPELLJAMMER = "ScreenSpelljammer"; +Parser.SRC_HF = "HF"; +Parser.SRC_HFFotM = "HFFotM"; +Parser.SRC_HFStCM = "HFStCM"; +Parser.SRC_CM = "CM"; +Parser.SRC_NRH = "NRH"; +Parser.SRC_NRH_TCMC = "NRH-TCMC"; +Parser.SRC_NRH_AVitW = "NRH-AVitW"; +Parser.SRC_NRH_ASS = "NRH-ASS"; // lmao +Parser.SRC_NRH_CoI = "NRH-CoI"; +Parser.SRC_NRH_TLT = "NRH-TLT"; +Parser.SRC_NRH_AWoL = "NRH-AWoL"; +Parser.SRC_NRH_AT = "NRH-AT"; +Parser.SRC_MGELFT = "MGELFT"; +Parser.SRC_VD = "VD"; +Parser.SRC_SjA = "SjA"; +Parser.SRC_HAT_TG = "HAT-TG"; +Parser.SRC_HAT_LMI = "HAT-LMI"; +Parser.SRC_GotSF = "GotSF"; +Parser.SRC_LK = "LK"; +Parser.SRC_CoA = "CoA"; +Parser.SRC_PiP = "PiP"; + +Parser.SRC_AL_PREFIX = "AL"; + +Parser.SRC_ALCoS = `${Parser.SRC_AL_PREFIX}CurseOfStrahd`; +Parser.SRC_ALEE = `${Parser.SRC_AL_PREFIX}ElementalEvil`; +Parser.SRC_ALRoD = `${Parser.SRC_AL_PREFIX}RageOfDemons`; + +Parser.SRC_PS_PREFIX = "PS"; + +Parser.SRC_PSA = `${Parser.SRC_PS_PREFIX}A`; +Parser.SRC_PSI = `${Parser.SRC_PS_PREFIX}I`; +Parser.SRC_PSK = `${Parser.SRC_PS_PREFIX}K`; +Parser.SRC_PSZ = `${Parser.SRC_PS_PREFIX}Z`; +Parser.SRC_PSX = `${Parser.SRC_PS_PREFIX}X`; +Parser.SRC_PSD = `${Parser.SRC_PS_PREFIX}D`; + +Parser.SRC_UA_PREFIX = "UA"; +Parser.SRC_UA_ONE_PREFIX = "XUA"; +Parser.SRC_MCVX_PREFIX = "MCV"; +Parser.SRC_MisMVX_PREFIX = "MisMV"; +Parser.SRC_AA_PREFIX = "AA"; + +Parser.SRC_UATMC = `${Parser.SRC_UA_PREFIX}TheMysticClass`; +Parser.SRC_MCV1SC = `${Parser.SRC_MCVX_PREFIX}1SC`; +Parser.SRC_MCV2DC = `${Parser.SRC_MCVX_PREFIX}2DC`; +Parser.SRC_MCV3MC = `${Parser.SRC_MCVX_PREFIX}3MC`; +Parser.SRC_MCV4EC = `${Parser.SRC_MCVX_PREFIX}4EC`; +Parser.SRC_MisMV1 = `${Parser.SRC_MisMVX_PREFIX}1`; +Parser.SRC_AATM = `${Parser.SRC_AA_PREFIX}TM`; + +Parser.AL_PREFIX = "Adventurers League: "; +Parser.AL_PREFIX_SHORT = "AL: "; +Parser.PS_PREFIX = "Plane Shift: "; +Parser.PS_PREFIX_SHORT = "PS: "; +Parser.UA_PREFIX = "Unearthed Arcana: "; +Parser.UA_PREFIX_SHORT = "UA: "; +Parser.TftYP_NAME = "Tales from the Yawning Portal"; +Parser.AitFR_NAME = "Adventures in the Forgotten Realms"; +Parser.NRH_NAME = "NERDS Restoring Harmony"; +Parser.MCVX_PREFIX = "Monstrous Compendium Volume "; +Parser.MisMVX_PREFIX = "Misplaced Monsters: Volume "; +Parser.AA_PREFIX = "Adventure Atlas: "; + +Parser.SOURCE_JSON_TO_FULL = {}; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_CoS] = "Curse of Strahd"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_DMG] = "Dungeon Master's Guide"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_EEPC] = "Elemental Evil Player's Companion"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_EET] = "Elemental Evil: Trinkets"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_HotDQ] = "Hoard of the Dragon Queen"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_LMoP] = "Lost Mine of Phandelver"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_MM] = "Monster Manual"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_OotA] = "Out of the Abyss"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_PHB] = "Player's Handbook"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_PotA] = "Princes of the Apocalypse"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_RoT] = "The Rise of Tiamat"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_RoTOS] = "The Rise of Tiamat Online Supplement"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_SCAG] = "Sword Coast Adventurer's Guide"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_SKT] = "Storm King's Thunder"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_ToA] = "Tomb of Annihilation"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_TLK] = "The Lost Kenku"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_ToD] = "Tyranny of Dragons"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_TTP] = "The Tortle Package"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_TYP] = Parser.TftYP_NAME; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_TYP_AtG] = `${Parser.TftYP_NAME}: Against the Giants`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_TYP_DiT] = `${Parser.TftYP_NAME}: Dead in Thay`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_TYP_TFoF] = `${Parser.TftYP_NAME}: The Forge of Fury`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_TYP_THSoT] = `${Parser.TftYP_NAME}: The Hidden Shrine of Tamoachan`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_TYP_TSC] = `${Parser.TftYP_NAME}: The Sunless Citadel`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_TYP_ToH] = `${Parser.TftYP_NAME}: Tomb of Horrors`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_TYP_WPM] = `${Parser.TftYP_NAME}: White Plume Mountain`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_VGM] = "Volo's Guide to Monsters"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_XGE] = "Xanathar's Guide to Everything"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_OGA] = "One Grung Above"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_MTF] = "Mordenkainen's Tome of Foes"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_WDH] = "Waterdeep: Dragon Heist"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_WDMM] = "Waterdeep: Dungeon of the Mad Mage"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_GGR] = "Guildmasters' Guide to Ravnica"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_KKW] = "Krenko's Way"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_LLK] = "Lost Laboratory of Kwalish"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_AZfyT] = "A Zib for your Thoughts"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_GoS] = "Ghosts of Saltmarsh"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_AI] = "Acquisitions Incorporated"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_OoW] = "The Orrery of the Wanderer"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_ESK] = "Essentials Kit"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_DIP] = "Dragon of Icespire Peak"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_HftT] = "Hunt for the Thessalhydra"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_DC] = "Divine Contention"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_SLW] = "Storm Lord's Wrath"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_SDW] = "Sleeping Dragon's Wake"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_BGDIA] = "Baldur's Gate: Descent Into Avernus"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_LR] = "Locathah Rising"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_AL] = "Adventurers' League"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_SAC] = "Sage Advice Compendium"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_ERLW] = "Eberron: Rising from the Last War"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_EFR] = "Eberron: Forgotten Relics"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_RMBRE] = "The Lost Dungeon of Rickedness: Big Rick Energy"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_RMR] = "Dungeons & Dragons vs. Rick and Morty: Basic Rules"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_MFF] = "Mordenkainen's Fiendish Folio"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_AWM] = "Adventure with Muk"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_IMR] = "Infernal Machine Rebuild"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_SADS] = "Sapphire Anniversary Dice Set"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_EGW] = "Explorer's Guide to Wildemount"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_EGW_ToR] = "Tide of Retribution"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_EGW_DD] = "Dangerous Designs"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_EGW_FS] = "Frozen Sick"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_EGW_US] = "Unwelcome Spirits"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_MOT] = "Mythic Odysseys of Theros"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_IDRotF] = "Icewind Dale: Rime of the Frostmaiden"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_TCE] = "Tasha's Cauldron of Everything"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_VRGR] = "Van Richten's Guide to Ravenloft"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_HoL] = "The House of Lament"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_RtG] = "Return to Glory"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_AitFR] = Parser.AitFR_NAME; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_AitFR_ISF] = `${Parser.AitFR_NAME}: In Scarlet Flames`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_AitFR_THP] = `${Parser.AitFR_NAME}: The Hidden Page`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_AitFR_AVT] = `${Parser.AitFR_NAME}: A Verdant Tomb`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_AitFR_DN] = `${Parser.AitFR_NAME}: Deepest Night`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_AitFR_FCD] = `${Parser.AitFR_NAME}: From Cyan Depths`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_WBtW] = "The Wild Beyond the Witchlight"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_DoD] = "Domains of Delight"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_MaBJoV] = "Minsc and Boo's Journal of Villainy"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_FTD] = "Fizban's Treasury of Dragons"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_SCC] = "Strixhaven: A Curriculum of Chaos"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_SCC_CK] = "Campus Kerfuffle"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_SCC_HfMT] = "Hunt for Mage Tower"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_SCC_TMM] = "The Magister's Masquerade"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_SCC_ARiR] = "A Reckoning in Ruins"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_MPMM] = "Mordenkainen Presents: Monsters of the Multiverse"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_CRCotN] = "Critical Role: Call of the Netherdeep"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_JttRC] = "Journeys through the Radiant Citadel"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_SAiS] = "Spelljammer: Adventures in Space"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_AAG] = "Astral Adventurer's Guide"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_BAM] = "Boo's Astral Menagerie"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_LoX] = "Light of Xaryxis"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_DoSI] = "Dragons of Stormwreck Isle"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_DSotDQ] = "Dragonlance: Shadow of the Dragon Queen"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_KftGV] = "Keys from the Golden Vault"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_BGG] = "Bigby Presents: Glory of the Giants"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_TDCSR] = "Tal'Dorei Campaign Setting Reborn"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_PaBTSO] = "Phandelver and Below: The Shattered Obelisk"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_PAitM] = "Planescape: Adventures in the Multiverse"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_SatO] = "Sigil and the Outlands"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_ToFW] = "Turn of Fortune's Wheel"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_MPP] = "Morte's Planar Parade"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_BMT] = "The Book of Many Things"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_GHLoE] = "Grim Hollow: Lairs of Etharis"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_DoDk] = "Dungeons of Drakkenheim"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_SCREEN] = "Dungeon Master's Screen"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_SCREEN_WILDERNESS_KIT] = "Dungeon Master's Screen: Wilderness Kit"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_SCREEN_DUNGEON_KIT] = "Dungeon Master's Screen: Dungeon Kit"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_SCREEN_SPELLJAMMER] = "Dungeon Master's Screen: Spelljammer"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_HF] = "Heroes' Feast"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_HFFotM] = "Heroes' Feast: Flavors of the Multiverse"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_HFStCM] = "Heroes' Feast: Saving the Childrens Menu"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_CM] = "Candlekeep Mysteries"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_NRH] = Parser.NRH_NAME; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_NRH_TCMC] = `${Parser.NRH_NAME}: The Candy Mountain Caper`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_NRH_AVitW] = `${Parser.NRH_NAME}: A Voice in the Wilderness`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_NRH_ASS] = `${Parser.NRH_NAME}: A Sticky Situation`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_NRH_CoI] = `${Parser.NRH_NAME}: Circus of Illusions`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_NRH_TLT] = `${Parser.NRH_NAME}: The Lost Tomb`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_NRH_AWoL] = `${Parser.NRH_NAME}: A Web of Lies`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_NRH_AT] = `${Parser.NRH_NAME}: Adventure Together`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_MGELFT] = "Muk's Guide To Everything He Learned From Tasha"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_VD] = "Vecna Dossier"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_SjA] = "Spelljammer Academy"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_HAT_TG] = "Honor Among Thieves: Thieves' Gallery"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_HAT_LMI] = "Honor Among Thieves: Legendary Magic Items"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_GotSF] = "Giants of the Star Forge"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_LK] = "Lightning Keep"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_CoA] = "Chains of Asmodeus"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_PiP] = "Peril in Pinebrook"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_ALCoS] = `${Parser.AL_PREFIX}Curse of Strahd`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_ALEE] = `${Parser.AL_PREFIX}Elemental Evil`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_ALRoD] = `${Parser.AL_PREFIX}Rage of Demons`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_PSA] = `${Parser.PS_PREFIX}Amonkhet`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_PSI] = `${Parser.PS_PREFIX}Innistrad`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_PSK] = `${Parser.PS_PREFIX}Kaladesh`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_PSZ] = `${Parser.PS_PREFIX}Zendikar`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_PSX] = `${Parser.PS_PREFIX}Ixalan`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_PSD] = `${Parser.PS_PREFIX}Dominaria`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_XMtS] = `X Marks the Spot`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_UATMC] = `${Parser.UA_PREFIX}The Mystic Class`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_MCV1SC] = `${Parser.MCVX_PREFIX}1: Spelljammer Creatures`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_MCV2DC] = `${Parser.MCVX_PREFIX}2: Dragonlance Creatures`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_MCV3MC] = `${Parser.MCVX_PREFIX}3: Minecraft Creatures`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_MCV4EC] = `${Parser.MCVX_PREFIX}4: Eldraine Creatures`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_MisMV1] = `${Parser.MisMVX_PREFIX}1`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_AATM] = `${Parser.AA_PREFIX}The Mortuary`; + +Parser.SOURCE_JSON_TO_ABV = {}; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_CoS] = "CoS"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_DMG] = "DMG"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_EEPC] = "EEPC"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_EET] = "EET"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_HotDQ] = "HotDQ"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_LMoP] = "LMoP"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_MM] = "MM"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_OotA] = "OotA"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_PHB] = "PHB"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_PotA] = "PotA"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_RoT] = "RoT"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_RoTOS] = "RoTOS"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_SCAG] = "SCAG"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_SKT] = "SKT"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_ToA] = "ToA"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_TLK] = "TLK"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_ToD] = "ToD"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_TTP] = "TTP"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_TYP] = "TftYP"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_TYP_AtG] = "TftYP"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_TYP_DiT] = "TftYP"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_TYP_TFoF] = "TftYP"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_TYP_THSoT] = "TftYP"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_TYP_TSC] = "TftYP"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_TYP_ToH] = "TftYP"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_TYP_WPM] = "TftYP"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_VGM] = "VGM"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_XGE] = "XGE"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_OGA] = "OGA"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_MTF] = "MTF"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_WDH] = "WDH"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_WDMM] = "WDMM"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_GGR] = "GGR"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_KKW] = "KKW"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_LLK] = "LLK"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_AZfyT] = "AZfyT"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_GoS] = "GoS"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_AI] = "AI"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_OoW] = "OoW"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_ESK] = "ESK"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_DIP] = "DIP"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_HftT] = "HftT"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_DC] = "DC"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_SLW] = "SLW"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_SDW] = "SDW"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_BGDIA] = "BGDIA"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_LR] = "LR"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_AL] = "AL"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_SAC] = "SAC"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_ERLW] = "ERLW"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_EFR] = "EFR"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_RMBRE] = "RMBRE"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_RMR] = "RMR"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_MFF] = "MFF"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_AWM] = "AWM"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_IMR] = "IMR"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_SADS] = "SADS"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_EGW] = "EGW"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_EGW_ToR] = "ToR"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_EGW_DD] = "DD"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_EGW_FS] = "FS"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_EGW_US] = "US"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_MOT] = "MOT"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_IDRotF] = "IDRotF"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_TCE] = "TCE"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_VRGR] = "VRGR"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_HoL] = "HoL"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_RtG] = "RtG"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_AitFR] = "AitFR"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_AitFR_ISF] = "AitFR-ISF"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_AitFR_THP] = "AitFR-THP"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_AitFR_AVT] = "AitFR-AVT"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_AitFR_DN] = "AitFR-DN"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_AitFR_FCD] = "AitFR-FCD"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_WBtW] = "WBtW"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_DoD] = "DoD"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_MaBJoV] = "MaBJoV"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_FTD] = "FTD"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_SCC] = "SCC"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_SCC_CK] = "SCC-CK"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_SCC_HfMT] = "SCC-HfMT"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_SCC_TMM] = "SCC-TMM"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_SCC_ARiR] = "SCC-ARiR"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_MPMM] = "MPMM"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_CRCotN] = "CRCotN"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_JttRC] = "JttRC"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_SAiS] = "SAiS"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_AAG] = "AAG"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_BAM] = "BAM"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_LoX] = "LoX"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_DoSI] = "DoSI"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_DSotDQ] = "DSotDQ"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_KftGV] = "KftGV"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_BGG] = "BGG"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_TDCSR] = "TDCSR"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_PaBTSO] = "PaBTSO"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_PAitM] = "PAitM"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_SatO] = "SatO"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_ToFW] = "ToFW"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_MPP] = "MPP"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_BMT] = "BMT"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_GHLoE] = "GHLoE"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_DoDk] = "DoDk"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_SCREEN] = "Screen"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_SCREEN_WILDERNESS_KIT] = "ScWild"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_SCREEN_DUNGEON_KIT] = "ScDun"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_SCREEN_SPELLJAMMER] = "ScSJ"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_HF] = "HF"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_HFFotM] = "HFFotM"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_HFStCM] = "HFStCM"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_CM] = "CM"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_NRH] = "NRH"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_NRH_TCMC] = "NRH-TCMC"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_NRH_AVitW] = "NRH-AVitW"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_NRH_ASS] = "NRH-ASS"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_NRH_CoI] = "NRH-CoI"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_NRH_TLT] = "NRH-TLT"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_NRH_AWoL] = "NRH-AWoL"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_NRH_AT] = "NRH-AT"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_MGELFT] = "MGELFT"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_VD] = "VD"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_SjA] = "SjA"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_HAT_TG] = "HAT-TG"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_HAT_LMI] = "HAT-LMI"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_GotSF] = "GotSF"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_LK] = "LK"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_CoA] = "CoA"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_PiP] = "PiP"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_ALCoS] = "ALCoS"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_ALEE] = "ALEE"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_ALRoD] = "ALRoD"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_PSA] = "PSA"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_PSI] = "PSI"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_PSK] = "PSK"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_PSZ] = "PSZ"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_PSX] = "PSX"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_PSD] = "PSD"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_XMtS] = "XMtS"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_UATMC] = "UAMy"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_MCV1SC] = "MCV1SC"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_MCV2DC] = "MCV2DC"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_MCV3MC] = "MCV3MC"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_MCV4EC] = "MCV4EC"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_MisMV1] = "MisMV1"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_AATM] = "AATM"; + +Parser.SOURCE_JSON_TO_DATE = {}; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_CoS] = "2016-03-15"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_DMG] = "2014-12-09"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_EEPC] = "2015-03-10"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_EET] = "2015-03-10"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_HotDQ] = "2014-08-19"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_LMoP] = "2014-07-15"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_MM] = "2014-09-30"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_OotA] = "2015-09-15"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_PHB] = "2014-08-19"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_PotA] = "2015-04-07"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_RoT] = "2014-11-04"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_RoTOS] = "2014-11-04"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_SCAG] = "2015-11-03"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_SKT] = "2016-09-06"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_ToA] = "2017-09-19"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_TLK] = "2017-11-28"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_ToD] = "2019-10-22"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_TTP] = "2017-09-19"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_TYP] = "2017-04-04"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_TYP_AtG] = "2017-04-04"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_TYP_DiT] = "2017-04-04"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_TYP_TFoF] = "2017-04-04"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_TYP_THSoT] = "2017-04-04"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_TYP_TSC] = "2017-04-04"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_TYP_ToH] = "2017-04-04"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_TYP_WPM] = "2017-04-04"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_VGM] = "2016-11-15"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_XGE] = "2017-11-21"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_OGA] = "2017-10-11"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_MTF] = "2018-05-29"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_WDH] = "2018-09-18"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_WDMM] = "2018-11-20"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_GGR] = "2018-11-20"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_KKW] = "2018-11-20"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_LLK] = "2018-11-10"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_AZfyT] = "2019-03-05"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_GoS] = "2019-05-21"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_AI] = "2019-06-18"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_OoW] = "2019-06-18"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_ESK] = "2019-06-24"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_DIP] = "2019-06-24"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_HftT] = "2019-05-01"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_DC] = "2019-06-24"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_SLW] = "2019-06-24"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_SDW] = "2019-06-24"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_BGDIA] = "2019-09-17"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_LR] = "2019-09-19"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_SAC] = "2019-01-31"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_ERLW] = "2019-11-19"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_EFR] = "2019-11-19"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_RMBRE] = "2019-11-19"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_RMR] = "2019-11-19"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_MFF] = "2019-11-12"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_AWM] = "2019-11-12"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_IMR] = "2019-11-12"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_SADS] = "2019-12-12"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_EGW] = "2020-03-17"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_EGW_ToR] = "2020-03-17"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_EGW_DD] = "2020-03-17"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_EGW_FS] = "2020-03-17"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_EGW_US] = "2020-03-17"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_MOT] = "2020-06-02"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_IDRotF] = "2020-09-15"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_TCE] = "2020-11-17"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_VRGR] = "2021-05-18"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_HoL] = "2021-05-18"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_RtG] = "2021-05-21"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_AitFR] = "2021-06-30"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_AitFR_ISF] = "2021-06-30"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_AitFR_THP] = "2021-07-07"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_AitFR_AVT] = "2021-07-14"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_AitFR_DN] = "2021-07-21"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_AitFR_FCD] = "2021-07-28"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_WBtW] = "2021-09-21"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_DoD] = "2021-09-21"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_MaBJoV] = "2021-10-05"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_FTD] = "2021-11-26"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_SCC] = "2021-12-07"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_SCC_CK] = "2021-12-07"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_SCC_HfMT] = "2021-12-07"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_SCC_TMM] = "2021-12-07"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_SCC_ARiR] = "2021-12-07"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_MPMM] = "2022-01-25"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_CRCotN] = "2022-03-15"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_JttRC] = "2022-07-19"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_SAiS] = "2022-08-16"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_AAG] = "2022-08-16"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_BAM] = "2022-08-16"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_LoX] = "2022-08-16"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_DoSI] = "2022-07-31"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_DSotDQ] = "2022-11-22"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_KftGV] = "2023-02-21"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_BGG] = "2023-08-15"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_TDCSR] = "2022-01-18"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_PaBTSO] = "2023-09-19"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_PAitM] = "2023-10-17"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_SatO] = "2023-10-17"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_ToFW] = "2023-10-17"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_MPP] = "2023-10-17"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_BMT] = "2023-11-14"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_GHLoE] = "2023-11-30"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_DoDk] = "2023-12-21"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_SCREEN] = "2015-01-20"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_SCREEN_WILDERNESS_KIT] = "2020-11-17"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_SCREEN_DUNGEON_KIT] = "2020-09-21"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_SCREEN_SPELLJAMMER] = "2022-08-16"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_HF] = "2020-10-27"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_HFFotM] = "2023-11-07"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_HFStCM] = "2023-11-21"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_CM] = "2021-03-16"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_NRH] = "2021-09-01"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_NRH_TCMC] = "2021-09-01"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_NRH_AVitW] = "2021-09-01"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_NRH_ASS] = "2021-09-01"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_NRH_CoI] = "2021-09-01"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_NRH_TLT] = "2021-09-01"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_NRH_AWoL] = "2021-09-01"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_NRH_AT] = "2021-09-01"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_MGELFT] = "2020-12-01"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_VD] = "2022-06-09"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_SjA] = "2022-07-11"; // pt1; pt2 2022-07-18; pt3 2022-07-25; pt4 2022-08-01 +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_HAT_TG] = "2023-03-06"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_HAT_LMI] = "2023-03-31"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_GotSF] = "2023-08-01"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_LK] = "2023-09-26"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_CoA] = "2023-10-30"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_PiP] = "2023-11-20"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_ALCoS] = "2016-03-15"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_ALEE] = "2015-04-07"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_ALRoD] = "2015-09-15"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_PSA] = "2017-07-06"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_PSI] = "2016-07-12"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_PSK] = "2017-02-16"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_PSZ] = "2016-04-27"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_PSX] = "2018-01-09"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_PSD] = "2018-07-31"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_XMtS] = "2017-12-11"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_UATMC] = "2017-03-13"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_MCV1SC] = "2022-04-21"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_MCV2DC] = "2022-12-05"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_MCV3MC] = "2023-03-28"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_MCV4EC] = "2023-09-21"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_MisMV1] = "2023-05-03"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_AATM] = "2023-10-17"; + +Parser.SOURCES_ADVENTURES = new Set([ + Parser.SRC_LMoP, + Parser.SRC_HotDQ, + Parser.SRC_RoT, + Parser.SRC_RoTOS, + Parser.SRC_PotA, + Parser.SRC_OotA, + Parser.SRC_CoS, + Parser.SRC_SKT, + Parser.SRC_TYP, + Parser.SRC_TYP_AtG, + Parser.SRC_TYP_DiT, + Parser.SRC_TYP_TFoF, + Parser.SRC_TYP_THSoT, + Parser.SRC_TYP_TSC, + Parser.SRC_TYP_ToH, + Parser.SRC_TYP_WPM, + Parser.SRC_ToA, + Parser.SRC_TLK, + Parser.SRC_TTP, + Parser.SRC_WDH, + Parser.SRC_LLK, + Parser.SRC_WDMM, + Parser.SRC_KKW, + Parser.SRC_AZfyT, + Parser.SRC_GoS, + Parser.SRC_HftT, + Parser.SRC_OoW, + Parser.SRC_DIP, + Parser.SRC_SLW, + Parser.SRC_SDW, + Parser.SRC_DC, + Parser.SRC_BGDIA, + Parser.SRC_LR, + Parser.SRC_EFR, + Parser.SRC_RMBRE, + Parser.SRC_IMR, + Parser.SRC_EGW_ToR, + Parser.SRC_EGW_DD, + Parser.SRC_EGW_FS, + Parser.SRC_EGW_US, + Parser.SRC_IDRotF, + Parser.SRC_CM, + Parser.SRC_HoL, + Parser.SRC_XMtS, + Parser.SRC_RtG, + Parser.SRC_AitFR, + Parser.SRC_AitFR_ISF, + Parser.SRC_AitFR_THP, + Parser.SRC_AitFR_AVT, + Parser.SRC_AitFR_DN, + Parser.SRC_AitFR_FCD, + Parser.SRC_WBtW, + Parser.SRC_NRH, + Parser.SRC_NRH_TCMC, + Parser.SRC_NRH_AVitW, + Parser.SRC_NRH_ASS, + Parser.SRC_NRH_CoI, + Parser.SRC_NRH_TLT, + Parser.SRC_NRH_AWoL, + Parser.SRC_NRH_AT, + Parser.SRC_SCC, + Parser.SRC_SCC_CK, + Parser.SRC_SCC_HfMT, + Parser.SRC_SCC_TMM, + Parser.SRC_SCC_ARiR, + Parser.SRC_CRCotN, + Parser.SRC_JttRC, + Parser.SRC_SjA, + Parser.SRC_LoX, + Parser.SRC_DoSI, + Parser.SRC_DSotDQ, + Parser.SRC_KftGV, + Parser.SRC_GotSF, + Parser.SRC_PaBTSO, + Parser.SRC_LK, + Parser.SRC_CoA, + Parser.SRC_PiP, + Parser.SRC_HFStCM, + Parser.SRC_GHLoE, + Parser.SRC_DoDk, + + Parser.SRC_AWM, +]); +Parser.SOURCES_CORE_SUPPLEMENTS = new Set(Object.keys(Parser.SOURCE_JSON_TO_FULL).filter(it => !Parser.SOURCES_ADVENTURES.has(it))); +Parser.SOURCES_NON_STANDARD_WOTC = new Set([ + Parser.SRC_OGA, + Parser.SRC_LLK, + Parser.SRC_AZfyT, + Parser.SRC_LR, + Parser.SRC_TLK, + Parser.SRC_TTP, + Parser.SRC_AWM, + Parser.SRC_IMR, + Parser.SRC_SADS, + Parser.SRC_MFF, + Parser.SRC_XMtS, + Parser.SRC_RtG, + Parser.SRC_AitFR, + Parser.SRC_AitFR_ISF, + Parser.SRC_AitFR_THP, + Parser.SRC_AitFR_AVT, + Parser.SRC_AitFR_DN, + Parser.SRC_AitFR_FCD, + Parser.SRC_DoD, + Parser.SRC_MaBJoV, + Parser.SRC_NRH, + Parser.SRC_NRH_TCMC, + Parser.SRC_NRH_AVitW, + Parser.SRC_NRH_ASS, + Parser.SRC_NRH_CoI, + Parser.SRC_NRH_TLT, + Parser.SRC_NRH_AWoL, + Parser.SRC_NRH_AT, + Parser.SRC_MGELFT, + Parser.SRC_VD, + Parser.SRC_SjA, + Parser.SRC_HAT_TG, + Parser.SRC_HAT_LMI, + Parser.SRC_GotSF, + Parser.SRC_MCV3MC, + Parser.SRC_MCV4EC, + Parser.SRC_MisMV1, + Parser.SRC_LK, + Parser.SRC_AATM, + Parser.SRC_CoA, + Parser.SRC_PiP, + Parser.SRC_HFStCM, +]); +Parser.SOURCES_PARTNERED_WOTC = new Set([ + Parser.SRC_RMBRE, + Parser.SRC_RMR, + Parser.SRC_EGW, + Parser.SRC_EGW_ToR, + Parser.SRC_EGW_DD, + Parser.SRC_EGW_FS, + Parser.SRC_EGW_US, + Parser.SRC_CRCotN, + Parser.SRC_TDCSR, + Parser.SRC_HftT, + Parser.SRC_GHLoE, + Parser.SRC_DoDk, +]); +// region Source categories + +// An opinionated set of source that could be considered "core-core" +Parser.SOURCES_VANILLA = new Set([ + Parser.SRC_DMG, + Parser.SRC_MM, + Parser.SRC_PHB, + Parser.SRC_SCAG, + // Parser.SRC_TTP, // "Legacy" source, removed in favor of MPMM + // Parser.SRC_VGM, // "Legacy" source, removed in favor of MPMM + Parser.SRC_XGE, + // Parser.SRC_MTF, // "Legacy" source, removed in favor of MPMM + Parser.SRC_SAC, + Parser.SRC_MFF, + Parser.SRC_SADS, + Parser.SRC_TCE, + Parser.SRC_FTD, + Parser.SRC_MPMM, + Parser.SRC_SCREEN, + Parser.SRC_SCREEN_WILDERNESS_KIT, + Parser.SRC_SCREEN_DUNGEON_KIT, + Parser.SRC_VD, + Parser.SRC_GotSF, + Parser.SRC_BGG, + Parser.SRC_MaBJoV, + Parser.SRC_CoA, + Parser.SRC_BMT, +]); + +// Any opinionated set of sources that are """hilarious, dude""" +Parser.SOURCES_COMEDY = new Set([ + Parser.SRC_AI, + Parser.SRC_OoW, + Parser.SRC_RMR, + Parser.SRC_RMBRE, + Parser.SRC_HftT, + Parser.SRC_AWM, + Parser.SRC_MGELFT, + Parser.SRC_HAT_TG, + Parser.SRC_HAT_LMI, + Parser.SRC_MCV3MC, + Parser.SRC_MisMV1, + Parser.SRC_LK, + Parser.SRC_PiP, +]); + +// Any opinionated set of sources that are "other settings" +Parser.SOURCES_NON_FR = new Set([ + Parser.SRC_GGR, + Parser.SRC_KKW, + Parser.SRC_ERLW, + Parser.SRC_EFR, + Parser.SRC_EGW, + Parser.SRC_EGW_ToR, + Parser.SRC_EGW_DD, + Parser.SRC_EGW_FS, + Parser.SRC_EGW_US, + Parser.SRC_MOT, + Parser.SRC_XMtS, + Parser.SRC_AZfyT, + Parser.SRC_SCC, + Parser.SRC_SCC_CK, + Parser.SRC_SCC_HfMT, + Parser.SRC_SCC_TMM, + Parser.SRC_SCC_ARiR, + Parser.SRC_CRCotN, + Parser.SRC_SjA, + Parser.SRC_SAiS, + Parser.SRC_AAG, + Parser.SRC_BAM, + Parser.SRC_LoX, + Parser.SRC_DSotDQ, + Parser.SRC_TDCSR, + Parser.SRC_PAitM, + Parser.SRC_SatO, + Parser.SRC_ToFW, + Parser.SRC_MPP, + Parser.SRC_MCV4EC, + Parser.SRC_LK, + Parser.SRC_GHLoE, + Parser.SRC_DoDk, +]); + +// endregion +Parser.SOURCES_AVAILABLE_DOCS_BOOK = {}; +[ + Parser.SRC_PHB, + Parser.SRC_MM, + Parser.SRC_DMG, + Parser.SRC_SCAG, + Parser.SRC_VGM, + Parser.SRC_OGA, + Parser.SRC_XGE, + Parser.SRC_MTF, + Parser.SRC_GGR, + Parser.SRC_AI, + Parser.SRC_ERLW, + Parser.SRC_RMR, + Parser.SRC_EGW, + Parser.SRC_MOT, + Parser.SRC_TCE, + Parser.SRC_VRGR, + Parser.SRC_DoD, + Parser.SRC_MaBJoV, + Parser.SRC_FTD, + Parser.SRC_SCC, + Parser.SRC_MPMM, + Parser.SRC_AAG, + Parser.SRC_BAM, + Parser.SRC_HAT_TG, + Parser.SRC_SCREEN, + Parser.SRC_SCREEN_WILDERNESS_KIT, + Parser.SRC_SCREEN_DUNGEON_KIT, + Parser.SRC_SCREEN_SPELLJAMMER, + Parser.SRC_BGG, + Parser.SRC_TDCSR, + Parser.SRC_SatO, + Parser.SRC_MPP, + Parser.SRC_HF, + Parser.SRC_HFFotM, + Parser.SRC_BMT, +].forEach(src => { + Parser.SOURCES_AVAILABLE_DOCS_BOOK[src] = src; + Parser.SOURCES_AVAILABLE_DOCS_BOOK[src.toLowerCase()] = src; +}); +[ + {src: Parser.SRC_PSA, id: "PS-A"}, + {src: Parser.SRC_PSI, id: "PS-I"}, + {src: Parser.SRC_PSK, id: "PS-K"}, + {src: Parser.SRC_PSZ, id: "PS-Z"}, + {src: Parser.SRC_PSX, id: "PS-X"}, + {src: Parser.SRC_PSD, id: "PS-D"}, +].forEach(({src, id}) => { + Parser.SOURCES_AVAILABLE_DOCS_BOOK[src] = id; + Parser.SOURCES_AVAILABLE_DOCS_BOOK[src.toLowerCase()] = id; +}); +Parser.SOURCES_AVAILABLE_DOCS_ADVENTURE = {}; +[ + Parser.SRC_LMoP, + Parser.SRC_HotDQ, + Parser.SRC_RoT, + Parser.SRC_PotA, + Parser.SRC_OotA, + Parser.SRC_CoS, + Parser.SRC_SKT, + Parser.SRC_TYP_AtG, + Parser.SRC_TYP_DiT, + Parser.SRC_TYP_TFoF, + Parser.SRC_TYP_THSoT, + Parser.SRC_TYP_TSC, + Parser.SRC_TYP_ToH, + Parser.SRC_TYP_WPM, + Parser.SRC_ToA, + Parser.SRC_TLK, + Parser.SRC_TTP, + Parser.SRC_WDH, + Parser.SRC_LLK, + Parser.SRC_WDMM, + Parser.SRC_KKW, + Parser.SRC_AZfyT, + Parser.SRC_GoS, + Parser.SRC_HftT, + Parser.SRC_OoW, + Parser.SRC_DIP, + Parser.SRC_SLW, + Parser.SRC_SDW, + Parser.SRC_DC, + Parser.SRC_BGDIA, + Parser.SRC_LR, + Parser.SRC_EFR, + Parser.SRC_RMBRE, + Parser.SRC_IMR, + Parser.SRC_EGW_ToR, + Parser.SRC_EGW_DD, + Parser.SRC_EGW_FS, + Parser.SRC_EGW_US, + Parser.SRC_IDRotF, + Parser.SRC_CM, + Parser.SRC_HoL, + Parser.SRC_XMtS, + Parser.SRC_RtG, + Parser.SRC_AitFR_ISF, + Parser.SRC_AitFR_THP, + Parser.SRC_AitFR_AVT, + Parser.SRC_AitFR_DN, + Parser.SRC_AitFR_FCD, + Parser.SRC_WBtW, + Parser.SRC_NRH, + Parser.SRC_NRH_TCMC, + Parser.SRC_NRH_AVitW, + Parser.SRC_NRH_ASS, + Parser.SRC_NRH_CoI, + Parser.SRC_NRH_TLT, + Parser.SRC_NRH_AWoL, + Parser.SRC_NRH_AT, + Parser.SRC_SCC_CK, + Parser.SRC_SCC_HfMT, + Parser.SRC_SCC_TMM, + Parser.SRC_SCC_ARiR, + Parser.SRC_CRCotN, + Parser.SRC_JttRC, + Parser.SRC_LoX, + Parser.SRC_DoSI, + Parser.SRC_DSotDQ, + Parser.SRC_KftGV, + Parser.SRC_GotSF, + Parser.SRC_PaBTSO, + Parser.SRC_ToFW, + Parser.SRC_LK, + Parser.SRC_CoA, + Parser.SRC_PiP, + Parser.SRC_HFStCM, + Parser.SRC_GHLoE, + Parser.SRC_DoDk, +].forEach(src => { + Parser.SOURCES_AVAILABLE_DOCS_ADVENTURE[src] = src; + Parser.SOURCES_AVAILABLE_DOCS_ADVENTURE[src.toLowerCase()] = src; +}); + +Parser.getTagSource = function (tag, source) { + if (source && source.trim()) return source; + + tag = tag.trim(); + + const tagMeta = Renderer.tag.TAG_LOOKUP[tag]; + + if (!tagMeta) throw new Error(`Unhandled tag "${tag}"`); + return tagMeta.defaultSource; +}; + +Parser.PROP_TO_TAG = { + "monster": "creature", + "optionalfeature": "optfeature", + "tableGroup": "table", + "vehicleUpgrade": "vehupgrade", + "baseitem": "item", + "itemGroup": "item", + "magicvariant": "item", +}; +Parser.getPropTag = function (prop) { + if (Parser.PROP_TO_TAG[prop]) return Parser.PROP_TO_TAG[prop]; + return prop; +}; + +Parser.PROP_TO_DISPLAY_NAME = { + "variantrule": "Variant Rule", + "optionalfeature": "Option/Feature", + "magicvariant": "Magic Item Variant", + "baseitem": "Item (Base)", + "item": "Item", + "adventure": "Adventure", + "adventureData": "Adventure Text", + "book": "Book", + "bookData": "Book Text", + "makebrewCreatureTrait": "Homebrew Builder Creature Trait", + "charoption": "Other Character Creation Option", + + "bonus": "Bonus Action", + "legendary": "Legendary Action", + "mythic": "Mythic Action", + "lairActions": "Lair Action", + "regionalEffects": "Regional Effect", +}; +Parser.getPropDisplayName = function (prop, {suffix = ""} = {}) { + if (Parser.PROP_TO_DISPLAY_NAME[prop]) return `${Parser.PROP_TO_DISPLAY_NAME[prop]}${suffix}`; + + const mFluff = /Fluff$/.exec(prop); + if (mFluff) return Parser.getPropDisplayName(prop.slice(0, -mFluff[0].length), {suffix: " Fluff"}); + + const mFoundry = /^foundry(?[A-Z].*)$/.exec(prop); + if (mFoundry) return Parser.getPropDisplayName(mFoundry.groups.prop.lowercaseFirst(), {suffix: " Foundry Data"}); + + return `${prop.split(/([A-Z][a-z]+)/g).filter(Boolean).join(" ").uppercaseFirst()}${suffix}`; +}; + +Parser.ITEM_TYPE_JSON_TO_ABV = { + "A": "ammunition", + "AF": "ammunition", + "AT": "artisan's tools", + "EM": "eldritch machine", + "EXP": "explosive", + "FD": "food and drink", + "G": "adventuring gear", + "GS": "gaming set", + "HA": "heavy armor", + "IDG": "illegal drug", + "INS": "instrument", + "LA": "light armor", + "M": "melee weapon", + "MA": "medium armor", + "MNT": "mount", + "MR": "master rune", + "GV": "generic variant", + "P": "potion", + "R": "ranged weapon", + "RD": "rod", + "RG": "ring", + "S": "shield", + "SC": "scroll", + "SCF": "spellcasting focus", + "OTH": "other", + "T": "tools", + "TAH": "tack and harness", + "TG": "trade good", + "$": "treasure", + "$A": "treasure (art object)", + "$C": "treasure (coinage)", + "$G": "treasure (gemstone)", + "VEH": "vehicle (land)", + "SHP": "vehicle (water)", + "AIR": "vehicle (air)", + "SPC": "vehicle (space)", + "WD": "wand", +}; + +Parser.DMGTYPE_JSON_TO_FULL = { + "A": "acid", + "B": "bludgeoning", + "C": "cold", + "F": "fire", + "O": "force", + "L": "lightning", + "N": "necrotic", + "P": "piercing", + "I": "poison", + "Y": "psychic", + "R": "radiant", + "S": "slashing", + "T": "thunder", +}; + +Parser.DMG_TYPES = ["acid", "bludgeoning", "cold", "fire", "force", "lightning", "necrotic", "piercing", "poison", "psychic", "radiant", "slashing", "thunder"]; +Parser.CONDITIONS = ["blinded", "charmed", "deafened", "exhaustion", "frightened", "grappled", "incapacitated", "invisible", "paralyzed", "petrified", "poisoned", "prone", "restrained", "stunned", "unconscious"]; + +Parser.SENSES = [ + {"name": "blindsight", "source": Parser.SRC_PHB}, + {"name": "darkvision", "source": Parser.SRC_PHB}, + {"name": "tremorsense", "source": Parser.SRC_MM}, + {"name": "truesight", "source": Parser.SRC_PHB}, +]; + +Parser.NUMBERS_ONES = ["", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine"]; +Parser.NUMBERS_TENS = ["", "", "twenty", "thirty", "forty", "fifty", "sixty", "seventy", "eighty", "ninety"]; +Parser.NUMBERS_TEENS = ["ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeen", "eighteen", "nineteen"]; + +// region Metric conversion +Parser.metric = { + // See MPMB's breakdown: https://old.reddit.com/r/dndnext/comments/6gkuec + MILES_TO_KILOMETRES: 1.6, + FEET_TO_METRES: 0.3, // 5 ft = 1.5 m + YARDS_TO_METRES: 0.9, // (as above) + POUNDS_TO_KILOGRAMS: 0.5, // 2 lb = 1 kg + + getMetricNumber ({originalValue, originalUnit, toFixed = null}) { + if (originalValue == null || isNaN(originalValue)) return originalValue; + + originalValue = Number(originalValue); + if (!originalValue) return originalValue; + + let out = null; + switch (originalUnit) { + case "ft.": case "ft": case Parser.UNT_FEET: out = originalValue * Parser.metric.FEET_TO_METRES; break; + case "yd.": case "yd": case Parser.UNT_YARDS: out = originalValue * Parser.metric.YARDS_TO_METRES; break; + case "mi.": case "mi": case Parser.UNT_MILES: out = originalValue * Parser.metric.MILES_TO_KILOMETRES; break; + case "lb.": case "lb": case "lbs": out = originalValue * Parser.metric.POUNDS_TO_KILOGRAMS; break; + default: return originalValue; + } + if (toFixed != null) return NumberUtil.toFixedNumber(out, toFixed); + return out; + }, + + getMetricUnit ({originalUnit, isShortForm = false, isPlural = true}) { + switch (originalUnit) { + case "ft.": case "ft": case Parser.UNT_FEET: return isShortForm ? "m" : `meter`[isPlural ? "toPlural" : "toString"](); + case "yd.": case "yd": case Parser.UNT_YARDS: return isShortForm ? "m" : `meter`[isPlural ? "toPlural" : "toString"](); + case "mi.": case "mi": case Parser.UNT_MILES: return isShortForm ? "km" : `kilometre`[isPlural ? "toPlural" : "toString"](); + case "lb.": case "lb": case "lbs": return isShortForm ? "kg" : `kilogram`[isPlural ? "toPlural" : "toString"](); + default: return originalUnit; + } + }, +}; +// endregion +// region Map grids + +Parser.MAP_GRID_TYPE_TO_FULL = {}; +Parser.MAP_GRID_TYPE_TO_FULL["none"] = "None"; +Parser.MAP_GRID_TYPE_TO_FULL["square"] = "Square"; +Parser.MAP_GRID_TYPE_TO_FULL["hexRowsOdd"] = "Hex Rows (Odd)"; +Parser.MAP_GRID_TYPE_TO_FULL["hexRowsEven"] = "Hex Rows (Even)"; +Parser.MAP_GRID_TYPE_TO_FULL["hexColsOdd"] = "Hex Columns (Odd)"; +Parser.MAP_GRID_TYPE_TO_FULL["hexColsEven"] = "Hex Columns (Even)"; + +Parser.mapGridTypeToFull = function (gridType) { + return Parser._parse_aToB(Parser.MAP_GRID_TYPE_TO_FULL, gridType); +}; +// endregion diff --git a/charbuilder/5etools-js/render-dice.js b/charbuilder/5etools-js/render-dice.js new file mode 100644 index 0000000..d53b662 --- /dev/null +++ b/charbuilder/5etools-js/render-dice.js @@ -0,0 +1,2268 @@ +"use strict"; + +Renderer.dice = { + SYSTEM_USER: { + name: "Avandra", // goddess of luck + }, + POS_INFINITE: 100000000000000000000, // larger than this, and we start to see "e" numbers appear + _SYMBOL_PARSE_FAILED: Symbol("parseFailed"), + + _$wrpRoll: null, + _$minRoll: null, + _$iptRoll: null, + _$outRoll: null, + _$head: null, + _hist: [], + _histIndex: null, + _$lastRolledBy: null, + _storage: null, + + _isManualMode: false, + + // region Utilities + DICE: [4, 6, 8, 10, 12, 20, 100], + getNextDice (faces) { + const idx = Renderer.dice.DICE.indexOf(faces); + if (~idx) return Renderer.dice.DICE[idx + 1]; + else return null; + }, + + getPreviousDice (faces) { + const idx = Renderer.dice.DICE.indexOf(faces); + if (~idx) return Renderer.dice.DICE[idx - 1]; + else return null; + }, + // endregion + + // region DM Screen integration + _panel: null, + bindDmScreenPanel (panel, title) { + if (Renderer.dice._panel) { // there can only be one roller box + Renderer.dice.unbindDmScreenPanel(); + } + Renderer.dice._showBox(); + Renderer.dice._panel = panel; + panel.doPopulate_Rollbox(title); + }, + + unbindDmScreenPanel () { + if (Renderer.dice._panel) { + $(`body`).append(Renderer.dice._$wrpRoll); + Renderer.dice._panel.close$TabContent(); + Renderer.dice._panel = null; + Renderer.dice._hideBox(); + Renderer.dice._$wrpRoll.removeClass("rollbox-panel"); + } + }, + + get$Roller () { + return Renderer.dice._$wrpRoll; + }, + // endregion + + /** + * Silently roll an expression and get the result. + * Note that this does not support dynamic variables (e.g. user proficiency bonus). + */ + parseRandomise2 (str) { + if (!str || !str.trim()) return null; + const wrpTree = Renderer.dice.lang.getTree3(str); + if (wrpTree) return wrpTree.tree.evl({}); + else return null; + }, + + /** + * Silently get the average of an expression. + * Note that this does not support dynamic variables (e.g. user proficiency bonus). + */ + parseAverage (str) { + if (!str || !str.trim()) return null; + const wrpTree = Renderer.dice.lang.getTree3(str); + if (wrpTree) return wrpTree.tree.avg({}); + else return null; + }, + + // region Roll box UI + _showBox () { + Renderer.dice._$minRoll.hideVe(); + Renderer.dice._$wrpRoll.showVe(); + Renderer.dice._$iptRoll.prop("placeholder", `${Renderer.dice._getRandomPlaceholder()} or "/help"`); + }, + + _hideBox () { + Renderer.dice._$minRoll.showVe(); + Renderer.dice._$wrpRoll.hideVe(); + }, + + _getRandomPlaceholder () { + const count = RollerUtil.randomise(10); + const faces = Renderer.dice.DICE[RollerUtil.randomise(Renderer.dice.DICE.length - 1)]; + const mod = (RollerUtil.randomise(3) - 2) * RollerUtil.randomise(10); + const drop = (count > 1) && RollerUtil.randomise(5) === 5; + const dropDir = drop ? RollerUtil.randomise(2) === 2 ? "h" : "l" : ""; + const dropAmount = drop ? RollerUtil.randomise(count - 1) : null; + return `${count}d${faces}${drop ? `d${dropDir}${dropAmount}` : ""}${mod < 0 ? mod : mod > 0 ? `+${mod}` : ""}`; + }, + + /** Initialise the roll box UI. */ + async _pInit () { + const $wrpRoll = $(`
`).hideVe(); + const $minRoll = $(``).on("click", () => { + Renderer.dice._showBox(); + Renderer.dice._$iptRoll.focus(); + }); + const $head = $(`
Dice Roller
`) + .on("click", () => { + if (!Renderer.dice._panel) Renderer.dice._hideBox(); + }); + const $outRoll = $(`
`); + const $iptRoll = $(``) + .on("keypress", async evt => { + evt.stopPropagation(); + if (evt.key !== "Enter") return; + + const strDice = $iptRoll.val(); + const result = await Renderer.dice.pRoll2( + strDice, + { + isUser: true, + name: "Anon", + }, + ); + $iptRoll.val(""); + + if (result === Renderer.dice._SYMBOL_PARSE_FAILED) { + Renderer.dice._showInvalid(); + $iptRoll.addClass("form-control--error"); + } + }).on("keydown", (evt) => { + $iptRoll.removeClass("form-control--error"); + + // arrow keys only work on keydown + if (evt.key === "ArrowUp") { + evt.preventDefault(); + Renderer.dice._prevHistory(); + return; + } + + if (evt.key === "ArrowDown") { + evt.preventDefault(); + Renderer.dice._nextHistory(); + } + }); + $wrpRoll.append($head).append($outRoll).append($iptRoll); + + Renderer.dice._$wrpRoll = $wrpRoll; + Renderer.dice._$minRoll = $minRoll; + Renderer.dice._$head = $head; + Renderer.dice._$outRoll = $outRoll; + Renderer.dice._$iptRoll = $iptRoll; + + $(`body`).append($minRoll).append($wrpRoll); + + $wrpRoll.on("click", ".out-roll-item-code", (evt) => Renderer.dice._$iptRoll.val($(evt.target).text()).focus()); + + Renderer.dice.storage = await StorageUtil.pGet(VeCt.STORAGE_ROLLER_MACRO) || {}; + }, + + _prevHistory () { Renderer.dice._histIndex--; Renderer.dice._prevNextHistory_load(); }, + _nextHistory () { Renderer.dice._histIndex++; Renderer.dice._prevNextHistory_load(); }, + + _prevNextHistory_load () { + Renderer.dice._cleanHistoryIndex(); + const nxtVal = Renderer.dice._hist[Renderer.dice._histIndex]; + Renderer.dice._$iptRoll.val(nxtVal); + if (nxtVal) Renderer.dice._$iptRoll[0].selectionStart = Renderer.dice._$iptRoll[0].selectionEnd = nxtVal.length; + }, + + _cleanHistoryIndex: () => { + if (!Renderer.dice._hist.length) { + Renderer.dice._histIndex = null; + } else { + Renderer.dice._histIndex = Math.min(Renderer.dice._hist.length, Math.max(Renderer.dice._histIndex, 0)); + } + }, + + _addHistory: (str) => { + Renderer.dice._hist.push(str); + // point index at the top of the stack + Renderer.dice._histIndex = Renderer.dice._hist.length; + }, + + _scrollBottom: () => { + Renderer.dice._$outRoll.scrollTop(1e10); + }, + // endregion + + // region Event handling + async pRollerClickUseData (evt, ele) { + evt.stopPropagation(); + evt.preventDefault(); + + const $ele = $(ele); + const rollData = $ele.data("packed-dice"); + let name = $ele.data("roll-name"); + let shiftKey = evt.shiftKey; + let ctrlKey = EventUtil.isCtrlMetaKey(evt); + + const options = rollData.toRoll.split(";").map(it => it.trim()).filter(Boolean); + + let chosenRollData; + if (options.length > 1) { + const cpyRollData = MiscUtil.copyFast(rollData); + const menu = ContextUtil.getMenu([ + new ContextUtil.Action( + "Choose Roll", + null, + {isDisabled: true}, + ), + null, + ...options.map(it => new ContextUtil.Action( + `Roll ${it}`, + evt => { + shiftKey = shiftKey || evt.shiftKey; + ctrlKey = ctrlKey || (EventUtil.isCtrlMetaKey(evt)); + cpyRollData.toRoll = it; + return cpyRollData; + }, + )), + ]); + + chosenRollData = await ContextUtil.pOpenMenu(evt, menu); + } else chosenRollData = rollData; + + if (!chosenRollData) return; + + const rePrompt = /#\$prompt_number:?([^$]*)\$#/g; + const results = []; + let m; + while ((m = rePrompt.exec(chosenRollData.toRoll))) { + const optionsRaw = m[1]; + const opts = {}; + if (optionsRaw) { + const spl = optionsRaw.split(","); + spl.map(it => it.trim()).forEach(part => { + const [k, v] = part.split("=").map(it => it.trim()); + switch (k) { + case "min": + case "max": + opts[k] = Number(v); break; + default: + opts[k] = v; break; + } + }); + } + + if (opts.min == null) opts.min = 0; + if (opts.max == null) opts.max = Renderer.dice.POS_INFINITE; + if (opts.default == null) opts.default = 0; + + const input = await InputUiUtil.pGetUserNumber(opts); + if (input == null) return; + results.push(input); + } + + const rollDataCpy = MiscUtil.copyFast(chosenRollData); + rePrompt.lastIndex = 0; + rollDataCpy.toRoll = rollDataCpy.toRoll.replace(rePrompt, () => results.shift()); + + // If there's a prompt, prompt the user to select the dice + let rollDataCpyToRoll; + if (rollData.prompt) { + const sortedKeys = Object.keys(rollDataCpy.prompt.options).sort(SortUtil.ascSortLower); + const menu = ContextUtil.getMenu([ + new ContextUtil.Action(rollDataCpy.prompt.entry, null, {isDisabled: true}), + null, + ...sortedKeys + .map(it => { + const title = rollDataCpy.prompt.mode === "psi" + ? `${it} point${it === "1" ? "" : "s"}` + : `${Parser.spLevelToFull(it)} level`; + + return new ContextUtil.Action( + title, + evt => { + shiftKey = shiftKey || evt.shiftKey; + ctrlKey = ctrlKey || (EventUtil.isCtrlMetaKey(evt)); + + const fromScaling = rollDataCpy.prompt.options[it]; + if (!fromScaling) { + name = ""; + return rollDataCpy; + } else { + name = rollDataCpy.prompt.mode === "psi" ? `${it} psi activation` : `${Parser.spLevelToFull(it)}-level cast`; + rollDataCpy.toRoll += `+${fromScaling}`; + return rollDataCpy; + } + }, + ); + }), + ]); + + rollDataCpyToRoll = await ContextUtil.pOpenMenu(evt, menu); + } else rollDataCpyToRoll = rollDataCpy; + + if (!rollDataCpyToRoll) return; + await Renderer.dice.pRollerClick({shiftKey, ctrlKey}, ele, JSON.stringify(rollDataCpyToRoll), name); + }, + + __rerollNextInlineResult (ele) { + const $ele = $(ele); + const $result = $ele.next(`.result`); + const r = Renderer.dice.__rollPackedData($ele); + $result.text(r); + }, + + __rollPackedData ($ele) { + // Note that this does not support dynamic variables (e.g. user proficiency bonus) + const wrpTree = Renderer.dice.lang.getTree3($ele.data("packed-dice").toRoll); + return wrpTree.tree.evl({}); + }, + + $getEleUnknownTableRoll (total) { return $(Renderer.dice._pRollerClick_getMsgBug(total)); }, + + _pRollerClick_getMsgBug (total) { return `No result found matching roll ${total}?! ๐Ÿ›`; }, + + async pRollerClick (evtMock, ele, packed, name) { + const $ele = $(ele); + const entry = JSON.parse(packed); + const additionalData = {...ele.dataset}; + + const rolledBy = { + name: Renderer.dice._pRollerClick_attemptToGetNameOfRoller({$ele}), + label: name != null ? name : Renderer.dice._pRollerClick_attemptToGetNameOfRoll({entry, $ele}), + }; + + const modRollMeta = Renderer.dice.getEventModifiedRollMeta(evtMock, entry); + const $parent = $ele.closest("th, p, table"); + + const rollResult = await this._pRollerClick_pGetResult({ + $parent, + $ele, + entry, + modRollMeta, + rolledBy, + additionalData, + }); + + if (!entry.autoRoll) return; + + const $tgt = $ele.next(`[data-rd-is-autodice-result="true"]`); + const curTxt = $tgt.text(); + $tgt.text(rollResult); + JqueryUtil.showCopiedEffect($tgt, curTxt, true); + }, + + async _pRollerClick_pGetResult ({$parent, $ele, entry, modRollMeta, rolledBy, additionalData}) { + const sharedRollOpts = { + rollCount: modRollMeta.rollCount, + additionalData, + isHidden: !!entry.autoRoll, + }; + + if ($parent.is("th") && $parent.attr("data-rd-isroller") === "true") { + if ($parent.attr("data-rd-namegeneratorrolls")) { + return Renderer.dice._pRollerClick_pRollGeneratorTable({ + $parent, + $ele, + rolledBy, + modRollMeta, + rollOpts: sharedRollOpts, + }); + } + + return Renderer.dice.pRollEntry( + modRollMeta.entry, + rolledBy, + { + ...sharedRollOpts, + fnGetMessage: Renderer.dice._pRollerClick_fnGetMessageTable.bind(Renderer.dice, $ele), + }, + ); + } + + return Renderer.dice.pRollEntry( + modRollMeta.entry, + rolledBy, + { + ...sharedRollOpts, + }, + ); + }, + + _pRollerClick_fnGetMessageTable ($ele, total) { + const elesTd = Renderer.dice._pRollerClick_$getTdsFromTotal($ele, total); + if (elesTd) { + const tableRow = elesTd.map(ele => ele.innerHTML.trim()).filter(it => it).join(" | "); + const $row = $(`${tableRow}`); + Renderer.dice._pRollerClick_rollInlineRollers($ele); + return $row.html(); + } + return Renderer.dice._pRollerClick_getMsgBug(total); + }, + + // Aka "getTableName", probably + _pRollerClick_attemptToGetNameOfRoll ({entry, $ele}) { + // Try to use the entry's built-in name + if (entry.name) return entry.name; + + // try to use table caption + let titleMaybe = $ele.closest(`table:not(.stats)`).children(`caption`).text(); + if (titleMaybe) return titleMaybe.trim(); + + // try to use list item title + titleMaybe = $ele.parent().children(`.rd__list-item-name`).text(); + if (titleMaybe) return titleMaybe.trim().replace(/[.,:]$/, ""); + + // use the section title, where applicable + titleMaybe = $ele.closest(`div`).children(`.rd__h`).first().find(`.entry-title-inner`).text(); + if (titleMaybe) { + titleMaybe = titleMaybe.trim().replace(/[.,:]$/, ""); + return titleMaybe; + } + + // try to use stats table name row + titleMaybe = $ele.closest(`table.stats`).children(`tbody`).first().children(`tr`).first().find(`.rnd-name .stats-name`).text(); + if (titleMaybe) return titleMaybe.trim(); + + if (UrlUtil.getCurrentPage() === UrlUtil.PG_CHARACTERS) { + // try use mini-entity name + titleMaybe = ($ele.closest(`.chr-entity__row`).find(".chr-entity__ipt-name").val() || "").trim(); + if (titleMaybe) return titleMaybe; + } + + return titleMaybe; + }, + + _pRollerClick_attemptToGetNameOfRoller ({$ele}) { + const $hov = $ele.closest(`.hwin`); + if ($hov.length) return $hov.find(`.stats-name`).first().text(); + const $roll = $ele.closest(`.out-roll-wrp`); + if ($roll.length) return $roll.data("name"); + const $dispPanelTitle = $ele.closest(`.dm-screen-panel`).children(`.panel-control-title`); + if ($dispPanelTitle.length) return $dispPanelTitle.text().trim(); + let name = document.title.replace("- 5etools", "").trim(); + return name === "DM Screen" ? "Dungeon Master" : name; + }, + + _pRollerClick_$getTdsFromTotal ($ele, total) { + const $table = $ele.closest(`table`); + const $tdRoll = $table.find(`td`).filter((i, e) => { + const $e = $(e); + if (!$e.closest(`table`).is($table)) return false; + return total >= Number($e.data("roll-min")) && total <= Number($e.data("roll-max")); + }); + if ($tdRoll.length && $tdRoll.nextAll().length) { + return $tdRoll.nextAll().get(); + } + return null; + }, + + // TODO erm + _pRollerClick_rollInlineRollers ($ele) { + $ele.find(`.render-roller`).each((i, e) => { + const $e = $(e); + const r = Renderer.dice.__rollPackedData($e); + $e.attr("onclick", `Renderer.dice.__rerollNextInlineResult(this)`); + $e.after(` (${r})`); + }); + }, + + _pRollerClick_fnGetMessageGeneratorTable ($ele, ix, total) { + const elesTd = Renderer.dice._pRollerClick_$getTdsFromTotal($ele, total); + if (elesTd) { + const $row = $(`${elesTd[ix].innerHTML.trim()}`); + Renderer.dice._pRollerClick_rollInlineRollers($ele); + return $row.html(); + } + return Renderer.dice._pRollerClick_getMsgBug(total); + }, + + async _pRollerClick_pRollGeneratorTable ({$parent, $ele, rolledBy, modRollMeta, rollOpts}) { + Renderer.dice.addElement({rolledBy, html: `${rolledBy.label}:`, isMessage: true}); + + // Track a total of all rolls--this is a bit meaningless, but this method is expected to return a result value + let total = 0; + + const out = []; + const numRolls = Number($parent.attr("data-rd-namegeneratorrolls")); + const $ths = $ele.closest(`table`).find(`th`); + for (let i = 0; i < numRolls; ++i) { + const cpyRolledBy = MiscUtil.copyFast(rolledBy); + cpyRolledBy.label = $($ths.get(i + 1)).text().trim(); + + const result = await Renderer.dice.pRollEntry( + modRollMeta.entry, + cpyRolledBy, + { + ...rollOpts, + fnGetMessage: Renderer.dice._pRollerClick_fnGetMessageGeneratorTable.bind(Renderer.dice, $ele, i), + }, + ); + total += result; + const elesTd = Renderer.dice._pRollerClick_$getTdsFromTotal($ele, result); + + if (!elesTd) { + out.push(`(no result)`); + continue; + } + + out.push(elesTd[i].innerHTML.trim()); + } + + Renderer.dice.addElement({rolledBy, html: `= ${out.join(" ")}`, isMessage: true}); + + return total; + }, + + getEventModifiedRollMeta (evt, entry) { + // Change roll type/count depending on CTRL/SHIFT status + const out = {rollCount: 1, entry}; + + if (evt.shiftKey) { + if (entry.subType === "damage") { // If SHIFT is held, roll crit + const dice = []; + // TODO(future) in order for this to correctly catch everything, would need to parse the toRoll as a tree and then pull all dice expressions from the first level of that tree + entry.toRoll + .replace(/\s+/g, "") // clean whitespace + .replace(/\d*?d\d+/gi, m0 => dice.push(m0)); + entry.toRoll = `${entry.toRoll}${dice.length ? `+${dice.join("+")}` : ""}`; + } else if (entry.subType === "d20") { // If SHIFT is held, roll advantage + // If we have a cached d20mod value, use it + if (entry.d20mod != null) entry.toRoll = `2d20dl1${entry.d20mod}`; + else entry.toRoll = entry.toRoll.replace(/^\s*1?\s*d\s*20/, "2d20dl1"); + } else out.rollCount = 2; // otherwise, just roll twice + } + + if (EventUtil.isCtrlMetaKey(evt)) { + if (entry.subType === "damage") { // If CTRL is held, half the damage + entry.toRoll = `floor((${entry.toRoll}) / 2)`; + } else if (entry.subType === "d20") { // If CTRL is held, roll disadvantage (assuming SHIFT is not held) + // If we have a cached d20mod value, use it + if (entry.d20mod != null) entry.toRoll = `2d20dh1${entry.d20mod}`; + else entry.toRoll = entry.toRoll.replace(/^\s*1?\s*d\s*20/, "2d20dh1"); + } else out.rollCount = 2; // otherwise, just roll twice + } + + return out; + }, + // endregion + + /** + * Parse and roll a string, and display the result in the roll box. + * Returns the total rolled, if available. + * @param str + * @param rolledBy + * @param rolledBy.isUser + * @param rolledBy.name The name of the roller. + * @param rolledBy.label The label for this roll. + * @param [opts] Options object. + * @param [opts.isResultUsed] If an input box should be provided for the user to enter the result (manual mode only). + */ + async pRoll2 (str, rolledBy, opts) { + opts = opts || {}; + str = str + .trim() + .replace(/\/r(?:oll)? /gi, "").trim() // Remove any leading "/r"s, for ease of use + ; + if (!str) return; + if (rolledBy.isUser) Renderer.dice._addHistory(str); + + if (str.startsWith("/")) return Renderer.dice._pHandleCommand(str, rolledBy); + if (str.startsWith("#")) return Renderer.dice._pHandleSavedRoll(str, rolledBy, opts); + + const [head, ...tail] = str.split(":"); + if (tail.length) { + str = tail.join(":"); + rolledBy.label = head; + } + const wrpTree = Renderer.dice.lang.getTree3(str); + if (!wrpTree) return Renderer.dice._SYMBOL_PARSE_FAILED; + return Renderer.dice._pHandleRoll2(wrpTree, rolledBy, opts); + }, + + /** + * Parse and roll an entry, and display the result in the roll box. + * Returns the total rolled, if available. + * @param entry + * @param rolledBy + * @param [opts] Options object. + * @param [opts.isResultUsed] If an input box should be provided for the user to enter the result (manual mode only). + * @param [opts.rollCount] + * @param [opts.additionalData] + * @param [opts.isHidden] If the result should not be posted to the rollbox. + */ + async pRollEntry (entry, rolledBy, opts) { + opts = opts || {}; + + const rollCount = Math.round(opts.rollCount || 1); + delete opts.rollCount; + if (rollCount <= 0) throw new Error(`Invalid roll count: ${rollCount} (must be a positive integer)`); + + const wrpTree = Renderer.dice.lang.getTree3(entry.toRoll); + wrpTree.tree.successThresh = entry.successThresh; + wrpTree.tree.successMax = entry.successMax; + wrpTree.tree.chanceSuccessText = entry.chanceSuccessText; + wrpTree.tree.chanceFailureText = entry.chanceFailureText; + wrpTree.tree.isColorSuccessFail = entry.isColorSuccessFail; + + // arbitrarily return the result of the highest roll if we roll multiple times + const results = []; + if (rollCount > 1 && !opts.isHidden) Renderer.dice._showMessage(`Rolling twice...`, rolledBy); + for (let i = 0; i < rollCount; ++i) { + const result = await Renderer.dice._pHandleRoll2(wrpTree, rolledBy, opts); + if (result == null) return null; + results.push(result); + } + return Math.max(...results); + }, + + /** + * @param wrpTree + * @param rolledBy + * @param [opts] Options object. + * @param [opts.fnGetMessage] + * @param [opts.isResultUsed] + * @param [opts.additionalData] + */ + async _pHandleRoll2 (wrpTree, rolledBy, opts) { + opts = {...opts}; + + if (wrpTree.meta && wrpTree.meta.hasPb) { + const userPb = await InputUiUtil.pGetUserNumber({ + min: 0, + int: true, + title: "Enter Proficiency Bonus", + default: 2, + storageKey_default: "dice.playerProficiencyBonus", + isGlobal_default: true, + }); + if (userPb == null) return null; + opts.pb = userPb; + } + + if (wrpTree.meta && wrpTree.meta.hasSummonSpellLevel) { + const predefinedSpellLevel = opts.additionalData?.summonedBySpellLevel != null && !isNaN(opts.additionalData?.summonedBySpellLevel) + ? Number(opts.additionalData.summonedBySpellLevel) + : null; + + const userSummonSpellLevel = await InputUiUtil.pGetUserNumber({ + min: predefinedSpellLevel ?? 0, + int: true, + title: "Enter Spell Level", + default: predefinedSpellLevel ?? 1, + }); + if (userSummonSpellLevel == null) return null; + opts.summonSpellLevel = userSummonSpellLevel; + } + + if (wrpTree.meta && wrpTree.meta.hasSummonClassLevel) { + const predefinedClassLevel = opts.additionalData?.summonedByClassLevel != null && !isNaN(opts.additionalData?.summonedByClassLevel) + ? Number(opts.additionalData.summonedByClassLevel) + : null; + + const userSummonClassLevel = await InputUiUtil.pGetUserNumber({ + min: predefinedClassLevel ?? 0, + int: true, + title: "Enter Class Level", + default: predefinedClassLevel ?? 1, + }); + if (userSummonClassLevel == null) return null; + opts.summonClassLevel = userSummonClassLevel; + } + + if (Renderer.dice._isManualMode) return Renderer.dice._pHandleRoll2_manual(wrpTree.tree, rolledBy, opts); + else return Renderer.dice._pHandleRoll2_automatic(wrpTree.tree, rolledBy, opts); + }, + + /** + * @param tree + * @param rolledBy + * @param [opts] Options object. + * @param [opts.fnGetMessage] + * @param [opts.pb] User-entered proficiency bonus, to be propagated to the meta. + * @param [opts.summonSpellLevel] User-entered summon spell level, to be propagated to the meta. + * @param [opts.summonClassLevel] User-entered summon class level, to be propagated to the meta. + * @param [opts.target] Generic target number (e.g. save DC, AC) to meet/beat. + * @param [opts.isHidden] If the result should not be posted to the rollbox. + */ + _pHandleRoll2_automatic (tree, rolledBy, opts) { + opts = opts || {}; + + if (!opts.isHidden) Renderer.dice._showBox(); + Renderer.dice._checkHandleName(rolledBy.name); + const $out = Renderer.dice._$lastRolledBy; + + if (tree) { + const meta = {}; + if (opts.pb) meta.pb = opts.pb; + if (opts.summonSpellLevel) meta.summonSpellLevel = opts.summonSpellLevel; + if (opts.summonClassLevel) meta.summonClassLevel = opts.summonClassLevel; + + const result = tree.evl(meta); + const fullHtml = (meta.html || []).join(""); + const allMax = meta.allMax && meta.allMax.length && !(meta.allMax.filter(it => !it).length); + const allMin = meta.allMin && meta.allMin.length && !(meta.allMin.filter(it => !it).length); + + const lbl = rolledBy.label && (!rolledBy.name || rolledBy.label.trim().toLowerCase() !== rolledBy.name.trim().toLowerCase()) ? rolledBy.label : null; + + const ptTarget = opts.target != null + ? result >= opts.target ? ` ≥${opts.target}` : ` <${opts.target}` + : ""; + + const isThreshSuccess = tree.successThresh != null && result > (tree.successMax || 100) - tree.successThresh; + const isColorSuccess = tree.isColorSuccessFail || !tree.chanceSuccessText; + const isColorFail = tree.isColorSuccessFail || !tree.chanceFailureText; + const totalPart = tree.successThresh != null + ? `${isThreshSuccess ? Renderer.get().render(tree.chanceSuccessText || "Success!") : Renderer.get().render(tree.chanceFailureText || "Failure")}` + : `${result}`; + + const title = `${rolledBy.name ? `${rolledBy.name} \u2014 ` : ""}${lbl ? `${lbl}: ` : ""}${tree}`; + + const message = opts.fnGetMessage ? opts.fnGetMessage(result) : null; + ExtensionUtil.doSendRoll({ + dice: tree.toString(), + result, + rolledBy: rolledBy.name, + label: [lbl, message].filter(Boolean).join(" \u2013 "), + }); + + if (!opts.isHidden) { + $out.append(` +
+
+ ${lbl ? `${lbl}: ` : ""} + ${totalPart} + ${ptTarget} + ${fullHtml} + ${message ? `${message}` : ""} +
+
+ +
+
`); + + Renderer.dice._scrollBottom(); + } + + return result; + } else { + if (!opts.isHidden) { + $out.append(`
Invalid input! Try "/help"
`); + Renderer.dice._scrollBottom(); + } + return null; + } + }, + + _pHandleRoll2_manual (tree, rolledBy, opts) { + opts = opts || {}; + + if (!tree) return JqueryUtil.doToast({type: "danger", content: `Invalid roll input!`}); + + const title = (rolledBy.label || "").toTitleCase() || "Roll Dice"; + const $dispDice = $(`
${tree.toString()}
`); + if (opts.isResultUsed) { + return InputUiUtil.pGetUserNumber({ + title, + $elePre: $dispDice, + }); + } else { + const {$modalInner} = UiUtil.getShowModal({ + title, + isMinHeight0: true, + }); + $dispDice.appendTo($modalInner); + return null; + } + }, + + _showMessage (message, rolledBy) { + Renderer.dice._showBox(); + Renderer.dice._checkHandleName(rolledBy.name); + const $out = Renderer.dice._$lastRolledBy; + $out.append(`
${message}
`); + Renderer.dice._scrollBottom(); + }, + + _showInvalid () { + Renderer.dice._showMessage("Invalid input! Try "/help"", Renderer.dice.SYSTEM_USER); + }, + + _validCommands: new Set(["/c", "/cls", "/clear", "/iterroll"]), + async _pHandleCommand (com, rolledBy) { + Renderer.dice._showMessage(`${com}`, rolledBy); // parrot the user's command back to them + + const comParsed = Renderer.dice._getParsedCommand(com); + const [comOp] = comParsed; + + if (comOp === "/help" || comOp === "/h") { + Renderer.dice._showMessage( + `
    +
  • Keep highest; 4d6kh3
  • +
  • Drop lowest; 4d6dl1
  • +
  • Drop highest; 3d4dh1
  • +
  • Keep lowest; 3d4kl1
  • + +
  • Reroll equal; 2d4r1
  • +
  • Reroll less; 2d4r<2
  • +
  • Reroll less or equal; 2d4r<=2
  • +
  • Reroll greater; 2d4r>2
  • +
  • Reroll greater equal; 2d4r>=3
  • + +
  • Explode equal; 2d4x4
  • +
  • Explode less; 2d4x<2
  • +
  • Explode less or equal; 2d4x<=2
  • +
  • Explode greater; 2d4x>2
  • +
  • Explode greater equal; 2d4x>=3
  • + +
  • Count Successes equal; 2d4cs=4
  • +
  • Count Successes less; 2d4cs<2
  • +
  • Count Successes less or equal; 2d4cs<=2
  • +
  • Count Successes greater; 2d4cs>2
  • +
  • Count Successes greater equal; 2d4cs>=3
  • + +
  • Margin of Success; 2d4ms=4
  • + +
  • Dice pools; {2d8, 1d6}
  • +
  • Dice pools with modifiers; {1d20+7, 10}kh1
  • + +
  • Rounding; floor(1.5), ceil(1.5), round(1.5)
  • + +
  • Average; avg(8d6)
  • +
  • Maximize dice; dmax(8d6)
  • +
  • Minimize dice; dmin(8d6)
  • + +
  • Other functions; sign(1d6-3), abs(1d6-3), ...etc.
  • +
+ Up and down arrow keys cycle input history.
+ Anything before a colon is treated as a label (Fireball: 8d6)
+Use /macro list to list saved macros.
+ Use /macro add myName 1d2+3 to add (or update) a macro. Macro names should not contain spaces or hashes.
+ Use /macro remove myName to remove a macro.
+ Use #myName to roll a macro.
+ Use /iterroll roll count [target] to roll multiple times, optionally against a target. + Use /clear to clear the roller.`, + Renderer.dice.SYSTEM_USER, + ); + return; + } + + if (comOp === "/macro") { + const [, mode, ...others] = comParsed; + + if (!["list", "add", "remove", "clear"].includes(mode)) Renderer.dice._showInvalid(); + else { + switch (mode) { + case "list": + if (!others.length) { + Object.keys(Renderer.dice.storage).forEach(name => { + Renderer.dice._showMessage(`#${name} \u2014 ${Renderer.dice.storage[name]}`, Renderer.dice.SYSTEM_USER); + }); + } else { + Renderer.dice._showInvalid(); + } + break; + case "add": { + if (others.length === 2) { + const [name, macro] = others; + if (name.includes(" ") || name.includes("#")) Renderer.dice._showInvalid(); + else { + Renderer.dice.storage[name] = macro; + await Renderer.dice._pSaveMacros(); + Renderer.dice._showMessage(`Saved macro #${name}`, Renderer.dice.SYSTEM_USER); + } + } else { + Renderer.dice._showInvalid(); + } + break; + } + case "remove": + if (others.length === 1) { + if (Renderer.dice.storage[others[0]]) { + delete Renderer.dice.storage[others[0]]; + await Renderer.dice._pSaveMacros(); + Renderer.dice._showMessage(`Removed macro #${others[0]}`, Renderer.dice.SYSTEM_USER); + } else { + Renderer.dice._showMessage(`Macro #${others[0]} not found`, Renderer.dice.SYSTEM_USER); + } + } else { + Renderer.dice._showInvalid(); + } + break; + } + } + return; + } + + if (Renderer.dice._validCommands.has(comOp)) { + switch (comOp) { + case "/c": + case "/cls": + case "/clear": + Renderer.dice._$outRoll.empty(); + Renderer.dice._$lastRolledBy.empty(); + Renderer.dice._$lastRolledBy = null; + return; + + case "/iterroll": { + let [, exp, count, target] = comParsed; + + if (!exp) return Renderer.dice._showInvalid(); + const wrpTree = Renderer.dice.lang.getTree3(exp); + if (!wrpTree) return Renderer.dice._showInvalid(); + + count = count && !isNaN(count) ? Number(count) : 1; + target = target && !isNaN(target) ? Number(target) : undefined; + + for (let i = 0; i < count; ++i) { + await Renderer.dice.pRoll2( + exp, + { + name: "Anon", + }, + { + target, + }, + ); + } + } + } + return; + } + + Renderer.dice._showInvalid(); + }, + + async _pSaveMacros () { + await StorageUtil.pSet(VeCt.STORAGE_ROLLER_MACRO, Renderer.dice.storage); + }, + + _getParsedCommand (str) { + // TODO(Future) this is probably too naive + return str.split(/\s+/); + }, + + _pHandleSavedRoll (id, rolledBy, opts) { + id = id.replace(/^#/, ""); + const macro = Renderer.dice.storage[id]; + if (macro) { + rolledBy.label = id; + const wrpTree = Renderer.dice.lang.getTree3(macro); + return Renderer.dice._pHandleRoll2(wrpTree, rolledBy, opts); + } else Renderer.dice._showMessage(`Macro #${id} not found`, Renderer.dice.SYSTEM_USER); + }, + + addRoll ({rolledBy, html, $ele}) { + if (html && $ele) throw new Error(`Must specify one of html or $ele!`); + + if (html != null && !html.trim()) return; + + Renderer.dice._showBox(); + Renderer.dice._checkHandleName(rolledBy.name); + + if (html) { + Renderer.dice._$lastRolledBy.append(`
${html}
`); + } else { + $$`
${$ele}
` + .appendTo(Renderer.dice._$lastRolledBy); + } + + Renderer.dice._scrollBottom(); + }, + + addElement ({rolledBy, html, $ele}) { + if (html && $ele) throw new Error(`Must specify one of html or $ele!`); + + if (html != null && !html.trim()) return; + + Renderer.dice._showBox(); + Renderer.dice._checkHandleName(rolledBy.name); + + if (html) { + Renderer.dice._$lastRolledBy.append(`
${html}
`); + } else { + $$`
${$ele}
` + .appendTo(Renderer.dice._$lastRolledBy); + } + + Renderer.dice._scrollBottom(); + }, + + _checkHandleName (name) { + if (!Renderer.dice._$lastRolledBy || Renderer.dice._$lastRolledBy.data("name") !== name) { + Renderer.dice._$outRoll.prepend(`
${name}
`); + Renderer.dice._$lastRolledBy = $(`
`).data("name", name); + Renderer.dice._$outRoll.prepend(Renderer.dice._$lastRolledBy); + } + }, +}; + +Renderer.dice.util = { + getReducedMeta (meta) { + return {pb: meta.pb}; + }, +}; + +Renderer.dice.lang = { + // region Public API + validate3 (str) { + str = str.trim(); + + // region Lexing + let lexed; + try { + lexed = Renderer.dice.lang._lex3(str).lexed; + } catch (e) { + return e.message; + } + // endregion + + // region Parsing + try { + Renderer.dice.lang._parse3(lexed); + } catch (e) { + return e.message; + } + // endregion + + return null; + }, + + getTree3 (str, isSilent = true) { + str = str.trim(); + if (isSilent) { + try { + const {lexed, lexedMeta} = Renderer.dice.lang._lex3(str); + return {tree: Renderer.dice.lang._parse3(lexed), meta: lexedMeta}; + } catch (e) { + return null; + } + } else { + const {lexed, lexedMeta} = Renderer.dice.lang._lex3(str); + return {tree: Renderer.dice.lang._parse3(lexed), meta: lexedMeta}; + } + }, + // endregion + + // region Lexer + _M_NUMBER_CHAR: /[0-9.]/, + _M_SYMBOL_CHAR: /[-+/*^=>) without opening (`); + this._lex3_outputToken(self); + self.token = ")"; + this._lex3_outputToken(self); + break; + case "{": + self.braceCount++; + this._lex3_outputToken(self); + self.token = "{"; + this._lex3_outputToken(self); + break; + case "}": + self.braceCount--; + if (self.parenCount < 0) throw new Error(`Syntax error: closing } without opening (`); + this._lex3_outputToken(self); + self.token = "}"; + this._lex3_outputToken(self); + break; + // single-character operators + case "+": case "-": case "*": case "/": case "^": case ",": + this._lex3_outputToken(self); + self.token += c; + this._lex3_outputToken(self); + break; + default: { + if (Renderer.dice.lang._M_NUMBER_CHAR.test(c)) { + if (self.mode === "symbol") this._lex3_outputToken(self); + self.token += c; + self.mode = "text"; + } else if (Renderer.dice.lang._M_SYMBOL_CHAR.test(c)) { + if (self.mode === "text") this._lex3_outputToken(self); + self.token += c; + self.mode = "symbol"; + } else throw new Error(`Syntax error: unexpected character ${c}`); + break; + } + } + } + + // empty the stack of any remaining content + this._lex3_outputToken(self); + }, + + _lex3_outputToken (self) { + if (!self.token) return; + + switch (self.token) { + case "(": self.tokenStack.push(Renderer.dice.tk.PAREN_OPEN); break; + case ")": self.tokenStack.push(Renderer.dice.tk.PAREN_CLOSE); break; + case "{": self.tokenStack.push(Renderer.dice.tk.BRACE_OPEN); break; + case "}": self.tokenStack.push(Renderer.dice.tk.BRACE_CLOSE); break; + case ",": self.tokenStack.push(Renderer.dice.tk.COMMA); break; + case "+": self.tokenStack.push(Renderer.dice.tk.ADD); break; + case "-": self.tokenStack.push(Renderer.dice.tk.SUB); break; + case "*": self.tokenStack.push(Renderer.dice.tk.MULT); break; + case "/": self.tokenStack.push(Renderer.dice.tk.DIV); break; + case "^": self.tokenStack.push(Renderer.dice.tk.POW); break; + case "pb": self.tokenStack.push(Renderer.dice.tk.PB); self.hasPb = true; break; + case "summonspelllevel": self.tokenStack.push(Renderer.dice.tk.SUMMON_SPELL_LEVEL); self.hasSummonSpellLevel = true; break; + case "summonclasslevel": self.tokenStack.push(Renderer.dice.tk.SUMMON_CLASS_LEVEL); self.hasSummonClassLevel = true; break; + case "floor": self.tokenStack.push(Renderer.dice.tk.FLOOR); break; + case "ceil": self.tokenStack.push(Renderer.dice.tk.CEIL); break; + case "round": self.tokenStack.push(Renderer.dice.tk.ROUND); break; + case "avg": self.tokenStack.push(Renderer.dice.tk.AVERAGE); break; + case "dmax": self.tokenStack.push(Renderer.dice.tk.DMAX); break; + case "dmin": self.tokenStack.push(Renderer.dice.tk.DMIN); break; + case "sign": self.tokenStack.push(Renderer.dice.tk.SIGN); break; + case "abs": self.tokenStack.push(Renderer.dice.tk.ABS); break; + case "cbrt": self.tokenStack.push(Renderer.dice.tk.CBRT); break; + case "sqrt": self.tokenStack.push(Renderer.dice.tk.SQRT); break; + case "exp": self.tokenStack.push(Renderer.dice.tk.EXP); break; + case "log": self.tokenStack.push(Renderer.dice.tk.LOG); break; + case "random": self.tokenStack.push(Renderer.dice.tk.RANDOM); break; + case "trunc": self.tokenStack.push(Renderer.dice.tk.TRUNC); break; + case "pow": self.tokenStack.push(Renderer.dice.tk.POW); break; + case "max": self.tokenStack.push(Renderer.dice.tk.MAX); break; + case "min": self.tokenStack.push(Renderer.dice.tk.MIN); break; + case "d": self.tokenStack.push(Renderer.dice.tk.DICE); break; + case "dh": self.tokenStack.push(Renderer.dice.tk.DROP_HIGHEST); break; + case "kh": self.tokenStack.push(Renderer.dice.tk.KEEP_HIGHEST); break; + case "dl": self.tokenStack.push(Renderer.dice.tk.DROP_LOWEST); break; + case "kl": self.tokenStack.push(Renderer.dice.tk.KEEP_LOWEST); break; + case "r": self.tokenStack.push(Renderer.dice.tk.REROLL_EXACT); break; + case "r>": self.tokenStack.push(Renderer.dice.tk.REROLL_GT); break; + case "r>=": self.tokenStack.push(Renderer.dice.tk.REROLL_GTEQ); break; + case "r<": self.tokenStack.push(Renderer.dice.tk.REROLL_LT); break; + case "r<=": self.tokenStack.push(Renderer.dice.tk.REROLL_LTEQ); break; + case "x": self.tokenStack.push(Renderer.dice.tk.EXPLODE_EXACT); break; + case "x>": self.tokenStack.push(Renderer.dice.tk.EXPLODE_GT); break; + case "x>=": self.tokenStack.push(Renderer.dice.tk.EXPLODE_GTEQ); break; + case "x<": self.tokenStack.push(Renderer.dice.tk.EXPLODE_LT); break; + case "x<=": self.tokenStack.push(Renderer.dice.tk.EXPLODE_LTEQ); break; + case "cs=": self.tokenStack.push(Renderer.dice.tk.COUNT_SUCCESS_EXACT); break; + case "cs>": self.tokenStack.push(Renderer.dice.tk.COUNT_SUCCESS_GT); break; + case "cs>=": self.tokenStack.push(Renderer.dice.tk.COUNT_SUCCESS_GTEQ); break; + case "cs<": self.tokenStack.push(Renderer.dice.tk.COUNT_SUCCESS_LT); break; + case "cs<=": self.tokenStack.push(Renderer.dice.tk.COUNT_SUCCESS_LTEQ); break; + case "ms=": self.tokenStack.push(Renderer.dice.tk.MARGIN_SUCCESS_EXACT); break; + case "ms>": self.tokenStack.push(Renderer.dice.tk.MARGIN_SUCCESS_GT); break; + case "ms>=": self.tokenStack.push(Renderer.dice.tk.MARGIN_SUCCESS_GTEQ); break; + case "ms<": self.tokenStack.push(Renderer.dice.tk.MARGIN_SUCCESS_LT); break; + case "ms<=": self.tokenStack.push(Renderer.dice.tk.MARGIN_SUCCESS_LTEQ); break; + default: { + if (Renderer.dice.lang._M_NUMBER.test(self.token)) { + if (self.token.split(Parser._decimalSeparator).length > 2) throw new Error(`Syntax error: too many decimal separators ${self.token}`); + self.tokenStack.push(Renderer.dice.tk.NUMBER(self.token)); + } else throw new Error(`Syntax error: unexpected token ${self.token}`); + } + } + + self.token = ""; + }, + // endregion + + // region Parser + _parse3 (lexed) { + const self = { + ixSym: -1, + syms: lexed, + sym: null, + lastAccepted: null, + // Workaround for comma-separated numbers--if we're e.g. inside a dice pool, treat the commas as dice pool + // separators. Otherwise, merge together adjacent numbers, to convert e.g. "1,000,000" to "1000000". + isIgnoreCommas: true, + }; + + this._parse3_nextSym(self); + return this._parse3_expression(self); + }, + + _parse3_nextSym (self) { + const cur = self.syms[self.ixSym]; + self.ixSym++; + self.sym = self.syms[self.ixSym]; + return cur; + }, + + _parse3_match (self, symbol) { + if (self.sym == null) return false; + if (symbol.type) symbol = symbol.type; // If it's a typed token, convert it to its underlying type + return self.sym.type === symbol; + }, + + _parse3_accept (self, symbol) { + if (this._parse3_match(self, symbol)) { + const out = self.sym; + this._parse3_nextSym(self); + self.lastAccepted = out; + return out; + } + return false; + }, + + _parse3_expect (self, symbol) { + const accepted = this._parse3_accept(self, symbol); + if (accepted) return accepted; + if (self.sym) throw new Error(`Unexpected input: Expected ${symbol} but found ${self.sym}`); + else throw new Error(`Unexpected end of input: Expected ${symbol}`); + }, + + _parse3_factor (self, {isSilent = false} = {}) { + if (this._parse3_accept(self, Renderer.dice.tk.TYP_NUMBER)) { + // Workaround for comma-separated numbers + if (self.isIgnoreCommas) { + // Combine comma-separated parts + const syms = [self.lastAccepted]; + while (this._parse3_accept(self, Renderer.dice.tk.COMMA)) { + const sym = this._parse3_expect(self, Renderer.dice.tk.TYP_NUMBER); + syms.push(sym); + } + const sym = Renderer.dice.tk.NUMBER(syms.map(it => it.value).join("")); + return new Renderer.dice.parsed.Factor(sym); + } + + return new Renderer.dice.parsed.Factor(self.lastAccepted); + } else if (this._parse3_accept(self, Renderer.dice.tk.PB)) { + return new Renderer.dice.parsed.Factor(Renderer.dice.tk.PB); + } else if (this._parse3_accept(self, Renderer.dice.tk.SUMMON_SPELL_LEVEL)) { + return new Renderer.dice.parsed.Factor(Renderer.dice.tk.SUMMON_SPELL_LEVEL); + } else if (this._parse3_accept(self, Renderer.dice.tk.SUMMON_CLASS_LEVEL)) { + return new Renderer.dice.parsed.Factor(Renderer.dice.tk.SUMMON_CLASS_LEVEL); + } else if ( + // Single-arg functions + this._parse3_match(self, Renderer.dice.tk.FLOOR) + || this._parse3_match(self, Renderer.dice.tk.CEIL) + || this._parse3_match(self, Renderer.dice.tk.ROUND) + || this._parse3_match(self, Renderer.dice.tk.AVERAGE) + || this._parse3_match(self, Renderer.dice.tk.DMAX) + || this._parse3_match(self, Renderer.dice.tk.DMIN) + || this._parse3_match(self, Renderer.dice.tk.SIGN) + || this._parse3_match(self, Renderer.dice.tk.ABS) + || this._parse3_match(self, Renderer.dice.tk.CBRT) + || this._parse3_match(self, Renderer.dice.tk.SQRT) + || this._parse3_match(self, Renderer.dice.tk.EXP) + || this._parse3_match(self, Renderer.dice.tk.LOG) + || this._parse3_match(self, Renderer.dice.tk.RANDOM) + || this._parse3_match(self, Renderer.dice.tk.TRUNC) + ) { + const children = []; + + children.push(this._parse3_nextSym(self)); + this._parse3_expect(self, Renderer.dice.tk.PAREN_OPEN); + children.push(this._parse3_expression(self)); + this._parse3_expect(self, Renderer.dice.tk.PAREN_CLOSE); + + return new Renderer.dice.parsed.Function(children); + } else if ( + // 2-arg functions + this._parse3_match(self, Renderer.dice.tk.POW) + ) { + self.isIgnoreCommas = false; + + const children = []; + + children.push(this._parse3_nextSym(self)); + this._parse3_expect(self, Renderer.dice.tk.PAREN_OPEN); + children.push(this._parse3_expression(self)); + this._parse3_expect(self, Renderer.dice.tk.COMMA); + children.push(this._parse3_expression(self)); + this._parse3_expect(self, Renderer.dice.tk.PAREN_CLOSE); + + self.isIgnoreCommas = true; + + return new Renderer.dice.parsed.Function(children); + } else if ( + // N-arg functions + this._parse3_match(self, Renderer.dice.tk.MAX) + || this._parse3_match(self, Renderer.dice.tk.MIN) + ) { + self.isIgnoreCommas = false; + + const children = []; + + children.push(this._parse3_nextSym(self)); + this._parse3_expect(self, Renderer.dice.tk.PAREN_OPEN); + children.push(this._parse3_expression(self)); + while (this._parse3_accept(self, Renderer.dice.tk.COMMA)) children.push(this._parse3_expression(self)); + this._parse3_expect(self, Renderer.dice.tk.PAREN_CLOSE); + + self.isIgnoreCommas = true; + + return new Renderer.dice.parsed.Function(children); + } else if (this._parse3_accept(self, Renderer.dice.tk.PAREN_OPEN)) { + const exp = this._parse3_expression(self); + this._parse3_expect(self, Renderer.dice.tk.PAREN_CLOSE); + return new Renderer.dice.parsed.Factor(exp, {hasParens: true}); + } else if (this._parse3_accept(self, Renderer.dice.tk.BRACE_OPEN)) { + self.isIgnoreCommas = false; + + const children = []; + + children.push(this._parse3_expression(self)); + while (this._parse3_accept(self, Renderer.dice.tk.COMMA)) children.push(this._parse3_expression(self)); + + this._parse3_expect(self, Renderer.dice.tk.BRACE_CLOSE); + + self.isIgnoreCommas = true; + + const modPart = []; + this._parse3__dice_modifiers(self, modPart); + + return new Renderer.dice.parsed.Pool(children, modPart[0]); + } else { + if (isSilent) return null; + + if (self.sym) throw new Error(`Unexpected input: ${self.sym}`); + else throw new Error(`Unexpected end of input`); + } + }, + + _parse3_dice (self) { + const children = []; + + // if we've omitted the X in XdY, add it here + if (this._parse3_match(self, Renderer.dice.tk.DICE)) children.push(new Renderer.dice.parsed.Factor(Renderer.dice.tk.NUMBER(1))); + else children.push(this._parse3_factor(self)); + + while (this._parse3_match(self, Renderer.dice.tk.DICE)) { + this._parse3_nextSym(self); + children.push(this._parse3_factor(self)); + this._parse3__dice_modifiers(self, children); + } + return new Renderer.dice.parsed.Dice(children); + }, + + _parse3__dice_modifiers (self, children) { // used in both dice and dice pools + // Collect together all dice mods + const modsMeta = new Renderer.dice.lang.DiceModMeta(); + + while ( + this._parse3_match(self, Renderer.dice.tk.DROP_HIGHEST) + || this._parse3_match(self, Renderer.dice.tk.KEEP_HIGHEST) + || this._parse3_match(self, Renderer.dice.tk.DROP_LOWEST) + || this._parse3_match(self, Renderer.dice.tk.KEEP_LOWEST) + || this._parse3_match(self, Renderer.dice.tk.REROLL_EXACT) + || this._parse3_match(self, Renderer.dice.tk.REROLL_GT) + || this._parse3_match(self, Renderer.dice.tk.REROLL_GTEQ) + || this._parse3_match(self, Renderer.dice.tk.REROLL_LT) + || this._parse3_match(self, Renderer.dice.tk.REROLL_LTEQ) + || this._parse3_match(self, Renderer.dice.tk.EXPLODE_EXACT) + || this._parse3_match(self, Renderer.dice.tk.EXPLODE_GT) + || this._parse3_match(self, Renderer.dice.tk.EXPLODE_GTEQ) + || this._parse3_match(self, Renderer.dice.tk.EXPLODE_LT) + || this._parse3_match(self, Renderer.dice.tk.EXPLODE_LTEQ) + || this._parse3_match(self, Renderer.dice.tk.COUNT_SUCCESS_EXACT) + || this._parse3_match(self, Renderer.dice.tk.COUNT_SUCCESS_GT) + || this._parse3_match(self, Renderer.dice.tk.COUNT_SUCCESS_GTEQ) + || this._parse3_match(self, Renderer.dice.tk.COUNT_SUCCESS_LT) + || this._parse3_match(self, Renderer.dice.tk.COUNT_SUCCESS_LTEQ) + || this._parse3_match(self, Renderer.dice.tk.MARGIN_SUCCESS_EXACT) + || this._parse3_match(self, Renderer.dice.tk.MARGIN_SUCCESS_GT) + || this._parse3_match(self, Renderer.dice.tk.MARGIN_SUCCESS_GTEQ) + || this._parse3_match(self, Renderer.dice.tk.MARGIN_SUCCESS_LT) + || this._parse3_match(self, Renderer.dice.tk.MARGIN_SUCCESS_LTEQ) + ) { + const nxtSym = this._parse3_nextSym(self); + const nxtFactor = this._parse3__dice_modifiers_nxtFactor(self, nxtSym); + + if (nxtSym.isSuccessMode) modsMeta.isSuccessMode = true; + modsMeta.mods.push({modSym: nxtSym, numSym: nxtFactor}); + } + + if (modsMeta.mods.length) children.push(modsMeta); + }, + + _parse3__dice_modifiers_nxtFactor (self, nxtSym) { + if (nxtSym.diceModifierImplicit == null) return this._parse3_factor(self, {isSilent: true}); + + const fallback = new Renderer.dice.parsed.Factor(Renderer.dice.tk.NUMBER(nxtSym.diceModifierImplicit)); + if (self.sym == null) return fallback; + + const out = this._parse3_factor(self, {isSilent: true}); + if (out) return out; + + return fallback; + }, + + _parse3_exponent (self) { + const children = []; + children.push(this._parse3_dice(self)); + while (this._parse3_match(self, Renderer.dice.tk.POW)) { + this._parse3_nextSym(self); + children.push(this._parse3_dice(self)); + } + return new Renderer.dice.parsed.Exponent(children); + }, + + _parse3_term (self) { + const children = []; + children.push(this._parse3_exponent(self)); + while (this._parse3_match(self, Renderer.dice.tk.MULT) || this._parse3_match(self, Renderer.dice.tk.DIV)) { + children.push(this._parse3_nextSym(self)); + children.push(this._parse3_exponent(self)); + } + return new Renderer.dice.parsed.Term(children); + }, + + _parse3_expression (self) { + const children = []; + if (this._parse3_match(self, Renderer.dice.tk.ADD) || this._parse3_match(self, Renderer.dice.tk.SUB)) children.push(this._parse3_nextSym(self)); + children.push(this._parse3_term(self)); + while (this._parse3_match(self, Renderer.dice.tk.ADD) || this._parse3_match(self, Renderer.dice.tk.SUB)) { + children.push(this._parse3_nextSym(self)); + children.push(this._parse3_term(self)); + } + return new Renderer.dice.parsed.Expression(children); + }, + // endregion + + // region Utilities + DiceModMeta: class { + constructor () { + this.isDiceModifierGroup = true; + this.isSuccessMode = false; + this.mods = []; + } + }, + // endregion +}; + +Renderer.dice.tk = { + Token: class { + /** + * @param type + * @param value + * @param asString + * @param [opts] Options object. + * @param [opts.isDiceModifier] If the token is a dice modifier, e.g. "dl" + * @param [opts.diceModifierImplicit] If the dice modifier has an implicit value (e.g. "kh" is shorthand for "kh1") + * @param [opts.isSuccessMode] If the token is a "success"-based dice modifier, e.g. "cs=" + */ + constructor (type, value, asString, opts) { + opts = opts || {}; + this.type = type; + this.value = value; + this._asString = asString; + if (opts.isDiceModifier) this.isDiceModifier = true; + if (opts.diceModifierImplicit) this.diceModifierImplicit = true; + if (opts.isSuccessMode) this.isSuccessMode = true; + } + + eq (other) { return other && other.type === this.type; } + + toString () { + if (this._asString) return this._asString; + return this.toDebugString(); + } + + toDebugString () { return `${this.type}${this.value ? ` :: ${this.value}` : ""}`; } + }, + + _new (type, asString, opts) { return new Renderer.dice.tk.Token(type, null, asString, opts); }, + + TYP_NUMBER: "NUMBER", + TYP_DICE: "DICE", + TYP_SYMBOL: "SYMBOL", // Cannot be created by lexing, only parsing + + NUMBER (val) { return new Renderer.dice.tk.Token(Renderer.dice.tk.TYP_NUMBER, val); }, +}; +Renderer.dice.tk.PAREN_OPEN = Renderer.dice.tk._new("PAREN_OPEN", "("); +Renderer.dice.tk.PAREN_CLOSE = Renderer.dice.tk._new("PAREN_CLOSE", ")"); +Renderer.dice.tk.BRACE_OPEN = Renderer.dice.tk._new("BRACE_OPEN", "{"); +Renderer.dice.tk.BRACE_CLOSE = Renderer.dice.tk._new("BRACE_CLOSE", "}"); +Renderer.dice.tk.COMMA = Renderer.dice.tk._new("COMMA", ","); +Renderer.dice.tk.ADD = Renderer.dice.tk._new("ADD", "+"); +Renderer.dice.tk.SUB = Renderer.dice.tk._new("SUB", "-"); +Renderer.dice.tk.MULT = Renderer.dice.tk._new("MULT", "*"); +Renderer.dice.tk.DIV = Renderer.dice.tk._new("DIV", "/"); +Renderer.dice.tk.POW = Renderer.dice.tk._new("POW", "^"); +Renderer.dice.tk.PB = Renderer.dice.tk._new("PB", "pb"); +Renderer.dice.tk.SUMMON_SPELL_LEVEL = Renderer.dice.tk._new("SUMMON_SPELL_LEVEL", "summonspelllevel"); +Renderer.dice.tk.SUMMON_CLASS_LEVEL = Renderer.dice.tk._new("SUMMON_CLASS_LEVEL", "summonclasslevel"); +Renderer.dice.tk.FLOOR = Renderer.dice.tk._new("FLOOR", "floor"); +Renderer.dice.tk.CEIL = Renderer.dice.tk._new("CEIL", "ceil"); +Renderer.dice.tk.ROUND = Renderer.dice.tk._new("ROUND", "round"); +Renderer.dice.tk.AVERAGE = Renderer.dice.tk._new("AVERAGE", "avg"); +Renderer.dice.tk.DMAX = Renderer.dice.tk._new("DMAX", "avg"); +Renderer.dice.tk.DMIN = Renderer.dice.tk._new("DMIN", "avg"); +Renderer.dice.tk.SIGN = Renderer.dice.tk._new("SIGN", "sign"); +Renderer.dice.tk.ABS = Renderer.dice.tk._new("ABS", "abs"); +Renderer.dice.tk.CBRT = Renderer.dice.tk._new("CBRT", "cbrt"); +Renderer.dice.tk.SQRT = Renderer.dice.tk._new("SQRT", "sqrt"); +Renderer.dice.tk.EXP = Renderer.dice.tk._new("EXP", "exp"); +Renderer.dice.tk.LOG = Renderer.dice.tk._new("LOG", "log"); +Renderer.dice.tk.RANDOM = Renderer.dice.tk._new("RANDOM", "random"); +Renderer.dice.tk.TRUNC = Renderer.dice.tk._new("TRUNC", "trunc"); +Renderer.dice.tk.POW = Renderer.dice.tk._new("POW", "pow"); +Renderer.dice.tk.MAX = Renderer.dice.tk._new("MAX", "max"); +Renderer.dice.tk.MIN = Renderer.dice.tk._new("MIN", "min"); +Renderer.dice.tk.DICE = Renderer.dice.tk._new("DICE", "d"); +Renderer.dice.tk.DROP_HIGHEST = Renderer.dice.tk._new("DH", "dh", {isDiceModifier: true, diceModifierImplicit: 1}); +Renderer.dice.tk.KEEP_HIGHEST = Renderer.dice.tk._new("KH", "kh", {isDiceModifier: true, diceModifierImplicit: 1}); +Renderer.dice.tk.DROP_LOWEST = Renderer.dice.tk._new("DL", "dl", {isDiceModifier: true, diceModifierImplicit: 1}); +Renderer.dice.tk.KEEP_LOWEST = Renderer.dice.tk._new("KL", "kl", {isDiceModifier: true, diceModifierImplicit: 1}); +Renderer.dice.tk.REROLL_EXACT = Renderer.dice.tk._new("REROLL", "r", {isDiceModifier: true}); +Renderer.dice.tk.REROLL_GT = Renderer.dice.tk._new("REROLL_GT", "r>", {isDiceModifier: true}); +Renderer.dice.tk.REROLL_GTEQ = Renderer.dice.tk._new("REROLL_GTEQ", "r>=", {isDiceModifier: true}); +Renderer.dice.tk.REROLL_LT = Renderer.dice.tk._new("REROLL_LT", "r<", {isDiceModifier: true}); +Renderer.dice.tk.REROLL_LTEQ = Renderer.dice.tk._new("REROLL_LTEQ", "r<=", {isDiceModifier: true}); +Renderer.dice.tk.EXPLODE_EXACT = Renderer.dice.tk._new("EXPLODE", "x", {isDiceModifier: true}); +Renderer.dice.tk.EXPLODE_GT = Renderer.dice.tk._new("EXPLODE_GT", "x>", {isDiceModifier: true}); +Renderer.dice.tk.EXPLODE_GTEQ = Renderer.dice.tk._new("EXPLODE_GTEQ", "x>=", {isDiceModifier: true}); +Renderer.dice.tk.EXPLODE_LT = Renderer.dice.tk._new("EXPLODE_LT", "x<", {isDiceModifier: true}); +Renderer.dice.tk.EXPLODE_LTEQ = Renderer.dice.tk._new("EXPLODE_LTEQ", "x<=", {isDiceModifier: true}); +Renderer.dice.tk.COUNT_SUCCESS_EXACT = Renderer.dice.tk._new("COUNT_SUCCESS_EXACT", "cs=", {isDiceModifier: true, isSuccessMode: true}); +Renderer.dice.tk.COUNT_SUCCESS_GT = Renderer.dice.tk._new("COUNT_SUCCESS_GT", "cs>", {isDiceModifier: true, isSuccessMode: true}); +Renderer.dice.tk.COUNT_SUCCESS_GTEQ = Renderer.dice.tk._new("COUNT_SUCCESS_GTEQ", "cs>=", {isDiceModifier: true, isSuccessMode: true}); +Renderer.dice.tk.COUNT_SUCCESS_LT = Renderer.dice.tk._new("COUNT_SUCCESS_LT", "cs<", {isDiceModifier: true, isSuccessMode: true}); +Renderer.dice.tk.COUNT_SUCCESS_LTEQ = Renderer.dice.tk._new("COUNT_SUCCESS_LTEQ", "cs<=", {isDiceModifier: true, isSuccessMode: true}); +Renderer.dice.tk.MARGIN_SUCCESS_EXACT = Renderer.dice.tk._new("MARGIN_SUCCESS_EXACT", "ms=", {isDiceModifier: true}); +Renderer.dice.tk.MARGIN_SUCCESS_GT = Renderer.dice.tk._new("MARGIN_SUCCESS_GT", "ms>", {isDiceModifier: true}); +Renderer.dice.tk.MARGIN_SUCCESS_GTEQ = Renderer.dice.tk._new("MARGIN_SUCCESS_GTEQ", "ms>=", {isDiceModifier: true}); +Renderer.dice.tk.MARGIN_SUCCESS_LT = Renderer.dice.tk._new("MARGIN_SUCCESS_LT", "ms<", {isDiceModifier: true}); +Renderer.dice.tk.MARGIN_SUCCESS_LTEQ = Renderer.dice.tk._new("MARGIN_SUCCESS_LTEQ", "ms<=", {isDiceModifier: true}); + +Renderer.dice.AbstractSymbol = class { + constructor () { this.type = Renderer.dice.tk.TYP_SYMBOL; } + eq (symbol) { return symbol && this.type === symbol.type; } + evl (meta) { this.meta = meta; return this._evl(meta); } + avg (meta) { this.meta = meta; return this._avg(meta); } + min (meta) { this.meta = meta; return this._min(meta); } // minimum value of all _rolls_, not the minimum possible result + max (meta) { this.meta = meta; return this._max(meta); } // maximum value of all _rolls_, not the maximum possible result + _evl () { throw new Error("Unimplemented!"); } + _avg () { throw new Error("Unimplemented!"); } + _min () { throw new Error("Unimplemented!"); } // minimum value of all _rolls_, not the minimum possible result + _max () { throw new Error("Unimplemented!"); } // maximum value of all _rolls_, not the maximum possible result + toString () { throw new Error("Unimplemented!"); } + addToMeta (meta, {text, html = null, md = null} = {}) { + if (!meta) return; + html = html || text; + md = md || text; + (meta.html = meta.html || []).push(html); + (meta.text = meta.text || []).push(text); + (meta.md = meta.md || []).push(md); + } +}; + +Renderer.dice.parsed = { + _PARTITION_EQ: (r, compareTo) => r === compareTo, + _PARTITION_GT: (r, compareTo) => r > compareTo, + _PARTITION_GTEQ: (r, compareTo) => r >= compareTo, + _PARTITION_LT: (r, compareTo) => r < compareTo, + _PARTITION_LTEQ: (r, compareTo) => r <= compareTo, + + /** + * @param fnName + * @param meta + * @param vals + * @param nodeMod + * @param opts Options object. + * @param [opts.fnGetRerolls] Function which takes a set of rolls to be rerolled and generates the next set of rolls. + * @param [opts.fnGetExplosions] Function which takes a set of rolls to be exploded and generates the next set of rolls. + * @param [opts.faces] + */ + _handleModifiers (fnName, meta, vals, nodeMod, opts) { + opts = opts || {}; + + const displayVals = vals.slice(); // copy the array so we can sort the original + + const {mods} = nodeMod; + + for (const mod of mods) { + vals.sort(SortUtil.ascSortProp.bind(null, "val")).reverse(); + const valsAlive = vals.filter(it => !it.isDropped); + + const modNum = mod.numSym[fnName](); + + switch (mod.modSym.type) { + case Renderer.dice.tk.DROP_HIGHEST.type: + case Renderer.dice.tk.KEEP_HIGHEST.type: + case Renderer.dice.tk.DROP_LOWEST.type: + case Renderer.dice.tk.KEEP_LOWEST.type: { + const isHighest = mod.modSym.type.endsWith("H"); + + const splitPoint = isHighest ? modNum : valsAlive.length - modNum; + + const highSlice = valsAlive.slice(0, splitPoint); + const lowSlice = valsAlive.slice(splitPoint, valsAlive.length); + + switch (mod.modSym.type) { + case Renderer.dice.tk.DROP_HIGHEST.type: + case Renderer.dice.tk.KEEP_LOWEST.type: + highSlice.forEach(val => val.isDropped = true); + break; + case Renderer.dice.tk.KEEP_HIGHEST.type: + case Renderer.dice.tk.DROP_LOWEST.type: + lowSlice.forEach(val => val.isDropped = true); + break; + default: throw new Error(`Unimplemented!`); + } + break; + } + + case Renderer.dice.tk.REROLL_EXACT.type: + case Renderer.dice.tk.REROLL_GT.type: + case Renderer.dice.tk.REROLL_GTEQ.type: + case Renderer.dice.tk.REROLL_LT.type: + case Renderer.dice.tk.REROLL_LTEQ.type: { + let fnPartition; + switch (mod.modSym.type) { + case Renderer.dice.tk.REROLL_EXACT.type: fnPartition = Renderer.dice.parsed._PARTITION_EQ; break; + case Renderer.dice.tk.REROLL_GT.type: fnPartition = Renderer.dice.parsed._PARTITION_GT; break; + case Renderer.dice.tk.REROLL_GTEQ.type: fnPartition = Renderer.dice.parsed._PARTITION_GTEQ; break; + case Renderer.dice.tk.REROLL_LT.type: fnPartition = Renderer.dice.parsed._PARTITION_LT; break; + case Renderer.dice.tk.REROLL_LTEQ.type: fnPartition = Renderer.dice.parsed._PARTITION_LTEQ; break; + default: throw new Error(`Unimplemented!`); + } + + const toReroll = valsAlive.filter(val => fnPartition(val.val, modNum)); + toReroll.forEach(val => val.isDropped = true); + + const nuVals = opts.fnGetRerolls(toReroll); + + vals.push(...nuVals); + displayVals.push(...nuVals); + break; + } + + case Renderer.dice.tk.EXPLODE_EXACT.type: + case Renderer.dice.tk.EXPLODE_GT.type: + case Renderer.dice.tk.EXPLODE_GTEQ.type: + case Renderer.dice.tk.EXPLODE_LT.type: + case Renderer.dice.tk.EXPLODE_LTEQ.type: { + let fnPartition; + switch (mod.modSym.type) { + case Renderer.dice.tk.EXPLODE_EXACT.type: fnPartition = Renderer.dice.parsed._PARTITION_EQ; break; + case Renderer.dice.tk.EXPLODE_GT.type: fnPartition = Renderer.dice.parsed._PARTITION_GT; break; + case Renderer.dice.tk.EXPLODE_GTEQ.type: fnPartition = Renderer.dice.parsed._PARTITION_GTEQ; break; + case Renderer.dice.tk.EXPLODE_LT.type: fnPartition = Renderer.dice.parsed._PARTITION_LT; break; + case Renderer.dice.tk.EXPLODE_LTEQ.type: fnPartition = Renderer.dice.parsed._PARTITION_LTEQ; break; + default: throw new Error(`Unimplemented!`); + } + + let tries = 999; // limit the maximum explosions to a sane amount + let lastLen; + let toExplodeNext = valsAlive; + do { + lastLen = vals.length; + + const [toExplode] = toExplodeNext.partition(roll => !roll.isExploded && fnPartition(roll.val, modNum)); + toExplode.forEach(roll => roll.isExploded = true); + + const nuVals = opts.fnGetExplosions(toExplode); + + // cache the new rolls, to improve performance over massive explosion sets + toExplodeNext = nuVals; + + vals.push(...nuVals); + displayVals.push(...nuVals); + } while (tries-- > 0 && vals.length !== lastLen); + + if (!~tries) JqueryUtil.doToast({type: "warning", content: `Stopped exploding after 999 additional rolls.`}); + + break; + } + + case Renderer.dice.tk.COUNT_SUCCESS_EXACT.type: + case Renderer.dice.tk.COUNT_SUCCESS_GT.type: + case Renderer.dice.tk.COUNT_SUCCESS_GTEQ.type: + case Renderer.dice.tk.COUNT_SUCCESS_LT.type: + case Renderer.dice.tk.COUNT_SUCCESS_LTEQ.type: { + let fnPartition; + switch (mod.modSym.type) { + case Renderer.dice.tk.COUNT_SUCCESS_EXACT.type: fnPartition = Renderer.dice.parsed._PARTITION_EQ; break; + case Renderer.dice.tk.COUNT_SUCCESS_GT.type: fnPartition = Renderer.dice.parsed._PARTITION_GT; break; + case Renderer.dice.tk.COUNT_SUCCESS_GTEQ.type: fnPartition = Renderer.dice.parsed._PARTITION_GTEQ; break; + case Renderer.dice.tk.COUNT_SUCCESS_LT.type: fnPartition = Renderer.dice.parsed._PARTITION_LT; break; + case Renderer.dice.tk.COUNT_SUCCESS_LTEQ.type: fnPartition = Renderer.dice.parsed._PARTITION_LTEQ; break; + default: throw new Error(`Unimplemented!`); + } + + const successes = valsAlive.filter(val => fnPartition(val.val, modNum)); + successes.forEach(val => val.isSuccess = true); + + break; + } + + case Renderer.dice.tk.MARGIN_SUCCESS_EXACT.type: + case Renderer.dice.tk.MARGIN_SUCCESS_GT.type: + case Renderer.dice.tk.MARGIN_SUCCESS_GTEQ.type: + case Renderer.dice.tk.MARGIN_SUCCESS_LT.type: + case Renderer.dice.tk.MARGIN_SUCCESS_LTEQ.type: { + const total = valsAlive.map(it => it.val).reduce((valA, valB) => valA + valB, 0); + + const subDisplayDice = displayVals.map(r => `[${Renderer.dice.parsed._rollToNumPart_html(r, opts.faces)}]`).join("+"); + + let delta; + let subDisplay; + switch (mod.modSym.type) { + case Renderer.dice.tk.MARGIN_SUCCESS_EXACT.type: + case Renderer.dice.tk.MARGIN_SUCCESS_GT.type: + case Renderer.dice.tk.MARGIN_SUCCESS_GTEQ.type: { + delta = total - modNum; + + subDisplay = `(${subDisplayDice})-${modNum}`; + + break; + } + case Renderer.dice.tk.MARGIN_SUCCESS_LT.type: + case Renderer.dice.tk.MARGIN_SUCCESS_LTEQ.type: { + delta = modNum - total; + + subDisplay = `${modNum}-(${subDisplayDice})`; + + break; + } + default: throw new Error(`Unimplemented!`); + } + + while (vals.length) { + vals.pop(); + displayVals.pop(); + } + + vals.push({val: delta}); + displayVals.push({val: delta, htmlDisplay: subDisplay}); + + break; + } + + default: throw new Error(`Unimplemented!`); + } + } + + return displayVals; + }, + + _rollToNumPart_html (r, faces) { + if (faces == null) return r.val; + return r.val === faces ? `${r.val}` : r.val === 1 ? `${r.val}` : r.val; + }, + + Function: class extends Renderer.dice.AbstractSymbol { + constructor (nodes) { + super(); + this._nodes = nodes; + } + + _evl (meta) { return this._invoke("evl", meta); } + _avg (meta) { return this._invoke("avg", meta); } + _min (meta) { return this._invoke("min", meta); } + _max (meta) { return this._invoke("max", meta); } + + _invoke (fnName, meta) { + const [symFunc] = this._nodes; + switch (symFunc.type) { + case Renderer.dice.tk.FLOOR.type: + case Renderer.dice.tk.CEIL.type: + case Renderer.dice.tk.ROUND.type: + case Renderer.dice.tk.SIGN.type: + case Renderer.dice.tk.CBRT.type: + case Renderer.dice.tk.SQRT.type: + case Renderer.dice.tk.EXP.type: + case Renderer.dice.tk.LOG.type: + case Renderer.dice.tk.RANDOM.type: + case Renderer.dice.tk.TRUNC.type: + case Renderer.dice.tk.POW.type: + case Renderer.dice.tk.MAX.type: + case Renderer.dice.tk.MIN.type: { + const [, ...symExps] = this._nodes; + this.addToMeta(meta, {text: `${symFunc.toString()}(`}); + const args = []; + symExps.forEach((symExp, i) => { + if (i !== 0) this.addToMeta(meta, {text: `, `}); + args.push(symExp[fnName](meta)); + }); + const out = Math[symFunc.toString()](...args); + this.addToMeta(meta, {text: ")"}); + return out; + } + case Renderer.dice.tk.AVERAGE.type: { + const [, symExp] = this._nodes; + return symExp.avg(meta); + } + case Renderer.dice.tk.DMAX.type: { + const [, symExp] = this._nodes; + return symExp.max(meta); + } + case Renderer.dice.tk.DMIN.type: { + const [, symExp] = this._nodes; + return symExp.min(meta); + } + default: throw new Error(`Unimplemented!`); + } + } + + toString () { + let out; + const [symFunc, symExp] = this._nodes; + switch (symFunc.type) { + case Renderer.dice.tk.FLOOR.type: + case Renderer.dice.tk.CEIL.type: + case Renderer.dice.tk.ROUND.type: + case Renderer.dice.tk.AVERAGE.type: + case Renderer.dice.tk.DMAX.type: + case Renderer.dice.tk.DMIN.type: + case Renderer.dice.tk.SIGN.type: + case Renderer.dice.tk.ABS.type: + case Renderer.dice.tk.CBRT.type: + case Renderer.dice.tk.SQRT.type: + case Renderer.dice.tk.EXP.type: + case Renderer.dice.tk.LOG.type: + case Renderer.dice.tk.RANDOM.type: + case Renderer.dice.tk.TRUNC.type: + case Renderer.dice.tk.POW.type: + case Renderer.dice.tk.MAX.type: + case Renderer.dice.tk.MIN.type: + out = symFunc.toString(); break; + default: throw new Error(`Unimplemented!`); + } + out += `(${symExp.toString()})`; + return out; + } + }, + + Pool: class extends Renderer.dice.AbstractSymbol { + constructor (nodesPool, nodeMod) { + super(); + this._nodesPool = nodesPool; + this._nodeMod = nodeMod; + } + + _evl (meta) { return this._invoke("evl", meta); } + _avg (meta) { return this._invoke("avg", meta); } + _min (meta) { return this._invoke("min", meta); } + _max (meta) { return this._invoke("max", meta); } + + _invoke (fnName, meta) { + const vals = this._nodesPool.map(it => { + const subMeta = {}; + return {node: it, val: it[fnName](subMeta), meta: subMeta}; + }); + + if (this._nodeMod && vals.length) { + const isSuccessMode = this._nodeMod.isSuccessMode; + + const modOpts = { + fnGetRerolls: toReroll => toReroll.map(val => { + const subMeta = {}; + return {node: val.node, val: val.node[fnName](subMeta), meta: subMeta}; + }), + fnGetExplosions: toExplode => toExplode.map(val => { + const subMeta = {}; + return {node: val.node, val: val.node[fnName](subMeta), meta: subMeta}; + }), + }; + + const displayVals = Renderer.dice.parsed._handleModifiers(fnName, meta, vals, this._nodeMod, modOpts); + + const asHtml = displayVals.map(v => { + const html = v.meta.html.join(""); + if (v.isDropped) return `(${html})`; + else if (v.isExploded) return `(${html})`; + else if (v.isSuccess) return `(${html})`; + else return `(${html})`; + }).join("+"); + + const asText = displayVals.map(v => `(${v.meta.text.join("")})`).join("+"); + const asMd = displayVals.map(v => `(${v.meta.md.join("")})`).join("+"); + + this.addToMeta(meta, {html: asHtml, text: asText, md: asMd}); + + if (isSuccessMode) { + return vals.filter(it => !it.isDropped && it.isSuccess).length; + } else { + return vals.filter(it => !it.isDropped).map(it => it.val).sum(); + } + } else { + this.addToMeta( + meta, + ["html", "text", "md"].mergeMap(prop => ({ + [prop]: `${vals.map(it => `(${it.meta[prop].join("")})`).join("+")}`, + })), + ); + return vals.map(it => it.val).sum(); + } + } + + toString () { + return `{${this._nodesPool.map(it => it.toString()).join(", ")}}${this._nodeMod ? this._nodeMod.toString() : ""}`; + } + }, + + Factor: class extends Renderer.dice.AbstractSymbol { + constructor (node, opts) { + super(); + opts = opts || {}; + this._node = node; + this._hasParens = !!opts.hasParens; + } + + _evl (meta) { return this._invoke("evl", meta); } + _avg (meta) { return this._invoke("avg", meta); } + _min (meta) { return this._invoke("min", meta); } + _max (meta) { return this._invoke("max", meta); } + + _invoke (fnName, meta) { + switch (this._node.type) { + case Renderer.dice.tk.TYP_NUMBER: { + this.addToMeta(meta, {text: this.toString()}); + return Number(this._node.value); + } + case Renderer.dice.tk.TYP_SYMBOL: { + if (this._hasParens) this.addToMeta(meta, {text: "("}); + const out = this._node[fnName](meta); + if (this._hasParens) this.addToMeta(meta, {text: ")"}); + return out; + } + case Renderer.dice.tk.PB.type: { + this.addToMeta(meta, {text: this.toString(meta)}); + return meta.pb == null ? 0 : meta.pb; + } + case Renderer.dice.tk.SUMMON_SPELL_LEVEL.type: { + this.addToMeta(meta, {text: this.toString(meta)}); + return meta.summonSpellLevel == null ? 0 : meta.summonSpellLevel; + } + case Renderer.dice.tk.SUMMON_CLASS_LEVEL.type: { + this.addToMeta(meta, {text: this.toString(meta)}); + return meta.summonClassLevel == null ? 0 : meta.summonClassLevel; + } + default: throw new Error(`Unimplemented!`); + } + } + + toString (indent) { + let out; + switch (this._node.type) { + case Renderer.dice.tk.TYP_NUMBER: out = this._node.value; break; + case Renderer.dice.tk.TYP_SYMBOL: out = this._node.toString(); break; + case Renderer.dice.tk.PB.type: out = this.meta ? (this.meta.pb || 0) : "PB"; break; + case Renderer.dice.tk.SUMMON_SPELL_LEVEL.type: out = this.meta ? (this.meta.summonSpellLevel || 0) : "the spell's level"; break; + case Renderer.dice.tk.SUMMON_CLASS_LEVEL.type: out = this.meta ? (this.meta.summonClassLevel || 0) : "your class level"; break; + default: throw new Error(`Unimplemented!`); + } + return this._hasParens ? `(${out})` : out; + } + }, + + Dice: class extends Renderer.dice.AbstractSymbol { + static _facesToValue (faces, fnName) { + switch (fnName) { + case "evl": return RollerUtil.randomise(faces); + case "avg": return (faces + 1) / 2; + case "min": return 1; + case "max": return faces; + } + } + + constructor (nodes) { + super(); + this._nodes = nodes; + } + + _evl (meta) { return this._invoke("evl", meta); } + _avg (meta) { return this._invoke("avg", meta); } + _min (meta) { return this._invoke("min", meta); } + _max (meta) { return this._invoke("max", meta); } + + _invoke (fnName, meta) { + if (this._nodes.length === 1) return this._nodes[0][fnName](meta); // if it's just a factor + + // N.B. we don't pass the full "meta" to symbol evaluation inside the dice expression--we therefore won't see + // the metadata from the nested rolls, but that's OK. + + const view = this._nodes.slice(); + // Shift the first symbol and use that as our initial number of dice + // e.g. the "2" in 2d3d5 + const numSym = view.shift(); + let tmp = numSym[fnName](Renderer.dice.util.getReducedMeta(meta)); + + while (view.length) { + if (Math.round(tmp) !== tmp) throw new Error(`Number of dice to roll (${tmp}) was not an integer!`); + + // Use the next symbol as our number of faces + // e.g. the "3" in `2d3d5` + // When looping, the number of dice may have been a complex expression with modifiers; take the next + // non-modifier symbol as the faces. + // e.g. the "20" in `(2d3kh1r1)d20` (parentheses for emphasis) + const facesSym = view.shift(); + const faces = facesSym[fnName](); + if (Math.round(faces) !== faces) throw new Error(`Dice face count (${faces}) was not an integer!`); + + const isLast = view.length === 0 || (view.length === 1 && view.last().isDiceModifierGroup); + tmp = this._invoke_handlePart(fnName, meta, view, tmp, faces, isLast); + } + + return tmp; + } + + _invoke_handlePart (fnName, meta, view, num, faces, isLast) { + const rolls = [...new Array(num)].map(() => ({val: Renderer.dice.parsed.Dice._facesToValue(faces, fnName)})); + let displayRolls; + let isSuccessMode = false; + + if (view.length && view[0].isDiceModifierGroup) { + const nodeMod = view[0]; + + if (fnName === "evl" || fnName === "min" || fnName === "max") { // avoid handling dice modifiers in "average" mode + isSuccessMode = nodeMod.isSuccessMode; + + const modOpts = { + faces, + fnGetRerolls: toReroll => [...new Array(toReroll.length)].map(() => ({val: Renderer.dice.parsed.Dice._facesToValue(faces, fnName)})), + fnGetExplosions: toExplode => [...new Array(toExplode.length)].map(() => ({val: Renderer.dice.parsed.Dice._facesToValue(faces, fnName)})), + }; + + displayRolls = Renderer.dice.parsed._handleModifiers(fnName, meta, rolls, nodeMod, modOpts); + } + + view.shift(); + } else displayRolls = rolls; + + if (isLast) { // only display the dice for the final roll, e.g. in 2d3d4 show the Xd4 + const asHtml = displayRolls.map(r => { + if (r.htmlDisplay) return r.htmlDisplay; + + const numPart = Renderer.dice.parsed._rollToNumPart_html(r, faces); + + if (r.isDropped) return `[${numPart}]`; + else if (r.isExploded) return `[${numPart}]`; + else if (r.isSuccess) return `[${numPart}]`; + else return `[${numPart}]`; + }).join("+"); + + const asText = displayRolls.map(r => `[${r.val}]`).join("+"); + + const asMd = displayRolls.map(r => { + if (r.isDropped) return `~~[${r.val}]~~`; + else if (r.isExploded) return `_[${r.val}]_`; + else if (r.isSuccess) return `**[${r.val}]**`; + else return `[${r.val}]`; + }).join("+"); + + this.addToMeta( + meta, + { + html: asHtml, + text: asText, + md: asMd, + }, + ); + } + + if (fnName === "evl") { + const maxRolls = rolls.filter(it => it.val === faces && !it.isDropped); + const minRolls = rolls.filter(it => it.val === 1 && !it.isDropped); + meta.allMax = meta.allMax || []; + meta.allMin = meta.allMin || []; + meta.allMax.push(maxRolls.length && maxRolls.length === rolls.length); + meta.allMin.push(minRolls.length && minRolls.length === rolls.length); + } + + if (isSuccessMode) { + return rolls.filter(it => !it.isDropped && it.isSuccess).length; + } else { + return rolls.filter(it => !it.isDropped).map(it => it.val).sum(); + } + } + + toString () { + if (this._nodes.length === 1) return this._nodes[0].toString(); // if it's just a factor + + const [numSym, facesSym] = this._nodes; + let out = `${numSym.toString()}d${facesSym.toString()}`; + + for (let i = 2; i < this._nodes.length; ++i) { + const n = this._nodes[i]; + if (n.isDiceModifierGroup) out += n.mods.map(it => `${it.modSym.toString()}${it.numSym.toString()}`).join(""); + else out += `d${n.toString()}`; + } + + return out; + } + }, + + Exponent: class extends Renderer.dice.AbstractSymbol { + constructor (nodes) { + super(); + this._nodes = nodes; + } + + _evl (meta) { return this._invoke("evl", meta); } + _avg (meta) { return this._invoke("avg", meta); } + _min (meta) { return this._invoke("min", meta); } + _max (meta) { return this._invoke("max", meta); } + + _invoke (fnName, meta) { + const view = this._nodes.slice(); + let val = view.pop()[fnName](meta); + while (view.length) { + this.addToMeta(meta, {text: "^"}); + val = view.pop()[fnName](meta) ** val; + } + return val; + } + + toString () { + const view = this._nodes.slice(); + let out = view.pop().toString(); + while (view.length) out = `${view.pop().toString()}^${out}`; + return out; + } + }, + + Term: class extends Renderer.dice.AbstractSymbol { + constructor (nodes) { + super(); + this._nodes = nodes; + } + + _evl (meta) { return this._invoke("evl", meta); } + _avg (meta) { return this._invoke("avg", meta); } + _min (meta) { return this._invoke("min", meta); } + _max (meta) { return this._invoke("max", meta); } + + _invoke (fnName, meta) { + let out = this._nodes[0][fnName](meta); + + for (let i = 1; i < this._nodes.length; i += 2) { + if (this._nodes[i].eq(Renderer.dice.tk.MULT)) { + this.addToMeta(meta, {text: " ร— "}); + out *= this._nodes[i + 1][fnName](meta); + } else if (this._nodes[i].eq(Renderer.dice.tk.DIV)) { + this.addToMeta(meta, {text: " รท "}); + out /= this._nodes[i + 1][fnName](meta); + } else throw new Error(`Unimplemented!`); + } + + return out; + } + + toString () { + let out = this._nodes[0].toString(); + for (let i = 1; i < this._nodes.length; i += 2) { + if (this._nodes[i].eq(Renderer.dice.tk.MULT)) out += ` * ${this._nodes[i + 1].toString()}`; + else if (this._nodes[i].eq(Renderer.dice.tk.DIV)) out += ` / ${this._nodes[i + 1].toString()}`; + else throw new Error(`Unimplemented!`); + } + return out; + } + }, + + Expression: class extends Renderer.dice.AbstractSymbol { + constructor (nodes) { + super(); + this._nodes = nodes; + } + + _evl (meta) { return this._invoke("evl", meta); } + _avg (meta) { return this._invoke("avg", meta); } + _min (meta) { return this._invoke("min", meta); } + _max (meta) { return this._invoke("max", meta); } + + _invoke (fnName, meta) { + const view = this._nodes.slice(); + + let isNeg = false; + if (view[0].eq(Renderer.dice.tk.ADD) || view[0].eq(Renderer.dice.tk.SUB)) { + isNeg = view.shift().eq(Renderer.dice.tk.SUB); + if (isNeg) this.addToMeta(meta, {text: "-"}); + } + + let out = view[0][fnName](meta); + if (isNeg) out = -out; + + for (let i = 1; i < view.length; i += 2) { + if (view[i].eq(Renderer.dice.tk.ADD)) { + this.addToMeta(meta, {text: " + "}); + out += view[i + 1][fnName](meta); + } else if (view[i].eq(Renderer.dice.tk.SUB)) { + this.addToMeta(meta, {text: " - "}); + out -= view[i + 1][fnName](meta); + } else throw new Error(`Unimplemented!`); + } + + return out; + } + + toString (indent = 0) { + let out = ""; + const view = this._nodes.slice(); + + let isNeg = false; + if (view[0].eq(Renderer.dice.tk.ADD) || view[0].eq(Renderer.dice.tk.SUB)) { + isNeg = view.shift().eq(Renderer.dice.tk.SUB); + if (isNeg) out += "-"; + } + + out += view[0].toString(indent); + for (let i = 1; i < view.length; i += 2) { + if (view[i].eq(Renderer.dice.tk.ADD)) out += ` + ${view[i + 1].toString(indent)}`; + else if (view[i].eq(Renderer.dice.tk.SUB)) out += ` - ${view[i + 1].toString(indent)}`; + else throw new Error(`Unimplemented!`); + } + return out; + } + }, +}; + +if (!IS_VTT && typeof window !== "undefined") { + window.addEventListener("load", Renderer.dice._pInit); +} diff --git a/charbuilder/5etools-js/render.js b/charbuilder/5etools-js/render.js new file mode 100644 index 0000000..54e57b1 --- /dev/null +++ b/charbuilder/5etools-js/render.js @@ -0,0 +1,12559 @@ +"use strict"; + +// ENTRY RENDERING ===================================================================================================== +/* + * // EXAMPLE USAGE // + * + * const entryRenderer = new Renderer(); + * + * const topLevelEntry = mydata[0]; + * // prepare an array to hold the string we collect while recursing + * const textStack = []; + * + * // recurse through the entry tree + * entryRenderer.renderEntries(topLevelEntry, textStack); + * + * // render the final product by joining together all the collected strings + * $("#myElement").html(toDisplay.join("")); + */ +globalThis.Renderer = function () { + this.wrapperTag = "div"; + this.baseUrl = ""; + this.baseMediaUrls = {}; + + if (globalThis.DEPLOYED_IMG_ROOT) { + this.baseMediaUrls["img"] = globalThis.DEPLOYED_IMG_ROOT; + } + + this._lazyImages = false; + this._subVariant = false; + this._firstSection = true; + this._isAddHandlers = true; + this._headerIndex = 1; + this._tagExportDict = null; + this._roll20Ids = null; + this._trackTitles = {enabled: false, titles: {}}; + this._enumerateTitlesRel = {enabled: false, titles: {}}; + this._isHeaderIndexIncludeTableCaptions = false; + this._isHeaderIndexIncludeImageTitles = false; + this._plugins = {}; + this._fnPostProcess = null; + this._extraSourceClasses = null; + this._depthTracker = null; + this._depthTrackerAdditionalProps = []; + this._depthTrackerAdditionalPropsInherited = []; + this._lastDepthTrackerInheritedProps = {}; + this._isInternalLinksDisabled = false; + this._isPartPageExpandCollapseDisabled = false; + this._fnsGetStyleClasses = {}; + + /** + * Enables/disables lazy-load image rendering. + * @param bool true to enable, false to disable. + */ + this.setLazyImages = function (bool) { + // hard-disable lazy loading if the Intersection API is unavailable (e.g. under iOS 12) + if (typeof IntersectionObserver === "undefined") this._lazyImages = false; + else this._lazyImages = !!bool; + return this; + }; + + /** + * Set the tag used to group rendered elements + * @param tag to use + */ + this.setWrapperTag = function (tag) { this.wrapperTag = tag; return this; }; + + /** + * Set the base url for rendered links. + * Usage: `renderer.setBaseUrl("https://www.example.com/")` (note the "http" prefix and "/" suffix) + * @param url to use + */ + this.setBaseUrl = function (url) { this.baseUrl = url; return this; }; + + this.setBaseMediaUrl = function (mediaDir, url) { this.baseMediaUrls[mediaDir] = url; return this; }; + + this.getMediaUrl = function (mediaDir, path) { + if (Renderer.get().baseMediaUrls[mediaDir]) return `${Renderer.get().baseMediaUrls[mediaDir]}${path}`; + return `${Renderer.get().baseUrl}${mediaDir}/${path}`; + }; + + /** + * Other sections should be prefixed with a vertical divider + * @param bool + */ + this.setFirstSection = function (bool) { this._firstSection = bool; return this; }; + + /** + * Disable adding JS event handlers on elements. + * @param bool + */ + this.setAddHandlers = function (bool) { this._isAddHandlers = bool; return this; }; + + /** + * Add a post-processing function which acts on the final rendered strings from a root call. + * @param fn + */ + this.setFnPostProcess = function (fn) { this._fnPostProcess = fn; return this; }; + + /** + * Specify a list of extra classes to be added to those rendered on entries with sources. + * @param arr + */ + this.setExtraSourceClasses = function (arr) { this._extraSourceClasses = arr; return this; }; + + // region Header index + /** + * Headers are ID'd using the attribute `data-title-index` using an incrementing int. This resets it to 1. + */ + this.resetHeaderIndex = function () { + this._headerIndex = 1; + this._trackTitles.titles = {}; + this._enumerateTitlesRel.titles = {}; + return this; + }; + + this.getHeaderIndex = function () { return this._headerIndex; }; + + this.setHeaderIndexTableCaptions = function (bool) { this._isHeaderIndexIncludeTableCaptions = bool; return this; }; + this.setHeaderIndexImageTitles = function (bool) { this._isHeaderIndexIncludeImageTitles = bool; return this; }; + // endregion + + /** + * Pass an object to have the renderer export lists of found @-tagged content during renders + * + * @param toObj the object to fill with exported data. Example results: + * { + * commoner_mm: {page: "bestiary.html", source: "MM", hash: "commoner_mm"}, + * storm%20giant_mm: {page: "bestiary.html", source: "MM", hash: "storm%20giant_mm"}, + * detect%20magic_phb: {page: "spells.html", source: "PHB", hash: "detect%20magic_phb"} + * } + * These results intentionally match those used for hover windows, so can use the same cache/loading paths + */ + this.doExportTags = function (toObj) { + this._tagExportDict = toObj; + return this; + }; + + /** + * Reset/disable tag export + */ + this.resetExportTags = function () { + this._tagExportDict = null; + return this; + }; + + this.setRoll20Ids = function (roll20Ids) { + this._roll20Ids = roll20Ids; + return this; + }; + + this.resetRoll20Ids = function () { + this._roll20Ids = null; + return this; + }; + + /** Used by Foundry config. */ + this.setInternalLinksDisabled = function (val) { this._isInternalLinksDisabled = !!val; return this; }; + this.isInternalLinksDisabled = function () { return !!this._isInternalLinksDisabled; }; + + this.setPartPageExpandCollapseDisabled = function (val) { this._isPartPageExpandCollapseDisabled = !!val; return this; }; + + /** Bind function which apply exta CSS classes to entry/list renders. */ + this.setFnGetStyleClasses = function (identifier, fn) { + if (fn == null) { + delete this._fnsGetStyleClasses[identifier]; + return this; + } + + this._fnsGetStyleClasses[identifier] = fn; + return this; + }; + + /** + * If enabled, titles with the same name will be given numerical identifiers. + * This identifier is stored in `data-title-relative-index` + */ + this.setEnumerateTitlesRel = function (bool) { + this._enumerateTitlesRel.enabled = bool; + return this; + }; + + this._getEnumeratedTitleRel = function (name) { + if (this._enumerateTitlesRel.enabled && name) { + const clean = name.toLowerCase(); + this._enumerateTitlesRel.titles[clean] = this._enumerateTitlesRel.titles[clean] || 0; + return `data-title-relative-index="${this._enumerateTitlesRel.titles[clean]++}"`; + } else return ""; + }; + + this.setTrackTitles = function (bool) { + this._trackTitles.enabled = bool; + return this; + }; + + this.getTrackedTitles = function () { + return MiscUtil.copyFast(this._trackTitles.titles); + }; + + this.getTrackedTitlesInverted = function ({isStripTags = false} = {}) { + // `this._trackTitles.titles` is a map of `{[data-title-index]: ""}` + // Invert it such that we have a map of `{"": ["data-title-index-0", ..., "data-title-index-n"]}` + const trackedTitlesInverse = {}; + Object.entries(this._trackTitles.titles || {}).forEach(([titleIx, titleName]) => { + if (isStripTags) titleName = Renderer.stripTags(titleName); + titleName = titleName.toLowerCase().trim(); + (trackedTitlesInverse[titleName] = trackedTitlesInverse[titleName] || []).push(titleIx); + }); + return trackedTitlesInverse; + }; + + this._handleTrackTitles = function (name, {isTable = false, isImage = false} = {}) { + if (!this._trackTitles.enabled) return; + if (isTable && !this._isHeaderIndexIncludeTableCaptions) return; + if (isImage && !this._isHeaderIndexIncludeImageTitles) return; + this._trackTitles.titles[this._headerIndex] = name; + }; + + this._handleTrackDepth = function (entry, depth) { + if (!entry.name || !this._depthTracker) return; + + this._lastDepthTrackerInheritedProps = MiscUtil.copyFast(this._lastDepthTrackerInheritedProps); + if (entry.source) this._lastDepthTrackerInheritedProps.source = entry.source; + if (this._depthTrackerAdditionalPropsInherited?.length) { + this._depthTrackerAdditionalPropsInherited.forEach(prop => this._lastDepthTrackerInheritedProps[prop] = entry[prop] || this._lastDepthTrackerInheritedProps[prop]); + } + + const additionalData = this._depthTrackerAdditionalProps.length + ? this._depthTrackerAdditionalProps.mergeMap(it => ({[it]: entry[it]})) + : {}; + + this._depthTracker.push({ + ...this._lastDepthTrackerInheritedProps, + ...additionalData, + depth, + name: entry.name, + type: entry.type, + ixHeader: this._headerIndex, + source: this._lastDepthTrackerInheritedProps.source, + data: entry.data, + page: entry.page, + alias: entry.alias, + entry, + }); + }; + + // region Plugins + this.addPlugin = function (pluginType, fnPlugin) { + MiscUtil.getOrSet(this._plugins, pluginType, []).push(fnPlugin); + }; + + this.removePlugin = function (pluginType, fnPlugin) { + if (!fnPlugin) return; + const ix = (MiscUtil.get(this._plugins, pluginType) || []).indexOf(fnPlugin); + if (~ix) this._plugins[pluginType].splice(ix, 1); + }; + + this.removePlugins = function (pluginType) { + MiscUtil.delete(this._plugins, pluginType); + }; + + this._getPlugins = function (pluginType) { return this._plugins[pluginType] || []; }; + + /** Run a function with the given plugin active. */ + this.withPlugin = function ({pluginTypes, fnPlugin, fn}) { + for (const pt of pluginTypes) this.addPlugin(pt, fnPlugin); + try { + return fn(this); + } finally { + for (const pt of pluginTypes) this.removePlugin(pt, fnPlugin); + } + }; + + /** Run an async function with the given plugin active. */ + this.pWithPlugin = async function ({pluginTypes, fnPlugin, pFn}) { + for (const pt of pluginTypes) this.addPlugin(pt, fnPlugin); + try { + const out = await pFn(this); + return out; + } finally { + for (const pt of pluginTypes) this.removePlugin(pt, fnPlugin); + } + }; + // endregion + + /** + * Specify an array where the renderer will record rendered header depths. + * Items added to the array are of the form: `{name: "Header Name", depth: 1, type: "entries", source: "PHB"}` + * @param arr + * @param additionalProps Additional data props which should be tracked per-entry. + * @param additionalPropsInherited As per additionalProps, but if a parent entry has the prop, it should be passed + * to its children. + */ + this.setDepthTracker = function (arr, {additionalProps, additionalPropsInherited} = {}) { + this._depthTracker = arr; + this._depthTrackerAdditionalProps = additionalProps || []; + this._depthTrackerAdditionalPropsInherited = additionalPropsInherited || []; + return this; + }; + + this.getLineBreak = function () { return "
"; }; + + /** + * Recursively walk down a tree of "entry" JSON items, adding to a stack of strings to be finally rendered to the + * page. Note that this function does _not_ actually do the rendering, see the example code above for how to display + * the result. + * + * @param entry An "entry" usually defined in JSON. A schema is available in tests/schema + * @param textStack A reference to an array, which will hold all our strings as we recurse + * @param [meta] Meta state. + * @param [meta.depth] The current recursion depth. Optional; default 0, or -1 for type "section" entries. + * @param [options] Render options. + * @param [options.prefix] String to prefix rendered lines with. + * @param [options.suffix] String to suffix rendered lines with. + */ + this.recursiveRender = function (entry, textStack, meta, options) { + if (entry instanceof Array) { + entry.forEach(nxt => this.recursiveRender(nxt, textStack, meta, options)); + setTimeout(() => { throw new Error(`Array passed to renderer! The renderer only guarantees support for primitives and basic objects.`); }); + return this; + } + + // respect the API of the original, but set up for using string concatenations + if (textStack.length === 0) textStack[0] = ""; + else textStack.reverse(); + + // initialise meta + meta = meta || {}; + meta._typeStack = []; + meta.depth = meta.depth == null ? 0 : meta.depth; + + this._recursiveRender(entry, textStack, meta, options); + if (this._fnPostProcess) textStack[0] = this._fnPostProcess(textStack[0]); + textStack.reverse(); + + return this; + }; + + /** + * Inner rendering code. Uses string concatenation instead of an array stack, for ~2x the speed. + * @param entry As above. + * @param textStack As above. + * @param meta As above, with the addition of... + * @param options + * .prefix The (optional) prefix to be added to the textStack before whatever is added by the current call + * .suffix The (optional) suffix to be added to the textStack after whatever is added by the current call + * @private + */ + this._recursiveRender = function (entry, textStack, meta, options) { + if (entry == null) return; // Avoid dying on nully entries + if (!textStack) throw new Error("Missing stack!"); + if (!meta) throw new Error("Missing metadata!"); + + options = options || {}; + + // For wrapped entries, simply recurse + if (entry.type === "wrapper") return this._recursiveRender(entry.wrapped, textStack, meta, options); + + if (entry.type === "section") meta.depth = -1; + + meta._didRenderPrefix = false; + meta._didRenderSuffix = false; + + if (typeof entry === "object") { + // the root entry (e.g. "Rage" in barbarian "classFeatures") is assumed to be of type "entries" + const type = entry.type == null || entry.type === "section" ? "entries" : entry.type; + + meta._typeStack.push(type); + + switch (type) { + // recursive + case "entries": this._renderEntries(entry, textStack, meta, options); break; + case "options": this._renderOptions(entry, textStack, meta, options); break; + case "list": this._renderList(entry, textStack, meta, options); break; + case "table": this._renderTable(entry, textStack, meta, options); break; + case "tableGroup": this._renderTableGroup(entry, textStack, meta, options); break; + case "inset": this._renderInset(entry, textStack, meta, options); break; + case "insetReadaloud": this._renderInsetReadaloud(entry, textStack, meta, options); break; + case "variant": this._renderVariant(entry, textStack, meta, options); break; + case "variantInner": this._renderVariantInner(entry, textStack, meta, options); break; + case "variantSub": this._renderVariantSub(entry, textStack, meta, options); break; + case "spellcasting": this._renderSpellcasting(entry, textStack, meta, options); break; + case "quote": this._renderQuote(entry, textStack, meta, options); break; + case "optfeature": this._renderOptfeature(entry, textStack, meta, options); break; + case "patron": this._renderPatron(entry, textStack, meta, options); break; + + // block + case "abilityDc": this._renderAbilityDc(entry, textStack, meta, options); break; + case "abilityAttackMod": this._renderAbilityAttackMod(entry, textStack, meta, options); break; + case "abilityGeneric": this._renderAbilityGeneric(entry, textStack, meta, options); break; + + // inline + case "inline": this._renderInline(entry, textStack, meta, options); break; + case "inlineBlock": this._renderInlineBlock(entry, textStack, meta, options); break; + case "bonus": this._renderBonus(entry, textStack, meta, options); break; + case "bonusSpeed": this._renderBonusSpeed(entry, textStack, meta, options); break; + case "dice": this._renderDice(entry, textStack, meta, options); break; + case "link": this._renderLink(entry, textStack, meta, options); break; + case "actions": this._renderActions(entry, textStack, meta, options); break; + case "attack": this._renderAttack(entry, textStack, meta, options); break; + case "ingredient": this._renderIngredient(entry, textStack, meta, options); break; + + // list items + case "item": this._renderItem(entry, textStack, meta, options); break; + case "itemSub": this._renderItemSub(entry, textStack, meta, options); break; + case "itemSpell": this._renderItemSpell(entry, textStack, meta, options); break; + + // embedded entities + case "statblockInline": this._renderStatblockInline(entry, textStack, meta, options); break; + case "statblock": this._renderStatblock(entry, textStack, meta, options); break; + + // images + case "image": this._renderImage(entry, textStack, meta, options); break; + case "gallery": this._renderGallery(entry, textStack, meta, options); break; + + // flowchart + case "flowchart": this._renderFlowchart(entry, textStack, meta, options); break; + case "flowBlock": this._renderFlowBlock(entry, textStack, meta, options); break; + + // homebrew changes + case "homebrew": this._renderHomebrew(entry, textStack, meta, options); break; + + // misc + case "code": this._renderCode(entry, textStack, meta, options); break; + case "hr": this._renderHr(entry, textStack, meta, options); break; + } + + meta._typeStack.pop(); + } else if (typeof entry === "string") { // block + this._renderPrefix(entry, textStack, meta, options); + this._renderString(entry, textStack, meta, options); + this._renderSuffix(entry, textStack, meta, options); + } else { // block + // for ints or any other types which do not require specific rendering + this._renderPrefix(entry, textStack, meta, options); + this._renderPrimitive(entry, textStack, meta, options); + this._renderSuffix(entry, textStack, meta, options); + } + }; + + this._RE_TEXT_CENTER = /\btext-center\b/; + + this._getMutatedStyleString = function (str) { + if (!str) return str; + return str.replace(this._RE_TEXT_CENTER, "ve-text-center"); + }; + + this._adjustDepth = function (meta, dDepth) { + const cachedDepth = meta.depth; + meta.depth += dDepth; + meta.depth = Math.min(Math.max(-1, meta.depth), 2); // cap depth between -1 and 2 for general use + return cachedDepth; + }; + + this._renderPrefix = function (entry, textStack, meta, options) { + if (meta._didRenderPrefix) return; + if (options.prefix != null) { + textStack[0] += options.prefix; + meta._didRenderPrefix = true; + } + }; + + this._renderSuffix = function (entry, textStack, meta, options) { + if (meta._didRenderSuffix) return; + if (options.suffix != null) { + textStack[0] += options.suffix; + meta._didRenderSuffix = true; + } + }; + + this._renderImage = function (entry, textStack, meta, options) { + if (entry.title) this._handleTrackTitles(entry.title, {isImage: true}); + + textStack[0] += `
`; + + if (entry.imageType === "map" || entry.imageType === "mapPlayer") textStack[0] += `
`; + textStack[0] += `
`; + + const href = this._renderImage_getUrl(entry); + const svg = this._lazyImages && entry.width != null && entry.height != null + ? `data:image/svg+xml,${encodeURIComponent(``)}` + : null; + const ptTitleCreditTooltip = this._renderImage_getTitleCreditTooltipText(entry); + const ptTitle = ptTitleCreditTooltip ? `title="${ptTitleCreditTooltip}"` : ""; + const pluginDataIsNoLink = this._getPlugins("image_isNoLink").map(plugin => plugin(entry, textStack, meta, options)).some(Boolean); + + textStack[0] += `
+ ${pluginDataIsNoLink ? "" : ``} + + ${pluginDataIsNoLink ? "" : ``} +
`; + + if (!this._renderImage_isComicStyling(entry) && (entry.title || entry.credit || entry.mapRegions)) { + const ptAdventureBookMeta = entry.mapRegions && meta.adventureBookPage && meta.adventureBookSource && meta.adventureBookHash + ? `data-rd-adventure-book-map-page="${meta.adventureBookPage.qq()}" data-rd-adventure-book-map-source="${meta.adventureBookSource.qq()}" data-rd-adventure-book-map-hash="${meta.adventureBookHash.qq()}"` + : ""; + + textStack[0] += `
`; + + const isDynamicViewer = entry.mapRegions && !IS_VTT; + + if (entry.title && !isDynamicViewer) textStack[0] += `
${this.render(entry.title)}
`; + + if (isDynamicViewer) { + textStack[0] += ``; + } + + if (entry.credit) textStack[0] += `
${this.render(entry.credit)}
`; + + textStack[0] += `
`; + } + + if (entry._galleryTitlePad) textStack[0] += `
 
`; + if (entry._galleryCreditPad) textStack[0] += `
 
`; + + textStack[0] += `
`; + if (entry.imageType === "map" || entry.imageType === "mapPlayer") textStack[0] += `
`; + }; + + this._renderImage_getTitleCreditTooltipText = function (entry) { + if (!entry.title && !entry.credit) return null; + return Renderer.stripTags( + [entry.title, entry.credit ? `Art credit: ${entry.credit}` : null] + .filter(Boolean) + .join(". "), + ).qq(); + }; + + this._renderImage_getStylePart = function (entry) { + const styles = [ + // N.b. this width/height should be reflected in the renderer image CSS + // Clamp the max width at 100%, as per the renderer styling + entry.maxWidth ? `max-width: min(100%, ${entry.maxWidth}${entry.maxWidthUnits || "px"})` : "", + // Clamp the max height at 60vh, as per the renderer styling + entry.maxHeight ? `max-height: min(60vh, ${entry.maxHeight}${entry.maxHeightUnits || "px"})` : "", + ].filter(Boolean).join("; "); + return styles ? `style="${styles}"` : ""; + }; + + this._renderImage_getMapRegionData = function (entry) { + return JSON.stringify(this.getMapRegionData(entry)).escapeQuotes(); + }; + + this.getMapRegionData = function (entry) { + return { + regions: entry.mapRegions, + width: entry.width, + height: entry.height, + href: this._renderImage_getUrl(entry), + hrefThumbnail: this._renderImage_getUrlThumbnail(entry), + page: entry.page, + source: entry.source, + hash: entry.hash, + }; + }; + + this._renderImage_isComicStyling = function (entry) { + if (!entry.style) return false; + return ["comic-speaker-left", "comic-speaker-right"].includes(entry.style); + }; + + this._renderImage_getWrapperClasses = function (entry) { + const out = ["rd__wrp-image", "relative"]; + if (entry.style) { + switch (entry.style) { + case "comic-speaker-left": out.push("rd__comic-img-speaker", "rd__comic-img-speaker--left"); break; + case "comic-speaker-right": out.push("rd__comic-img-speaker", "rd__comic-img-speaker--right"); break; + } + } + return out.join(" "); + }; + + this._renderImage_getImageClasses = function (entry) { + const out = ["rd__image"]; + if (entry.style) { + switch (entry.style) { + case "deity-symbol": out.push("rd__img-small"); break; + } + } + return out.join(" "); + }; + + this._renderImage_getUrl = function (entry) { + let url = Renderer.utils.getEntryMediaUrl(entry, "href", "img"); + for (const plugin of this._getPlugins(`image_urlPostProcess`)) { + url = plugin(entry, url) || url; + } + return url; + }; + + this._renderImage_getUrlThumbnail = function (entry) { + let url = Renderer.utils.getEntryMediaUrl(entry, "hrefThumbnail", "img"); + for (const plugin of this._getPlugins(`image_urlThumbnailPostProcess`)) { + url = plugin(entry, url) || url; + } + return url; + }; + + this._renderList_getListCssClasses = function (entry, textStack, meta, options) { + const out = [`rd__list`]; + if (entry.style || entry.columns) { + if (entry.style) out.push(...entry.style.split(" ").map(it => `rd__${it}`)); + if (entry.columns) out.push(`columns-${entry.columns}`); + } + return out.join(" "); + }; + + this._renderTableGroup = function (entry, textStack, meta, options) { + const len = entry.tables.length; + for (let i = 0; i < len; ++i) this._recursiveRender(entry.tables[i], textStack, meta); + }; + + this._renderTable = function (entry, textStack, meta, options) { + // TODO add handling for rowLabel property + if (entry.intro) { + const len = entry.intro.length; + for (let i = 0; i < len; ++i) { + this._recursiveRender(entry.intro[i], textStack, meta, {prefix: "

", suffix: "

"}); + } + } + + textStack[0] += ``; + + const headerRowMetas = Renderer.table.getHeaderRowMetas(entry); + const autoRollMode = Renderer.table.getAutoConvertedRollMode(entry, {headerRowMetas}); + const toRenderLabel = autoRollMode ? RollerUtil.getFullRollCol(headerRowMetas.last()[0]) : null; + const isInfiniteResults = autoRollMode === RollerUtil.ROLL_COL_VARIABLE; + + // caption + if (entry.caption != null) { + this._handleTrackTitles(entry.caption, {isTable: true}); + textStack[0] += ``; + } + + // body -- temporarily build this to own string; append after headers + const rollCols = []; + let bodyStack = [""]; + bodyStack[0] += ""; + const lenRows = entry.rows.length; + for (let ixRow = 0; ixRow < lenRows; ++ixRow) { + bodyStack[0] += ""; + const r = entry.rows[ixRow]; + let roRender = r.type === "row" ? r.row : r; + + const len = roRender.length; + for (let ixCell = 0; ixCell < len; ++ixCell) { + rollCols[ixCell] = rollCols[ixCell] || false; + + // pre-convert rollables + if (autoRollMode && ixCell === 0) { + roRender = Renderer.getRollableRow( + roRender, + { + isForceInfiniteResults: isInfiniteResults, + isFirstRow: ixRow === 0, + isLastRow: ixRow === lenRows - 1, + }, + ); + rollCols[ixCell] = true; + } + + let toRenderCell; + if (roRender[ixCell].type === "cell") { + if (roRender[ixCell].roll) { + rollCols[ixCell] = true; + if (roRender[ixCell].entry) { + toRenderCell = roRender[ixCell].entry; + } else if (roRender[ixCell].roll.exact != null) { + toRenderCell = roRender[ixCell].roll.pad ? StrUtil.padNumber(roRender[ixCell].roll.exact, 2, "0") : roRender[ixCell].roll.exact; + } else { + // TODO(Future) render "negative infinite" minimum nicely (or based on an example from a book, if one ever occurs) + // "Selling a Magic Item" from DMG p129 almost meets this, but it has its own display + + const dispMin = roRender[ixCell].roll.displayMin != null ? roRender[ixCell].roll.displayMin : roRender[ixCell].roll.min; + const dispMax = roRender[ixCell].roll.displayMax != null ? roRender[ixCell].roll.displayMax : roRender[ixCell].roll.max; + + if (dispMax === Renderer.dice.POS_INFINITE) { + toRenderCell = roRender[ixCell].roll.pad + ? `${StrUtil.padNumber(dispMin, 2, "0")}+` + : `${dispMin}+`; + } else { + toRenderCell = roRender[ixCell].roll.pad + ? `${StrUtil.padNumber(dispMin, 2, "0")}-${StrUtil.padNumber(dispMax, 2, "0")}` + : `${dispMin}-${dispMax}`; + } + } + } else if (roRender[ixCell].entry) { + toRenderCell = roRender[ixCell].entry; + } + } else { + toRenderCell = roRender[ixCell]; + } + bodyStack[0] += `"; + } + bodyStack[0] += ""; + } + bodyStack[0] += ""; + + // header + if (headerRowMetas) { + textStack[0] += ""; + + for (let ixRow = 0, lenRows = headerRowMetas.length; ixRow < lenRows; ++ixRow) { + textStack[0] += ""; + + const headerRowMeta = headerRowMetas[ixRow]; + for (let ixCell = 0, lenCells = headerRowMeta.length; ixCell < lenCells; ++ixCell) { + const lbl = headerRowMeta[ixCell]; + textStack[0] += ``; + } + + textStack[0] += ""; + } + + textStack[0] += ""; + } + + textStack[0] += bodyStack[0]; + + // footer + if (entry.footnotes != null) { + textStack[0] += ""; + const len = entry.footnotes.length; + for (let i = 0; i < len; ++i) { + textStack[0] += `"; + } + textStack[0] += ""; + } + textStack[0] += "
${entry.caption}
`; + if (r.style === "row-indent-first" && ixCell === 0) bodyStack[0] += `
`; + const cacheDepth = this._adjustDepth(meta, 1); + this._recursiveRender(toRenderCell, bodyStack, meta); + meta.depth = cacheDepth; + bodyStack[0] += "
`; + this._recursiveRender(autoRollMode && ixCell === 0 ? RollerUtil.getFullRollCol(lbl) : lbl, textStack, meta); + textStack[0] += `
`; + const cacheDepth = this._adjustDepth(meta, 1); + this._recursiveRender(entry.footnotes[i], textStack, meta); + meta.depth = cacheDepth; + textStack[0] += "
"; + + if (entry.outro) { + const len = entry.outro.length; + for (let i = 0; i < len; ++i) { + this._recursiveRender(entry.outro[i], textStack, meta, {prefix: "

", suffix: "

"}); + } + } + }; + + this._renderTable_getCellDataStr = function (ent) { + function convertZeros (num) { + if (num === 0) return 100; + return num; + } + + if (ent.roll) { + return `data-roll-min="${convertZeros(ent.roll.exact != null ? ent.roll.exact : ent.roll.min)}" data-roll-max="${convertZeros(ent.roll.exact != null ? ent.roll.exact : ent.roll.max)}"`; + } + + return ""; + }; + + this._renderTable_getTableThClassText = function (entry, i) { + return entry.colStyles == null || i >= entry.colStyles.length ? "" : `class="${this._getMutatedStyleString(entry.colStyles[i])}"`; + }; + + this._renderTable_makeTableTdClassText = function (entry, i) { + if (entry.rowStyles != null) return i >= entry.rowStyles.length ? "" : `class="${this._getMutatedStyleString(entry.rowStyles[i])}"`; + else return this._renderTable_getTableThClassText(entry, i); + }; + + this._renderEntries = function (entry, textStack, meta, options) { + this._renderEntriesSubtypes(entry, textStack, meta, options, true); + }; + + this._getPagePart = function (entry, isInset) { + if (!Renderer.utils.isDisplayPage(entry.page)) return ""; + return ` ${entry.source ? `${Parser.sourceJsonToAbv(entry.source)} ` : ""}p${entry.page}`; + }; + + this._renderEntriesSubtypes = function (entry, textStack, meta, options, incDepth) { + const type = entry.type || "entries"; + const isInlineTitle = meta.depth >= 2; + const isAddPeriod = isInlineTitle && entry.name && !Renderer._INLINE_HEADER_TERMINATORS.has(entry.name[entry.name.length - 1]); + const pagePart = !this._isPartPageExpandCollapseDisabled && !isInlineTitle + ? this._getPagePart(entry) + : ""; + const partExpandCollapse = !this._isPartPageExpandCollapseDisabled && !isInlineTitle + ? this._getPtExpandCollapse() + : ""; + const partPageExpandCollapse = !this._isPartPageExpandCollapseDisabled && (pagePart || partExpandCollapse) + ? `${[pagePart, partExpandCollapse].filter(Boolean).join("")}` + : ""; + const nextDepth = incDepth && meta.depth < 2 ? meta.depth + 1 : meta.depth; + const styleString = this._renderEntriesSubtypes_getStyleString(entry, meta, isInlineTitle); + const dataString = this._renderEntriesSubtypes_getDataString(entry); + if (entry.name != null && Renderer.ENTRIES_WITH_ENUMERATED_TITLES_LOOKUP[entry.type]) this._handleTrackTitles(entry.name); + + const headerTag = isInlineTitle ? "span" : `h${Math.min(Math.max(meta.depth + 2, 1), 6)}`; + const headerClass = `rd__h--${meta.depth + 1}`; // adjust as the CSS is 0..4 rather than -1..3 + + const cachedLastDepthTrackerProps = MiscUtil.copyFast(this._lastDepthTrackerInheritedProps); + this._handleTrackDepth(entry, meta.depth); + + const pluginDataNamePrefix = this._getPlugins(`${type}_namePrefix`).map(plugin => plugin(entry, textStack, meta, options)).filter(Boolean); + + const headerSpan = entry.name ? `<${headerTag} class="rd__h ${headerClass}" data-title-index="${this._headerIndex++}" ${this._getEnumeratedTitleRel(entry.name)}> ${pluginDataNamePrefix.join("")}${this.render({type: "inline", entries: [entry.name]})}${isAddPeriod ? "." : ""}${partPageExpandCollapse} ` : ""; + + if (meta.depth === -1) { + if (!this._firstSection) textStack[0] += `
`; + this._firstSection = false; + } + + if (entry.entries || entry.name) { + textStack[0] += `<${this.wrapperTag} ${dataString} ${styleString}>${headerSpan}`; + this._renderEntriesSubtypes_renderPreReqText(entry, textStack, meta); + if (entry.entries) { + const cacheDepth = meta.depth; + const len = entry.entries.length; + for (let i = 0; i < len; ++i) { + meta.depth = nextDepth; + this._recursiveRender(entry.entries[i], textStack, meta, {prefix: "

", suffix: "

"}); + // Add a spacer for style sets that have vertical whitespace instead of indents + if (i === 0 && cacheDepth >= 2) textStack[0] += `
`; + } + meta.depth = cacheDepth; + } + textStack[0] += ``; + } + + this._lastDepthTrackerInheritedProps = cachedLastDepthTrackerProps; + }; + + this._renderEntriesSubtypes_getDataString = function (entry) { + let dataString = ""; + if (entry.source) dataString += `data-source="${entry.source}"`; + if (entry.data) { + for (const k in entry.data) { + if (!k.startsWith("rd-")) continue; + dataString += ` data-${k}="${`${entry.data[k]}`.escapeQuotes()}"`; + } + } + return dataString; + }; + + this._renderEntriesSubtypes_renderPreReqText = function (entry, textStack, meta) { + if (entry.prerequisite) { + textStack[0] += `Prerequisite: `; + this._recursiveRender({type: "inline", entries: [entry.prerequisite]}, textStack, meta); + textStack[0] += ``; + } + }; + + this._renderEntriesSubtypes_getStyleString = function (entry, meta, isInlineTitle) { + const styleClasses = ["rd__b"]; + styleClasses.push(this._getStyleClass(entry.type || "entries", entry)); + if (isInlineTitle) { + if (this._subVariant) styleClasses.push(Renderer.HEAD_2_SUB_VARIANT); + else styleClasses.push(Renderer.HEAD_2); + } else styleClasses.push(meta.depth === -1 ? Renderer.HEAD_NEG_1 : meta.depth === 0 ? Renderer.HEAD_0 : Renderer.HEAD_1); + return styleClasses.length > 0 ? `class="${styleClasses.join(" ")}"` : ""; + }; + + this._renderOptions = function (entry, textStack, meta, options) { + if (!entry.entries) return; + entry.entries = entry.entries.sort((a, b) => a.name && b.name ? SortUtil.ascSort(a.name, b.name) : a.name ? -1 : b.name ? 1 : 0); + + if (entry.style && entry.style === "list-hang-notitle") { + const fauxEntry = { + type: "list", + style: "list-hang-notitle", + items: entry.entries.map(ent => { + if (typeof ent === "string") return ent; + if (ent.type === "item") return ent; + + const out = {...ent, type: "item"}; + if (ent.name) out.name = Renderer._INLINE_HEADER_TERMINATORS.has(ent.name[ent.name.length - 1]) ? out.name : `${out.name}.`; + return out; + }), + }; + this._renderList(fauxEntry, textStack, meta, options); + } else this._renderEntriesSubtypes(entry, textStack, meta, options, false); + }; + + this._renderList = function (entry, textStack, meta, options) { + if (entry.items) { + const tag = entry.start ? "ol" : "ul"; + const cssClasses = this._renderList_getListCssClasses(entry, textStack, meta, options); + textStack[0] += `<${tag} ${cssClasses ? `class="${cssClasses}"` : ""} ${entry.start ? `start="${entry.start}"` : ""}>`; + if (entry.name) textStack[0] += `
  • ${entry.name}
  • `; + const isListHang = entry.style && entry.style.split(" ").includes("list-hang"); + const len = entry.items.length; + for (let i = 0; i < len; ++i) { + const item = entry.items[i]; + // Special case for child lists -- avoid wrapping in LI tags to avoid double-bullet + if (item.type !== "list") { + const className = `${this._getStyleClass(entry.type, item)}${item.type === "itemSpell" ? " rd__li-spell" : ""}`; + textStack[0] += `
  • `; + } + // If it's a raw string in a hanging list, wrap it in a div to allow for the correct styling + if (isListHang && typeof item === "string") textStack[0] += "
    "; + this._recursiveRender(item, textStack, meta); + if (isListHang && typeof item === "string") textStack[0] += "
    "; + if (item.type !== "list") textStack[0] += "
  • "; + } + textStack[0] += ``; + } + }; + + this._getPtExpandCollapse = function () { + return `[\u2013]`; + }; + + this._getPtExpandCollapseSpecial = function () { + return `[\u2013]`; + }; + + this._renderInset = function (entry, textStack, meta, options) { + const dataString = this._renderEntriesSubtypes_getDataString(entry); + textStack[0] += `<${this.wrapperTag} class="rd__b-special rd__b-inset ${this._getMutatedStyleString(entry.style || "")}" ${dataString}>`; + + const cachedLastDepthTrackerProps = MiscUtil.copyFast(this._lastDepthTrackerInheritedProps); + this._handleTrackDepth(entry, 1); + + const pagePart = this._getPagePart(entry, true); + const partExpandCollapse = !this._isPartPageExpandCollapseDisabled ? this._getPtExpandCollapseSpecial() : ""; + const partPageExpandCollapse = `${[pagePart, partExpandCollapse].filter(Boolean).join("")}`; + + if (entry.name != null) { + if (Renderer.ENTRIES_WITH_ENUMERATED_TITLES_LOOKUP[entry.type]) this._handleTrackTitles(entry.name); + textStack[0] += `

    ${entry.name}

    ${partPageExpandCollapse}
    `; + } else { + textStack[0] += `${partPageExpandCollapse}`; + } + + if (entry.entries) { + const len = entry.entries.length; + for (let i = 0; i < len; ++i) { + const cacheDepth = meta.depth; + meta.depth = 2; + this._recursiveRender(entry.entries[i], textStack, meta, {prefix: "

    ", suffix: "

    "}); + meta.depth = cacheDepth; + } + } + textStack[0] += `
    `; + textStack[0] += ``; + + this._lastDepthTrackerInheritedProps = cachedLastDepthTrackerProps; + }; + + this._renderInsetReadaloud = function (entry, textStack, meta, options) { + const dataString = this._renderEntriesSubtypes_getDataString(entry); + textStack[0] += `<${this.wrapperTag} class="rd__b-special rd__b-inset rd__b-inset--readaloud ${this._getMutatedStyleString(entry.style || "")}" ${dataString}>`; + + const cachedLastDepthTrackerProps = MiscUtil.copyFast(this._lastDepthTrackerInheritedProps); + this._handleTrackDepth(entry, 1); + + const pagePart = this._getPagePart(entry, true); + const partExpandCollapse = !this._isPartPageExpandCollapseDisabled ? this._getPtExpandCollapseSpecial() : ""; + const partPageExpandCollapse = `${[pagePart, partExpandCollapse].filter(Boolean).join("")}`; + + if (entry.name != null) { + if (Renderer.ENTRIES_WITH_ENUMERATED_TITLES_LOOKUP[entry.type]) this._handleTrackTitles(entry.name); + textStack[0] += `

    ${entry.name}

    ${this._getPagePart(entry, true)}
    `; + } else { + textStack[0] += `${partPageExpandCollapse}`; + } + + const len = entry.entries.length; + for (let i = 0; i < len; ++i) { + const cacheDepth = meta.depth; + meta.depth = 2; + this._recursiveRender(entry.entries[i], textStack, meta, {prefix: "

    ", suffix: "

    "}); + meta.depth = cacheDepth; + } + textStack[0] += `
    `; + textStack[0] += ``; + + this._lastDepthTrackerInheritedProps = cachedLastDepthTrackerProps; + }; + + this._renderVariant = function (entry, textStack, meta, options) { + const dataString = this._renderEntriesSubtypes_getDataString(entry); + + if (entry.name != null && Renderer.ENTRIES_WITH_ENUMERATED_TITLES_LOOKUP[entry.type]) this._handleTrackTitles(entry.name); + const cachedLastDepthTrackerProps = MiscUtil.copyFast(this._lastDepthTrackerInheritedProps); + this._handleTrackDepth(entry, 1); + + const pagePart = this._getPagePart(entry, true); + const partExpandCollapse = !this._isPartPageExpandCollapseDisabled ? this._getPtExpandCollapseSpecial() : ""; + const partPageExpandCollapse = `${[pagePart, partExpandCollapse].filter(Boolean).join("")}`; + + textStack[0] += `<${this.wrapperTag} class="rd__b-special rd__b-inset" ${dataString}>`; + textStack[0] += `

    Variant: ${entry.name}

    ${partPageExpandCollapse}
    `; + const len = entry.entries.length; + for (let i = 0; i < len; ++i) { + const cacheDepth = meta.depth; + meta.depth = 2; + this._recursiveRender(entry.entries[i], textStack, meta, {prefix: "

    ", suffix: "

    "}); + meta.depth = cacheDepth; + } + if (entry.source) textStack[0] += Renderer.utils.getSourceAndPageTrHtml({source: entry.source, page: entry.page}); + textStack[0] += ``; + + this._lastDepthTrackerInheritedProps = cachedLastDepthTrackerProps; + }; + + this._renderVariantInner = function (entry, textStack, meta, options) { + const dataString = this._renderEntriesSubtypes_getDataString(entry); + + if (entry.name != null && Renderer.ENTRIES_WITH_ENUMERATED_TITLES_LOOKUP[entry.type]) this._handleTrackTitles(entry.name); + const cachedLastDepthTrackerProps = MiscUtil.copyFast(this._lastDepthTrackerInheritedProps); + this._handleTrackDepth(entry, 1); + + textStack[0] += `<${this.wrapperTag} class="rd__b-inset-inner" ${dataString}>`; + textStack[0] += `

    ${entry.name}

    `; + const len = entry.entries.length; + for (let i = 0; i < len; ++i) { + const cacheDepth = meta.depth; + meta.depth = 2; + this._recursiveRender(entry.entries[i], textStack, meta, {prefix: "

    ", suffix: "

    "}); + meta.depth = cacheDepth; + } + if (entry.source) textStack[0] += Renderer.utils.getSourceAndPageTrHtml({source: entry.source, page: entry.page}); + textStack[0] += ``; + + this._lastDepthTrackerInheritedProps = cachedLastDepthTrackerProps; + }; + + this._renderVariantSub = function (entry, textStack, meta, options) { + // pretend this is an inline-header'd entry, but set a flag so we know not to add bold + this._subVariant = true; + const fauxEntry = entry; + fauxEntry.type = "entries"; + const cacheDepth = meta.depth; + meta.depth = 3; + this._recursiveRender(fauxEntry, textStack, meta, {prefix: "

    ", suffix: "

    "}); + meta.depth = cacheDepth; + this._subVariant = false; + }; + + this._renderSpellcasting_getEntries = function (entry) { + const hidden = new Set(entry.hidden || []); + const toRender = [{type: "entries", name: entry.name, entries: entry.headerEntries ? MiscUtil.copyFast(entry.headerEntries) : []}]; + + if (entry.constant || entry.will || entry.recharge || entry.charges || entry.rest || entry.daily || entry.weekly || entry.monthly || entry.yearly || entry.ritual) { + const tempList = {type: "list", style: "list-hang-notitle", items: [], data: {isSpellList: true}}; + if (entry.constant && !hidden.has("constant")) tempList.items.push({type: "itemSpell", name: `Constant:`, entry: this._renderSpellcasting_getRenderableList(entry.constant).join(", ")}); + if (entry.will && !hidden.has("will")) tempList.items.push({type: "itemSpell", name: `At will:`, entry: this._renderSpellcasting_getRenderableList(entry.will).join(", ")}); + + this._renderSpellcasting_getEntries_procPerDuration({entry, tempList, hidden, prop: "recharge", fnGetDurationText: num => `{@recharge ${num}|m}`, isSkipPrefix: true}); + this._renderSpellcasting_getEntries_procPerDuration({entry, tempList, hidden, prop: "charges", fnGetDurationText: num => ` charge${num === 1 ? "" : "s"}`}); + this._renderSpellcasting_getEntries_procPerDuration({entry, tempList, hidden, prop: "rest", durationText: "/rest"}); + this._renderSpellcasting_getEntries_procPerDuration({entry, tempList, hidden, prop: "daily", durationText: "/day"}); + this._renderSpellcasting_getEntries_procPerDuration({entry, tempList, hidden, prop: "weekly", durationText: "/week"}); + this._renderSpellcasting_getEntries_procPerDuration({entry, tempList, hidden, prop: "monthly", durationText: "/month"}); + this._renderSpellcasting_getEntries_procPerDuration({entry, tempList, hidden, prop: "yearly", durationText: "/year"}); + + if (entry.ritual && !hidden.has("ritual")) tempList.items.push({type: "itemSpell", name: `Rituals:`, entry: this._renderSpellcasting_getRenderableList(entry.ritual).join(", ")}); + tempList.items = tempList.items.filter(it => it.entry !== ""); + if (tempList.items.length) toRender[0].entries.push(tempList); + } + + if (entry.spells && !hidden.has("spells")) { + const tempList = {type: "list", style: "list-hang-notitle", items: [], data: {isSpellList: true}}; + + const lvls = Object.keys(entry.spells) + .map(lvl => Number(lvl)) + .sort(SortUtil.ascSort); + + for (const lvl of lvls) { + const spells = entry.spells[lvl]; + if (spells) { + let levelCantrip = `${Parser.spLevelToFull(lvl)}${(lvl === 0 ? "s" : " level")}`; + let slotsAtWill = ` (at will)`; + const slots = spells.slots; + if (slots >= 0) slotsAtWill = slots > 0 ? ` (${slots} slot${slots > 1 ? "s" : ""})` : ``; + if (spells.lower && spells.lower !== lvl) { + levelCantrip = `${Parser.spLevelToFull(spells.lower)}-${levelCantrip}`; + if (slots >= 0) slotsAtWill = slots > 0 ? ` (${slots} ${Parser.spLevelToFull(lvl)}-level slot${slots > 1 ? "s" : ""})` : ``; + } + tempList.items.push({type: "itemSpell", name: `${levelCantrip}${slotsAtWill}:`, entry: this._renderSpellcasting_getRenderableList(spells.spells).join(", ") || "\u2014"}); + } + } + + toRender[0].entries.push(tempList); + } + + if (entry.footerEntries) toRender.push({type: "entries", entries: entry.footerEntries}); + return toRender; + }; + + this._renderSpellcasting_getEntries_procPerDuration = function ({entry, hidden, tempList, prop, durationText, fnGetDurationText, isSkipPrefix}) { + if (!entry[prop] || hidden.has(prop)) return; + + for (let lvl = 9; lvl > 0; lvl--) { + const perDur = entry[prop]; + if (perDur[lvl]) { + tempList.items.push({ + type: "itemSpell", + name: `${isSkipPrefix ? "" : lvl}${fnGetDurationText ? fnGetDurationText(lvl) : durationText}:`, + entry: this._renderSpellcasting_getRenderableList(perDur[lvl]).join(", "), + }); + } + + const lvlEach = `${lvl}e`; + if (perDur[lvlEach]) { + const isHideEach = !perDur[lvl] && perDur[lvlEach].length === 1; + tempList.items.push({ + type: "itemSpell", + name: `${isSkipPrefix ? "" : lvl}${fnGetDurationText ? fnGetDurationText(lvl) : durationText}${isHideEach ? "" : ` each`}:`, + entry: this._renderSpellcasting_getRenderableList(perDur[lvlEach]).join(", "), + }); + } + } + }; + + this._renderSpellcasting_getRenderableList = function (spellList) { + return spellList.filter(it => !it.hidden).map(it => it.entry || it); + }; + + this._renderSpellcasting = function (entry, textStack, meta, options) { + const toRender = this._renderSpellcasting_getEntries(entry); + if (!toRender?.[0].entries?.length) return; + this._recursiveRender({type: "entries", entries: toRender}, textStack, meta); + }; + + this._renderQuote = function (entry, textStack, meta, options) { + textStack[0] += `
    `; + + const len = entry.entries.length; + for (let i = 0; i < len; ++i) { + textStack[0] += `

    ${i === 0 && !entry.skipMarks ? "“" : ""}`; + this._recursiveRender(entry.entries[i], textStack, meta, {prefix: entry.skipItalics ? "" : "", suffix: entry.skipItalics ? "" : ""}); + textStack[0] += `${i === len - 1 && !entry.skipMarks ? "”" : ""}

    `; + } + + if (entry.by || entry.from) { + textStack[0] += `

    `; + const tempStack = [""]; + const byArr = this._renderQuote_getBy(entry); + if (byArr) { + for (let i = 0, len = byArr.length; i < len; ++i) { + const by = byArr[i]; + this._recursiveRender(by, tempStack, meta); + if (i < len - 1) tempStack[0] += "
    "; + } + } + textStack[0] += `\u2014 ${byArr ? tempStack.join("") : ""}${byArr && entry.from ? `, ` : ""}${entry.from ? `${entry.from}` : ""}`; + textStack[0] += `

    `; + } + + textStack[0] += `
    `; + }; + + this._renderList_getQuoteCssClasses = function (entry, textStack, meta, options) { + const out = [`rd__quote`]; + if (entry.style) { + if (entry.style) out.push(...entry.style.split(" ").map(it => `rd__${it}`)); + } + return out.join(" "); + }; + + this._renderQuote_getBy = function (entry) { + if (!entry.by?.length) return null; + return entry.by instanceof Array ? entry.by : [entry.by]; + }; + + this._renderOptfeature = function (entry, textStack, meta, options) { + this._renderEntriesSubtypes(entry, textStack, meta, options, true); + }; + + this._renderPatron = function (entry, textStack, meta, options) { + this._renderEntriesSubtypes(entry, textStack, meta, options, false); + }; + + this._renderAbilityDc = function (entry, textStack, meta, options) { + this._renderPrefix(entry, textStack, meta, options); + textStack[0] += `
    `; + this._recursiveRender(entry.name, textStack, meta); + textStack[0] += ` save DC = 8 + your proficiency bonus + your ${Parser.attrChooseToFull(entry.attributes)}
    `; + this._renderSuffix(entry, textStack, meta, options); + }; + + this._renderAbilityAttackMod = function (entry, textStack, meta, options) { + this._renderPrefix(entry, textStack, meta, options); + textStack[0] += `
    `; + this._recursiveRender(entry.name, textStack, meta); + textStack[0] += ` attack modifier = your proficiency bonus + your ${Parser.attrChooseToFull(entry.attributes)}
    `; + this._renderSuffix(entry, textStack, meta, options); + }; + + this._renderAbilityGeneric = function (entry, textStack, meta, options) { + this._renderPrefix(entry, textStack, meta, options); + textStack[0] += `
    `; + if (entry.name) this._recursiveRender(entry.name, textStack, meta, {prefix: "", suffix: " = "}); + textStack[0] += `${entry.text}${entry.attributes ? ` ${Parser.attrChooseToFull(entry.attributes)}` : ""}
    `; + this._renderSuffix(entry, textStack, meta, options); + }; + + this._renderInline = function (entry, textStack, meta, options) { + if (entry.entries) { + const len = entry.entries.length; + for (let i = 0; i < len; ++i) this._recursiveRender(entry.entries[i], textStack, meta); + } + }; + + this._renderInlineBlock = function (entry, textStack, meta, options) { + this._renderPrefix(entry, textStack, meta, options); + if (entry.entries) { + const len = entry.entries.length; + for (let i = 0; i < len; ++i) this._recursiveRender(entry.entries[i], textStack, meta); + } + this._renderSuffix(entry, textStack, meta, options); + }; + + this._renderBonus = function (entry, textStack, meta, options) { + textStack[0] += (entry.value < 0 ? "" : "+") + entry.value; + }; + + this._renderBonusSpeed = function (entry, textStack, meta, options) { + textStack[0] += entry.value === 0 ? "\u2014" : `${entry.value < 0 ? "" : "+"}${entry.value} ft.`; + }; + + this._renderDice = function (entry, textStack, meta, options) { + const pluginResults = this._getPlugins("dice").map(plugin => plugin(entry, textStack, meta, options)).filter(Boolean); + + textStack[0] += Renderer.getEntryDice(entry, entry.name, {isAddHandlers: this._isAddHandlers, pluginResults}); + }; + + this._renderActions = function (entry, textStack, meta, options) { + const dataString = this._renderEntriesSubtypes_getDataString(entry); + + if (entry.name != null && Renderer.ENTRIES_WITH_ENUMERATED_TITLES_LOOKUP[entry.type]) this._handleTrackTitles(entry.name); + const cachedLastDepthTrackerProps = MiscUtil.copyFast(this._lastDepthTrackerInheritedProps); + this._handleTrackDepth(entry, 2); + + textStack[0] += `<${this.wrapperTag} class="${Renderer.HEAD_2}" ${dataString}>${entry.name}. `; + const len = entry.entries.length; + for (let i = 0; i < len; ++i) this._recursiveRender(entry.entries[i], textStack, meta, {prefix: "

    ", suffix: "

    "}); + textStack[0] += ``; + + this._lastDepthTrackerInheritedProps = cachedLastDepthTrackerProps; + }; + + this._renderAttack = function (entry, textStack, meta, options) { + this._renderPrefix(entry, textStack, meta, options); + textStack[0] += `${Parser.attackTypeToFull(entry.attackType)}: `; + const len = entry.attackEntries.length; + for (let i = 0; i < len; ++i) this._recursiveRender(entry.attackEntries[i], textStack, meta); + textStack[0] += ` Hit: `; + const len2 = entry.hitEntries.length; + for (let i = 0; i < len2; ++i) this._recursiveRender(entry.hitEntries[i], textStack, meta); + this._renderSuffix(entry, textStack, meta, options); + }; + + this._renderIngredient = function (entry, textStack, meta, options) { + this._renderPrefix(entry, textStack, meta, options); + this._recursiveRender(entry.entry, textStack, meta); + this._renderSuffix(entry, textStack, meta, options); + }; + + this._renderItem = function (entry, textStack, meta, options) { + this._renderPrefix(entry, textStack, meta, options); + textStack[0] += `

    ${this.render(entry.name)}${this._renderItem_isAddPeriod(entry) ? "." : ""} `; + if (entry.entry) this._recursiveRender(entry.entry, textStack, meta); + else if (entry.entries) { + const len = entry.entries.length; + for (let i = 0; i < len; ++i) this._recursiveRender(entry.entries[i], textStack, meta, {prefix: i > 0 ? `` : "", suffix: i > 0 ? "" : ""}); + } + textStack[0] += "

    "; + this._renderSuffix(entry, textStack, meta, options); + }; + + this._renderItem_isAddPeriod = function (entry) { + return entry.name && entry.nameDot !== false && !Renderer._INLINE_HEADER_TERMINATORS.has(entry.name[entry.name.length - 1]); + }; + + this._renderItemSub = function (entry, textStack, meta, options) { + this._renderPrefix(entry, textStack, meta, options); + const isAddPeriod = entry.name && entry.nameDot !== false && !Renderer._INLINE_HEADER_TERMINATORS.has(entry.name[entry.name.length - 1]); + this._recursiveRender(entry.entry, textStack, meta, {prefix: `

    ${entry.name}${isAddPeriod ? "." : ""} `, suffix: "

    "}); + this._renderSuffix(entry, textStack, meta, options); + }; + + this._renderItemSpell = function (entry, textStack, meta, options) { + this._renderPrefix(entry, textStack, meta, options); + + const tempStack = [""]; + this._recursiveRender(entry.name || "", tempStack, meta); + + this._recursiveRender(entry.entry, textStack, meta, {prefix: `

    ${tempStack.join("")} `, suffix: "

    "}); + this._renderSuffix(entry, textStack, meta, options); + }; + + this._InlineStatblockStrategy = function ( + { + pFnPreProcess, + }, + ) { + this.pFnPreProcess = pFnPreProcess; + }; + + this._INLINE_STATBLOCK_STRATEGIES = { + "item": new this._InlineStatblockStrategy({ + pFnPreProcess: async (ent) => { + await Renderer.item.pPopulatePropertyAndTypeReference(); + Renderer.item.enhanceItem(ent); + return ent; + }, + }), + }; + + this._renderStatblockInline = function (entry, textStack, meta, options) { + const fnGetRenderCompact = Renderer.hover.getFnRenderCompact(entry.dataType); + + const headerName = entry.displayName || entry.data?.name; + const headerStyle = entry.style; + + if (!fnGetRenderCompact) { + this._renderPrefix(entry, textStack, meta, options); + this._renderDataHeader(textStack, headerName, headerStyle); + textStack[0] += ` + + Cannot render "${entry.type}"—unknown data type "${entry.dataType}"! + + `; + this._renderDataFooter(textStack); + this._renderSuffix(entry, textStack, meta, options); + return; + } + + const strategy = this._INLINE_STATBLOCK_STRATEGIES[entry.dataType]; + + if (!strategy?.pFnPreProcess && !entry.data?._copy) { + this._renderPrefix(entry, textStack, meta, options); + this._renderDataHeader(textStack, headerName, headerStyle, {isCollapsed: entry.collapsed}); + textStack[0] += fnGetRenderCompact(entry.data, {isEmbeddedEntity: true}); + this._renderDataFooter(textStack); + this._renderSuffix(entry, textStack, meta, options); + return; + } + + this._renderPrefix(entry, textStack, meta, options); + this._renderDataHeader(textStack, headerName, headerStyle, {isCollapsed: entry.collapsed}); + + const id = CryptUtil.uid(); + Renderer._cache.inlineStatblock[id] = { + pFn: async (ele) => { + const entLoaded = entry.data?._copy + ? (await DataUtil.pDoMetaMergeSingle( + entry.dataType, + {dependencies: {[entry.dataType]: entry.dependencies}}, + entry.data, + )) + : entry.data; + + const ent = strategy?.pFnPreProcess ? await strategy.pFnPreProcess(entLoaded) : entLoaded; + + const tbl = ele.closest("table"); + const nxt = e_({ + outer: Renderer.utils.getEmbeddedDataHeader(headerName, headerStyle, {isCollapsed: entry.collapsed}) + + fnGetRenderCompact(ent, {isEmbeddedEntity: true}) + + Renderer.utils.getEmbeddedDataFooter(), + }); + tbl.parentNode.replaceChild( + nxt, + tbl, + ); + }, + }; + + textStack[0] += ``; + this._renderDataFooter(textStack); + this._renderSuffix(entry, textStack, meta, options); + }; + + this._renderDataHeader = function (textStack, name, style, {isCollapsed = false} = {}) { + textStack[0] += Renderer.utils.getEmbeddedDataHeader(name, style, {isCollapsed}); + }; + + this._renderDataFooter = function (textStack) { + textStack[0] += Renderer.utils.getEmbeddedDataFooter(); + }; + + this._renderStatblock = function (entry, textStack, meta, options) { + this._renderPrefix(entry, textStack, meta, options); + + const page = entry.prop || Renderer.tag.getPage(entry.tag); + const source = Parser.getTagSource(entry.tag, entry.source); + const hash = entry.hash || (UrlUtil.URL_TO_HASH_BUILDER[page] ? UrlUtil.URL_TO_HASH_BUILDER[page]({...entry, name: entry.name, source}) : null); + + const asTag = `{@${entry.tag} ${entry.name}|${source}${entry.displayName ? `|${entry.displayName}` : ""}}`; + + if (!page || !source || !hash) { + this._renderDataHeader(textStack, entry.name, entry.style); + textStack[0] += ` + + Cannot load ${entry.tag ? `"${asTag}"` : entry.displayName || entry.name}! An unknown tag/prop, source, or hash was provided. + + `; + this._renderDataFooter(textStack); + this._renderSuffix(entry, textStack, meta, options); + + return; + } + + this._renderDataHeader(textStack, entry.displayName || entry.name, entry.style, {isCollapsed: entry.collapsed}); + textStack[0] += ` + + Loading ${entry.tag ? `${Renderer.get().render(asTag)}` : entry.displayName || entry.name}... + + + `; + this._renderDataFooter(textStack); + this._renderSuffix(entry, textStack, meta, options); + }; + + this._renderGallery = function (entry, textStack, meta, options) { + if (entry.name) textStack[0] += ``; + textStack[0] += ``; + }; + + this._renderFlowchart = function (entry, textStack, meta, options) { + textStack[0] += `
    `; + const len = entry.blocks.length; + for (let i = 0; i < len; ++i) { + this._recursiveRender(entry.blocks[i], textStack, meta, options); + if (i !== len - 1) { + textStack[0] += `
    `; + } + } + textStack[0] += `
    `; + }; + + this._renderFlowBlock = function (entry, textStack, meta, options) { + const dataString = this._renderEntriesSubtypes_getDataString(entry); + textStack[0] += `<${this.wrapperTag} class="rd__b-special rd__b-flow ve-text-center" ${dataString}>`; + + const cachedLastDepthTrackerProps = MiscUtil.copyFast(this._lastDepthTrackerInheritedProps); + this._handleTrackDepth(entry, 1); + + if (entry.name != null) { + if (Renderer.ENTRIES_WITH_ENUMERATED_TITLES_LOOKUP[entry.type]) this._handleTrackTitles(entry.name); + textStack[0] += `

    ${this.render({type: "inline", entries: [entry.name]})}

    `; + } + if (entry.entries) { + const len = entry.entries.length; + for (let i = 0; i < len; ++i) { + const cacheDepth = meta.depth; + meta.depth = 2; + this._recursiveRender(entry.entries[i], textStack, meta, {prefix: "

    ", suffix: "

    "}); + meta.depth = cacheDepth; + } + } + textStack[0] += `
    `; + textStack[0] += ``; + + this._lastDepthTrackerInheritedProps = cachedLastDepthTrackerProps; + }; + + this._renderHomebrew = function (entry, textStack, meta, options) { + textStack[0] += `
    `; + + if (entry.oldEntries) { + const hoverMeta = Renderer.hover.getInlineHover({type: "entries", name: "Homebrew", entries: entry.oldEntries}); + let markerText; + if (entry.movedTo) { + markerText = "(See moved content)"; + } else if (entry.entries) { + markerText = "(See replaced content)"; + } else { + markerText = "(See removed content)"; + } + textStack[0] += `${markerText}`; + } + + textStack[0] += `
    `; + + if (entry.entries) { + const len = entry.entries.length; + for (let i = 0; i < len; ++i) this._recursiveRender(entry.entries[i], textStack, meta, {prefix: "

    ", suffix: "

    "}); + } else if (entry.movedTo) { + textStack[0] += `This content has been moved to ${entry.movedTo}.`; + } else { + textStack[0] += "This content has been deleted."; + } + + textStack[0] += `
    `; + }; + + this._renderCode = function (entry, textStack, meta, options) { + const isWrapped = !!StorageUtil.syncGet("rendererCodeWrap"); + textStack[0] += ` +
    +
    + + +
    +
    ${entry.preformatted}
    +
    + `; + }; + + this._renderHr = function (entry, textStack, meta, options) { + textStack[0] += `
    `; + }; + + this._getStyleClass = function (entryType, entry) { + const outList = []; + + const pluginResults = this._getPlugins(`${entryType}_styleClass_fromSource`) + .map(plugin => plugin(entryType, entry)).filter(Boolean); + + if (!pluginResults.some(it => it.isSkip)) { + if ( + SourceUtil.isNonstandardSource(entry.source) + || (typeof PrereleaseUtil !== "undefined" && PrereleaseUtil.hasSourceJson(entry.source)) + ) outList.push("spicy-sauce"); + if (typeof BrewUtil2 !== "undefined" && BrewUtil2.hasSourceJson(entry.source)) outList.push("refreshing-brew"); + } + + if (this._extraSourceClasses) outList.push(...this._extraSourceClasses); + for (const k in this._fnsGetStyleClasses) { + const fromFn = this._fnsGetStyleClasses[k](entry); + if (fromFn) outList.push(...fromFn); + } + if (entry.style) outList.push(this._getMutatedStyleString(entry.style)); + return outList.join(" "); + }; + + this._renderString = function (entry, textStack, meta, options) { + const tagSplit = Renderer.splitByTags(entry); + const len = tagSplit.length; + for (let i = 0; i < len; ++i) { + const s = tagSplit[i]; + if (!s) continue; + if (s.startsWith("{@")) { + const [tag, text] = Renderer.splitFirstSpace(s.slice(1, -1)); + this._renderString_renderTag(textStack, meta, options, tag, text); + } else textStack[0] += s; + } + }; + + this._renderString_renderTag = function (textStack, meta, options, tag, text) { + // region Plugins + // Generic + for (const plugin of this._getPlugins("string_tag")) { + const out = plugin(tag, text, textStack, meta, options); + if (out) return void (textStack[0] += out); + } + + // Tag-specific + for (const plugin of this._getPlugins(`string_${tag}`)) { + const out = plugin(tag, text, textStack, meta, options); + if (out) return void (textStack[0] += out); + } + // endregion + + switch (tag) { + // BASIC STYLES/TEXT /////////////////////////////////////////////////////////////////////////////// + case "@b": + case "@bold": + textStack[0] += ``; + this._recursiveRender(text, textStack, meta); + textStack[0] += ``; + break; + case "@i": + case "@italic": + textStack[0] += ``; + this._recursiveRender(text, textStack, meta); + textStack[0] += ``; + break; + case "@s": + case "@strike": + textStack[0] += ``; + this._recursiveRender(text, textStack, meta); + textStack[0] += ``; + break; + case "@u": + case "@underline": + textStack[0] += ``; + this._recursiveRender(text, textStack, meta); + textStack[0] += ``; + break; + case "@sup": + textStack[0] += ``; + this._recursiveRender(text, textStack, meta); + textStack[0] += ``; + break; + case "@sub": + textStack[0] += ``; + this._recursiveRender(text, textStack, meta); + textStack[0] += ``; + break; + case "@kbd": + textStack[0] += ``; + this._recursiveRender(text, textStack, meta); + textStack[0] += ``; + break; + case "@code": + textStack[0] += ``; + this._recursiveRender(text, textStack, meta); + textStack[0] += ``; + break; + case "@style": { + const [displayText, styles] = Renderer.splitTagByPipe(text); + const classNames = (styles || "").split(";").map(it => Renderer._STYLE_TAG_ID_TO_STYLE[it.trim()]).filter(Boolean).join(" "); + textStack[0] += ``; + this._recursiveRender(displayText, textStack, meta); + textStack[0] += ``; + break; + } + case "@font": { + const [displayText, fontFamily] = Renderer.splitTagByPipe(text); + textStack[0] += ``; + this._recursiveRender(displayText, textStack, meta); + textStack[0] += ``; + break; + } + case "@note": + textStack[0] += ``; + this._recursiveRender(text, textStack, meta); + textStack[0] += ``; + break; + case "@tip": { + const [displayText, titielText] = Renderer.splitTagByPipe(text); + textStack[0] += ``; + this._recursiveRender(displayText, textStack, meta); + textStack[0] += ``; + break; + } + case "@atk": + textStack[0] += `${Renderer.attackTagToFull(text)}`; + break; + case "@h": textStack[0] += `Hit: `; break; + case "@m": textStack[0] += `Miss: `; break; + case "@color": { + const [toDisplay, color] = Renderer.splitTagByPipe(text); + const ptColor = this._renderString_renderTag_getBrewColorPart(color); + + textStack[0] += ``; + this._recursiveRender(toDisplay, textStack, meta); + textStack[0] += ``; + break; + } + case "@highlight": { + const [toDisplay, color] = Renderer.splitTagByPipe(text); + const ptColor = this._renderString_renderTag_getBrewColorPart(color); + + textStack[0] += ptColor ? `` : ``; + textStack[0] += toDisplay; + textStack[0] += ``; + break; + } + case "@help": { + const [toDisplay, title = ""] = Renderer.splitTagByPipe(text); + textStack[0] += ``; + this._recursiveRender(toDisplay, textStack, meta); + textStack[0] += ``; + break; + } + + // Misc utilities ////////////////////////////////////////////////////////////////////////////////// + case "@unit": { + const [amount, unitSingle, unitPlural] = Renderer.splitTagByPipe(text); + textStack[0] += isNaN(amount) ? unitSingle : Number(amount) > 1 ? (unitPlural || unitSingle.toPlural()) : unitSingle; + break; + } + + // Comic styles //////////////////////////////////////////////////////////////////////////////////// + case "@comic": + textStack[0] += ``; + this._recursiveRender(text, textStack, meta); + textStack[0] += ``; + break; + case "@comicH1": + textStack[0] += ``; + this._recursiveRender(text, textStack, meta); + textStack[0] += ``; + break; + case "@comicH2": + textStack[0] += ``; + this._recursiveRender(text, textStack, meta); + textStack[0] += ``; + break; + case "@comicH3": + textStack[0] += ``; + this._recursiveRender(text, textStack, meta); + textStack[0] += ``; + break; + case "@comicH4": + textStack[0] += ``; + this._recursiveRender(text, textStack, meta); + textStack[0] += ``; + break; + case "@comicNote": + textStack[0] += ``; + this._recursiveRender(text, textStack, meta); + textStack[0] += ``; + break; + + // DCs ///////////////////////////////////////////////////////////////////////////////////////////// + case "@dc": { + const [dcText, displayText] = Renderer.splitTagByPipe(text); + textStack[0] += `DC ${displayText || dcText}`; + break; + } + + case "@dcYourSpellSave": { + const [displayText] = Renderer.splitTagByPipe(text); + textStack[0] += displayText || "your spell save DC"; + break; + } + + // DICE //////////////////////////////////////////////////////////////////////////////////////////// + case "@dice": + case "@autodice": + case "@damage": + case "@hit": + case "@d20": + case "@chance": + case "@coinflip": + case "@recharge": + case "@ability": + case "@savingThrow": + case "@skillCheck": { + const fauxEntry = Renderer.utils.getTagEntry(tag, text); + + if (tag === "@recharge") { + const [, flagsRaw] = Renderer.splitTagByPipe(text); + const flags = flagsRaw ? flagsRaw.split("") : null; + textStack[0] += `${flags && flags.includes("m") ? "" : "("}Recharge `; + this._recursiveRender(fauxEntry, textStack, meta); + textStack[0] += `${flags && flags.includes("m") ? "" : ")"}`; + } else { + this._recursiveRender(fauxEntry, textStack, meta); + } + + break; + } + + case "@hitYourSpellAttack": this._renderString_renderTag_hitYourSpellAttack(textStack, meta, options, tag, text); break; + + // SCALE DICE ////////////////////////////////////////////////////////////////////////////////////// + case "@scaledice": + case "@scaledamage": { + const fauxEntry = Renderer.parseScaleDice(tag, text); + this._recursiveRender(fauxEntry, textStack, meta); + break; + } + + // LINKS /////////////////////////////////////////////////////////////////////////////////////////// + case "@filter": { + // format: {@filter Warlock Spells|spells|level=1;2|class=Warlock} + const [displayText, page, ...filters] = Renderer.splitTagByPipe(text); + + const filterSubhashMeta = Renderer.getFilterSubhashes(filters); + + const fauxEntry = { + type: "link", + text: displayText, + href: { + type: "internal", + path: `${page}.html`, + hash: HASH_BLANK, + hashPreEncoded: true, + subhashes: filterSubhashMeta.subhashes, + }, + }; + + if (filterSubhashMeta.customHash) fauxEntry.href.hash = filterSubhashMeta.customHash; + + this._recursiveRender(fauxEntry, textStack, meta); + + break; + } + case "@link": { + const [displayText, url] = Renderer.splitTagByPipe(text); + let outUrl = url == null ? displayText : url; + if (!outUrl.startsWith("http")) outUrl = `http://${outUrl}`; // avoid HTTPS, as the D&D homepage doesn't support it + const fauxEntry = { + type: "link", + href: { + type: "external", + url: outUrl, + }, + text: displayText, + }; + this._recursiveRender(fauxEntry, textStack, meta); + + break; + } + case "@5etools": { + const [displayText, page, hash] = Renderer.splitTagByPipe(text); + const fauxEntry = { + type: "link", + href: { + type: "internal", + path: page, + }, + text: displayText, + }; + if (hash) { + fauxEntry.hash = hash; + fauxEntry.hashPreEncoded = true; + } + this._recursiveRender(fauxEntry, textStack, meta); + + break; + } + + // OTHER HOVERABLES //////////////////////////////////////////////////////////////////////////////// + case "@footnote": { + const [displayText, footnoteText, optTitle] = Renderer.splitTagByPipe(text); + const hoverMeta = Renderer.hover.getInlineHover({ + type: "entries", + name: optTitle ? optTitle.toTitleCase() : "Footnote", + entries: [footnoteText, optTitle ? `{@note ${optTitle}}` : ""].filter(Boolean), + }); + textStack[0] += ``; + this._recursiveRender(displayText, textStack, meta); + textStack[0] += ``; + + break; + } + case "@homebrew": { + const [newText, oldText] = Renderer.splitTagByPipe(text); + const tooltipEntries = []; + if (newText && oldText) { + tooltipEntries.push("{@b This is a homebrew addition, replacing the following:}"); + } else if (newText) { + tooltipEntries.push("{@b This is a homebrew addition.}"); + } else if (oldText) { + tooltipEntries.push("{@b The following text has been removed with this homebrew:}"); + } + if (oldText) { + tooltipEntries.push(oldText); + } + const hoverMeta = Renderer.hover.getInlineHover({ + type: "entries", + name: "Homebrew Modifications", + entries: tooltipEntries, + }); + textStack[0] += ``; + this._recursiveRender(newText || "[...]", textStack, meta); + textStack[0] += ``; + + break; + } + case "@area": { + const {areaId, displayText} = Renderer.tag.TAG_LOOKUP.area.getMeta(tag, text); + + if (typeof BookUtil === "undefined") { // for the roll20 script + textStack[0] += displayText; + } else { + const area = BookUtil.curRender.headerMap[areaId] || {entry: {name: ""}}; // default to prevent rendering crash on bad tag + const hoverMeta = Renderer.hover.getInlineHover(area.entry, {isLargeBookContent: true, depth: area.depth}); + textStack[0] += `${displayText}`; + } + + break; + } + + // HOMEBREW LOADING //////////////////////////////////////////////////////////////////////////////// + case "@loader": { + const {name, path, mode} = this._renderString_getLoaderTagMeta(text); + + const brewUtilName = mode === "homebrew" ? "BrewUtil2" : mode === "prerelease" ? "PrereleaseUtil" : null; + const brewUtil = globalThis[brewUtilName]; + + if (!brewUtil) { + textStack[0] += `${name}`; + + break; + } + + textStack[0] += `${name}`; + break; + } + + // CONTENT TAGS //////////////////////////////////////////////////////////////////////////////////// + case "@book": + case "@adventure": { + // format: {@tag Display Text|DMG< |chapter< |section >< |number > >} + const page = tag === "@book" ? "book.html" : "adventure.html"; + const [displayText, book, chapter, section, rawNumber] = Renderer.splitTagByPipe(text); + const number = rawNumber || 0; + const hash = `${book}${chapter ? `${HASH_PART_SEP}${chapter}${section ? `${HASH_PART_SEP}${UrlUtil.encodeForHash(section)}${number != null ? `${HASH_PART_SEP}${UrlUtil.encodeForHash(number)}` : ""}` : ""}` : ""}`; + const fauxEntry = { + type: "link", + href: { + type: "internal", + path: page, + hash, + hashPreEncoded: true, + }, + text: displayText, + }; + this._recursiveRender(fauxEntry, textStack, meta); + + break; + } + + default: { + const {name, source, displayText, others, page, hash, hashPreEncoded, pageHover, hashHover, hashPreEncodedHover, preloadId, linkText, subhashes, subhashesHover, isFauxPage} = Renderer.utils.getTagMeta(tag, text); + + const fauxEntry = { + type: "link", + href: { + type: "internal", + path: page, + hash, + hover: { + page, + isFauxPage, + source, + }, + }, + text: (displayText || name), + }; + + if (hashPreEncoded != null) fauxEntry.href.hashPreEncoded = hashPreEncoded; + if (pageHover != null) fauxEntry.href.hover.page = pageHover; + if (hashHover != null) fauxEntry.href.hover.hash = hashHover; + if (hashPreEncodedHover != null) fauxEntry.href.hover.hashPreEncoded = hashPreEncodedHover; + if (preloadId != null) fauxEntry.href.hover.preloadId = preloadId; + if (linkText) fauxEntry.text = linkText; + if (subhashes) fauxEntry.href.subhashes = subhashes; + if (subhashesHover) fauxEntry.href.hover.subhashes = subhashesHover; + + this._recursiveRender(fauxEntry, textStack, meta); + + break; + } + } + }; + + this._renderString_renderTag_getBrewColorPart = function (color) { + if (!color) return ""; + const scrubbedColor = BrewUtilShared.getValidColor(color, {isExtended: true}); + return scrubbedColor.startsWith("--") ? `var(${scrubbedColor})` : `#${scrubbedColor}`; + }; + + this._renderString_renderTag_hitYourSpellAttack = function (textStack, meta, options, tag, text) { + const [displayText] = Renderer.splitTagByPipe(text); + + const fauxEntry = { + type: "dice", + rollable: true, + subType: "d20", + displayText: displayText || "your spell attack modifier", + toRoll: `1d20 + #$prompt_number:title=Enter your Spell Attack Modifier$#`, + }; + return this._recursiveRender(fauxEntry, textStack, meta); + }; + + this._renderString_getLoaderTagMeta = function (text, {isDefaultUrl = false} = {}) { + const [name, file, mode = "homebrew"] = Renderer.splitTagByPipe(text); + + if (!isDefaultUrl) return {name, path: file, mode}; + + const path = /^.*?:\/\//.test(file) ? file : `${VeCt.URL_ROOT_BREW}${file}`; + return {name, path, mode}; + }; + + this._renderPrimitive = function (entry, textStack, meta, options) { textStack[0] += entry; }; + + this._renderLink = function (entry, textStack, meta, options) { + let href = this._renderLink_getHref(entry); + + // overwrite href if there's an available Roll20 handout/character + if (entry.href.hover && this._roll20Ids) { + const procHash = UrlUtil.encodeForHash(entry.href.hash); + const id = this._roll20Ids[procHash]; + if (id) { + href = `http://journal.roll20.net/${id.type}/${id.roll20Id}`; + } + } + + const pluginData = this._getPlugins("link").map(plugin => plugin(entry, textStack, meta, options)).filter(Boolean); + const isDisableEvents = pluginData.some(it => it.isDisableEvents); + const additionalAttributes = pluginData.map(it => it.attributes).filter(Boolean); + + if (this._isInternalLinksDisabled && entry.href.type === "internal") { + textStack[0] += `${this.render(entry.text)}`; + } else if (entry.href.hover?.isFauxPage) { + textStack[0] += `${this.render(entry.text)}`; + } else { + textStack[0] += `${this.render(entry.text)}`; + } + }; + + this._renderLink_getHref = function (entry) { + let href; + if (entry.href.type === "internal") { + // baseURL is blank by default + href = `${this.baseUrl}${entry.href.path}#`; + if (entry.href.hash != null) { + href += entry.href.hashPreEncoded ? entry.href.hash : UrlUtil.encodeForHash(entry.href.hash); + } + if (entry.href.subhashes != null) { + href += Renderer.utils.getLinkSubhashString(entry.href.subhashes); + } + } else if (entry.href.type === "external") { + href = entry.href.url; + } + return href; + }; + + this._renderLink_getHoverString = function (entry) { + if (!entry.href.hover || !this._isAddHandlers) return ""; + + let procHash = entry.href.hover.hash + ? entry.href.hover.hashPreEncoded ? entry.href.hover.hash : UrlUtil.encodeForHash(entry.href.hover.hash) + : entry.href.hashPreEncoded ? entry.href.hash : UrlUtil.encodeForHash(entry.href.hash); + + if (this._tagExportDict) { + this._tagExportDict[procHash] = { + page: entry.href.hover.page, + source: entry.href.hover.source, + hash: procHash, + }; + } + + if (entry.href.hover.subhashes) { + procHash += Renderer.utils.getLinkSubhashString(entry.href.hover.subhashes); + } + + const pluginData = this._getPlugins("link_attributesHover") + .map(plugin => plugin(entry, procHash)) + .filter(Boolean); + const replacementAttributes = pluginData.map(it => it.attributesHoverReplace).filter(Boolean); + if (replacementAttributes.length) return replacementAttributes.join(" "); + + return `onmouseover="Renderer.hover.pHandleLinkMouseOver(event, this)" onmouseleave="Renderer.hover.handleLinkMouseLeave(event, this)" onmousemove="Renderer.hover.handleLinkMouseMove(event, this)" ondragstart="Renderer.hover.handleLinkDragStart(event, this)" data-vet-page="${entry.href.hover.page.qq()}" data-vet-source="${entry.href.hover.source.qq()}" data-vet-hash="${procHash.qq()}" ${entry.href.hover.preloadId != null ? `data-vet-preload-id="${`${entry.href.hover.preloadId}`.qq()}"` : ""} ${entry.href.hover.isFauxPage ? `data-vet-is-faux-page="true"` : ""} ${Renderer.hover.getPreventTouchString()}`; + }; + + /** + * Helper function to render an entity using this renderer + * @param entry + * @param depth + * @returns {string} + */ + this.render = function (entry, depth = 0) { + const tempStack = []; + this.recursiveRender(entry, tempStack, {depth}); + return tempStack.join(""); + }; +}; + +// Unless otherwise specified, these use `"name"` as their name title prop +Renderer.ENTRIES_WITH_ENUMERATED_TITLES = [ + {type: "section", key: "entries", depth: -1}, + {type: "entries", key: "entries", depthIncrement: 1}, + {type: "options", key: "entries"}, + {type: "inset", key: "entries", depth: 2}, + {type: "insetReadaloud", key: "entries", depth: 2}, + {type: "variant", key: "entries", depth: 2}, + {type: "variantInner", key: "entries", depth: 2}, + {type: "actions", key: "entries", depth: 2}, + {type: "flowBlock", key: "entries", depth: 2}, + {type: "optfeature", key: "entries", depthIncrement: 1}, + {type: "patron", key: "entries"}, +]; + +Renderer.ENTRIES_WITH_ENUMERATED_TITLES_LOOKUP = Renderer.ENTRIES_WITH_ENUMERATED_TITLES.mergeMap(it => ({[it.type]: it})); + +Renderer.ENTRIES_WITH_CHILDREN = [ + ...Renderer.ENTRIES_WITH_ENUMERATED_TITLES, + {type: "list", key: "items"}, + {type: "table", key: "rows"}, +]; + +Renderer._INLINE_HEADER_TERMINATORS = new Set([".", ",", "!", "?", ";", ":", `"`]); + +Renderer._STYLE_TAG_ID_TO_STYLE = { + "small-caps": "small-caps", + "small": "ve-small", + "capitalize": "capitalize", + "dnd-font": "dnd-font", +}; + +Renderer.get = () => { + if (!Renderer.defaultRenderer) Renderer.defaultRenderer = new Renderer(); + return Renderer.defaultRenderer; +}; + +Renderer.applyProperties = function (entry, object) { + const propSplit = Renderer.splitByPropertyInjectors(entry); + const len = propSplit.length; + if (len === 1) return entry; + + let textStack = ""; + + for (let i = 0; i < len; ++i) { + const s = propSplit[i]; + if (!s) continue; + + if (!s.startsWith("{=")) { + textStack += s; + continue; + } + + if (s.startsWith("{=")) { + const [path, modifiers] = s.slice(2, -1).split("/"); + let fromProp = object[path]; + + if (!modifiers) { + textStack += fromProp; + continue; + } + + if (fromProp == null) throw new Error(`Could not apply property in "${s}"; "${path}" value was null!`); + + modifiers + .split("") + .sort((a, b) => Renderer.applyProperties._OP_ORDER.indexOf(a) - Renderer.applyProperties._OP_ORDER.indexOf(b)); + + for (const modifier of modifiers) { + switch (modifier) { + case "a": // render "a"/"an" depending on prop value + fromProp = Renderer.applyProperties._LEADING_AN.has(fromProp[0].toLowerCase()) ? "an" : "a"; + break; + + case "l": fromProp = fromProp.toLowerCase(); break; // convert text to lower case + case "t": fromProp = fromProp.toTitleCase(); break; // title-case text + case "u": fromProp = fromProp.toUpperCase(); break; // uppercase text + case "v": fromProp = Parser.numberToVulgar(fromProp); break; // vulgarize number + case "x": fromProp = Parser.numberToText(fromProp); break; // convert number to text + case "r": fromProp = Math.round(fromProp); break; // round number + case "f": fromProp = Math.floor(fromProp); break; // floor number + case "c": fromProp = Math.ceil(fromProp); break; // ceiling number + + default: throw new Error(`Unhandled property modifier "${modifier}"`); + } + } + + textStack += fromProp; + } + } + + return textStack; +}; +Renderer.applyProperties._LEADING_AN = new Set(["a", "e", "i", "o", "u"]); +Renderer.applyProperties._OP_ORDER = [ + "r", "f", "c", // operate on value first + "v", "x", // cast to desired type + "l", "t", "u", "a", // operate on value representation +]; + +Renderer.applyAllProperties = function (entries, object = null) { + let lastObj = null; + const handlers = { + object: (obj) => { + lastObj = obj; + return obj; + }, + string: (str) => Renderer.applyProperties(str, object || lastObj), + }; + return MiscUtil.getWalker().walk(entries, handlers); +}; + +Renderer.attackTagToFull = function (tagStr) { + function renderTag (tags) { + return `${tags.includes("m") ? "Melee " : tags.includes("r") ? "Ranged " : tags.includes("g") ? "Magical " : tags.includes("a") ? "Area " : ""}${tags.includes("w") ? "Weapon " : tags.includes("s") ? "Spell " : tags.includes("p") ? "Power " : ""}`; + } + + const tagGroups = tagStr.toLowerCase().split(",").map(it => it.trim()).filter(it => it).map(it => it.split("")); + if (tagGroups.length > 1) { + const seen = new Set(tagGroups.last()); + for (let i = tagGroups.length - 2; i >= 0; --i) { + tagGroups[i] = tagGroups[i].filter(it => { + const out = !seen.has(it); + seen.add(it); + return out; + }); + } + } + return `${tagGroups.map(it => renderTag(it)).join(" or ")}Attack:`; +}; + +Renderer.splitFirstSpace = function (string) { + const firstIndex = string.indexOf(" "); + return firstIndex === -1 ? [string, ""] : [string.substr(0, firstIndex), string.substr(firstIndex + 1)]; +}; + +Renderer._splitByTagsBase = function (leadingCharacter) { + return function (string) { + let tagDepth = 0; + let char, char2; + const out = []; + let curStr = ""; + let isLastOpen = false; + + const len = string.length; + for (let i = 0; i < len; ++i) { + char = string[i]; + char2 = string[i + 1]; + + switch (char) { + case "{": + isLastOpen = true; + if (char2 === leadingCharacter) { + if (tagDepth++ > 0) { + curStr += "{"; + } else { + out.push(curStr.replace(//g, leadingCharacter)); + curStr = `{${leadingCharacter}`; + ++i; + } + } else curStr += "{"; + break; + + case "}": + isLastOpen = false; + curStr += "}"; + if (tagDepth !== 0 && --tagDepth === 0) { + out.push(curStr.replace(//g, leadingCharacter)); + curStr = ""; + } + break; + + case leadingCharacter: { + if (!isLastOpen) curStr += ""; + else curStr += leadingCharacter; + break; + } + + default: isLastOpen = false; curStr += char; break; + } + } + + if (curStr) out.push(curStr.replace(//g, leadingCharacter)); + + return out; + }; +}; + +Renderer.splitByTags = Renderer._splitByTagsBase("@"); +Renderer.splitByPropertyInjectors = Renderer._splitByTagsBase("="); + +Renderer._splitByPipeBase = function (leadingCharacter) { + return function (string) { + let tagDepth = 0; + let char, char2; + const out = []; + let curStr = ""; + + const len = string.length; + for (let i = 0; i < len; ++i) { + char = string[i]; + char2 = string[i + 1]; + + switch (char) { + case "{": + if (char2 === leadingCharacter) tagDepth++; + curStr += "{"; + + break; + + case "}": + if (tagDepth) tagDepth--; + curStr += "}"; + + break; + + case "|": { + if (tagDepth) curStr += "|"; + else { + out.push(curStr); + curStr = ""; + } + break; + } + + default: { + curStr += char; + break; + } + } + } + + if (curStr) out.push(curStr); + return out; + }; +}; + +Renderer.splitTagByPipe = Renderer._splitByPipeBase("@"); + +Renderer.getEntryDice = function (entry, name, opts = {}) { + const toDisplay = Renderer.getEntryDiceDisplayText(entry); + + if (entry.rollable === true) return Renderer.getRollableEntryDice(entry, name, toDisplay, opts); + else return toDisplay; +}; + +Renderer.getRollableEntryDice = function ( + entry, + name, + toDisplay, + { + isAddHandlers = true, + pluginResults = null, + } = {}, +) { + const toPack = MiscUtil.copyFast(entry); + if (typeof toPack.toRoll !== "string") { + // handle legacy format + toPack.toRoll = Renderer.legacyDiceToString(toPack.toRoll); + } + + const handlerPart = isAddHandlers ? `onmousedown="event.preventDefault()" data-packed-dice='${JSON.stringify(toPack).qq()}'` : ""; + + const rollableTitlePart = isAddHandlers ? Renderer.getEntryDiceTitle(toPack.subType) : null; + const titlePart = isAddHandlers + ? `title="${[name, rollableTitlePart].filter(Boolean).join(". ").qq()}" ${name ? `data-roll-name="${name}"` : ""}` + : name ? `title="${name.qq()}" data-roll-name="${name.qq()}"` : ""; + + const additionalDataPart = (pluginResults || []) + .filter(it => it.additionalData) + .map(it => { + return Object.entries(it.additionalData) + .map(([dataKey, val]) => `${dataKey}='${typeof val === "object" ? JSON.stringify(val).qq() : `${val}`.qq()}'`) + .join(" "); + }) + .join(" "); + + toDisplay = (pluginResults || []).filter(it => it.toDisplay)[0]?.toDisplay ?? toDisplay; + + const ptRoll = Renderer.getRollableEntryDice._getPtRoll(toPack); + + return `${toDisplay}${ptRoll}`; +}; + +Renderer.getRollableEntryDice._getPtRoll = (toPack) => { + if (!toPack.autoRoll) return ""; + + const r = Renderer.dice.parseRandomise2(toPack.toRoll); + return ` (${r})`; +}; + +Renderer.getEntryDiceTitle = function (subType) { + return `Click to roll. ${subType === "damage" ? "SHIFT to roll a critical hit, CTRL to half damage (rounding down)." : subType === "d20" ? "SHIFT to roll with advantage, CTRL to roll with disadvantage." : "SHIFT/CTRL to roll twice."}`; +}; + +Renderer.legacyDiceToString = function (array) { + let stack = ""; + array.forEach(r => { + stack += `${r.neg ? "-" : stack === "" ? "" : "+"}${r.number || 1}d${r.faces}${r.mod ? r.mod > 0 ? `+${r.mod}` : r.mod : ""}`; + }); + return stack; +}; + +Renderer.getEntryDiceDisplayText = function (entry) { + if (entry.displayText) return entry.displayText; + return Renderer._getEntryDiceDisplayText_getDiceAsStr(entry); +}; + +Renderer._getEntryDiceDisplayText_getDiceAsStr = function (entry) { + if (entry.successThresh != null) return `${entry.successThresh} percent`; + if (typeof entry.toRoll === "string") return entry.toRoll; + // handle legacy format + return Renderer.legacyDiceToString(entry.toRoll); +}; + +Renderer.parseScaleDice = function (tag, text) { + // format: {@scaledice 2d6;3d6|2-8,9|1d6|psi|display text} (or @scaledamage) + const [baseRoll, progression, addPerProgress, renderMode, displayText] = Renderer.splitTagByPipe(text); + const progressionParse = MiscUtil.parseNumberRange(progression, 1, 9); + const baseLevel = Math.min(...progressionParse); + const options = {}; + const isMultableDice = /^(\d+)d(\d+)$/i.exec(addPerProgress); + + const getSpacing = () => { + let diff = null; + const sorted = [...progressionParse].sort(SortUtil.ascSort); + for (let i = 1; i < sorted.length; ++i) { + const prev = sorted[i - 1]; + const curr = sorted[i]; + if (diff == null) diff = curr - prev; + else if (curr - prev !== diff) return null; + } + return diff; + }; + + const spacing = getSpacing(); + progressionParse.forEach(k => { + const offset = k - baseLevel; + if (isMultableDice && spacing != null) { + options[k] = offset ? `${Number(isMultableDice[1]) * (offset / spacing)}d${isMultableDice[2]}` : ""; + } else { + options[k] = offset ? [...new Array(Math.floor(offset / spacing))].map(_ => addPerProgress).join("+") : ""; + } + }); + + const out = { + type: "dice", + rollable: true, + toRoll: baseRoll, + displayText: displayText || addPerProgress, + prompt: { + entry: renderMode === "psi" ? "Spend Psi Points..." : "Cast at...", + mode: renderMode, + options, + }, + }; + if (tag === "@scaledamage") out.subType = "damage"; + + return out; +}; + +Renderer.getAbilityData = function (abArr, {isOnlyShort, isCurrentLineage} = {}) { + if (isOnlyShort && isCurrentLineage) return new Renderer._AbilityData({asTextShort: "Lineage (choose)"}); + + const outerStack = (abArr || [null]).map(it => Renderer.getAbilityData._doRenderOuter(it)); + if (outerStack.length <= 1) return outerStack[0]; + return new Renderer._AbilityData({ + asText: `Choose one of: ${outerStack.map((it, i) => `(${Parser.ALPHABET[i].toLowerCase()}) ${it.asText}`).join(" ")}`, + asTextShort: `${outerStack.map((it, i) => `(${Parser.ALPHABET[i].toLowerCase()}) ${it.asTextShort}`).join(" ")}`, + asCollection: [...new Set(outerStack.map(it => it.asCollection).flat())], + areNegative: [...new Set(outerStack.map(it => it.areNegative).flat())], + }); +}; + +Renderer.getAbilityData._doRenderOuter = function (abObj) { + const mainAbs = []; + const asCollection = []; + const areNegative = []; + const toConvertToText = []; + const toConvertToShortText = []; + + if (abObj != null) { + handleAllAbilities(abObj); + handleAbilitiesChoose(); + return new Renderer._AbilityData({ + asText: toConvertToText.join("; "), + asTextShort: toConvertToShortText.join("; "), + asCollection: asCollection, + areNegative: areNegative, + }); + } + + return new Renderer._AbilityData(); + + function handleAllAbilities (abObj, targetList) { + MiscUtil.copyFast(Parser.ABIL_ABVS) + .sort((a, b) => SortUtil.ascSort(abObj[b] || 0, abObj[a] || 0)) + .forEach(shortLabel => handleAbility(abObj, shortLabel, targetList)); + } + + function handleAbility (abObj, shortLabel, optToConvertToTextStorage) { + if (abObj[shortLabel] != null) { + const isNegMod = abObj[shortLabel] < 0; + const toAdd = `${shortLabel.uppercaseFirst()} ${(isNegMod ? "" : "+")}${abObj[shortLabel]}`; + + if (optToConvertToTextStorage) { + optToConvertToTextStorage.push(toAdd); + } else { + toConvertToText.push(toAdd); + toConvertToShortText.push(toAdd); + } + + mainAbs.push(shortLabel.uppercaseFirst()); + asCollection.push(shortLabel); + if (isNegMod) areNegative.push(shortLabel); + } + } + + function handleAbilitiesChoose () { + if (abObj.choose != null) { + const ch = abObj.choose; + let outStack = ""; + if (ch.weighted) { + const w = ch.weighted; + const froms = w.from.map(it => it.uppercaseFirst()); + const isAny = froms.length === 6; + const isAllEqual = w.weights.unique().length === 1; + let cntProcessed = 0; + + const weightsIncrease = w.weights.filter(it => it >= 0).sort(SortUtil.ascSort).reverse(); + const weightsReduce = w.weights.filter(it => it < 0).map(it => -it).sort(SortUtil.ascSort); + + const areIncreaseShort = []; + const areIncrease = isAny && isAllEqual && w.weights.length > 1 && w.weights[0] >= 0 + ? (() => { + weightsIncrease.forEach(it => areIncreaseShort.push(`+${it}`)); + return [`${cntProcessed ? "choose " : ""}${Parser.numberToText(w.weights.length)} different +${weightsIncrease[0]}`]; + })() + : weightsIncrease.map(it => { + areIncreaseShort.push(`+${it}`); + if (isAny) return `${cntProcessed ? "choose " : ""}any ${cntProcessed++ ? `other ` : ""}+${it}`; + return `one ${cntProcessed++ ? `other ` : ""}ability to increase by ${it}`; + }); + + const areReduceShort = []; + const areReduce = isAny && isAllEqual && w.weights.length > 1 && w.weights[0] < 0 + ? (() => { + weightsReduce.forEach(it => areReduceShort.push(`-${it}`)); + return [`${cntProcessed ? "choose " : ""}${Parser.numberToText(w.weights.length)} different -${weightsReduce[0]}`]; + })() + : weightsReduce.map(it => { + areReduceShort.push(`-${it}`); + if (isAny) return `${cntProcessed ? "choose " : ""}any ${cntProcessed++ ? `other ` : ""}-${it}`; + return `one ${cntProcessed++ ? `other ` : ""}ability to decrease by ${it}`; + }); + + const startText = isAny + ? `Choose ` + : `From ${froms.joinConjunct(", ", " and ")} choose `; + + const ptAreaIncrease = isAny + ? areIncrease.concat(areReduce).join("; ") + : areIncrease.concat(areReduce).joinConjunct(", ", isAny ? "; " : " and "); + toConvertToText.push(`${startText}${ptAreaIncrease}`); + toConvertToShortText.push(`${isAny ? "Any combination " : ""}${areIncreaseShort.concat(areReduceShort).join("/")}${isAny ? "" : ` from ${froms.join("/")}`}`); + } else { + const allAbilities = ch.from.length === 6; + const allAbilitiesWithParent = isAllAbilitiesWithParent(ch); + let amount = ch.amount === undefined ? 1 : ch.amount; + amount = (amount < 0 ? "" : "+") + amount; + if (allAbilities) { + outStack += "any "; + } else if (allAbilitiesWithParent) { + outStack += "any other "; + } + if (ch.count != null && ch.count > 1) { + outStack += `${Parser.numberToText(ch.count)} `; + } + if (allAbilities || allAbilitiesWithParent) { + outStack += `${ch.count > 1 ? "unique " : ""}${amount}`; + } else { + for (let j = 0; j < ch.from.length; ++j) { + let suffix = ""; + if (ch.from.length > 1) { + if (j === ch.from.length - 2) { + suffix = " or "; + } else if (j < ch.from.length - 2) { + suffix = ", "; + } + } + let thsAmount = ` ${amount}`; + if (ch.from.length > 1) { + if (j !== ch.from.length - 1) { + thsAmount = ""; + } + } + outStack += ch.from[j].uppercaseFirst() + thsAmount + suffix; + } + } + } + + if (outStack.trim()) { + toConvertToText.push(`Choose ${outStack}`); + toConvertToShortText.push(outStack.uppercaseFirst()); + } + } + } + + function isAllAbilitiesWithParent (chooseAbs) { + const tempAbilities = []; + for (let i = 0; i < mainAbs.length; ++i) { + tempAbilities.push(mainAbs[i].toLowerCase()); + } + for (let i = 0; i < chooseAbs.from.length; ++i) { + const ab = chooseAbs.from[i].toLowerCase(); + if (!tempAbilities.includes(ab)) tempAbilities.push(ab); + if (!asCollection.includes(ab.toLowerCase)) asCollection.push(ab.toLowerCase()); + } + return tempAbilities.length === 6; + } +}; + +Renderer._AbilityData = function ({asText, asTextShort, asCollection, areNegative} = {}) { + this.asText = asText || ""; + this.asTextShort = asTextShort || ""; + this.asCollection = asCollection || []; + this.areNegative = areNegative || []; +}; + +/** + * @param filters String of the form `"level=1;2|class=Warlock"` + * @param namespace Filter namespace to use + */ +Renderer.getFilterSubhashes = function (filters, namespace = null) { + let customHash = null; + + const subhashes = filters.map(f => { + const [fName, fVals, fMeta, fOpts] = f.split("=").map(s => s.trim()); + const isBoxData = fName.startsWith("fb"); + const key = isBoxData ? `${fName}${namespace ? `.${namespace}` : ""}` : `flst${namespace ? `.${namespace}` : ""}${UrlUtil.encodeForHash(fName)}`; + + let value; + // special cases for "search" and "hash" keywords + if (isBoxData) { + return { + key, + value: fVals, + preEncoded: true, + }; + } else if (fName === "search") { + // "search" as a filter name is hackily converted to a box meta option + return { + key: VeCt.FILTER_BOX_SUB_HASH_SEARCH_PREFIX, + value: UrlUtil.encodeForHash(fVals), + preEncoded: true, + }; + } else if (fName === "hash") { + customHash = fVals; + return null; + } else if (fVals.startsWith("[") && fVals.endsWith("]")) { // range + const [min, max] = fVals.substring(1, fVals.length - 1).split(";").map(it => it.trim()); + if (max == null) { // shorthand version, with only one value, becomes min _and_ max + value = [ + `min=${min}`, + `max=${min}`, + ].join(HASH_SUB_LIST_SEP); + } else { + value = [ + min ? `min=${min}` : "", + max ? `max=${max}` : "", + ].filter(Boolean).join(HASH_SUB_LIST_SEP); + } + } else if (fVals.startsWith("::") && fVals.endsWith("::")) { // options + value = fVals.substring(2, fVals.length - 2).split(";") + .map(it => it.trim()) + .map(it => { + if (it.startsWith("!")) return `${UrlUtil.encodeForHash(it.slice(1))}=${UrlUtil.mini.compress(false)}`; + return `${UrlUtil.encodeForHash(it)}=${UrlUtil.mini.compress(true)}`; + }) + .join(HASH_SUB_LIST_SEP); + } else { + value = fVals.split(";") + .map(s => s.trim()) + .filter(Boolean) + .map(s => { + if (s.startsWith("!")) return `${UrlUtil.encodeForHash(s.slice(1))}=2`; + return `${UrlUtil.encodeForHash(s)}=1`; + }) + .join(HASH_SUB_LIST_SEP); + } + + const out = [{ + key, + value, + preEncoded: true, + }]; + + if (fMeta) { + out.push({ + key: `flmt${UrlUtil.encodeForHash(fName)}`, + value: fMeta, + preEncoded: true, + }); + } + + if (fOpts) { + out.push({ + key: `flop${UrlUtil.encodeForHash(fName)}`, + value: fOpts, + preEncoded: true, + }); + } + + return out; + }).flat().filter(Boolean); + + return { + customHash, + subhashes, + }; +}; + +Renderer._cache = { + inlineStatblock: {}, + + async pRunFromEle (ele) { + const cached = Renderer._cache[ele.dataset.rdCache][ele.dataset.rdCacheId]; + await cached.pFn(ele); + }, +}; + +Renderer.utils = class { + static getBorderTr (optText = null) { + return `${optText || ""}`; + } + + static getDividerTr () { + return `
    `; + } + + static getSourceSubText (it) { + return it.sourceSub ? ` \u2014 ${it.sourceSub}` : ""; + } + + /** + * @param it Entity to render the name row for. + * @param [opts] Options object. + * @param [opts.prefix] Prefix to display before the name. + * @param [opts.suffix] Suffix to display after the name. + * @param [opts.controlRhs] Additional control(s) to display after the name. + * @param [opts.extraThClasses] Additional TH classes to include. + * @param [opts.page] The hover page for this entity. + * @param [opts.asJquery] If the element should be returned as a jQuery object. + * @param [opts.extensionData] Additional data to pass to listening extensions when the send button is clicked. + * @param [opts.isEmbeddedEntity] True if this is an embedded entity, i.e. one from a `"dataX"` entry. + */ + static getNameTr (it, opts) { + opts = opts || {}; + + let dataPart = ""; + let pageLinkPart; + if (opts.page) { + const hash = UrlUtil.URL_TO_HASH_BUILDER[opts.page](it); + dataPart = `data-page="${opts.page}" data-source="${it.source.escapeQuotes()}" data-hash="${hash.escapeQuotes()}" ${opts.extensionData != null ? `data-extension='${JSON.stringify(opts.extensionData).escapeQuotes()}` : ""}'`; + pageLinkPart = SourceUtil.getAdventureBookSourceHref(it.source, it.page); + + // Enable Rivet import for entities embedded in entries + if (opts.isEmbeddedEntity) ExtensionUtil.addEmbeddedToCache(opts.page, it.source, hash, it); + } + + const tagPartSourceStart = `<${pageLinkPart ? `a href="${Renderer.get().baseUrl}${pageLinkPart}"` : "span"}`; + const tagPartSourceEnd = ``; + + const ptBrewSourceLink = Renderer.utils._getNameTr_getPtPrereleaseBrewSourceLink({ent: it, brewUtil: PrereleaseUtil}) + || Renderer.utils._getNameTr_getPtPrereleaseBrewSourceLink({ent: it, brewUtil: BrewUtil2}); + + // Add data-page/source/hash attributes for external script use (e.g. Rivet) + const $ele = $$` + +
    +
    +

    ${opts.prefix || ""}${it._displayName || it.name}${opts.suffix || ""}

    + ${opts.controlRhs || ""} + ${!IS_VTT && ExtensionUtil.ACTIVE && opts.page ? Renderer.utils.getBtnSendToFoundryHtml() : ""} +
    +
    + ${tagPartSourceStart} class="help-subtle stats-source-abbreviation ${it.source ? `${Parser.sourceJsonToColor(it.source)}" title="${Parser.sourceJsonToFull(it.source)}${Renderer.utils.getSourceSubText(it)}` : ""}" ${Parser.sourceJsonToStyle(it.source)}>${it.source ? Parser.sourceJsonToAbv(it.source) : ""}${tagPartSourceEnd} + + ${Renderer.utils.isDisplayPage(it.page) ? ` ${tagPartSourceStart} class="rd__stats-name-page ml-1" title="Page ${it.page}">p${it.page}${tagPartSourceEnd}` : ""} + + ${ptBrewSourceLink} +
    +
    + + `; + + if (opts.asJquery) return $ele; + else return $ele[0].outerHTML; + } + + static _getNameTr_getPtPrereleaseBrewSourceLink ({ent, brewUtil}) { + if (!brewUtil.hasSourceJson(ent.source) || !brewUtil.sourceJsonToSource(ent.source)?.url) return ""; + + return ``; + } + + static getBtnSendToFoundryHtml ({isMb = true} = {}) { + return ``; + } + + static isDisplayPage (page) { return page != null && ((!isNaN(page) && page > 0) || isNaN(page)); } + + static getExcludedTr ({entity, dataProp, page, isExcluded}) { + const excludedHtml = Renderer.utils.getExcludedHtml({entity, dataProp, page, isExcluded}); + if (!excludedHtml) return ""; + return `${excludedHtml}`; + } + + static getExcludedHtml ({entity, dataProp, page, isExcluded}) { + if (isExcluded != null && !isExcluded) return ""; + if (isExcluded == null) { + if (!ExcludeUtil.isInitialised) return ""; + if (page && !UrlUtil.URL_TO_HASH_BUILDER[page]) return ""; + const hash = page ? UrlUtil.URL_TO_HASH_BUILDER[page](entity) : UrlUtil.autoEncodeHash(entity); + isExcluded = isExcluded + || dataProp === "item" ? Renderer.item.isExcluded(entity, {hash}) : ExcludeUtil.isExcluded(hash, dataProp, entity.source); + } + return isExcluded ? `
    Warning: This content has been blocklisted.
    ` : ""; + } + + static getSourceAndPageTrHtml (it, {tag, fnUnpackUid} = {}) { + const html = Renderer.utils.getSourceAndPageHtml(it, {tag, fnUnpackUid}); + return html ? `Source: ${html}` : ""; + } + + static _getAltSourceHtmlOrText (it, prop, introText, isText) { + if (!it[prop] || !it[prop].length) return ""; + + return `${introText} ${it[prop].map(as => { + if (as.entry) return (isText ? Renderer.stripTags : Renderer.get().render)(as.entry); + return `${isText ? "" : ``}${Parser.sourceJsonToAbv(as.source)}${isText ? "" : ``}${Renderer.utils.isDisplayPage(as.page) ? `, page ${as.page}` : ""}`; + }).join("; ")}`; + } + + static _getReprintedAsHtmlOrText (ent, {isText, tag, fnUnpackUid} = {}) { + if (!ent.reprintedAs) return ""; + if (!tag || !fnUnpackUid) return ""; + + const ptReprinted = ent.reprintedAs + .map(it => { + const uid = it.uid ?? it; + const tag_ = it.tag ?? tag; + + const {name, source, displayText} = fnUnpackUid(uid); + + if (isText) { + return `${Renderer.stripTags(displayText || name)} in ${Parser.sourceJsonToAbv(source)}`; + } + + const asTag = `{@${tag_} ${name}|${source}${displayText ? `|${displayText}` : ""}}`; + + return `${Renderer.get().render(asTag)} in ${Parser.sourceJsonToAbv(source)}`; + }) + .join("; "); + + return `Reprinted as ${ptReprinted}`; + } + + static getSourceAndPageHtml (it, {tag, fnUnpackUid} = {}) { return this._getSourceAndPageHtmlOrText(it, {tag, fnUnpackUid}); } + static getSourceAndPageText (it, {tag, fnUnpackUid} = {}) { return this._getSourceAndPageHtmlOrText(it, {isText: true, tag, fnUnpackUid}); } + + static _getSourceAndPageHtmlOrText (it, {isText, tag, fnUnpackUid} = {}) { + const sourceSub = Renderer.utils.getSourceSubText(it); + const baseText = `${isText ? `` : ``}${Parser.sourceJsonToAbv(it.source)}${sourceSub}${isText ? "" : ``}${Renderer.utils.isDisplayPage(it.page) ? `, page ${it.page}` : ""}`; + const reprintedAsText = Renderer.utils._getReprintedAsHtmlOrText(it, {isText, tag, fnUnpackUid}); + const addSourceText = Renderer.utils._getAltSourceHtmlOrText(it, "additionalSources", "Additional information from", isText); + const otherSourceText = Renderer.utils._getAltSourceHtmlOrText(it, "otherSources", "Also found in", isText); + const externalSourceText = Renderer.utils._getAltSourceHtmlOrText(it, "externalSources", "External sources:", isText); + + const srdText = it.srd ? `${isText ? "" : `the `}SRD${isText ? "" : ``}${typeof it.srd === "string" ? ` (as "${it.srd}")` : ""}` : ""; + const basicRulesText = it.basicRules ? `the Basic Rules${typeof it.basicRules === "string" ? ` (as "${it.basicRules}")` : ""}` : ""; + const srdAndBasicRulesText = (srdText || basicRulesText) ? `Available in ${[srdText, basicRulesText].filter(it => it).join(" and ")}` : ""; + + return `${[baseText, addSourceText, reprintedAsText, otherSourceText, srdAndBasicRulesText, externalSourceText].filter(it => it).join(". ")}${baseText && (addSourceText || otherSourceText || srdAndBasicRulesText || externalSourceText) ? "." : ""}`; + } + + static async _pHandleNameClick (ele) { + await MiscUtil.pCopyTextToClipboard($(ele).text()); + JqueryUtil.showCopiedEffect($(ele)); + } + + static getPageTr (it, {tag, fnUnpackUid} = {}) { + return `${Renderer.utils.getSourceAndPageTrHtml(it, {tag, fnUnpackUid})}`; + } + + static getAbilityRollerEntry (statblock, ability) { + if (statblock[ability] == null) return "\u2014"; + return `{@ability ${ability} ${statblock[ability]}}`; + } + + static getAbilityRoller (statblock, ability) { + return Renderer.get().render(Renderer.utils.getAbilityRollerEntry(statblock, ability)); + } + + static getEmbeddedDataHeader (name, style, {isCollapsed = false} = {}) { + return ` + `; + } + + static getEmbeddedDataFooter () { + return `
    ${name}[${isCollapsed ? "+" : "\u2013"}]
    `; + } + + static TabButton = function ({label, fnChange, fnPopulate, isVisible}) { + this.label = label; + this.fnChange = fnChange; + this.fnPopulate = fnPopulate; + this.isVisible = isVisible; + }; + + static _tabs = {}; + static _curTab = null; + static _tabsPreferredLabel = null; + static bindTabButtons ({tabButtons, tabLabelReference, $wrpTabs, $pgContent}) { + Renderer.utils._tabs = {}; + Renderer.utils._curTab = null; + + $wrpTabs.find(`.stat-tab-gen`).remove(); + + tabButtons.forEach((tb, i) => { + tb.ix = i; + + tb.$t = $(``) + .click(() => tb.fnActivateTab({isUserInput: true})); + + tb.fnActivateTab = ({isUserInput = false} = {}) => { + const curTab = Renderer.utils._curTab; + const tabs = Renderer.utils._tabs; + + if (!curTab || curTab.label !== tb.label) { + if (curTab) curTab.$t.removeClass(`ui-tab__btn-tab-head--active`); + Renderer.utils._curTab = tb; + tb.$t.addClass(`ui-tab__btn-tab-head--active`); + if (curTab) tabs[curTab.label].$content = $pgContent.children().detach(); + + tabs[tb.label] = tb; + if (!tabs[tb.label].$content && tb.fnPopulate) tb.fnPopulate(); + else $pgContent.append(tabs[tb.label].$content); + if (tb.fnChange) tb.fnChange(); + } + + // If the user clicked a tab, save it as their chosen tab + if (isUserInput) Renderer.utils._tabsPreferredLabel = tb.label; + }; + }); + + // Avoid displaying a tab button for single tabs + if (tabButtons.length !== 1) tabButtons.slice().reverse().forEach(tb => $wrpTabs.prepend(tb.$t)); + + // If there was no previous selection, select the first tab + if (!Renderer.utils._tabsPreferredLabel) return tabButtons[0].fnActivateTab(); + + // If the exact tab exist, select it + const tabButton = tabButtons.find(tb => tb.label === Renderer.utils._tabsPreferredLabel); + if (tabButton) return tabButton.fnActivateTab(); + + // If the user's preferred tab is not present, find the closest tab, and activate it instead. + // Always prefer later tabs. + const ixDesired = tabLabelReference.indexOf(Renderer.utils._tabsPreferredLabel); + if (!~ixDesired) return tabButtons[0].fnActivateTab(); // Should never occur + + const ixsAvailableMetas = tabButtons + .map(tb => { + const ixMapped = tabLabelReference.indexOf(tb.label); + if (!~ixMapped) return null; + return { + ixMapped, + label: tb.label, + }; + }) + .filter(Boolean); + if (!ixsAvailableMetas.length) return tabButtons[0].fnActivateTab(); // Should never occur + + // Find a later tab and activate it, if possible + const ixMetaHigher = ixsAvailableMetas.find(({ixMapped}) => ixMapped > ixDesired); + if (ixMetaHigher != null) return (tabButtons.find(it => it.label === ixMetaHigher.label) || tabButtons[0]).fnActivateTab(); + + // Otherwise, click the highest tab + const ixMetaMax = ixsAvailableMetas.last(); + (tabButtons.find(it => it.label === ixMetaMax.label) || tabButtons[0]).fnActivateTab(); + } + + static _pronounceButtonsBound = false; + static bindPronounceButtons () { + if (Renderer.utils._pronounceButtonsBound) return; + Renderer.utils._pronounceButtonsBound = true; + $(`body`).on("click", ".btn-name-pronounce", function () { + const audio = $(this).find(`.name-pronounce`)[0]; + audio.currentTime = 0; + audio.play(); + }); + } + + static async pHasFluffText (entity, prop) { + return entity.hasFluff || ((await Renderer.utils.pGetPredefinedFluff(entity, prop))?.entries?.length || 0) > 0; + } + + static async pHasFluffImages (entity, prop) { + return entity.hasFluffImages || (((await Renderer.utils.pGetPredefinedFluff(entity, prop))?.images?.length || 0) > 0); + } + + /** + * @param entry Data entry to search for fluff on, e.g. a monster + * @param prop The fluff index reference prop, e.g. `"monsterFluff"` + */ + static async pGetPredefinedFluff (entry, prop) { + if (!entry.fluff) return null; + + const mappedProp = `_${prop}`; + const mappedPropAppend = `_append${prop.uppercaseFirst()}`; + const fluff = {}; + + const assignPropsIfExist = (fromObj, ...props) => { + props.forEach(prop => { + if (fromObj[prop]) fluff[prop] = fromObj[prop]; + }); + }; + + assignPropsIfExist(entry.fluff, "name", "type", "entries", "images"); + + if (entry.fluff[mappedProp]) { + const fromList = [ + ...((await PrereleaseUtil.pGetBrewProcessed())[prop] || []), + ...((await BrewUtil2.pGetBrewProcessed())[prop] || []), + ] + .find(it => + it.name === entry.fluff[mappedProp].name + && it.source === entry.fluff[mappedProp].source, + ); + if (fromList) { + assignPropsIfExist(fromList, "name", "type", "entries", "images"); + } + } + + if (entry.fluff[mappedPropAppend]) { + const fromList = [ + ...((await PrereleaseUtil.pGetBrewProcessed())[prop] || []), + ...((await BrewUtil2.pGetBrewProcessed())[prop] || []), + ] + .find(it => + it.name === entry.fluff[mappedPropAppend].name + && it.source === entry.fluff[mappedPropAppend].source, + ); + if (fromList) { + if (fromList.entries) { + fluff.entries = MiscUtil.copyFast(fluff.entries || []); + fluff.entries.push(...MiscUtil.copyFast(fromList.entries)); + } + if (fromList.images) { + fluff.images = MiscUtil.copyFast(fluff.images || []); + fluff.images.push(...MiscUtil.copyFast(fromList.images)); + } + } + } + + return fluff; + } + + static async pGetFluff ({entity, pFnPostProcess, fnGetFluffData, fluffUrl, fluffBaseUrl, fluffProp} = {}) { + let predefinedFluff = await Renderer.utils.pGetPredefinedFluff(entity, fluffProp); + if (predefinedFluff) { + if (pFnPostProcess) predefinedFluff = await pFnPostProcess(predefinedFluff); + return predefinedFluff; + } + if (!fnGetFluffData && !fluffBaseUrl && !fluffUrl) return null; + + const fluffIndex = fluffBaseUrl ? await DataUtil.loadJSON(`${Renderer.get().baseUrl}${fluffBaseUrl}fluff-index.json`) : null; + if (fluffIndex && !fluffIndex[entity.source]) return null; + + const data = fnGetFluffData ? await fnGetFluffData() : fluffIndex && fluffIndex[entity.source] + ? await DataUtil.loadJSON(`${Renderer.get().baseUrl}${fluffBaseUrl}${fluffIndex[entity.source]}`) + : await DataUtil.loadJSON(`${Renderer.get().baseUrl}${fluffUrl}`); + if (!data) return null; + + let fluff = (data[fluffProp] || []).find(it => it.name === entity.name && it.source === entity.source); + if (!fluff && entity._versionBase_name && entity._versionBase_source) fluff = (data[fluffProp] || []).find(it => it.name === entity._versionBase_name && it.source === entity._versionBase_source); + if (!fluff) return null; + + // Avoid modifying the original object + if (pFnPostProcess) fluff = await pFnPostProcess(fluff); + return fluff; + } + + static _TITLE_SKIP_TYPES = new Set(["entries", "section"]); + /** + * @param isImageTab True if this is the "Images" tab, false otherwise + * @param $content The statblock wrapper + * @param entity Entity to build tab for (e.g. a monster; an item) + * @param pFnGetFluff Function which gets the entity's fluff. + * @param $headerControls + */ + static async pBuildFluffTab ({isImageTab, $content, entity, $headerControls, pFnGetFluff} = {}) { + $content.append(Renderer.utils.getBorderTr()); + $content.append(Renderer.utils.getNameTr(entity, {controlRhs: $headerControls, asJquery: true})); + const $td = $(``); + $$`${$td}`.appendTo($content); + $content.append(Renderer.utils.getBorderTr()); + + const fluff = MiscUtil.copyFast((await pFnGetFluff(entity)) || {}); + fluff.entries = fluff.entries || [Renderer.utils.HTML_NO_INFO]; + fluff.images = fluff.images || [Renderer.utils.HTML_NO_IMAGES]; + + $td.fastSetHtml(Renderer.utils.getFluffTabContent({entity, fluff, isImageTab})); + } + + static getFluffTabContent ({entity, fluff, isImageTab = false}) { + Renderer.get().setFirstSection(true); + return (fluff[isImageTab ? "images" : "entries"] || []).map((ent, i) => { + if (isImageTab) return Renderer.get().render(ent); + + // If the first entry has a name, and it matches the name of the statblock, remove it to avoid having two + // of the same title stacked on top of each other. + if (i === 0 && ent.name && entity.name && (Renderer.utils._TITLE_SKIP_TYPES).has(ent.type)) { + const entryLowName = ent.name.toLowerCase().trim(); + const entityLowName = entity.name.toLowerCase().trim(); + + if (entryLowName.includes(entityLowName) || entityLowName.includes(entryLowName)) { + const cpy = MiscUtil.copyFast(ent); + delete cpy.name; + return Renderer.get().render(cpy); + } else return Renderer.get().render(ent); + } else { + if (typeof ent === "string") return `

    ${Renderer.get().render(ent)}

    `; + else return Renderer.get().render(ent); + } + }).join(""); + } + + static HTML_NO_INFO = "No information available."; + static HTML_NO_IMAGES = "No images available."; + + static prerequisite = class { + static _WEIGHTS = [ + "level", + "pact", + "patron", + "spell", + "race", + "alignment", + "ability", + "proficiency", + "spellcasting", + "spellcasting2020", + "spellcastingFeature", + "spellcastingPrepared", + "psionics", + "feature", + "feat", + "background", + "item", + "itemType", + "itemProperty", + "campaign", + "group", + "other", + "otherSummary", + undefined, + ] + .mergeMap((k, i) => ({[k]: i})); + + static _getShortClassName (className) { + // remove all the vowels except the first + const ixFirstVowel = /[aeiou]/.exec(className).index; + const start = className.slice(0, ixFirstVowel + 1); + let end = className.slice(ixFirstVowel + 1); + end = end.replace(/[aeiou]/g, ""); + return `${start}${end}`.toTitleCase(); + } + + static getHtml (prerequisites, {isListMode = false, blocklistKeys = new Set(), isTextOnly = false, isSkipPrefix = false} = {}) { + if (!prerequisites?.length) return isListMode ? "\u2014" : ""; + + const prereqsShared = prerequisites.length === 1 + ? {} + : Object.entries( + prerequisites + .slice(1) + .reduce((a, b) => CollectionUtil.objectIntersect(a, b), prerequisites[0]), + ) + .filter(([k, v]) => prerequisites.every(pre => CollectionUtil.deepEquals(pre[k], v))) + .mergeMap(([k, v]) => ({[k]: v})); + + const shared = Object.keys(prereqsShared).length + ? this.getHtml([prereqsShared], {isListMode, blocklistKeys, isTextOnly, isSkipPrefix: true}) + : null; + + let cntPrerequisites = 0; + let hasNote = false; + const listOfChoices = prerequisites + .map(pr => { + // Never include notes in list mode + const ptNote = !isListMode && pr.note ? Renderer.get().render(pr.note) : null; + if (ptNote) { + hasNote = true; + } + + const prereqsToJoin = Object.entries(pr) + .filter(([k]) => !prereqsShared[k]) + .sort(([kA], [kB]) => this._WEIGHTS[kA] - this._WEIGHTS[kB]) + .map(([k, v]) => { + if (k === "note" || blocklistKeys.has(k)) return false; + + cntPrerequisites += 1; + + switch (k) { + case "level": return this._getHtml_level({v, isListMode, isTextOnly}); + case "pact": return this._getHtml_pact({v, isListMode, isTextOnly}); + case "patron": return this._getHtml_patron({v, isListMode, isTextOnly}); + case "spell": return this._getHtml_spell({v, isListMode, isTextOnly}); + case "feat": return this._getHtml_feat({v, isListMode, isTextOnly}); + case "feature": return this._getHtml_feature({v, isListMode, isTextOnly}); + case "item": return this._getHtml_item({v, isListMode, isTextOnly}); + case "itemType": return this._getHtml_itemType({v, isListMode, isTextOnly}); + case "itemProperty": return this._getHtml_itemProperty({v, isListMode, isTextOnly}); + case "otherSummary": return this._getHtml_otherSummary({v, isListMode, isTextOnly}); + case "other": return this._getHtml_other({v, isListMode, isTextOnly}); + case "race": return this._getHtml_race({v, isListMode, isTextOnly}); + case "background": return this._getHtml_background({v, isListMode, isTextOnly}); + case "ability": return this._getHtml_ability({v, isListMode, isTextOnly}); + case "proficiency": return this._getHtml_proficiency({v, isListMode, isTextOnly}); + case "spellcasting": return this._getHtml_spellcasting({v, isListMode, isTextOnly}); + case "spellcasting2020": return this._getHtml_spellcasting2020({v, isListMode, isTextOnly}); + case "spellcastingFeature": return this._getHtml_spellcastingFeature({v, isListMode, isTextOnly}); + case "spellcastingPrepared": return this._getHtml_spellcastingPrepared({v, isListMode, isTextOnly}); + case "psionics": return this._getHtml_psionics({v, isListMode, isTextOnly}); + case "alignment": return this._getHtml_alignment({v, isListMode, isTextOnly}); + case "campaign": return this._getHtml_campaign({v, isListMode, isTextOnly}); + case "group": return this._getHtml_group({v, isListMode, isTextOnly}); + default: throw new Error(`Unhandled key: ${k}`); + } + }) + .filter(Boolean); + + const ptPrereqs = prereqsToJoin + .join(prereqsToJoin.some(it => / or /.test(it)) ? "; " : ", "); + + return [ptPrereqs, ptNote] + .filter(Boolean) + .join(". "); + }) + .filter(Boolean); + + if (!listOfChoices.length && !shared) return isListMode ? "\u2014" : ""; + if (isListMode) return [shared, listOfChoices.join("/")].filter(Boolean).join(" + "); + + const sharedSuffix = MiscUtil.findCommonSuffix(listOfChoices, {isRespectWordBoundaries: true}); + const listOfChoicesTrimmed = sharedSuffix + ? listOfChoices.map(it => it.slice(0, -sharedSuffix.length)) + : listOfChoices; + + const joinedChoices = ( + hasNote + ? listOfChoicesTrimmed.join(" Or, ") + : listOfChoicesTrimmed.joinConjunct(listOfChoicesTrimmed.some(it => / or /.test(it)) ? "; " : ", ", " or ") + ) + sharedSuffix; + return `${isSkipPrefix ? "" : `Prerequisite${cntPrerequisites === 1 ? "" : "s"}: `}${[shared, joinedChoices].filter(Boolean).join(", plus ")}`; + } + + static _getHtml_level ({v, isListMode}) { + // a generic level requirement + if (typeof v === "number") { + if (isListMode) return `Lvl ${v}`; + else return `${Parser.getOrdinalForm(v)} level`; + } else if (!v.class && !v.subclass) { + if (isListMode) return `Lvl ${v.level}`; + else return `${Parser.getOrdinalForm(v.level)} level`; + } + + const isLevelVisible = v.level !== 1; // Hide the "implicit" 1st level. + const isSubclassVisible = v.subclass && v.subclass.visible; + const isClassVisible = v.class && (v.class.visible || isSubclassVisible); // force the class name to be displayed if there's a subclass being displayed + if (isListMode) { + const shortNameRaw = isClassVisible ? this._getShortClassName(v.class.name) : null; + return `${isClassVisible ? `${shortNameRaw.slice(0, 4)}${isSubclassVisible ? "*" : "."}` : ""}${isLevelVisible ? ` Lvl ${v.level}` : ""}`; + } else { + let classPart = ""; + if (isClassVisible && isSubclassVisible) classPart = ` ${v.class.name} (${v.subclass.name})`; + else if (isClassVisible) classPart = ` ${v.class.name}`; + else if (isSubclassVisible) classPart = ` <remember to insert class name here> (${v.subclass.name})`; // :^) + return `${isLevelVisible ? `${Parser.getOrdinalForm(v.level)} level` : ""}${isClassVisible ? ` ${classPart}` : ""}`; + } + } + + static _getHtml_pact ({v, isListMode}) { + return Parser.prereqPactToFull(v); + } + + static _getHtml_patron ({v, isListMode}) { + return isListMode ? `${Parser.prereqPatronToShort(v)} patron` : `${v} patron`; + } + + static _getHtml_spell ({v, isListMode, isTextOnly}) { + return isListMode + ? v.map(sp => { + if (typeof sp === "string") return sp.split("#")[0].split("|")[0].toTitleCase(); + return sp.entrySummary || sp.entry; + }) + .join("/") + : v.map(sp => { + if (typeof sp === "string") return Parser.prereqSpellToFull(sp, {isTextOnly}); + return isTextOnly ? Renderer.stripTags(sp.entry) : Renderer.get().render(`{@filter ${sp.entry}|spells|${sp.choose}}`); + }) + .joinConjunct(", ", " or "); + } + + static _getHtml_feat ({v, isListMode, isTextOnly}) { + return isListMode + ? v.map(x => x.split("|")[0].toTitleCase()).join("/") + : v.map(it => (isTextOnly ? Renderer.stripTags.bind(Renderer) : Renderer.get().render.bind(Renderer.get()))(`{@feat ${it}} feat`)).joinConjunct(", ", " or "); + } + + static _getHtml_feature ({v, isListMode, isTextOnly}) { + return isListMode + ? v.map(x => Renderer.stripTags(x).toTitleCase()).join("/") + : v.map(it => isTextOnly ? Renderer.stripTags(it) : Renderer.get().render(it)).joinConjunct(", ", " or "); + } + + static _getHtml_item ({v, isListMode}) { + return isListMode ? v.map(x => x.toTitleCase()).join("/") : v.joinConjunct(", ", " or "); + } + + static _getHtml_itemType ({v, isListMode}) { + return isListMode + ? v + .map(it => Renderer.item.getType(it)) + .map(it => it?.abbreviation) + .join("+") + : v + .map(it => Renderer.item.getType(it)) + .map(it => it?.name?.toTitleCase()) + .joinConjunct(", ", " and "); + } + + static _getHtml_itemProperty ({v, isListMode}) { + if (v == null) return isListMode ? "No Prop." : "No Other Properties"; + + return isListMode + ? v + .map(it => Renderer.item.getProperty(it)) + .map(it => it?.abbreviation) + .join("+") + : ( + `${v + .map(it => Renderer.item.getProperty(it)) + .map(it => it?.name?.toTitleCase()) + .joinConjunct(", ", " and ") + } Property` + ); + } + + static _getHtml_otherSummary ({v, isListMode, isTextOnly}) { + return isListMode + ? (v.entrySummary || Renderer.stripTags(v.entry)) + : (isTextOnly ? Renderer.stripTags(v.entry) : Renderer.get().render(v.entry)); + } + + static _getHtml_other ({v, isListMode, isTextOnly}) { + return isListMode ? "Special" : (isTextOnly ? Renderer.stripTags(v) : Renderer.get().render(v)); + } + + static _getHtml_race ({v, isListMode, isTextOnly}) { + const parts = v.map((it, i) => { + if (isListMode) { + return `${it.name.toTitleCase()}${it.subrace != null ? ` (${it.subrace})` : ""}`; + } else { + const raceName = it.displayEntry ? (isTextOnly ? Renderer.stripTags(it.displayEntry) : Renderer.get().render(it.displayEntry)) : i === 0 ? it.name.toTitleCase() : it.name; + return `${raceName}${it.subrace != null ? ` (${it.subrace})` : ""}`; + } + }); + return isListMode ? parts.join("/") : parts.joinConjunct(", ", " or "); + } + + static _getHtml_background ({v, isListMode, isTextOnly}) { + const parts = v.map((it, i) => { + if (isListMode) { + return `${it.name.toTitleCase()}`; + } else { + return it.displayEntry ? (isTextOnly ? Renderer.stripTags(it.displayEntry) : Renderer.get().render(it.displayEntry)) : i === 0 ? it.name.toTitleCase() : it.name; + } + }); + return isListMode ? parts.join("/") : parts.joinConjunct(", ", " or "); + } + + static _getHtml_ability ({v, isListMode, isTextOnly}) { + // `v` is an array or objects with str/dex/... properties; array is "OR"'d togther, object is "AND"'d together + + let hadMultipleInner = false; + let hadMultiMultipleInner = false; + let allValuesEqual = null; + + outer: for (const abMeta of v) { + for (const req of Object.values(abMeta)) { + if (allValuesEqual == null) allValuesEqual = req; + else { + if (req !== allValuesEqual) { + allValuesEqual = null; + break outer; + } + } + } + } + + const abilityOptions = v.map(abMeta => { + if (allValuesEqual) { + const abList = Object.keys(abMeta); + hadMultipleInner = hadMultipleInner || abList.length > 1; + return isListMode ? abList.map(ab => ab.uppercaseFirst()).join(", ") : abList.map(ab => Parser.attAbvToFull(ab)).joinConjunct(", ", " and "); + } else { + const groups = {}; + + Object.entries(abMeta).forEach(([ab, req]) => { + (groups[req] = groups[req] || []).push(ab); + }); + + let isMulti = false; + const byScore = Object.entries(groups) + .sort(([reqA], [reqB]) => SortUtil.ascSort(Number(reqB), Number(reqA))) + .map(([req, abs]) => { + hadMultipleInner = hadMultipleInner || abs.length > 1; + if (abs.length > 1) hadMultiMultipleInner = isMulti = true; + + abs = abs.sort(SortUtil.ascSortAtts); + return isListMode + ? `${abs.map(ab => ab.uppercaseFirst()).join(", ")} ${req}+` + : `${abs.map(ab => Parser.attAbvToFull(ab)).joinConjunct(", ", " and ")} ${req} or higher`; + }); + + return isListMode + ? `${isMulti || byScore.length > 1 ? "(" : ""}${byScore.join(" & ")}${isMulti || byScore.length > 1 ? ")" : ""}` + : isMulti ? byScore.joinConjunct("; ", " and ") : byScore.joinConjunct(", ", " and "); + } + }); + + // if all values were equal, add the "X+" text at the end, as the options render doesn't include it + if (isListMode) { + return `${abilityOptions.join("/")}${allValuesEqual != null ? ` ${allValuesEqual}+` : ""}`; + } else { + const isComplex = hadMultiMultipleInner || hadMultipleInner || allValuesEqual == null; + const joined = abilityOptions.joinConjunct( + hadMultiMultipleInner ? " - " : hadMultipleInner ? "; " : ", ", + isComplex ? (isTextOnly ? ` /or/ ` : ` or `) : " or ", + ); + return `${joined}${allValuesEqual != null ? ` ${allValuesEqual} or higher` : ""}`; + } + } + + static _getHtml_proficiency ({v, isListMode}) { + const parts = v.map(obj => { + return Object.entries(obj).map(([profType, prof]) => { + switch (profType) { + case "armor": { + return isListMode ? `Prof ${Parser.armorFullToAbv(prof)} armor` : `Proficiency with ${prof} armor`; + } + case "weapon": { + return isListMode ? `Prof ${Parser.weaponFullToAbv(prof)} weapon` : `Proficiency with a ${prof} weapon`; + } + case "weaponGroup": { + return isListMode ? `Prof ${Parser.weaponFullToAbv(prof)} weapons` : `${prof.toTitleCase()} Proficiency`; + } + default: throw new Error(`Unhandled proficiency type: "${profType}"`); + } + }); + }); + return isListMode ? parts.join("/") : parts.joinConjunct(", ", " or "); + } + + static _getHtml_spellcasting ({v, isListMode}) { + return isListMode ? "Spellcasting" : "The ability to cast at least one spell"; + } + + static _getHtml_spellcasting2020 ({v, isListMode}) { + return isListMode ? "Spellcasting" : "Spellcasting or Pact Magic feature"; + } + + static _getHtml_spellcastingFeature ({v, isListMode}) { + return isListMode ? "Spellcasting" : "Spellcasting Feature"; + } + + static _getHtml_spellcastingPrepared ({v, isListMode}) { + return isListMode ? "Spellcasting" : "Spellcasting feature from a class that prepares spells"; + } + + static _getHtml_psionics ({v, isListMode, isTextOnly}) { + return isListMode + ? "Psionics" + : (isTextOnly ? Renderer.stripTags : Renderer.get().render.bind(Renderer.get()))("Psionic Talent feature or Wild Talent feat"); + } + + static _getHtml_alignment ({v, isListMode}) { + return isListMode + ? Parser.alignmentListToFull(v) + .replace(/\bany\b/gi, "").trim() + .replace(/\balignment\b/gi, "align").trim() + .toTitleCase() + : Parser.alignmentListToFull(v); + } + + static _getHtml_campaign ({v, isListMode}) { + return isListMode + ? v.join("/") + : `${v.joinConjunct(", ", " or ")} Campaign`; + } + + static _getHtml_group ({v, isListMode}) { + return isListMode + ? v.map(it => it.toTitleCase()).join("/") + : `${v.map(it => it.toTitleCase()).joinConjunct(", ", " or ")} Group`; + } + }; + + static getRepeatableEntry (ent) { + if (!ent.repeatable) return null; + return `{@b Repeatable:} ${ent.repeatableNote || (ent.repeatable ? "Yes" : "No")}`; + } + + static getRepeatableHtml (ent, {isListMode = false} = {}) { + const entryRepeatable = Renderer.utils.getRepeatableEntry(ent); + if (entryRepeatable == null) return isListMode ? "\u2014" : ""; + return Renderer.get().render(entryRepeatable); + } + + static getRenderedSize (size) { + return [...(size ? [size].flat() : [])] + .sort(SortUtil.ascSortSize) + .map(sz => Parser.sizeAbvToFull(sz)) + .joinConjunct(", ", " or "); + } + + static _FN_TAG_SENSES = null; + static _SENSE_TAG_METAS = null; + static getSensesEntry (senses) { + if (typeof senses === "string") senses = [senses]; // handle legacy format + + if (!Renderer.utils._FN_TAG_SENSES) { + Renderer.utils._SENSE_TAG_METAS = [ + ...MiscUtil.copyFast(Parser.SENSES), + ...(PrereleaseUtil.getBrewProcessedFromCache("sense") || []), + ...(BrewUtil2.getBrewProcessedFromCache("sense") || []), + ]; + const seenNames = new Set(); + Renderer.utils._SENSE_TAG_METAS + .filter(it => { + if (seenNames.has(it.name.toLowerCase())) return false; + seenNames.add(it.name.toLowerCase()); + return true; + }) + .forEach(it => it._re = new RegExp(`\\b(?${it.name.escapeRegexp()})\\b`, "gi")); + Renderer.utils._FN_TAG_SENSES = str => { + Renderer.utils._SENSE_TAG_METAS + .forEach(({name, source, _re}) => str = str.replace(_re, (...m) => `{@sense ${m.last().sense}|${source}}`)); + return str; + }; + } + + return senses + .map(str => { + const tagSplit = Renderer.splitByTags(str); + str = ""; + const len = tagSplit.length; + for (let i = 0; i < len; ++i) { + const s = tagSplit[i]; + + if (!s) continue; + + if (s.startsWith("{@")) { + str += s; + continue; + } + + str += Renderer.utils._FN_TAG_SENSES(s); + } + return str; + }) + .join(", ") + .replace(/(^| |\()(blind|blinded)(\)| |$)/gi, (...m) => `${m[1]}{@condition blinded||${m[2]}}${m[3]}`); + } + + static getRenderedSenses (senses, isPlainText) { + const sensesEntry = Renderer.utils.getSensesEntry(senses); + if (isPlainText) return Renderer.stripTags(sensesEntry); + return Renderer.get().render(sensesEntry); + } + + static getEntryMediaUrl (entry, prop, mediaDir) { + if (!entry[prop]) return ""; + + let href = ""; + if (entry[prop].type === "internal") { + href = UrlUtil.link(Renderer.get().getMediaUrl(mediaDir, entry[prop].path)); + } else if (entry[prop].type === "external") { + href = entry[prop].url; + } + return href; + } + + static getTagEntry (tag, text) { + switch (tag) { + case "@dice": + case "@autodice": + case "@damage": + case "@hit": + case "@d20": + case "@chance": + case "@recharge": { + const fauxEntry = { + type: "dice", + rollable: true, + }; + const [rollText, displayText, name, ...others] = Renderer.splitTagByPipe(text); + if (displayText) fauxEntry.displayText = displayText; + + if ((!fauxEntry.displayText && (rollText || "").includes("summonSpellLevel")) || (fauxEntry.displayText && fauxEntry.displayText.includes("summonSpellLevel"))) fauxEntry.displayText = (fauxEntry.displayText || rollText || "").replace(/summonSpellLevel/g, "the spell's level"); + + if ((!fauxEntry.displayText && (rollText || "").includes("summonClassLevel")) || (fauxEntry.displayText && fauxEntry.displayText.includes("summonClassLevel"))) fauxEntry.displayText = (fauxEntry.displayText || rollText || "").replace(/summonClassLevel/g, "your class level"); + + if (name) fauxEntry.name = name; + + switch (tag) { + case "@dice": + case "@autodice": + case "@damage": { + // format: {@dice 1d2 + 3 + 4d5 - 6} + fauxEntry.toRoll = rollText; + + if (!fauxEntry.displayText && (rollText || "").includes(";")) fauxEntry.displayText = rollText.replace(/;/g, "/"); + if ((!fauxEntry.displayText && (rollText || "").includes("#$")) || (fauxEntry.displayText && fauxEntry.displayText.includes("#$"))) fauxEntry.displayText = (fauxEntry.displayText || rollText).replace(/#\$prompt_number[^$]*\$#/g, "(n)"); + fauxEntry.displayText = fauxEntry.displayText || fauxEntry.toRoll; + + if (tag === "@damage") fauxEntry.subType = "damage"; + if (tag === "@autodice") fauxEntry.autoRoll = true; + + return fauxEntry; + } + case "@d20": + case "@hit": { + // format: {@hit +1} or {@hit -2} + let mod; + if (!isNaN(rollText)) { + const n = Number(rollText); + mod = `${n >= 0 ? "+" : ""}${n}`; + } else mod = /^\s+[-+]/.test(rollText) ? rollText : `+${rollText}`; + fauxEntry.displayText = fauxEntry.displayText || mod; + fauxEntry.toRoll = `1d20${mod}`; + fauxEntry.subType = "d20"; + fauxEntry.d20mod = mod; + if (tag === "@hit") fauxEntry.context = {type: "hit"}; + return fauxEntry; + } + case "@chance": { + // format: {@chance 25|display text|rollbox rollee name|success text|failure text} + const [textSuccess, textFailure] = others; + fauxEntry.toRoll = `1d100`; + fauxEntry.successThresh = Number(rollText); + fauxEntry.chanceSuccessText = textSuccess; + fauxEntry.chanceFailureText = textFailure; + return fauxEntry; + } + case "@recharge": { + // format: {@recharge 4|flags} + const flags = displayText ? displayText.split("") : null; // "m" for "minimal" = no brackets + fauxEntry.toRoll = "1d6"; + const asNum = Number(rollText || 6); + fauxEntry.successThresh = 7 - asNum; + fauxEntry.successMax = 6; + fauxEntry.displayText = `${asNum}${asNum < 6 ? `\u20136` : ""}`; + fauxEntry.chanceSuccessText = "Recharged!"; + fauxEntry.chanceFailureText = "Did not recharge"; + fauxEntry.isColorSuccessFail = true; + return fauxEntry; + } + } + + return fauxEntry; + } + + case "@ability": // format: {@ability str 20} or {@ability str 20|Display Text} or {@ability str 20|Display Text|Roll Name Text} + case "@savingThrow": { // format: {@savingThrow str 5} or {@savingThrow str 5|Display Text} or {@savingThrow str 5|Display Text|Roll Name Text} + const fauxEntry = { + type: "dice", + rollable: true, + subType: "d20", + context: {type: tag === "@ability" ? "abilityCheck" : "savingThrow"}, + }; + + const [abilAndScoreOrScore, displayText, name, ...others] = Renderer.splitTagByPipe(text); + + let [abil, ...rawScoreOrModParts] = abilAndScoreOrScore.split(" ").map(it => it.trim()).filter(Boolean); + abil = abil.toLowerCase(); + + fauxEntry.context.ability = abil; + + if (name) fauxEntry.name = name; + else { + if (tag === "@ability") fauxEntry.name = Parser.attAbvToFull(abil); + else if (tag === "@savingThrow") fauxEntry.name = `${Parser.attAbvToFull(abil)} save`; + } + + const rawScoreOrMod = rawScoreOrModParts.join(" "); + // Saving throws can have e.g. `+ PB` + if (isNaN(rawScoreOrMod) && tag === "@savingThrow") { + if (displayText) fauxEntry.displayText = displayText; + else fauxEntry.displayText = rawScoreOrMod; + + fauxEntry.toRoll = `1d20${rawScoreOrMod}`; + fauxEntry.d20mod = rawScoreOrMod; + } else { + const scoreOrMod = Number(rawScoreOrMod) || 0; + const mod = (tag === "@ability" ? Parser.getAbilityModifier : UiUtil.intToBonus)(scoreOrMod); + + if (displayText) fauxEntry.displayText = displayText; + else { + if (tag === "@ability") fauxEntry.displayText = `${scoreOrMod} (${mod})`; + else fauxEntry.displayText = mod; + } + + fauxEntry.toRoll = `1d20${mod}`; + fauxEntry.d20mod = mod; + } + + return fauxEntry; + } + + // format: {@skillCheck animal_handling 5} or {@skillCheck animal_handling 5|Display Text} + // or {@skillCheck animal_handling 5|Display Text|Roll Name Text} + case "@skillCheck": { + const fauxEntry = { + type: "dice", + rollable: true, + subType: "d20", + context: {type: "skillCheck"}, + }; + + const [skillAndMod, displayText, name, ...others] = Renderer.splitTagByPipe(text); + + const parts = skillAndMod.split(" ").map(it => it.trim()).filter(Boolean); + const namePart = parts.shift(); + const bonusPart = parts.join(" "); + const skill = namePart.replace(/_/g, " "); + + let mod = bonusPart; + if (!isNaN(bonusPart)) mod = UiUtil.intToBonus(Number(bonusPart) || 0); + else if (bonusPart.startsWith("#$")) mod = `+${bonusPart}`; + + fauxEntry.context.skill = skill; + fauxEntry.displayText = displayText || mod; + + if (name) fauxEntry.name = name; + else fauxEntry.name = skill.toTitleCase(); + + fauxEntry.toRoll = `1d20${mod}`; + fauxEntry.d20mod = mod; + + return fauxEntry; + } + + // format: {@coinflip} or {@coinflip display text|rollbox rollee name|success text|failure text} + case "@coinflip": { + const [displayText, name, textSuccess, textFailure] = Renderer.splitTagByPipe(text); + + const fauxEntry = { + type: "dice", + toRoll: "1d2", + successThresh: 1, + successMax: 2, + displayText: displayText || "flip a coin", + chanceSuccessText: textSuccess || `Heads`, + chanceFailureText: textFailure || `Tails`, + isColorSuccessFail: !textSuccess && !textFailure, + rollable: true, + }; + + return fauxEntry; + } + + default: throw new Error(`Unhandled tag "${tag}"`); + } + } + + static getTagMeta (tag, text) { + switch (tag) { + case "@deity": { + let [name, pantheon, source, displayText, ...others] = Renderer.splitTagByPipe(text); + pantheon = pantheon || "forgotten realms"; + source = source || Parser.getTagSource(tag, source); + const hash = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_DEITIES]({name, pantheon, source}); + + return { + name, + displayText, + others, + + page: UrlUtil.PG_DEITIES, + source, + hash, + + hashPreEncoded: true, + }; + } + + case "@card": { + const unpacked = DataUtil.deck.unpackUidCard(text); + const {name, set, source, displayText} = unpacked; + const hash = UrlUtil.URL_TO_HASH_BUILDER["card"]({name, set, source}); + + return { + name, + displayText, + + isFauxPage: true, + page: "card", + source, + hash, + hashPreEncoded: true, + }; + } + + case "@classFeature": { + const unpacked = DataUtil.class.unpackUidClassFeature(text); + + const classPageHash = `${UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_CLASSES]({name: unpacked.className, source: unpacked.classSource})}${HASH_PART_SEP}${UrlUtil.getClassesPageStatePart({feature: {ixLevel: unpacked.level - 1, ixFeature: 0}})}`; + + return { + name: unpacked.name, + displayText: unpacked.displayText, + + page: UrlUtil.PG_CLASSES, + source: unpacked.source, + hash: classPageHash, + hashPreEncoded: true, + + pageHover: "classfeature", + hashHover: UrlUtil.URL_TO_HASH_BUILDER["classFeature"](unpacked), + hashPreEncodedHover: true, + }; + } + + case "@subclassFeature": { + const unpacked = DataUtil.class.unpackUidSubclassFeature(text); + + const classPageHash = `${UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_CLASSES]({name: unpacked.className, source: unpacked.classSource})}${HASH_PART_SEP}${UrlUtil.getClassesPageStatePart({feature: {ixLevel: unpacked.level - 1, ixFeature: 0}})}`; + + return { + name: unpacked.name, + displayText: unpacked.displayText, + + page: UrlUtil.PG_CLASSES, + source: unpacked.source, + hash: classPageHash, + hashPreEncoded: true, + + pageHover: "subclassfeature", + hashHover: UrlUtil.URL_TO_HASH_BUILDER["subclassFeature"](unpacked), + hashPreEncodedHover: true, + }; + } + + case "@quickref": { + const unpacked = DataUtil.quickreference.unpackUid(text); + + const hash = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_QUICKREF](unpacked); + + return { + name: unpacked.name, + displayText: unpacked.displayText, + + page: UrlUtil.PG_QUICKREF, + source: unpacked.source, + hash, + hashPreEncoded: true, + }; + } + + default: return Renderer.utils._getTagMeta_generic(tag, text); + } + } + + static _getTagMeta_generic (tag, text) { + const {name, source, displayText, others} = DataUtil.generic.unpackUid(text, tag); + const hash = UrlUtil.encodeForHash([name, source]); + + const out = { + name, + displayText, + others, + + page: null, + source, + hash, + + preloadId: null, + subhashes: null, + linkText: null, + + hashPreEncoded: true, + }; + + switch (tag) { + case "@spell": out.page = UrlUtil.PG_SPELLS; break; + case "@item": out.page = UrlUtil.PG_ITEMS; break; + case "@condition": + case "@disease": + case "@status": out.page = UrlUtil.PG_CONDITIONS_DISEASES; break; + case "@background": out.page = UrlUtil.PG_BACKGROUNDS; break; + case "@race": out.page = UrlUtil.PG_RACES; break; + case "@optfeature": out.page = UrlUtil.PG_OPT_FEATURES; break; + case "@reward": out.page = UrlUtil.PG_REWARDS; break; + case "@feat": out.page = UrlUtil.PG_FEATS; break; + case "@psionic": out.page = UrlUtil.PG_PSIONICS; break; + case "@object": out.page = UrlUtil.PG_OBJECTS; break; + case "@boon": + case "@cult": out.page = UrlUtil.PG_CULTS_BOONS; break; + case "@trap": + case "@hazard": out.page = UrlUtil.PG_TRAPS_HAZARDS; break; + case "@variantrule": out.page = UrlUtil.PG_VARIANTRULES; break; + case "@table": out.page = UrlUtil.PG_TABLES; break; + case "@vehicle": + case "@vehupgrade": out.page = UrlUtil.PG_VEHICLES; break; + case "@action": out.page = UrlUtil.PG_ACTIONS; break; + case "@language": out.page = UrlUtil.PG_LANGUAGES; break; + case "@charoption": out.page = UrlUtil.PG_CHAR_CREATION_OPTIONS; break; + case "@recipe": out.page = UrlUtil.PG_RECIPES; break; + case "@deck": out.page = UrlUtil.PG_DECKS; break; + + case "@legroup": { + out.page = "legendaryGroup"; + out.isFauxPage = true; + break; + } + + case "@creature": { + out.page = UrlUtil.PG_BESTIARY; + + // "...|scaled=scaledCr}" or "...|scaledsummon=scaledSummonLevel}" + if (others.length) { + const [type, value] = others[0].split("=").map(it => it.trim().toLowerCase()).filter(Boolean); + if (type && value) { + switch (type) { + case VeCt.HASH_SCALED: { + const targetCrNum = Parser.crToNumber(value); + out.preloadId = Renderer.monster.getCustomHashId({name, source, _isScaledCr: true, _scaledCr: targetCrNum}); + out.subhashes = [ + {key: VeCt.HASH_SCALED, value: targetCrNum}, + ]; + out.linkText = displayText || `${name} (CR ${value})`; + break; + } + + case VeCt.HASH_SCALED_SPELL_SUMMON: { + const scaledSpellNum = Number(value); + out.preloadId = Renderer.monster.getCustomHashId({name, source, _isScaledSpellSummon: true, _scaledSpellSummonLevel: scaledSpellNum}); + out.subhashes = [ + {key: VeCt.HASH_SCALED_SPELL_SUMMON, value: scaledSpellNum}, + ]; + out.linkText = displayText || `${name} (Spell Level ${value})`; + break; + } + + case VeCt.HASH_SCALED_CLASS_SUMMON: { + const scaledClassNum = Number(value); + out.preloadId = Renderer.monster.getCustomHashId({name, source, _isScaledClassSummon: true, _scaledClassSummonLevel: scaledClassNum}); + out.subhashes = [ + {key: VeCt.HASH_SCALED_CLASS_SUMMON, value: scaledClassNum}, + ]; + out.linkText = displayText || `${name} (Class Level ${value})`; + break; + } + } + } + } + + break; + } + + case "@class": { + out.page = UrlUtil.PG_CLASSES; + + if (others.length) { + const [subclassShortName, subclassSource, featurePart] = others; + + if (subclassSource) out.source = subclassSource; + + const classStateOpts = { + subclass: { + shortName: subclassShortName.trim(), + source: subclassSource + ? subclassSource.trim() + : Parser.SRC_PHB, + }, + }; + + // Don't include the feature part for hovers, as it is unsupported + const hoverSubhashObj = UrlUtil.unpackSubHash(UrlUtil.getClassesPageStatePart(classStateOpts)); + out.subhashesHover = [{key: "state", value: hoverSubhashObj.state, preEncoded: true}]; + + if (featurePart) { + const featureParts = featurePart.trim().split("-"); + classStateOpts.feature = { + ixLevel: featureParts[0] || "0", + ixFeature: featureParts[1] || "0", + }; + } + + const subhashObj = UrlUtil.unpackSubHash(UrlUtil.getClassesPageStatePart(classStateOpts)); + + out.subhashes = [ + {key: "state", value: subhashObj.state.join(HASH_SUB_LIST_SEP), preEncoded: true}, + {key: "fltsource", value: "clear"}, + {key: "flstmiscellaneous", value: "clear"}, + ]; + } + + break; + } + + case "@skill": { out.isFauxPage = true; out.page = "skill"; break; } + case "@sense": { out.isFauxPage = true; out.page = "sense"; break; } + case "@itemMastery": { out.isFauxPage = true; out.page = "itemMastery"; break; } + case "@cite": { out.isFauxPage = true; out.page = "citation"; break; } + + default: throw new Error(`Unhandled tag "${tag}"`); + } + + return out; + } + + // region Templating + static applyTemplate (ent, templateString, {fnPreApply, mapCustom} = {}) { + return templateString.replace(/{{([^}]+)}}/g, (fullMatch, strArgs) => { + if (fnPreApply) fnPreApply(fullMatch, strArgs); + + // Special case for damage dice -- need to add @damage tags + if (strArgs === "item.dmg1") { + return Renderer.item._getTaggedDamage(ent.dmg1); + } else if (strArgs === "item.dmg2") { + return Renderer.item._getTaggedDamage(ent.dmg2); + } + + if (mapCustom && mapCustom[strArgs]) return mapCustom[strArgs]; + + const args = strArgs.split(" ").map(arg => arg.trim()).filter(Boolean); + + // Args can either be a static property, or a function and a static property + + if (args.length === 1) { + return Renderer.utils._applyTemplate_getValue(ent, args[0]); + } else if (args.length === 2) { + const val = Renderer.utils._applyTemplate_getValue(ent, args[1]); + switch (args[0]) { + case "getFullImmRes": return Parser.getFullImmRes(val); + default: throw new Error(`Unknown template function "${args[0]}"`); + } + } else throw new Error(`Unhandled number of arguments ${args.length}`); + }); + } + + static _applyTemplate_getValue (ent, prop) { + const spl = prop.split("."); + switch (spl[0]) { + case "item": { + const path = spl.slice(1); + if (!path.length) return `{@i missing key path}`; + return MiscUtil.get(ent, ...path); + } + default: return `{@i unknown template root: "${spl[0]}"}`; + } + } + // endregion + + /** + * Convert a nested entry structure into a flat list of entry metadata with depth info. + **/ + static getFlatEntries (entry) { + const out = []; + const depthStack = []; + + const recurse = ({obj}) => { + let isPopDepth = false; + + Renderer.ENTRIES_WITH_ENUMERATED_TITLES + .forEach(meta => { + if (obj.type !== meta.type) return; + + const kName = "name"; // Note: allow this to be specified on the `meta` if needed in future + if (obj[kName] == null) return; + + isPopDepth = true; + + const curDepth = depthStack.length ? depthStack.last() : 0; + const nxtDepth = meta.depth ? meta.depth : meta.depthIncrement ? curDepth + meta.depthIncrement : curDepth; + + depthStack.push( + Math.min( + nxtDepth, + 2, + ), + ); + + const cpyObj = MiscUtil.copyFast(obj); + + out.push({ + depth: curDepth, + entry: cpyObj, + key: meta.key, + ix: out.length, + name: cpyObj.name, + }); + + cpyObj[meta.key] = cpyObj[meta.key].map(child => { + if (!child.type) return child; + const childMeta = Renderer.ENTRIES_WITH_ENUMERATED_TITLES_LOOKUP[child.type]; + if (!childMeta) return child; + + const kNameChild = "name"; // Note: allow this to be specified on the `meta` if needed in future + if (child[kName] == null) return child; + + // Predict what index the child will have in the output array + const ixNextRef = out.length; + + // Allow the child to add its entries to the output array + recurse({obj: child}); + + // Return a reference pointing forwards to the child's flat data + return {IX_FLAT_REF: ixNextRef}; + }); + }); + + if (isPopDepth) depthStack.pop(); + }; + + recurse({obj: entry}); + + return out; + } + + static getLinkSubhashString (subhashes) { + let out = ""; + const len = subhashes.length; + for (let i = 0; i < len; ++i) { + const subHash = subhashes[i]; + if (subHash.preEncoded) out += `${HASH_PART_SEP}${subHash.key}${HASH_SUB_KV_SEP}`; + else out += `${HASH_PART_SEP}${UrlUtil.encodeForHash(subHash.key)}${HASH_SUB_KV_SEP}`; + if (subHash.value != null) { + if (subHash.preEncoded) out += subHash.value; + else out += UrlUtil.encodeForHash(subHash.value); + } else { + // TODO allow list of values + out += subHash.values.map(v => UrlUtil.encodeForHash(v)).join(HASH_SUB_LIST_SEP); + } + } + return out; + } + + static initFullEntries_ (ent, {propEntries = "entries", propFullEntries = "_fullEntries"} = {}) { + ent[propFullEntries] = ent[propFullEntries] || (ent[propEntries] ? MiscUtil.copyFast(ent[propEntries]) : []); + } + + static lazy = { + _getIntersectionConfig () { + return { + rootMargin: "150px 0px", // if the element gets within 150px of the viewport + threshold: 0.01, + }; + }, + + _OBSERVERS: {}, + getCreateObserver ({observerId, fnOnObserve}) { + if (!Renderer.utils.lazy._OBSERVERS[observerId]) { + const observer = Renderer.utils.lazy._OBSERVERS[observerId] = new IntersectionObserver( + Renderer.utils.lazy.getFnOnIntersect({ + observerId, + fnOnObserve, + }), + Renderer.utils.lazy._getIntersectionConfig(), + ); + + observer._TRACKED = new Set(); + + observer.track = it => { + observer._TRACKED.add(it); + return observer.observe(it); + }; + + observer.untrack = it => { + observer._TRACKED.delete(it); + return observer.unobserve(it); + }; + + // If we try to print a page with e.g. un-loaded images, attempt to load them all first + observer._printListener = evt => { + if (!observer._TRACKED.size) return; + + // region Sadly we cannot cancel or delay the print event, so, show a blocking alert + [...observer._TRACKED].forEach(it => { + observer.untrack(it); + fnOnObserve({ + observer, + entry: { + target: it, + }, + }); + }); + + alert(`All content must be loaded prior to printing. Please cancel the print and wait a few moments for loading to complete!`); + // endregion + }; + window.addEventListener("beforeprint", observer._printListener); + } + return Renderer.utils.lazy._OBSERVERS[observerId]; + }, + + destroyObserver ({observerId}) { + const observer = Renderer.utils.lazy._OBSERVERS[observerId]; + if (!observer) return; + + observer.disconnect(); + window.removeEventListener("beforeprint", observer._printListener); + }, + + getFnOnIntersect ({observerId, fnOnObserve}) { + return obsEntries => { + const observer = Renderer.utils.lazy._OBSERVERS[observerId]; + + obsEntries.forEach(entry => { + // filter observed entries for those that intersect + if (entry.intersectionRatio <= 0) return; + + observer.untrack(entry.target); + fnOnObserve({ + observer, + entry, + }); + }); + }; + }, + }; +}; + +Renderer.tag = class { + static _TagBase = class { + tagName; + defaultSource = null; + page = null; + + get tag () { return `@${this.tagName}`; } + + getStripped (tag, text) { + text = text.replace(/<\$([^$]+)\$>/gi, ""); // remove any variable tags + return this._getStripped(tag, text); + } + + /** @abstract */ + _getStripped (tag, text) { throw new Error("Unimplemented!"); } + + getMeta (tag, text) { return this._getMeta(tag, text); } + _getMeta (tag, text) { throw new Error("Unimplemented!"); } + }; + + static _TagBaseAt = class extends this._TagBase { + get tag () { return `@${this.tagName}`; } + }; + + static _TagBaseHash = class extends this._TagBase { + get tag () { return `#${this.tagName}`; } + }; + + static _TagTextStyle = class extends this._TagBaseAt { + _getStripped (tag, text) { return text; } + }; + + static TagBoldShort = class extends this._TagTextStyle { + tagName = "b"; + }; + + static TagBoldLong = class extends this._TagTextStyle { + tagName = "bold"; + }; + + static TagItalicShort = class extends this._TagTextStyle { + tagName = "i"; + }; + + static TagItalicLong = class extends this._TagTextStyle { + tagName = "italic"; + }; + + static TagStrikethroughShort = class extends this._TagTextStyle { + tagName = "s"; + }; + + static TagStrikethroughLong = class extends this._TagTextStyle { + tagName = "strike"; + }; + + static TagUnderlineShort = class extends this._TagTextStyle { + tagName = "u"; + }; + + static TagUnderlineLong = class extends this._TagTextStyle { + tagName = "underline"; + }; + + static TagSup = class extends this._TagTextStyle { + tagName = "sup"; + }; + + static TagSub = class extends this._TagTextStyle { + tagName = "sub"; + }; + + static TagKbd = class extends this._TagTextStyle { + tagName = "kbd"; + }; + + static TagCode = class extends this._TagTextStyle { + tagName = "code"; + }; + + static TagStyle = class extends this._TagTextStyle { + tagName = "style"; + }; + + static TagFont = class extends this._TagTextStyle { + tagName = "font"; + }; + + static TagComic = class extends this._TagTextStyle { + tagName = "comic"; + }; + + static TagComicH1 = class extends this._TagTextStyle { + tagName = "comicH1"; + }; + + static TagComicH2 = class extends this._TagTextStyle { + tagName = "comicH2"; + }; + + static TagComicH3 = class extends this._TagTextStyle { + tagName = "comicH3"; + }; + + static TagComicH4 = class extends this._TagTextStyle { + tagName = "comicH4"; + }; + + static TagComicNote = class extends this._TagTextStyle { + tagName = "comicNote"; + }; + + static TagNote = class extends this._TagTextStyle { + tagName = "note"; + }; + + static TagTip = class extends this._TagTextStyle { + tagName = "tip"; + }; + + static TagUnit = class extends this._TagBaseAt { + tagName = "unit"; + + _getStripped (tag, text) { + const [amount, unitSingle, unitPlural] = Renderer.splitTagByPipe(text); + return isNaN(amount) ? unitSingle : Number(amount) > 1 ? (unitPlural || unitSingle.toPlural()) : unitSingle; + } + }; + + static TagHit = class extends this._TagBaseAt { + tagName = "h"; + + _getStripped (tag, text) { return "Hit: "; } + }; + + static TagMiss = class extends this._TagBaseAt { + tagName = "m"; + + _getStripped (tag, text) { return "Miss: "; } + }; + + static TagAtk = class extends this._TagBaseAt { + tagName = "atk"; + + _getStripped (tag, text) { return Renderer.attackTagToFull(text); } + }; + + static TagHitYourSpellAttack = class extends this._TagBaseAt { + tagName = "hitYourSpellAttack"; + + _getStripped (tag, text) { + const [displayText] = Renderer.splitTagByPipe(text); + return displayText || "your spell attack modifier"; + } + }; + + static TagDc = class extends this._TagBaseAt { + tagName = "dc"; + + _getStripped (tag, text) { + const [dcText, displayText] = Renderer.splitTagByPipe(text); + return `DC ${displayText || dcText}`; + } + }; + + static TagDcYourSpellSave = class extends this._TagBaseAt { + tagName = "dcYourSpellSave"; + + _getStripped (tag, text) { + const [displayText] = Renderer.splitTagByPipe(text); + return displayText || "your spell save DC"; + } + }; + + static _TagDiceFlavor = class extends this._TagBaseAt { + _getStripped (tag, text) { + const [rollText, displayText] = Renderer.splitTagByPipe(text); + switch (tag) { + case "@damage": + case "@dice": + case "@autodice": { + return displayText || rollText.replace(/;/g, "/"); + } + case "@d20": + case "@hit": { + return displayText || (() => { + const n = Number(rollText); + if (!isNaN(n)) return `${n >= 0 ? "+" : ""}${n}`; + return rollText; + })(); + } + case "@recharge": { + const asNum = Number(rollText || 6); + if (isNaN(asNum)) { + throw new Error(`Could not parse "${rollText}" as a number!`); + } + return `(Recharge ${asNum}${asNum < 6 ? `\u20136` : ""})`; + } + case "@chance": { + return displayText || `${rollText} percent`; + } + case "@ability": { + const [, rawScore] = rollText.split(" ").map(it => it.trim().toLowerCase()).filter(Boolean); + const score = Number(rawScore) || 0; + return displayText || `${score} (${Parser.getAbilityModifier(score)})`; + } + case "@savingThrow": + case "@skillCheck": { + return displayText || rollText; + } + } + throw new Error(`Unhandled tag: ${tag}`); + } + }; + + static TaChance = class extends this._TagDiceFlavor { + tagName = "chance"; + }; + + static TaD20 = class extends this._TagDiceFlavor { + tagName = "d20"; + }; + + static TaDamage = class extends this._TagDiceFlavor { + tagName = "damage"; + }; + + static TaDice = class extends this._TagDiceFlavor { + tagName = "dice"; + }; + + static TaAutodice = class extends this._TagDiceFlavor { + tagName = "autodice"; + }; + + static TaHit = class extends this._TagDiceFlavor { + tagName = "hit"; + }; + + static TaRecharge = class extends this._TagDiceFlavor { + tagName = "recharge"; + }; + + static TaAbility = class extends this._TagDiceFlavor { + tagName = "ability"; + }; + + static TaSavingThrow = class extends this._TagDiceFlavor { + tagName = "savingThrow"; + }; + + static TaSkillCheck = class extends this._TagDiceFlavor { + tagName = "skillCheck"; + }; + + static _TagDiceFlavorScaling = class extends this._TagBaseAt { + _getStripped (tag, text) { + const [, , addPerProgress, , displayText] = Renderer.splitTagByPipe(text); + return displayText || addPerProgress; + } + }; + + static TagScaledice = class extends this._TagDiceFlavorScaling { + tagName = "scaledice"; + }; + + static TagScaledamage = class extends this._TagDiceFlavorScaling { + tagName = "scaledamage"; + }; + + static TagCoinflip = class extends this._TagBaseAt { + tagName = "coinflip"; + + _getStripped (tag, text) { + const [displayText] = Renderer.splitTagByPipe(text); + return displayText || "flip a coin"; + } + }; + + static _TagPipedNoDisplayText = class extends this._TagBaseAt { + _getStripped (tag, text) { + const parts = Renderer.splitTagByPipe(text); + return parts[0]; + } + }; + + static Tag5etools = class extends this._TagPipedNoDisplayText { + tagName = "5etools"; + }; + + static TagAdventure = class extends this._TagPipedNoDisplayText { + tagName = "adventure"; + }; + + static TagBook = class extends this._TagPipedNoDisplayText { + tagName = "book"; + }; + + static TagFilter = class extends this._TagPipedNoDisplayText { + tagName = "filter"; + }; + + static TagFootnote = class extends this._TagPipedNoDisplayText { + tagName = "footnote"; + }; + + static TagLink = class extends this._TagPipedNoDisplayText { + tagName = "link"; + }; + + static TagLoader = class extends this._TagPipedNoDisplayText { + tagName = "loader"; + }; + + static TagColor = class extends this._TagPipedNoDisplayText { + tagName = "color"; + }; + + static TagHighlight = class extends this._TagPipedNoDisplayText { + tagName = "highlight"; + }; + + static TagHelp = class extends this._TagPipedNoDisplayText { + tagName = "help"; + }; + + static _TagPipedDisplayTextThird = class extends this._TagBaseAt { + _getStripped (tag, text) { + const parts = Renderer.splitTagByPipe(text); + return parts.length >= 3 ? parts[2] : parts[0]; + } + }; + + static TagAction = class extends this._TagPipedDisplayTextThird { + tagName = "action"; + defaultSource = Parser.SRC_PHB; + page = UrlUtil.PG_ACTIONS; + }; + + static TagBackground = class extends this._TagPipedDisplayTextThird { + tagName = "background"; + defaultSource = Parser.SRC_PHB; + page = UrlUtil.PG_BACKGROUNDS; + }; + + static TagBoon = class extends this._TagPipedDisplayTextThird { + tagName = "boon"; + defaultSource = Parser.SRC_MTF; + page = UrlUtil.PG_CULTS_BOONS; + }; + + static TagCharoption = class extends this._TagPipedDisplayTextThird { + tagName = "charoption"; + defaultSource = Parser.SRC_MOT; + page = UrlUtil.PG_CHAR_CREATION_OPTIONS; + }; + + static TagClass = class extends this._TagPipedDisplayTextThird { + tagName = "class"; + defaultSource = Parser.SRC_PHB; + page = UrlUtil.PG_CLASSES; + }; + + static TagCondition = class extends this._TagPipedDisplayTextThird { + tagName = "condition"; + defaultSource = Parser.SRC_PHB; + page = UrlUtil.PG_CONDITIONS_DISEASES; + }; + + static TagCreature = class extends this._TagPipedDisplayTextThird { + tagName = "creature"; + defaultSource = Parser.SRC_MM; + page = UrlUtil.PG_BESTIARY; + }; + + static TagCult = class extends this._TagPipedDisplayTextThird { + tagName = "cult"; + defaultSource = Parser.SRC_MTF; + page = UrlUtil.PG_CULTS_BOONS; + }; + + static TagDeck = class extends this._TagPipedDisplayTextThird { + tagName = "deck"; + defaultSource = Parser.SRC_DMG; + page = UrlUtil.PG_DECKS; + }; + + static TagDisease = class extends this._TagPipedDisplayTextThird { + tagName = "disease"; + defaultSource = Parser.SRC_DMG; + page = UrlUtil.PG_CONDITIONS_DISEASES; + }; + + static TagFeat = class extends this._TagPipedDisplayTextThird { + tagName = "feat"; + defaultSource = Parser.SRC_PHB; + page = UrlUtil.PG_FEATS; + }; + + static TagHazard = class extends this._TagPipedDisplayTextThird { + tagName = "hazard"; + defaultSource = Parser.SRC_DMG; + page = UrlUtil.PG_TRAPS_HAZARDS; + }; + + static TagItem = class extends this._TagPipedDisplayTextThird { + tagName = "item"; + defaultSource = Parser.SRC_DMG; + page = UrlUtil.PG_ITEMS; + }; + + static TagItemMastery = class extends this._TagPipedDisplayTextThird { + tagName = "itemMastery"; + defaultSource = VeCt.STR_GENERIC; // TODO(Future) adjust as/when these are published + page = "itemMastery"; + }; + + static TagLanguage = class extends this._TagPipedDisplayTextThird { + tagName = "language"; + defaultSource = Parser.SRC_PHB; + page = UrlUtil.PG_LANGUAGES; + }; + + static TagLegroup = class extends this._TagPipedDisplayTextThird { + tagName = "legroup"; + defaultSource = Parser.SRC_MM; + page = "legendaryGroup"; + }; + + static TagObject = class extends this._TagPipedDisplayTextThird { + tagName = "object"; + defaultSource = Parser.SRC_DMG; + page = UrlUtil.PG_OBJECTS; + }; + + static TagOptfeature = class extends this._TagPipedDisplayTextThird { + tagName = "optfeature"; + defaultSource = Parser.SRC_PHB; + page = UrlUtil.PG_OPT_FEATURES; + }; + + static TagPsionic = class extends this._TagPipedDisplayTextThird { + tagName = "psionic"; + defaultSource = Parser.SRC_UATMC; + page = UrlUtil.PG_PSIONICS; + }; + + static TagRace = class extends this._TagPipedDisplayTextThird { + tagName = "race"; + defaultSource = Parser.SRC_PHB; + page = UrlUtil.PG_RACES; + }; + + static TagRecipe = class extends this._TagPipedDisplayTextThird { + tagName = "recipe"; + defaultSource = Parser.SRC_HF; + page = UrlUtil.PG_RECIPES; + }; + + static TagReward = class extends this._TagPipedDisplayTextThird { + tagName = "reward"; + defaultSource = Parser.SRC_DMG; + page = UrlUtil.PG_REWARDS; + }; + + static TagVehicle = class extends this._TagPipedDisplayTextThird { + tagName = "vehicle"; + defaultSource = Parser.SRC_GoS; + page = UrlUtil.PG_VEHICLES; + }; + + static TagVehupgrade = class extends this._TagPipedDisplayTextThird { + tagName = "vehupgrade"; + defaultSource = Parser.SRC_GoS; + page = UrlUtil.PG_VEHICLES; + }; + + static TagSense = class extends this._TagPipedDisplayTextThird { + tagName = "sense"; + defaultSource = Parser.SRC_PHB; + page = "sense"; + }; + + static TagSkill = class extends this._TagPipedDisplayTextThird { + tagName = "skill"; + defaultSource = Parser.SRC_PHB; + page = "skill"; + }; + + static TagSpell = class extends this._TagPipedDisplayTextThird { + tagName = "spell"; + defaultSource = Parser.SRC_PHB; + page = UrlUtil.PG_SPELLS; + }; + + static TagStatus = class extends this._TagPipedDisplayTextThird { + tagName = "status"; + defaultSource = Parser.SRC_PHB; + page = UrlUtil.PG_CONDITIONS_DISEASES; + }; + + static TagTable = class extends this._TagPipedDisplayTextThird { + tagName = "table"; + defaultSource = Parser.SRC_DMG; + page = UrlUtil.PG_TABLES; + }; + + static TagTrap = class extends this._TagPipedDisplayTextThird { + tagName = "trap"; + defaultSource = Parser.SRC_DMG; + page = UrlUtil.PG_TRAPS_HAZARDS; + }; + + static TagVariantrule = class extends this._TagPipedDisplayTextThird { + tagName = "variantrule"; + defaultSource = Parser.SRC_DMG; + page = UrlUtil.PG_VARIANTRULES; + }; + + static TagCite = class extends this._TagPipedDisplayTextThird { + tagName = "cite"; + defaultSource = Parser.SRC_PHB; + page = "citation"; + }; + + static _TagPipedDisplayTextFourth = class extends this._TagBaseAt { + _getStripped (tag, text) { + const parts = Renderer.splitTagByPipe(text); + return parts.length >= 4 ? parts[3] : parts[0]; + } + }; + + static TagCard = class extends this._TagPipedDisplayTextFourth { + tagName = "card"; + defaultSource = Parser.SRC_DMG; + page = "card"; + }; + + static TagDeity = class extends this._TagPipedDisplayTextFourth { + tagName = "deity"; + defaultSource = Parser.SRC_PHB; + page = UrlUtil.PG_DEITIES; + }; + + static _TagPipedDisplayTextSixth = class extends this._TagBaseAt { + _getStripped (tag, text) { + const parts = Renderer.splitTagByPipe(text); + return parts.length >= 6 ? parts[5] : parts[0]; + } + }; + + static TagClassFeature = class extends this._TagPipedDisplayTextSixth { + tagName = "classFeature"; + defaultSource = Parser.SRC_PHB; + page = UrlUtil.PG_CLASSES; + }; + + static _TagPipedDisplayTextEight = class extends this._TagBaseAt { + _getStripped (tag, text) { + const parts = Renderer.splitTagByPipe(text); + return parts.length >= 8 ? parts[7] : parts[0]; + } + }; + + static TagSubclassFeature = class extends this._TagPipedDisplayTextEight { + tagName = "subclassFeature"; + defaultSource = Parser.SRC_PHB; + page = UrlUtil.PG_CLASSES; + }; + + static TagQuickref = class extends this._TagBaseAt { + tagName = "quickref"; + defaultSource = Parser.SRC_PHB; + page = UrlUtil.PG_QUICKREF; + + _getStripped (tag, text) { + const {name, displayText} = DataUtil.quickreference.unpackUid(text); + return displayText || name; + } + }; + + static TagArea = class extends this._TagBaseAt { + tagName = "area"; + + _getStripped (tag, text) { + const [compactText, , flags] = Renderer.splitTagByPipe(text); + + return flags && flags.includes("x") + ? compactText + : `${flags && flags.includes("u") ? "A" : "a"}rea ${compactText}`; + } + + _getMeta (tag, text) { + const [compactText, areaId, flags] = Renderer.splitTagByPipe(text); + + const displayText = flags && flags.includes("x") + ? compactText + : `${flags && flags.includes("u") ? "A" : "a"}rea ${compactText}`; + + return { + areaId, + displayText, + }; + } + }; + + static TagHomebrew = class extends this._TagBaseAt { + tagName = "homebrew"; + + _getStripped (tag, text) { + const [newText, oldText] = Renderer.splitTagByPipe(text); + if (newText && oldText) { + return `${newText} [this is a homebrew addition, replacing the following: "${oldText}"]`; + } else if (newText) { + return `${newText} [this is a homebrew addition]`; + } else if (oldText) { + return `[the following text has been removed due to homebrew: ${oldText}]`; + } else throw new Error(`Homebrew tag had neither old nor new text!`); + } + }; + + static TagItemEntry = class extends this._TagBaseHash { + tagName = "itemEntry"; + defaultSource = Parser.SRC_DMG; + }; + + /* -------------------------------------------- */ + + static TAGS = [ + new this.TagBoldShort(), + new this.TagBoldLong(), + new this.TagItalicShort(), + new this.TagItalicLong(), + new this.TagStrikethroughShort(), + new this.TagStrikethroughLong(), + new this.TagUnderlineShort(), + new this.TagUnderlineLong(), + new this.TagSup(), + new this.TagSub(), + new this.TagKbd(), + new this.TagCode(), + new this.TagStyle(), + new this.TagFont(), + + new this.TagComic(), + new this.TagComicH1(), + new this.TagComicH2(), + new this.TagComicH3(), + new this.TagComicH4(), + new this.TagComicNote(), + + new this.TagNote(), + new this.TagTip(), + + new this.TagUnit(), + + new this.TagHit(), + new this.TagMiss(), + + new this.TagAtk(), + + new this.TagHitYourSpellAttack(), + + new this.TagDc(), + + new this.TagDcYourSpellSave(), + + new this.TaChance(), + new this.TaD20(), + new this.TaDamage(), + new this.TaDice(), + new this.TaAutodice(), + new this.TaHit(), + new this.TaRecharge(), + new this.TaAbility(), + new this.TaSavingThrow(), + new this.TaSkillCheck(), + + new this.TagScaledice(), + new this.TagScaledamage(), + + new this.TagCoinflip(), + + new this.Tag5etools(), + new this.TagAdventure(), + new this.TagBook(), + new this.TagFilter(), + new this.TagFootnote(), + new this.TagLink(), + new this.TagLoader(), + new this.TagColor(), + new this.TagHighlight(), + new this.TagHelp(), + + new this.TagQuickref(), + + new this.TagArea(), + + new this.TagAction(), + new this.TagBackground(), + new this.TagBoon(), + new this.TagCharoption(), + new this.TagClass(), + new this.TagCondition(), + new this.TagCreature(), + new this.TagCult(), + new this.TagDeck(), + new this.TagDisease(), + new this.TagFeat(), + new this.TagHazard(), + new this.TagItem(), + new this.TagItemMastery(), + new this.TagLanguage(), + new this.TagLegroup(), + new this.TagObject(), + new this.TagOptfeature(), + new this.TagPsionic(), + new this.TagRace(), + new this.TagRecipe(), + new this.TagReward(), + new this.TagVehicle(), + new this.TagVehupgrade(), + new this.TagSense(), + new this.TagSkill(), + new this.TagSpell(), + new this.TagStatus(), + new this.TagTable(), + new this.TagTrap(), + new this.TagVariantrule(), + new this.TagCite(), + + new this.TagCard(), + new this.TagDeity(), + + new this.TagClassFeature({tagName: "classFeature"}), + + new this.TagSubclassFeature({tagName: "subclassFeature"}), + + new this.TagHomebrew(), + + /* ----------------------------------------- */ + + new this.TagItemEntry(), + ]; + + static TAG_LOOKUP = {}; + + static _init () { + this.TAGS.forEach(tag => { + this.TAG_LOOKUP[tag.tag] = tag; + this.TAG_LOOKUP[tag.tagName] = tag; + }); + + return null; + } + + static _ = this._init(); + + /* ----------------------------------------- */ + + static getPage (tag) { + const tagInfo = this.TAG_LOOKUP[tag]; + return tagInfo?.page; + } +}; + +Renderer.events = class { + static handleClick_copyCode (evt, ele) { + const $e = $(ele).parent().next("pre"); + MiscUtil.pCopyTextToClipboard($e.text()); + JqueryUtil.showCopiedEffect($e); + } + + static handleClick_toggleCodeWrap (evt, ele) { + const nxt = !StorageUtil.syncGet("rendererCodeWrap"); + StorageUtil.syncSet("rendererCodeWrap", nxt); + const $btn = $(ele).toggleClass("active", nxt); + const $e = $btn.parent().next("pre"); + $e.toggleClass("rd__pre-wrap", nxt); + } + + static bindGeneric ({element = document.body} = {}) { + const $ele = $(element) + .on("click", `[data-rd-data-embed-header]`, evt => { + Renderer.events.handleClick_dataEmbedHeader(evt, evt.currentTarget); + }); + + Renderer.events._HEADER_TOGGLE_CLICK_SELECTORS + .forEach(selector => { + $ele + .on("click", selector, evt => { + Renderer.events.handleClick_headerToggleButton(evt, evt.currentTarget, {selector}); + }); + }) + ; + } + + static handleClick_dataEmbedHeader (evt, ele) { + evt.stopPropagation(); + evt.preventDefault(); + + const $ele = $(ele); + $ele.find(".rd__data-embed-name").toggleVe(); + $ele.find(".rd__data-embed-toggle").text($ele.text().includes("+") ? "[\u2013]" : "[+]"); + $ele.closest("table").find("tbody").toggleVe(); + } + + static _HEADER_TOGGLE_CLICK_SELECTORS = [ + `[data-rd-h-toggle-button]`, + `[data-rd-h-special-toggle-button]`, + ]; + + static handleClick_headerToggleButton (evt, ele, {selector = false} = {}) { + evt.stopPropagation(); + evt.preventDefault(); + + const isShow = this._handleClick_headerToggleButton_doToggleEle(ele, {selector}); + + if (!EventUtil.isCtrlMetaKey(evt)) return; + + Renderer.events._HEADER_TOGGLE_CLICK_SELECTORS + .forEach(selector => { + [...document.querySelectorAll(selector)] + .filter(eleOther => eleOther !== ele) + .forEach(eleOther => { + Renderer.events._handleClick_headerToggleButton_doToggleEle(eleOther, {selector, force: isShow}); + }); + }) + ; + } + + static _handleClick_headerToggleButton_doToggleEle (ele, {selector = false, force = null} = {}) { + const isShow = force != null ? force : ele.innerHTML.includes("+"); + + let eleNxt = ele.closest(".rd__h").nextElementSibling; + + while (eleNxt) { + // Never hide float-fixing elements + if (eleNxt.classList.contains("float-clear")) { + eleNxt = eleNxt.nextElementSibling; + continue; + } + + // For special sections, always collapse the whole thing. + if (selector !== `[data-rd-h-special-toggle-button]`) { + const eleToCheck = Renderer.events._handleClick_headerToggleButton_getEleToCheck(eleNxt); + if ( + eleToCheck.classList.contains("rd__b-special") + || (eleToCheck.classList.contains("rd__h") && !eleToCheck.classList.contains("rd__h--3")) + || (eleToCheck.classList.contains("rd__b") && !eleToCheck.classList.contains("rd__b--3")) + ) break; + } + + eleNxt.classList.toggle("rd__ele-toggled-hidden", !isShow); + eleNxt = eleNxt.nextElementSibling; + } + + ele.innerHTML = isShow ? "[\u2013]" : "[+]"; + + return isShow; + } + + static _handleClick_headerToggleButton_getEleToCheck (eleNxt) { + if (eleNxt.type === 3) return eleNxt; // Text nodes + + // If the element is a block with only one child which is itself a block, treat it as a "wrapper" block, and dig + if (!eleNxt.classList.contains("rd__b") || eleNxt.classList.contains("rd__b--3")) return eleNxt; + const childNodes = [...eleNxt.childNodes].filter(it => (it.type === 3 && (it.textContent || "").trim()) || it.type !== 3); + if (childNodes.length !== 1) return eleNxt; + if (childNodes[0].classList.contains("rd__b")) return Renderer.events._handleClick_headerToggleButton_getEleToCheck(childNodes[0]); + return eleNxt; + } + + static handleLoad_inlineStatblock (ele) { + const observer = Renderer.utils.lazy.getCreateObserver({ + observerId: "inlineStatblock", + fnOnObserve: Renderer.events._handleLoad_inlineStatblock_fnOnObserve.bind(Renderer.events), + }); + + observer.track(ele.parentNode); + } + + static _handleLoad_inlineStatblock_fnOnObserve ({entry}) { + const ele = entry.target; + + const tag = ele.dataset.rdTag.uq(); + const page = ele.dataset.rdPage.uq(); + const source = ele.dataset.rdSource.uq(); + const name = ele.dataset.rdName.uq(); + const displayName = ele.dataset.rdDisplayName.uq(); + const hash = ele.dataset.rdHash.uq(); + const style = ele.dataset.rdStyle.uq(); + + DataLoader.pCacheAndGet(page, Parser.getTagSource(tag, source), hash) + .then(toRender => { + const tr = ele.closest("tr"); + + if (!toRender) { + tr.innerHTML = `Failed to load ${tag ? Renderer.get().render(`{@${tag} ${name}|${source}${displayName ? `|${displayName}` : ""}}`) : displayName || name}!`; + throw new Error(`Could not find tag: "${tag}" (page/prop: "${page}") hash: "${hash}"`); + } + + const headerName = displayName + || (name ?? toRender.name ?? (toRender.entries?.length ? toRender.entries?.[0]?.name : "(Unknown)")); + + const fnRender = Renderer.hover.getFnRenderCompact(page); + const tbl = tr.closest("table"); + const nxt = e_({ + outer: Renderer.utils.getEmbeddedDataHeader(headerName, style) + + fnRender(toRender, {isEmbeddedEntity: true}) + + Renderer.utils.getEmbeddedDataFooter(), + }); + tbl.parentNode.replaceChild( + nxt, + tbl, + ); + + const nxtTgt = nxt.querySelector(`[data-rd-embedded-data-render-target="true"]`); + + const fnBind = Renderer.hover.getFnBindListenersCompact(page); + if (fnBind) fnBind(toRender, nxtTgt); + }); + } +}; + +Renderer.feat = class { + static _mergeAbilityIncrease_getListItemText (abilityObj) { + return Renderer.feat._mergeAbilityIncrease_getText(abilityObj); + } + + static _mergeAbilityIncrease_getListItemItem (abilityObj) { + return { + type: "item", + name: "Ability Score Increase.", + entry: Renderer.feat._mergeAbilityIncrease_getText(abilityObj), + }; + } + + static _mergeAbilityIncrease_getText (abilityObj) { + const maxScore = abilityObj.max ?? 20; + + if (!abilityObj.choose) { + return Object.keys(abilityObj) + .filter(k => k !== "max") + .map(ab => `Increase your ${Parser.attAbvToFull(ab)} score by ${abilityObj[ab]}, to a maximum of ${maxScore}.`) + .join(" "); + } + + if (abilityObj.choose.from.length === 6) { + return abilityObj.choose.entry + ? Renderer.get().render(abilityObj.choose.entry) // only used in "Resilient" + : `Increase one ability score of your choice by ${abilityObj.choose.amount ?? 1}, to a maximum of ${maxScore}.`; + } + + const abbChoicesText = abilityObj.choose.from.map(it => Parser.attAbvToFull(it)).joinConjunct(", ", " or "); + return `Increase your ${abbChoicesText} by ${abilityObj.choose.amount ?? 1}, to a maximum of ${maxScore}.`; + } + + static initFullEntries (feat) { + if (!feat.ability || feat._fullEntries || !feat.ability.length) return; + + const abilsToDisplay = feat.ability.filter(it => !it.hidden); + if (!abilsToDisplay.length) return; + + Renderer.utils.initFullEntries_(feat); + + const targetList = feat._fullEntries.find(e => e.type === "list"); + + // FTD+ style + if (targetList && targetList.items.every(it => it.type === "item")) { + abilsToDisplay.forEach(abilObj => targetList.items.unshift(Renderer.feat._mergeAbilityIncrease_getListItemItem(abilObj))); + return; + } + + if (targetList) { + abilsToDisplay.forEach(abilObj => targetList.items.unshift(Renderer.feat._mergeAbilityIncrease_getListItemText(abilObj))); + return; + } + + // this should never happen, but display sane output anyway, and throw an out-of-order exception + abilsToDisplay.forEach(abilObj => feat._fullEntries.unshift(Renderer.feat._mergeAbilityIncrease_getListItemText(abilObj))); + + setTimeout(() => { + throw new Error(`Could not find object of type "list" in "entries" for feat "${feat.name}" from source "${feat.source}" when merging ability scores! Reformat the feat to include a "list"-type entry.`); + }, 1); + } + + static getFeatRendereableEntriesMeta (ent) { + Renderer.feat.initFullEntries(ent); + return { + entryMain: {entries: ent._fullEntries || ent.entries}, + }; + } + + static getJoinedCategoryPrerequisites (category, rdPrereqs) { + const ptCategory = category ? `${category.toTitleCase()} Feat` : ""; + + return ptCategory && rdPrereqs + ? `${ptCategory} (${rdPrereqs})` + : (ptCategory || rdPrereqs); + } + + /** + * @param feat + * @param [opts] + * @param [opts.isSkipNameRow] + */ + static getCompactRenderedString (feat, opts) { + opts = opts || {}; + + const renderer = Renderer.get().setFirstSection(true); + const renderStack = []; + + const ptCategoryPrerequisite = Renderer.feat.getJoinedCategoryPrerequisites( + feat.category, + Renderer.utils.prerequisite.getHtml(feat.prerequisite), + ); + const ptRepeatable = Renderer.utils.getRepeatableHtml(feat); + + renderStack.push(` + ${Renderer.utils.getExcludedTr({entity: feat, dataProp: "feat", page: UrlUtil.PG_FEATS})} + ${opts.isSkipNameRow ? "" : Renderer.utils.getNameTr(feat, {page: UrlUtil.PG_FEATS})} + + ${ptCategoryPrerequisite ? `

    ${ptCategoryPrerequisite}

    ` : ""} + ${ptRepeatable ? `

    ${ptRepeatable}

    ` : ""} + `); + renderer.recursiveRender(Renderer.feat.getFeatRendereableEntriesMeta(feat)?.entryMain, renderStack, {depth: 2}); + renderStack.push(``); + + return renderStack.join(""); + } + + static pGetFluff (feat) { + return Renderer.utils.pGetFluff({ + entity: feat, + fnGetFluffData: DataUtil.featFluff.loadJSON.bind(DataUtil.featFluff), + fluffProp: "featFluff", + }); + } +}; + +Renderer.class = class { + static getCompactRenderedString (cls) { + if (cls.__prop === "subclass") return Renderer.subclass.getCompactRenderedString(cls); + + const clsEntry = { + type: "section", + name: cls.name, + source: cls.source, + page: cls.page, + entries: MiscUtil.copyFast((cls.classFeatures || []).flat()), + }; + + return Renderer.hover.getGenericCompactRenderedString(clsEntry); + } + + static getHitDiceEntry (clsHd) { return clsHd ? {toRoll: `${clsHd.number}d${clsHd.faces}`, rollable: true} : null; } + static getHitPointsAtFirstLevel (clsHd) { return clsHd ? `${clsHd.number * clsHd.faces} + your Constitution modifier` : null; } + static getHitPointsAtHigherLevels (className, clsHd, hdEntry) { return className && clsHd && hdEntry ? `${Renderer.getEntryDice(hdEntry, "Hit die")} (or ${((clsHd.number * clsHd.faces) / 2 + 1)}) + your Constitution modifier per ${className} level after 1st` : null; } + + static getRenderedArmorProfs (armorProfs) { return armorProfs.map(a => Renderer.get().render(a.full ? a.full : a === "light" || a === "medium" || a === "heavy" ? `{@filter ${a} armor|items|type=${a} armor}` : a)).join(", "); } + static getRenderedWeaponProfs (weaponProfs) { return weaponProfs.map(w => Renderer.get().render(w === "simple" || w === "martial" ? `{@filter ${w} weapons|items|type=${w} weapon}` : w.optional ? `${w.proficiency}` : w)).join(", "); } + static getRenderedToolProfs (toolProfs) { return toolProfs.map(it => Renderer.get().render(it)).join(", "); } + static getRenderedSkillProfs (skills) { return `${Parser.skillProficienciesToFull(skills).uppercaseFirst()}.`; } + + static getWalkerFilterDereferencedFeatures () { + return MiscUtil.getWalker({ + keyBlocklist: MiscUtil.GENERIC_WALKER_ENTRIES_KEY_BLOCKLIST, + isAllowDeleteObjects: true, + isDepthFirst: true, + }); + } + + static mutFilterDereferencedClassFeatures ( + { + walker, + cpyCls, + pageFilter, + filterValues, + isUseSubclassSources = false, + }, + ) { + walker = walker || Renderer.class.getWalkerFilterDereferencedFeatures(); + + cpyCls.classFeatures = cpyCls.classFeatures.map((lvlFeatures, ixLvl) => { + return walker.walk( + lvlFeatures, + { + object: (obj) => { + if (!obj.source) return obj; + const fText = obj.isClassFeatureVariant ? {isClassFeatureVariant: true} : null; + + const isDisplay = [obj.source, ...(obj.otherSources || []) + .map(it => it.source)] + .some(src => pageFilter.filterBox.toDisplayByFilters( + filterValues, + ...[ + { + filter: pageFilter.sourceFilter, + value: isUseSubclassSources && src === cpyCls.source + ? pageFilter.getActiveSource(filterValues) + : src, + }, + pageFilter.levelFilter + ? { + filter: pageFilter.levelFilter, + value: ixLvl + 1, + } + : null, + { + filter: pageFilter.optionsFilter, + value: fText, + }, + ].filter(Boolean), + )); + + return isDisplay ? obj : null; + }, + array: (arr) => { + return arr.filter(it => it != null); + }, + }, + ); + }); + } + + static mutFilterDereferencedSubclassFeatures ( + { + walker, + cpySc, + pageFilter, + filterValues, + }, + ) { + walker = walker || Renderer.class.getWalkerFilterDereferencedFeatures(); + + cpySc.subclassFeatures = cpySc.subclassFeatures.map(lvlFeatures => { + const level = CollectionUtil.bfs(lvlFeatures, {prop: "level"}); + + return walker.walk( + lvlFeatures, + { + object: (obj) => { + if (obj.entries && !obj.entries.length) return null; + if (!obj.source) return obj; + const fText = obj.isClassFeatureVariant ? {isClassFeatureVariant: true} : null; + + const isDisplay = [obj.source, ...(obj.otherSources || []) + .map(it => it.source)] + .some(src => pageFilter.filterBox.toDisplayByFilters( + filterValues, + ...[ + { + filter: pageFilter.sourceFilter, + value: src, + }, + pageFilter.levelFilter + ? { + filter: pageFilter.levelFilter, + value: level, + } + : null, + { + filter: pageFilter.optionsFilter, + value: fText, + }, + ].filter(Boolean), + )); + + return isDisplay ? obj : null; + }, + array: (arr) => { + return arr.filter(it => it != null); + }, + }, + ); + }); + } +}; + +Renderer.subclass = class { + static getCompactRenderedString (sc) { + const scEntry = { + type: "section", + name: sc.name, + source: sc.source, + page: sc.page, + entries: MiscUtil.copyFast((sc.subclassFeatures || []).flat()), + }; + + return Renderer.hover.getGenericCompactRenderedString(scEntry); + } +}; + +Renderer.spell = class { + static getCompactRenderedString (spell, opts) { + opts = opts || {}; + + const renderer = Renderer.get(); + const renderStack = []; + + renderStack.push(` + ${Renderer.utils.getExcludedTr({entity: spell, dataProp: "spell", page: UrlUtil.PG_SPELLS})} + ${Renderer.utils.getNameTr(spell, {page: UrlUtil.PG_SPELLS, isEmbeddedEntity: opts.isEmbeddedEntity})} + + + + + + + + + + + + + + + + + + + + + + +
    LevelSchoolCasting TimeRange
    ${Parser.spLevelToFull(spell.level)}${Parser.spMetaToFull(spell.meta)}${Parser.spSchoolAndSubschoolsAbvsToFull(spell.school, spell.subschools)}${Parser.spTimeListToFull(spell.time)}${Parser.spRangeToFull(spell.range)}
    ComponentsDuration
    ${Parser.spComponentsToFull(spell.components, spell.level)}${Parser.spDurationToFull(spell.duration)}
    + + `); + + renderStack.push(``); + const entryList = {type: "entries", entries: spell.entries}; + renderer.recursiveRender(entryList, renderStack, {depth: 1}); + if (spell.entriesHigherLevel) { + const higherLevelsEntryList = {type: "entries", entries: spell.entriesHigherLevel}; + renderer.recursiveRender(higherLevelsEntryList, renderStack, {depth: 2}); + } + const fromClassList = Renderer.spell.getCombinedClasses(spell, "fromClassList"); + if (fromClassList.length) { + const [current] = Parser.spClassesToCurrentAndLegacy(fromClassList); + renderStack.push(`
    Classes: ${Parser.spMainClassesToFull(current)}
    `); + } + renderStack.push(``); + + return renderStack.join(""); + } + + static _SpellSourceManager = class { + _cache = null; + + populate ({brew, isForce = false}) { + if (this._cache && !isForce) return; + + this._cache = { + classes: {}, + + groups: {}, + + // region Unused + races: {}, + backgrounds: {}, + feats: {}, + optionalfeatures: {}, + // endregion + }; + + // region Load homebrew class spell list addons + // Two formats are available: a string UID, or "class" object (object with a `className`, etc.). + (brew.class || []) + .forEach(c => { + c.source = c.source || Parser.SRC_PHB; + + (c.classSpells || []) + .forEach(itm => { + this._populate_fromClass_classSubclass({ + itm, + className: c.name, + classSource: c.source, + }); + + this._populate_fromClass_group({ + itm, + className: c.name, + classSource: c.source, + }); + }); + }); + + (brew.subclass || []) + .forEach(sc => { + sc.classSource = sc.classSource || Parser.SRC_PHB; + sc.shortName = sc.shortName || sc.name; + sc.source = sc.source || sc.classSource; + + (sc.subclassSpells || []) + .forEach(itm => { + this._populate_fromClass_classSubclass({ + itm, + className: sc.className, + classSource: sc.classSource, + subclassShortName: sc.shortName, + subclassName: sc.name, + subclassSource: sc.source, + }); + + this._populate_fromClass_group({ + itm, + className: sc.className, + classSource: sc.classSource, + subclassShortName: sc.shortName, + subclassName: sc.name, + subclassSource: sc.source, + }); + }); + + Object.entries(sc.subSubclassSpells || {}) + .forEach(([subSubclassName, arr]) => { + arr + .forEach(itm => { + this._populate_fromClass_classSubclass({ + itm, + className: sc.className, + classSource: sc.classSource, + subclassShortName: sc.shortName, + subclassName: sc.name, + subclassSource: sc.source, + subSubclassName, + }); + + this._populate_fromClass_group({ + itm, + className: sc.className, + classSource: sc.classSource, + subclassShortName: sc.shortName, + subclassName: sc.name, + subclassSource: sc.source, + subSubclassName, + }); + }); + }); + }); + // endregion + + (brew.spellList || []) + .forEach(spellList => this._populate_fromGroup_group({spellList})); + } + + _populate_fromClass_classSubclass ( + { + itm, + className, + classSource, + subclassShortName, + subclassName, + subclassSource, + subSubclassName, + }, + ) { + if (itm.groupName) return; + + // region Duplicate the spell list of another class/subclass/sub-subclass + if (itm.className) { + return this._populate_fromClass_doAdd({ + tgt: MiscUtil.getOrSet( + this._cache.classes, + "class", + (itm.classSource || Parser.SRC_PHB).toLowerCase(), + itm.className.toLowerCase(), + {}, + ), + className, + classSource, + subclassShortName, + subclassName, + subclassSource, + subSubclassName, + }); + } + // endregion + + // region Individual spell + let [name, source] = `${itm}`.toLowerCase().split("|"); + source = source || Parser.SRC_PHB.toLowerCase(); + + this._populate_fromClass_doAdd({ + tgt: MiscUtil.getOrSet( + this._cache.classes, + "spell", + source, + name, + {fromClassList: [], fromSubclass: []}, + ), + className, + classSource, + subclassShortName, + subclassName, + subclassSource, + subSubclassName, + }); + // endregion + } + + _populate_fromClass_doAdd ( + { + tgt, + className, + classSource, + subclassShortName, + subclassName, + subclassSource, + subSubclassName, + schools, + }, + ) { + if (subclassShortName) { + const toAdd = { + class: {name: className, source: classSource}, + subclass: {name: subclassName || subclassShortName, shortName: subclassShortName, source: subclassSource}, + }; + if (subSubclassName) toAdd.subclass.subSubclass = subSubclassName; + if (schools) toAdd.schools = schools; + + tgt.fromSubclass = tgt.fromSubclass || []; + tgt.fromSubclass.push(toAdd); + return; + } + + const toAdd = {name: className, source: classSource}; + if (schools) toAdd.schools = schools; + + tgt.fromClassList = tgt.fromClassList || []; + tgt.fromClassList.push(toAdd); + } + + _populate_fromClass_group ( + { + itm, + className, + classSource, + subclassShortName, + subclassName, + subclassSource, + subSubclassName, + }, + ) { + if (!itm.groupName) return; + + return this._populate_fromClass_doAdd({ + tgt: MiscUtil.getOrSet( + this._cache.classes, + "group", + (itm.groupSource || Parser.SRC_PHB).toLowerCase(), + itm.groupName.toLowerCase(), + {}, + ), + className, + classSource, + subclassShortName, + subclassName, + subclassSource, + subSubclassName, + schools: itm.spellSchools, + }); + } + + _populate_fromGroup_group ( + { + spellList, + }, + ) { + const spellListSourceLower = (spellList.source || "").toLowerCase(); + const spellListNameLower = (spellList.name || "").toLowerCase(); + + spellList.spells + .forEach(spell => { + if (typeof spell === "string") { + const {name, source} = DataUtil.proxy.unpackUid("spell", spell, "spell", {isLower: true}); + return MiscUtil.set(this._cache.groups, "spell", source, name, spellListSourceLower, spellListNameLower, {name: spellList.name, source: spellList.source}); + } + + // TODO(Future) implement "copy existing list" + throw new Error(`Grouping spells based on other spell lists is not yet supported!`); + }); + } + + /* -------------------------------------------- */ + + mutateSpell ({spell: sp, lowName, lowSource}) { + lowName = lowName || sp.name.toLowerCase(); + lowSource = lowSource || sp.source.toLowerCase(); + + this._mutateSpell_brewGeneric({sp, lowName, lowSource, propSpell: "races", prop: "race"}); + this._mutateSpell_brewGeneric({sp, lowName, lowSource, propSpell: "backgrounds", prop: "background"}); + this._mutateSpell_brewGeneric({sp, lowName, lowSource, propSpell: "feats", prop: "feat"}); + this._mutateSpell_brewGeneric({sp, lowName, lowSource, propSpell: "optionalfeatures", prop: "optionalfeature"}); + this._mutateSpell_brewGroup({sp, lowName, lowSource}); + this._mutateSpell_brewClassesSubclasses({sp, lowName, lowSource}); + } + + _mutateSpell_brewClassesSubclasses ({sp, lowName, lowSource}) { + if (!this._cache?.classes) return; + + if (this._cache.classes.spell?.[lowSource]?.[lowName]?.fromClassList?.length) { + sp._tmpClasses.fromClassList = sp._tmpClasses.fromClassList || []; + sp._tmpClasses.fromClassList.push(...this._cache.classes.spell[lowSource][lowName].fromClassList); + } + + if (this._cache.classes.spell?.[lowSource]?.[lowName]?.fromSubclass?.length) { + sp._tmpClasses.fromSubclass = sp._tmpClasses.fromSubclass || []; + sp._tmpClasses.fromSubclass.push(...this._cache.classes.spell[lowSource][lowName].fromSubclass); + } + + if (this._cache.classes.class && sp.classes?.fromClassList) { + (sp._tmpClasses = sp._tmpClasses || {}).fromClassList = sp._tmpClasses.fromClassList || []; + + // speed over safety + outer: for (const srcLower in this._cache.classes.class) { + const searchForClasses = this._cache.classes.class[srcLower]; + + for (const clsLowName in searchForClasses) { + const spellHasClass = sp.classes?.fromClassList?.some(cls => (cls.source || "").toLowerCase() === srcLower && cls.name.toLowerCase() === clsLowName); + if (!spellHasClass) continue; + + const fromDetails = searchForClasses[clsLowName]; + + if (fromDetails.fromClassList) { + sp._tmpClasses.fromClassList.push(...this._mutateSpell_getListFilteredBySchool({sp, arr: fromDetails.fromClassList})); + } + + if (fromDetails.fromSubclass) { + sp._tmpClasses.fromSubclass = sp._tmpClasses.fromSubclass || []; + sp._tmpClasses.fromSubclass.push(...this._mutateSpell_getListFilteredBySchool({sp, arr: fromDetails.fromSubclass})); + } + + // Only add it once regardless of how many classes match + break outer; + } + } + } + + if (this._cache.classes.group && (sp.groups?.length || sp._tmpGroups?.length)) { + const groups = Renderer.spell.getCombinedGeneric(sp, {propSpell: "groups"}); + + (sp._tmpClasses = sp._tmpClasses || {}).fromClassList = sp._tmpClasses.fromClassList || []; + + // speed over safety + outer: for (const srcLower in this._cache.classes.group) { + const searchForGroups = this._cache.classes.group[srcLower]; + + for (const groupLowName in searchForGroups) { + const spellHasGroup = groups?.some(grp => (grp.source || "").toLowerCase() === srcLower && grp.name.toLowerCase() === groupLowName); + if (!spellHasGroup) continue; + + const fromDetails = searchForGroups[groupLowName]; + + if (fromDetails.fromClassList) { + sp._tmpClasses.fromClassList.push(...this._mutateSpell_getListFilteredBySchool({sp, arr: fromDetails.fromClassList})); + } + + if (fromDetails.fromSubclass) { + sp._tmpClasses.fromSubclass = sp._tmpClasses.fromSubclass || []; + sp._tmpClasses.fromSubclass.push(...this._mutateSpell_getListFilteredBySchool({sp, arr: fromDetails.fromSubclass})); + } + + // Only add it once regardless of how many classes match + break outer; + } + } + } + } + + _mutateSpell_getListFilteredBySchool ({arr, sp}) { + return arr + .filter(it => { + if (!it.schools) return true; + return it.schools.includes(sp.school); + }) + .map(it => { + if (!it.schools) return it; + const out = MiscUtil.copyFast(it); + delete it.schools; + return it; + }); + } + + _mutateSpell_brewGeneric ({sp, lowName, lowSource, propSpell, prop}) { + if (!this._cache?.[propSpell]) return; + + const propTmp = `_tmp${propSpell.uppercaseFirst()}`; + + // If a precise spell has been specified + if (this._cache[propSpell]?.spell?.[lowSource]?.[lowName]?.length) { + (sp[propTmp] = sp[propTmp] || []) + .push(...this._cache[propSpell].spell[lowSource][lowName]); + } + + // If we have a copy of an existing entity's spells + if (this._cache?.[propSpell]?.[prop] && sp[propSpell]) { + sp[propTmp] = sp[propTmp] || []; + + // speed over safety + outer: for (const srcLower in this._cache[propSpell][prop]) { + const searchForExisting = this._cache[propSpell][prop][srcLower]; + + for (const lowName in searchForExisting) { + const spellHasEnt = sp[propSpell].some(it => (it.source || "").toLowerCase() === srcLower && it.name.toLowerCase() === lowName); + if (!spellHasEnt) continue; + + const fromDetails = searchForExisting[lowName]; + + sp[propTmp].push(...fromDetails); + + // Only add it once regardless of how many entities match + break outer; + } + } + } + } + + _mutateSpell_brewGroup ({sp, lowName, lowSource}) { + if (!this._cache?.groups) return; + + if (this._cache.groups.spell?.[lowSource]?.[lowName]) { + Object.values(this._cache.groups.spell[lowSource][lowName]) + .forEach(bySource => { + Object.values(bySource) + .forEach(byName => { + sp._tmpGroups.push(byName); + }); + }); + } + + // TODO(Future) implement "copy existing list" + } + }; + + static populatePrereleaseLookup (brew, {isForce = false} = {}) { + Renderer.spell._spellSourceManagerPrerelease.populate({brew, isForce}); + } + + static populateBrewLookup (brew, {isForce = false} = {}) { + Renderer.spell._spellSourceManagerBrew.populate({brew, isForce}); + } + + static prePopulateHover (data) { + (data.spell || []).forEach(sp => Renderer.spell.initBrewSources(sp)); + } + + static prePopulateHoverPrerelease (data) { + Renderer.spell.populatePrereleaseLookup(data); + } + + static prePopulateHoverBrew (data) { + Renderer.spell.populateBrewLookup(data); + } + + /* -------------------------------------------- */ + + static _BREW_SOURCES_TMP_PROPS = [ + "_tmpSourcesInit", + "_tmpClasses", + "_tmpRaces", + "_tmpBackgrounds", + "_tmpFeats", + "_tmpOptionalfeatures", + "_tmpGroups", + ]; + static uninitBrewSources (sp) { + Renderer.spell._BREW_SOURCES_TMP_PROPS.forEach(prop => delete sp[prop]); + } + + static initBrewSources (sp) { + if (sp._tmpSourcesInit) return; + sp._tmpSourcesInit = true; + + sp._tmpClasses = {}; + sp._tmpRaces = []; + sp._tmpBackgrounds = []; + sp._tmpFeats = []; + sp._tmpOptionalfeatures = []; + sp._tmpGroups = []; + + const lowName = sp.name.toLowerCase(); + const lowSource = sp.source.toLowerCase(); + + for (const manager of [Renderer.spell._spellSourceManagerPrerelease, Renderer.spell._spellSourceManagerBrew]) { + manager.mutateSpell({spell: sp, lowName, lowSource}); + } + } + + static getCombinedClasses (sp, prop) { + return [ + ...((sp.classes || {})[prop] || []), + ...((sp._tmpClasses || {})[prop] || []), + ] + .filter(it => { + if (!ExcludeUtil.isInitialised) return true; + + switch (prop) { + case "fromClassList": + case "fromClassListVariant": { + const hash = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_CLASSES](it); + if (ExcludeUtil.isExcluded(hash, "class", it.source, {isNoCount: true})) return false; + + if (prop !== "fromClassListVariant") return true; + if (it.definedInSource) return !ExcludeUtil.isExcluded("*", "classFeature", it.definedInSource, {isNoCount: true}); + + return true; + } + case "fromSubclass": + case "fromSubclassVariant": { + const hash = UrlUtil.URL_TO_HASH_BUILDER["subclass"]({ + name: it.subclass.name, + shortName: it.subclass.shortName, + source: it.subclass.source, + className: it.class.name, + classSource: it.class.source, + }); + + if (prop !== "fromSubclassVariant") return !ExcludeUtil.isExcluded(hash, "subclass", it.subclass.source, {isNoCount: true}); + if (it.class.definedInSource) return !Renderer.spell.isExcludedSubclassVariantSource({classDefinedInSource: it.class.definedInSource}); + + return true; + } + default: throw new Error(`Unhandled prop "${prop}"`); + } + }); + } + + static isExcludedSubclassVariantSource ({classDefinedInSource, subclassDefinedInSource}) { + return (classDefinedInSource != null && ExcludeUtil.isExcluded("*", "classFeature", classDefinedInSource, {isNoCount: true})) + || (subclassDefinedInSource != null && ExcludeUtil.isExcluded("*", "subclassFeature", subclassDefinedInSource, {isNoCount: true})); + } + + static getCombinedGeneric (sp, {propSpell, prop}) { + const propSpellTmp = `_tmp${propSpell.uppercaseFirst()}`; + return [ + ...(sp[propSpell] || []), + ...(sp[propSpellTmp] || []), + ] + .filter(it => { + if (!ExcludeUtil.isInitialised || !prop) return true; + const hash = UrlUtil.URL_TO_HASH_BUILDER[prop](it); + return !ExcludeUtil.isExcluded(hash, prop, it.source, {isNoCount: true}); + }) + .sort(SortUtil.ascSortGenericEntity.bind(SortUtil)); + } + + /* -------------------------------------------- */ + + static pGetFluff (sp) { + return Renderer.utils.pGetFluff({ + entity: sp, + fluffBaseUrl: `data/spells/`, + fluffProp: "spellFluff", + }); + } +}; + +Renderer.spell._spellSourceManagerPrerelease = new Renderer.spell._SpellSourceManager(); +Renderer.spell._spellSourceManagerBrew = new Renderer.spell._SpellSourceManager(); + +Renderer.condition = class { + static getCompactRenderedString (cond) { + const renderer = Renderer.get(); + const renderStack = []; + + renderStack.push(` + ${Renderer.utils.getExcludedTr({entity: cond, dataProp: cond.__prop || cond._type, page: UrlUtil.PG_CONDITIONS_DISEASES})} + ${Renderer.utils.getNameTr(cond, {page: UrlUtil.PG_CONDITIONS_DISEASES})} + + `); + renderer.recursiveRender({entries: cond.entries}, renderStack); + renderStack.push(``); + + return renderStack.join(""); + } + + static pGetFluff (it) { + return Renderer.utils.pGetFluff({ + entity: it, + fnGetFluffData: it.__prop === "condition" ? DataUtil.conditionFluff.loadJSON.bind(DataUtil.conditionFluff) : null, + fluffProp: it.__prop === "condition" ? "conditionFluff" : "diseaseFluff", + }); + } +}; + +Renderer.background = class { + static getCompactRenderedString (bg) { + return Renderer.generic.getCompactRenderedString( + bg, + { + dataProp: "background", + page: UrlUtil.PG_BACKGROUNDS, + }, + ); + } + + static pGetFluff (bg) { + return Renderer.utils.pGetFluff({ + entity: bg, + fnGetFluffData: DataUtil.backgroundFluff.loadJSON.bind(DataUtil.backgroundFluff), + fluffProp: "backgroundFluff", + }); + } +}; + +Renderer.backgroundFeature = class { + static getCompactRenderedString (ent) { + return Renderer.generic.getCompactRenderedString(ent); + } +}; + +Renderer.optionalfeature = class { + static getListPrerequisiteLevelText (prerequisites) { + if (!prerequisites || !prerequisites.some(it => it.level)) return "\u2014"; + const levelPart = prerequisites.find(it => it.level).level; + return levelPart.level || levelPart; + } + + /* -------------------------------------------- */ + + static getPreviouslyPrintedEntry (ent) { + if (!ent.previousVersion) return null; + return `{@i An earlier version of this ${ent.featureType.map(t => Parser.optFeatureTypeToFull(t)).join("/")} is available in }${Parser.sourceJsonToFull(ent.previousVersion.source)} {@i as {@optfeature ${ent.previousVersion.name}|${ent.previousVersion.source}}.}`; + } + + static getTypeEntry (ent) { + return `{@note Type: ${Renderer.optionalfeature.getTypeText(ent)}}`; + } + + static getCostEntry (ent) { + if (!ent.consumes?.name) return null; + + const ptPrefix = "Cost: "; + const tksUnit = ent.consumes.name + .split(" ") + .map(it => it.trim()) + .filter(Boolean); + tksUnit.last(tksUnit.last()[ent.consumes.amount != null && ent.consumes.amount !== 1 ? "toPlural" : "toString"]()); + const ptUnit = ` ${tksUnit.join(" ")}`; + + if (ent.consumes?.amountMin != null && ent.consumes?.amountMax != null) return `{@i ${ptPrefix}${ent.consumes.amountMin}\u2013${ent.consumes.amountMax}${ptUnit}}`; + return `{@i ${ptPrefix}${ent.consumes.amount ?? 1}${ptUnit}}`; + } + + /* -------------------------------------------- */ + + static getPreviouslyPrintedText (ent) { + const entry = Renderer.optionalfeature.getPreviouslyPrintedEntry(ent); + if (!entry) return ""; + return `

    ${Renderer.get().render(entry)}

    `; + } + + static getTypeText (ent) { + const commonPrefix = ent.featureType.length > 1 ? MiscUtil.findCommonPrefix(ent.featureType.map(fs => Parser.optFeatureTypeToFull(fs)), {isRespectWordBoundaries: true}) : ""; + + return [ + commonPrefix.trim() || null, + ent.featureType.map(ft => Parser.optFeatureTypeToFull(ft).substring(commonPrefix.length)).join("/"), + ] + .filter(Boolean).join(" "); + } + + static getCostHtml (ent) { + const entry = Renderer.optionalfeature.getCostEntry(ent); + if (!entry) return ""; + + return Renderer.get().render(entry); + } + + static getCompactRenderedString (ent) { + const ptCost = Renderer.optionalfeature.getCostHtml(ent); + return ` + ${Renderer.utils.getExcludedTr({entity: ent, dataProp: "optionalfeature", page: UrlUtil.PG_OPT_FEATURES})} + ${Renderer.utils.getNameTr(ent, {page: UrlUtil.PG_OPT_FEATURES})} + + ${ent.prerequisite ? `

    ${Renderer.utils.prerequisite.getHtml(ent.prerequisite)}

    ` : ""} + ${ptCost ? `

    ${ptCost}

    ` : ""} + ${Renderer.get().render({entries: ent.entries}, 1)} + + ${Renderer.optionalfeature.getPreviouslyPrintedText(ent)} +

    ${Renderer.get().render(Renderer.optionalfeature.getTypeEntry(ent))}

    + `; + } + + static pGetFluff (ent) { + return Renderer.utils.pGetFluff({ + entity: ent, + fnGetFluffData: DataUtil.optionalfeatureFluff.loadJSON.bind(DataUtil.optionalfeatureFluff), + fluffProp: "optionalfeatureFluff", + }); + } +}; + +Renderer.reward = class { + static getRewardRenderableEntriesMeta (ent) { + const ptSubtitle = [ + (ent.type || "").toTitleCase(), + ent.rarity ? ent.rarity.toTitleCase() : "", + ] + .filter(Boolean) + .join(", "); + + return { + entriesContent: [ + ptSubtitle ? `{@i ${ptSubtitle}}` : "", + ...ent.entries, + ] + .filter(Boolean), + }; + } + + static getRenderedString (ent) { + const entriesMeta = Renderer.reward.getRewardRenderableEntriesMeta(ent); + return `${Renderer.get().setFirstSection(true).render({entries: entriesMeta.entriesContent}, 1)}`; + } + + static getCompactRenderedString (ent) { + return ` + ${Renderer.utils.getExcludedTr({entity: ent, dataProp: "reward", page: UrlUtil.PG_REWARDS})} + ${Renderer.utils.getNameTr(ent, {page: UrlUtil.PG_REWARDS})} + ${Renderer.reward.getRenderedString(ent)} + `; + } + + static pGetFluff (ent) { + return Renderer.utils.pGetFluff({ + entity: ent, + fnGetFluffData: DataUtil.rewardFluff.loadJSON.bind(DataUtil.rewardFluff), + fluffProp: "rewardFluff", + }); + } +}; + +Renderer.race = class { + static getRaceRenderableEntriesMeta (race) { + return { + entryMain: race._isBaseRace + ? {type: "entries", entries: race._baseRaceEntries} + : {type: "entries", entries: race.entries}, + }; + } + + static getCompactRenderedString (race, {isStatic = false} = {}) { + const renderer = Renderer.get(); + const renderStack = []; + + renderStack.push(` + ${Renderer.utils.getExcludedTr({entity: race, dataProp: "race", page: UrlUtil.PG_RACES})} + ${Renderer.utils.getNameTr(race, {page: UrlUtil.PG_RACES})} + + + + + + + + + + + + +
    Ability ScoresSizeSpeed
    ${Renderer.getAbilityData(race.ability).asText}${(race.size || [Parser.SZ_VARIES]).map(sz => Parser.sizeAbvToFull(sz)).join("/")}${Parser.getSpeedString(race)}
    + + + `); + renderer.recursiveRender(Renderer.race.getRaceRenderableEntriesMeta(race).entryMain, renderStack, {depth: 1}); + renderStack.push(""); + + const ptHeightWeight = Renderer.race.getHeightAndWeightPart(race, {isStatic}); + if (ptHeightWeight) renderStack.push(`
    ${ptHeightWeight}`); + + return renderStack.join(""); + } + + static getRenderedSize (race) { + return (race.size || [Parser.SZ_VARIES]).map(sz => Parser.sizeAbvToFull(sz)).join("/"); + } + + static getHeightAndWeightPart (race, {isStatic = false} = {}) { + if (!race.heightAndWeight) return null; + if (race._isBaseRace) return null; + return Renderer.get().render({entries: Renderer.race.getHeightAndWeightEntries(race, {isStatic})}); + } + + static getHeightAndWeightEntries (race, {isStatic = false} = {}) { + const colLabels = ["Base Height", "Base Weight", "Height Modifier", "Weight Modifier"]; + const colStyles = ["col-2-3 ve-text-center", "col-2-3 ve-text-center", "col-2-3 ve-text-center", "col-2 ve-text-center"]; + + const cellHeightMod = !isStatic + ? `+${race.heightAndWeight.heightMod}` + : `+${race.heightAndWeight.heightMod}`; + const cellWeightMod = !isStatic + ? `ร— ${race.heightAndWeight.weightMod || "1"} lb.` + : `ร— ${race.heightAndWeight.weightMod || "1"} lb.`; + + const row = [ + Renderer.race.getRenderedHeight(race.heightAndWeight.baseHeight), + `${race.heightAndWeight.baseWeight} lb.`, + cellHeightMod, + cellWeightMod, + ]; + + if (!isStatic) { + colLabels.push(""); + colStyles.push("col-3-1 ve-text-center"); + row.push(`
    +
    +
    =
    +
    +
    ;
    +
    +
    lb.
    +
    + +
    `); + } + + return [ + "You may roll for your character's height and weight on the Random Height and Weight table. The roll in the Height Modifier column adds a number (in inches) to the character's base height. To get a weight, multiply the number you rolled for height by the roll in the Weight Modifier column and add the result (in pounds) to the base weight.", + { + type: "table", + caption: "Random Height and Weight", + colLabels, + colStyles, + rows: [row], + }, + ]; + } + + static getRenderedHeight (height) { + const heightFeet = Number(Math.floor(height / 12).toFixed(3)); + const heightInches = Number((height % 12).toFixed(3)); + return `${heightFeet ? `${heightFeet}'` : ""}${heightInches ? `${heightInches}"` : ""}`; + } + + /** + * @param races + * @param [opts] Options object. + * @param [opts.isAddBaseRaces] If an entity should be created for each base race. + */ + static mergeSubraces (races, opts) { + opts = opts || {}; + + const out = []; + races.forEach(r => { + // FIXME(Deprecated) Backwards compatibility for old race data; remove at some point + if (r.size && typeof r.size === "string") r.size = [r.size]; + + // Ignore `"lineage": true`, as it is only used for filters + if (r.lineage && r.lineage !== true) { + r = MiscUtil.copyFast(r); + + if (r.lineage === "VRGR") { + r.ability = r.ability || [ + { + choose: { + weighted: { + from: [...Parser.ABIL_ABVS], + weights: [2, 1], + }, + }, + }, + { + choose: { + weighted: { + from: [...Parser.ABIL_ABVS], + weights: [1, 1, 1], + }, + }, + }, + ]; + } else if (r.lineage === "UA1") { + r.ability = r.ability || [ + { + choose: { + weighted: { + from: [...Parser.ABIL_ABVS], + weights: [2, 1], + }, + }, + }, + ]; + } + + r.entries = r.entries || []; + r.entries.push({ + type: "entries", + name: "Languages", + entries: ["You can speak, read, and write Common and one other language that you and your DM agree is appropriate for your character."], + }); + + r.languageProficiencies = r.languageProficiencies || [{"common": true, "anyStandard": 1}]; + } + + if (r.subraces && !r.subraces.length) delete r.subraces; + + if (r.subraces) { + r.subraces.forEach(sr => { + sr.source = sr.source || r.source; + sr._isSubRace = true; + }); + + r.subraces.sort((a, b) => SortUtil.ascSortLower(a.name || "_", b.name || "_") || SortUtil.ascSortLower(Parser.sourceJsonToAbv(a.source), Parser.sourceJsonToAbv(b.source))); + } + + if (opts.isAddBaseRaces && r.subraces) { + const baseRace = MiscUtil.copyFast(r); + + baseRace._isBaseRace = true; + + const isAnyNoName = r.subraces.some(it => !it.name); + if (isAnyNoName) { + baseRace._rawName = baseRace.name; + baseRace.name = `${baseRace.name} (Base)`; + } + + const nameCounts = {}; + r.subraces.filter(sr => sr.name).forEach(sr => nameCounts[sr.name.toLowerCase()] = (nameCounts[sr.name.toLowerCase()] || 0) + 1); + nameCounts._ = r.subraces.filter(sr => !sr.name).length; + + const lst = { + type: "list", + items: r.subraces.map(sr => { + const count = nameCounts[(sr.name || "_").toLowerCase()]; + const idName = Renderer.race.getSubraceName(r.name, sr.name); + return `{@race ${idName}|${sr.source}${count > 1 ? `|${idName} (${Parser.sourceJsonToAbv(sr.source)})` : ""}}`; + }), + }; + + Renderer.race._mutBaseRaceEntries(baseRace, lst); + baseRace._subraces = r.subraces.map(sr => ({name: Renderer.race.getSubraceName(r.name, sr.name), source: sr.source})); + + delete baseRace.subraces; + + out.push(baseRace); + } + + out.push(...Renderer.race._mergeSubraces(r)); + }); + + return out; + } + + static _mutMakeBaseRace (baseRace) { + if (baseRace._isBaseRace) return; + + baseRace._isBaseRace = true; + + Renderer.race._mutBaseRaceEntries(baseRace, {type: "list", items: []}); + } + + static _mutBaseRaceEntries (baseRace, lst) { + baseRace._baseRaceEntries = [ + { + type: "section", + entries: [ + "This race has multiple subraces, as listed below:", + lst, + ], + }, + { + type: "section", + entries: [ + { + type: "entries", + entries: [ + { + type: "entries", + name: "Traits", + entries: [ + ...MiscUtil.copyFast(baseRace.entries), + ], + }, + ], + }, + ], + }, + ]; + } + + static getSubraceName (raceName, subraceName) { + if (!subraceName) return raceName; + + const mBrackets = /^(.*?)(\(.*?\))$/i.exec(raceName || ""); + if (!mBrackets) return `${raceName} (${subraceName})`; + + const bracketPart = mBrackets[2].substring(1, mBrackets[2].length - 1); + return `${mBrackets[1]}(${[bracketPart, subraceName].join("; ")})`; + } + + static _mergeSubraces (race) { + if (!race.subraces) return [race]; + return MiscUtil.copyFast(race.subraces).map(s => Renderer.race._getMergedSubrace(race, s)); + } + + static _getMergedSubrace (race, cpySr) { + const cpy = MiscUtil.copyFast(race); + cpy._baseName = cpy.name; + cpy._baseSource = cpy.source; + cpy._baseSrd = cpy.srd; + cpy._baseBasicRules = cpy.basicRules; + delete cpy.subraces; + delete cpy.srd; + delete cpy.basicRules; + delete cpy._versions; + delete cpy.hasFluff; + delete cpy.hasFluffImages; + delete cpySr.__prop; + + // merge names, abilities, entries, tags + if (cpySr.name) { + cpy._subraceName = cpySr.name; + + if (cpySr.alias) { + cpy.alias = cpySr.alias.map(it => Renderer.race.getSubraceName(cpy.name, it)); + delete cpySr.alias; + } + + cpy.name = Renderer.race.getSubraceName(cpy.name, cpySr.name); + delete cpySr.name; + } + if (cpySr.ability) { + // If the base race doesn't have any ability scores, make a set of empty records + if ((cpySr.overwrite && cpySr.overwrite.ability) || !cpy.ability) cpy.ability = cpySr.ability.map(() => ({})); + + if (cpy.ability.length !== cpySr.ability.length) throw new Error(`Race and subrace ability array lengths did not match!`); + cpySr.ability.forEach((obj, i) => Object.assign(cpy.ability[i], obj)); + delete cpySr.ability; + } + if (cpySr.entries) { + cpySr.entries.forEach(ent => { + if (!ent.data?.overwrite) return cpy.entries.push(ent); + + const toOverwrite = cpy.entries.findIndex(it => it.name?.toLowerCase()?.trim() === ent.data.overwrite.toLowerCase().trim()); + if (~toOverwrite) cpy.entries[toOverwrite] = ent; + else cpy.entries.push(ent); + }); + delete cpySr.entries; + } + + if (cpySr.traitTags) { + if (cpySr.overwrite && cpySr.overwrite.traitTags) cpy.traitTags = cpySr.traitTags; + else cpy.traitTags = (cpy.traitTags || []).concat(cpySr.traitTags); + delete cpySr.traitTags; + } + + if (cpySr.languageProficiencies) { + if (cpySr.overwrite && cpySr.overwrite.languageProficiencies) cpy.languageProficiencies = cpySr.languageProficiencies; + else cpy.languageProficiencies = cpy.languageProficiencies = (cpy.languageProficiencies || []).concat(cpySr.languageProficiencies); + delete cpySr.languageProficiencies; + } + + // TODO make a generalised merge system? Probably have one of those lying around somewhere [bestiary schema?] + if (cpySr.skillProficiencies) { + // Overwrite if possible + if (!cpy.skillProficiencies || (cpySr.overwrite && cpySr.overwrite["skillProficiencies"])) cpy.skillProficiencies = cpySr.skillProficiencies; + else { + if (!cpySr.skillProficiencies.length || !cpy.skillProficiencies.length) throw new Error(`No items!`); + if (cpySr.skillProficiencies.length > 1 || cpy.skillProficiencies.length > 1) throw new Error(`Subrace merging does not handle choices!`); // Implement if required + + // Otherwise, merge + if (cpySr.skillProficiencies.choose) { + if (cpy.skillProficiencies.choose) throw new Error(`Subrace choose merging is not supported!!`); // Implement if required + cpy.skillProficiencies.choose = cpySr.skillProficiencies.choose; + delete cpySr.skillProficiencies.choose; + } + Object.assign(cpy.skillProficiencies[0], cpySr.skillProficiencies[0]); + } + + delete cpySr.skillProficiencies; + } + + // overwrite everything else + Object.assign(cpy, cpySr); + + // For any null'd out fields on the subrace, delete the field + Object.entries(cpy) + .forEach(([k, v]) => { + if (v != null) return; + delete cpy[k]; + }); + + return cpy; + } + + static adoptSubraces (allRaces, subraces) { + const nxtData = []; + + subraces.forEach(sr => { + if (!sr.raceName || !sr.raceSource) throw new Error(`Subrace was missing parent "raceName" and/or "raceSource"!`); + + const _baseRace = allRaces.find(r => r.name === sr.raceName && r.source === sr.raceSource); + if (!_baseRace) throw new Error(`Could not find parent race for subrace "${sr.name}" (${sr.source})!`); + + // Avoid adding duplicates, by tracking already-seen subraces + if ((_baseRace._seenSubraces || []).some(it => it.name === sr.name && it.source === sr.source)) return; + (_baseRace._seenSubraces = _baseRace._seenSubraces || []).push({name: sr.name, source: sr.source}); + + // If this is a prerelease/homebrew "base race" which is not marked as such, upgrade it to a base race + if ( + !_baseRace._isBaseRace + && (PrereleaseUtil.hasSourceJson(_baseRace.source) || BrewUtil2.hasSourceJson(_baseRace.source)) + ) { + Renderer.race._mutMakeBaseRace(_baseRace); + } + + // If the base race is a _real_ base race, add our new subrace to its list of subraces + if (_baseRace._isBaseRace) { + const subraceListEntry = ((_baseRace._baseRaceEntries[0] || {}).entries || []).find(it => it.type === "list"); + subraceListEntry.items.push(`{@race ${_baseRace._rawName || _baseRace.name} (${sr.name})|${sr.source || _baseRace.source}}`); + } + + // Attempt to graft multiple subraces from the same data set onto the same base race copy + let baseRace = nxtData.find(r => r.name === sr.raceName && r.source === sr.raceSource); + if (!baseRace) { + // copy and remove base-race-specific data + baseRace = MiscUtil.copyFast(_baseRace); + if (baseRace._rawName) { + baseRace.name = baseRace._rawName; + delete baseRace._rawName; + } + delete baseRace._isBaseRace; + delete baseRace._baseRaceEntries; + + nxtData.push(baseRace); + } + + baseRace.subraces = baseRace.subraces || []; + baseRace.subraces.push(sr); + }); + + return nxtData; + } + + static bindListenersHeightAndWeight (race, ele) { + if (!race.heightAndWeight) return; + if (race._isBaseRace) return; + + const $render = $(ele); + + const $dispResult = $render.find(`.race__disp-result-height-weight`); + const $dispHeight = $render.find(`.race__disp-result-height`); + const $dispWeight = $render.find(`.race__disp-result-weight`); + + const lock = new VeLock(); + let hasRolled = false; + let resultHeight; + let resultWeightMod; + + const $btnRollHeight = $render + .find(`[data-race-heightmod="true"]`) + .html(race.heightAndWeight.heightMod) + .addClass("roller") + .mousedown(evt => evt.preventDefault()) + .click(async () => { + try { + await lock.pLock(); + + if (!hasRolled) return pDoFullRoll(true); + await pRollHeight(); + updateDisplay(); + } finally { + lock.unlock(); + } + }); + + const isWeightRoller = race.heightAndWeight.weightMod && isNaN(race.heightAndWeight.weightMod); + const $btnRollWeight = $render + .find(`[data-race-weightmod="true"]`) + .html(isWeightRoller ? `(${race.heightAndWeight.weightMod})` : race.heightAndWeight.weightMod || "1") + .click(async () => { + try { + await lock.pLock(); + + if (!hasRolled) return pDoFullRoll(true); + await pRollWeight(); + updateDisplay(); + } finally { + lock.unlock(); + } + }); + if (isWeightRoller) $btnRollWeight.mousedown(evt => evt.preventDefault()); + + const $btnRoll = $render + .find(`button.race__btn-roll-height-weight`) + .click(async () => pDoFullRoll()); + + const pRollHeight = async () => { + const mResultHeight = await Renderer.dice.pRoll2(race.heightAndWeight.heightMod, { + isUser: false, + label: "Height Modifier", + name: race.name, + }); + if (mResultHeight == null) return; + resultHeight = mResultHeight; + }; + + const pRollWeight = async () => { + const weightModRaw = race.heightAndWeight.weightMod || "1"; + const mResultWeightMod = isNaN(weightModRaw) ? await Renderer.dice.pRoll2(weightModRaw, { + isUser: false, + label: "Weight Modifier", + name: race.name, + }) : Number(weightModRaw); + if (mResultWeightMod == null) return; + resultWeightMod = mResultWeightMod; + }; + + const updateDisplay = () => { + const renderedHeight = Renderer.race.getRenderedHeight(race.heightAndWeight.baseHeight + resultHeight); + const totalWeight = race.heightAndWeight.baseWeight + (resultWeightMod * resultHeight); + $dispHeight.text(renderedHeight); + $dispWeight.text(Number(totalWeight.toFixed(3))); + }; + + const pDoFullRoll = async isPreLocked => { + try { + if (!isPreLocked) await lock.pLock(); + + $btnRoll.parent().removeClass(`ve-flex-vh-center`).addClass(`split-v-center`); + await pRollHeight(); + await pRollWeight(); + $dispResult.removeClass(`ve-hidden`); + updateDisplay(); + + hasRolled = true; + } finally { + if (!isPreLocked) lock.unlock(); + } + }; + } + + static bindListenersCompact (race, ele) { + Renderer.race.bindListenersHeightAndWeight(race, ele); + } + + static pGetFluff (race) { + return Renderer.utils.pGetFluff({ + entity: race, + fnGetFluffData: DataUtil.raceFluff.loadJSON.bind(DataUtil.raceFluff), + fluffProp: "raceFluff", + }); + } +}; + +Renderer.raceFeature = class { + static getCompactRenderedString (ent) { + return Renderer.generic.getCompactRenderedString(ent); + } +}; + +Renderer.deity = class { + static _BASE_PART_TRANSLATORS = { + "alignment": { + name: "Alignment", + displayFn: (it) => it.map(a => Parser.alignmentAbvToFull(a)).join(" ").toTitleCase(), + }, + "pantheon": { + name: "Pantheon", + }, + "category": { + name: "Category", + displayFn: it => typeof it === "string" ? it : it.join(", "), + }, + "domains": { + name: "Domains", + displayFn: (it) => it.join(", "), + }, + "province": { + name: "Province", + }, + "altNames": { + name: "Alternate Names", + displayFn: (it) => it.join(", "), + }, + "symbol": { + name: "Symbol", + }, + }; + + static getDeityRenderableEntriesMeta (ent) { + return { + entriesAttributes: [ + ...Object.entries(Renderer.deity._BASE_PART_TRANSLATORS) + .map(([prop, {name, displayFn}]) => { + if (ent[prop] == null) return null; + + const displayVal = displayFn ? displayFn(ent[prop]) : ent[prop]; + return { + name, + entry: `{@b ${name}:} ${displayVal}`, + }; + }) + .filter(Boolean), + ...Object.entries(ent.customProperties || {}) + .map(([name, val]) => ({ + name, + entry: `{@b ${name}:} ${val}`, + })), + ] + .sort(({name: nameA}, {name: nameB}) => SortUtil.ascSortLower(nameA, nameB)) + .map(({entry}) => entry), + }; + } + + static getCompactRenderedString (ent) { + const renderer = Renderer.get(); + const entriesMeta = Renderer.deity.getDeityRenderableEntriesMeta(ent); + return ` + ${Renderer.utils.getExcludedTr({entity: ent, dataProp: "deity", page: UrlUtil.PG_DEITIES})} + ${Renderer.utils.getNameTr(ent, {suffix: ent.title ? `, ${ent.title.toTitleCase()}` : "", page: UrlUtil.PG_DEITIES})} + + ${entriesMeta.entriesAttributes.map(entry => `
    ${Renderer.get().render(entry)}
    `).join("")} + + ${ent.entries ? `
    ${renderer.render({entries: ent.entries}, 1)}` : ""} + `; + } +}; + +Renderer.object = class { + static CHILD_PROPS = ["actionEntries"]; + + /* -------------------------------------------- */ + + static RENDERABLE_ENTRIES_PROP_ORDER__ATTRIBUTES = [ + "entryCreatureCapacity", + "entryCargoCapacity", + "entryArmorClass", + "entryHitPoints", + "entrySpeed", + "entryAbilityScores", + "entryDamageImmunities", + "entryDamageResistances", + "entryDamageVulnerabilities", + "entryConditionImmunities", + "entrySenses", + ]; + + static getObjectRenderableEntriesMeta (ent) { + return { + entrySize: `{@i ${ent.objectType !== "GEN" ? `${Renderer.utils.getRenderedSize(ent.size)} ${ent.creatureType ? Parser.monTypeToFullObj(ent.creatureType).asText : "object"}` : `Variable size object`}}`, + + entryCreatureCapacity: ent.capCrew != null || ent.capPassenger != null + ? `{@b Creature Capacity:} ${Renderer.vehicle.getShipCreatureCapacity(ent)}` + : null, + entryCargoCapacity: ent.capCargo != null + ? `{@b Cargo Capacity:} ${Renderer.vehicle.getShipCargoCapacity(ent)}` + : null, + entryArmorClass: ent.ac != null + ? `{@b Armor Class:} ${ent.ac.special ?? ent.ac}` + : null, + entryHitPoints: ent.hp != null + ? `{@b Hit Points:} ${ent.hp.special ?? ent.hp}` + : null, + entrySpeed: ent.speed != null + ? `{@b Speed:} ${Parser.getSpeedString(ent)}` + : null, + entryAbilityScores: Parser.ABIL_ABVS.some(ab => ent[ab] != null) + ? `{@b Ability Scores:} ${Parser.ABIL_ABVS.filter(ab => ent[ab] != null).map(ab => `${ab.toUpperCase()} ${Renderer.utils.getAbilityRollerEntry(ent, ab)}`).join(", ")}` + : null, + entryDamageImmunities: ent.immune != null + ? `{@b Damage Immunities:} ${Parser.getFullImmRes(ent.immune)}` + : null, + entryDamageResistances: ent.resist + ? `{@b Damage Resistances:} ${Parser.getFullImmRes(ent.resist)}` + : null, + entryDamageVulnerabilities: ent.vulnerable + ? `{@b Damage Vulnerabilities:} ${Parser.getFullImmRes(ent.vulnerable)}` + : null, + entryConditionImmunities: ent.conditionImmune + ? `{@b Condition Immunities:} ${Parser.getFullCondImm(ent.conditionImmune, {isEntry: true})}` + : null, + entrySenses: ent.senses + ? `{@b Senses:} ${Renderer.utils.getSensesEntry(ent.senses)}` + : null, + }; + } + + /* -------------------------------------------- */ + + static getCompactRenderedString (obj, opts) { + return Renderer.object.getRenderedString(obj, {...opts, isCompact: true}); + } + + static getRenderedString (ent, opts) { + opts = opts || {}; + + const renderer = Renderer.get().setFirstSection(true); + + const hasToken = Renderer.object.hasToken(ent); + const extraThClasses = !opts.isCompact && hasToken ? ["objs__name--token"] : null; + + const entriesMeta = Renderer.object.getObjectRenderableEntriesMeta(ent); + + const ptAttribs = Renderer.object.RENDERABLE_ENTRIES_PROP_ORDER__ATTRIBUTES + .filter(prop => entriesMeta[prop]) + .map(prop => `${Renderer.get().render(entriesMeta[prop])}
    `) + .join(""); + + return ` + ${Renderer.utils.getExcludedTr({entity: ent, dataProp: "object", page: opts.page || UrlUtil.PG_OBJECTS})} + ${Renderer.utils.getNameTr(ent, {page: opts.page || UrlUtil.PG_OBJECTS, extraThClasses, isEmbeddedEntity: opts.isEmbeddedEntity})} + ${Renderer.get().render(entriesMeta.entrySize)} + ${ptAttribs} + + ${ent.entries ? renderer.render({entries: ent.entries}, 2) : ""} + ${ent.actionEntries ? renderer.render({entries: ent.actionEntries}, 2) : ""} + + `; + } + + static hasToken (obj, opts) { + return Renderer.generic.hasToken(obj, opts); + } + + static getTokenUrl (obj, opts) { + return Renderer.generic.getTokenUrl(obj, "objects/tokens", opts); + } + + static pGetFluff (obj) { + return Renderer.utils.pGetFluff({ + entity: obj, + fnGetFluffData: DataUtil.objectFluff.loadJSON.bind(DataUtil.objectFluff), + fluffProp: "objectFluff", + }); + } +}; + +Renderer.trap = class { + static CHILD_PROPS = ["trigger", "effect", "eActive", "eDynamic", "eConstant", "countermeasures"]; + + static getTrapRenderableEntriesMeta (ent) { + return { + entriesAttributes: [ + // region Shared between simple/complex + ent.trigger ? { + type: "entries", + name: "Trigger", + entries: ent.trigger, + } : null, + // endregion + + // region Simple traps + ent.effect ? { + type: "entries", + name: "Effect", + entries: ent.effect, + } : null, + // endregion + + // region Complex traps + ent.initiative ? { + type: "entries", + name: "Initiative", + entries: Renderer.trap.getTrapInitiativeEntries(ent), + } : null, + ent.eActive ? { + type: "entries", + name: "Active Elements", + entries: ent.eActive, + } : null, + ent.eDynamic ? { + type: "entries", + name: "Dynamic Elements", + entries: ent.eDynamic, + } : null, + ent.eConstant ? { + type: "entries", + name: "Constant Elements", + entries: ent.eConstant, + } : null, + // endregion + + // region Shared between simple/complex + ent.countermeasures ? { + type: "entries", + name: "Countermeasures", + entries: ent.countermeasures, + } : null, + // endregion + ] + .filter(Boolean), + }; + } + + static getTrapInitiativeEntries (ent) { return [`The trap acts on ${Parser.trapInitToFull(ent.initiative)}${ent.initiativeNote ? ` (${ent.initiativeNote})` : ""}.`]; } + + static getRenderedTrapPart (renderer, ent) { + const entriesMeta = Renderer.trap.getTrapRenderableEntriesMeta(ent); + if (!entriesMeta.entriesAttributes.length) return ""; + return renderer.render({entries: entriesMeta.entriesAttributes}, 1); + } + + static getCompactRenderedString (ent, opts) { + return Renderer.traphazard.getCompactRenderedString(ent, opts); + } + + static pGetFluff (ent) { return Renderer.traphazard.pGetFluff(ent); } +}; + +Renderer.hazard = class { + static getCompactRenderedString (ent, opts) { + return Renderer.traphazard.getCompactRenderedString(ent, opts); + } + + static pGetFluff (ent) { return Renderer.traphazard.pGetFluff(ent); } +}; + +Renderer.traphazard = class { + static getSubtitle (ent) { + const type = ent.trapHazType || "HAZ"; + if (type === "GEN") return null; + + const ptThreat = ent.threat ? ent.threat.toTitleCase() : null; + + const ptTypeThreat = [ + Parser.trapHazTypeToFull(type), + ent.threat ? ent.threat.toTitleCase() : null, + ] + .filter(Boolean) + .join(", "); + + const parenPart = [ + ent.tier ? Parser.tierToFullLevel(ent.tier) : null, + Renderer.traphazard.getTrapLevelPart(ent), + ] + .filter(Boolean) + .join(", "); + + return parenPart ? `${ptTypeThreat} (${parenPart})` : ptTypeThreat; + } + + static getTrapLevelPart (ent) { + return ent.level?.min != null && ent.level?.max != null + ? `level ${ent.level.min}${ent.level.min !== ent.level.max ? `\u2013${ent.level.max}` : ""}` + : null; + } + + static getCompactRenderedString (ent, opts) { + opts = opts || {}; + + const renderer = Renderer.get(); + const subtitle = Renderer.traphazard.getSubtitle(ent); + return ` + ${Renderer.utils.getExcludedTr({entity: ent, dataProp: ent.__prop, page: UrlUtil.PG_TRAPS_HAZARDS})} + ${Renderer.utils.getNameTr(ent, {page: UrlUtil.PG_TRAPS_HAZARDS, isEmbeddedEntity: opts.isEmbeddedEntity})} + ${subtitle ? `${subtitle}` : ""} + + ${renderer.render({entries: ent.entries}, 2)} + ${Renderer.trap.getRenderedTrapPart(renderer, ent)} + + `; + } + + static pGetFluff (ent) { + return Renderer.utils.pGetFluff({ + entity: ent, + fnGetFluffData: ent.__prop === "trap" ? DataUtil.trapFluff.loadJSON.bind(DataUtil.trapFluff) : DataUtil.hazardFluff.loadJSON.bind(DataUtil.hazardFluff), + fluffProp: ent.__prop === "trap" ? "trapFluff" : "hazardFluff", + }); + } +}; + +Renderer.cultboon = class { + static getCultRenderableEntriesMeta (ent) { + if (!ent.goal && !ent.cultists && !ent.signaturespells) return null; + + const fauxList = { + type: "list", + style: "list-hang-notitle", + items: [], + }; + + if (ent.goal) { + fauxList.items.push({ + type: "item", + name: "Goals:", + entry: ent.goal.entry, + }); + } + + if (ent.cultists) { + fauxList.items.push({ + type: "item", + name: "Typical Cultists:", + entry: ent.cultists.entry, + }); + } + if (ent.signaturespells) { + fauxList.items.push({ + type: "item", + name: "Signature Spells:", + entry: ent.signaturespells.entry, + }); + } + + return {listGoalsCultistsSpells: fauxList}; + } + + static doRenderCultParts (ent, renderer, renderStack) { + const cultEntriesMeta = Renderer.cultboon.getCultRenderableEntriesMeta(ent); + if (!cultEntriesMeta) return; + renderer.recursiveRender(cultEntriesMeta.listGoalsCultistsSpells, renderStack, {depth: 2}); + } + + /* -------------------------------------------- */ + + static getBoonRenderableEntriesMeta (ent) { + if (!ent.ability && !ent.signaturespells) return null; + + const benefits = {type: "list", style: "list-hang-notitle", items: []}; + + if (ent.ability) { + benefits.items.push({ + type: "item", + name: "Ability Score Adjustment:", + entry: ent.ability ? ent.ability.entry : "None", + }); + } + + if (ent.signaturespells) { + benefits.items.push({ + type: "item", + name: "Signature Spells:", + entry: ent.signaturespells ? ent.signaturespells.entry : "None", + }); + } + + return {listBenefits: benefits}; + } + + static doRenderBoonParts (ent, renderer, renderStack) { + const boonEntriesMeta = Renderer.cultboon.getBoonRenderableEntriesMeta(ent); + if (!boonEntriesMeta) return; + renderer.recursiveRender(boonEntriesMeta.listBenefits, renderStack, {depth: 1}); + } + + /* -------------------------------------------- */ + + static _getCompactRenderedString_cult ({ent, renderer}) { + const renderStack = []; + + Renderer.cultboon.doRenderCultParts(ent, renderer, renderStack); + renderer.recursiveRender({entries: ent.entries}, renderStack, {depth: 2}); + + return `${Renderer.utils.getExcludedTr({entity: ent, dataProp: "cult", page: UrlUtil.PG_CULTS_BOONS})} + ${Renderer.utils.getNameTr(ent, {page: UrlUtil.PG_CULTS_BOONS})} +
    + ${renderStack.join("")}`; + } + + static _getCompactRenderedString_boon ({ent, renderer}) { + const renderStack = []; + + Renderer.cultboon.doRenderBoonParts(ent, renderer, renderStack); + renderer.recursiveRender({entries: ent.entries}, renderStack, {depth: 1}); + ent._displayName = ent._displayName || ent.name; + + return `${Renderer.utils.getExcludedTr({entity: ent, dataProp: "boon", page: UrlUtil.PG_CULTS_BOONS})} + ${Renderer.utils.getNameTr(ent, {page: UrlUtil.PG_CULTS_BOONS})} + ${renderStack.join("")}`; + } + + static getCompactRenderedString (ent) { + const renderer = Renderer.get(); + switch (ent.__prop) { + case "cult": return Renderer.cultboon._getCompactRenderedString_cult({ent, renderer}); + case "boon": return Renderer.cultboon._getCompactRenderedString_boon({ent, renderer}); + default: throw new Error(`Unhandled prop "${ent.__prop}"`); + } + } +}; + +Renderer.monster = class { + static CHILD_PROPS = ["action", "bonus", "reaction", "trait", "legendary", "mythic", "variant", "spellcasting"]; + + static getShortName (mon, {isTitleCase = false, isSentenceCase = false, isUseDisplayName = false} = {}) { + const name = isUseDisplayName ? (mon._displayName ?? mon.name) : mon.name; + const shortName = isUseDisplayName ? (mon._displayShortName ?? mon.shortName) : mon.shortName; + + const prefix = mon.isNamedCreature ? "" : isTitleCase || isSentenceCase ? "The " : "the "; + if (shortName === true) return `${prefix}${name}`; + else if (shortName) return `${prefix}${!prefix && isTitleCase ? shortName.toTitleCase() : shortName.toLowerCase()}`; + + const out = Renderer.monster.getShortNameFromName(name, {isNamedCreature: mon.isNamedCreature}); + return `${prefix}${out}`; + } + + static getShortNameFromName (name, {isNamedCreature = false} = {}) { + const base = name.split(",")[0]; + let out = base + .replace(/(?:adult|ancient|young) \w+ (dragon|dracolich)/gi, "$1"); + out = isNamedCreature ? out.split(" ")[0] : out.toLowerCase(); + return out; + } + + static getLegendaryActionIntro (mon, {renderer = Renderer.get(), isUseDisplayName = false} = {}) { + return renderer.render(Renderer.monster.getLegendaryActionIntroEntry(mon, {isUseDisplayName})); + } + + static getLegendaryActionIntroEntry (mon, {isUseDisplayName = false} = {}) { + if (mon.legendaryHeader) { + return {entries: mon.legendaryHeader}; + } + + const legendaryActions = mon.legendaryActions || 3; + const legendaryNameTitle = Renderer.monster.getShortName(mon, {isTitleCase: true, isUseDisplayName}); + return { + entries: [ + `${legendaryNameTitle} can take ${legendaryActions} legendary action${legendaryActions > 1 ? "s" : ""}, choosing from the options below. Only one legendary action can be used at a time and only at the end of another creature's turn. ${legendaryNameTitle} regains spent legendary actions at the start of its turn.`, + ], + }; + } + + static getSectionIntro (mon, {renderer = Renderer.get(), prop}) { + const headerProp = `${prop}Header`; + if (mon[headerProp]) return renderer.render({entries: mon[headerProp]}); + return ""; + } + + static getSave (renderer, attr, mod) { + if (attr === "special") return renderer.render(mod); + return renderer.render(`${attr.uppercaseFirst()} {@savingThrow ${attr} ${mod}}`); + } + + static dragonCasterVariant = class { + // Community-created (legacy) + static _LVL_TO_COLOR_TO_SPELLS__UNOFFICIAL = { + 2: { + black: ["darkness", "Melf's acid arrow", "fog cloud", "scorching ray"], + green: ["ray of sickness", "charm person", "detect thoughts", "invisibility", "suggestion"], + white: ["ice knife|XGE", "Snilloc's snowball swarm|XGE"], + brass: ["see invisibility", "magic mouth", "blindness/deafness", "sleep", "detect thoughts"], + bronze: ["gust of wind", "misty step", "locate object", "blur", "witch bolt", "thunderwave", "shield"], + copper: ["knock", "sleep", "detect thoughts", "blindness/deafness", "tasha's hideous laughter"], + }, + 3: { + blue: ["wall of sand|XGE", "thunder step|XGE", "lightning bolt", "blink", "magic missile", "slow"], + red: ["fireball", "scorching ray", "haste", "erupting earth|XGE", "Aganazzar's scorcher|XGE"], + gold: ["slow", "fireball", "dispel magic", "counterspell", "Aganazzar's scorcher|XGE", "shield"], + silver: ["sleet storm", "protection from energy", "catnap|XGE", "locate object", "identify", "Leomund's tiny hut"], + }, + 4: { + black: ["vitriolic sphere|XGE", "sickening radiance|XGE", "Evard's black tentacles", "blight", "hunger of Hadar"], + white: ["fire shield", "ice storm", "sleet storm"], + brass: ["charm monster|XGE", "sending", "wall of sand|XGE", "hypnotic pattern", "tongues"], + copper: ["polymorph", "greater invisibility", "confusion", "stinking cloud", "major image", "charm monster|XGE"], + }, + 5: { + blue: ["telekinesis", "hold monster", "dimension door", "wall of stone", "wall of force"], + green: ["cloudkill", "charm monster|XGE", "modify memory", "mislead", "hallucinatory terrain", "dimension door"], + bronze: ["steel wind strike|XGE", "control winds|XGE", "watery sphere|XGE", "storm sphere|XGE", "tidal wave|XGE"], + gold: ["hold monster", "immolation|XGE", "wall of fire", "greater invisibility", "dimension door"], + silver: ["cone of cold", "ice storm", "teleportation circle", "skill empowerment|XGE", "creation", "Mordenkainen's private sanctum"], + }, + 6: { + white: ["cone of cold", "wall of ice"], + brass: ["scrying", "Rary's telepathic bond", "Otto's irresistible dance", "legend lore", "hold monster", "dream"], + }, + 7: { + black: ["power word pain|XGE", "finger of death", "disintegrate", "hold monster"], + blue: ["chain lightning", "forcecage", "teleport", "etherealness"], + green: ["project image", "mirage arcane", "prismatic spray", "teleport"], + bronze: ["whirlwind|XGE", "chain lightning", "scatter|XGE", "teleport", "disintegrate", "lightning bolt"], + copper: ["symbol", "simulacrum", "reverse gravity", "project image", "Bigby's hand", "mental prison|XGE", "seeming"], + silver: ["Otiluke's freezing sphere", "prismatic spray", "wall of ice", "contingency", "arcane gate"], + }, + 8: { + gold: ["sunburst", "delayed blast fireball", "antimagic field", "teleport", "globe of invulnerability", "maze"], + }, + }; + // From Fizban's Treasury of Dragons + static _LVL_TO_COLOR_TO_SPELLS__FTD = { + 1: { + deep: ["command", "dissonant whispers", "faerie fire"], + }, + 2: { + black: ["blindness/deafness", "create or destroy water"], + green: ["invisibility", "speak with animals"], + white: ["gust of wind"], + brass: ["create or destroy water", "speak with animals"], + bronze: ["beast sense", "detect thoughts", "speak with animals"], + copper: ["lesser restoration", "phantasmal force"], + }, + 3: { + blue: ["create or destroy water", "major image"], + red: ["bane", "heat metal", "hypnotic pattern", "suggestion"], + gold: ["bless", "cure wounds", "slow", "suggestion", "zone of truth"], + silver: ["beacon of hope", "calm emotions", "hold person", "zone of truth"], + deep: ["command", "dissonant whispers", "faerie fire", "water breathing"], + }, + 4: { + black: ["blindness/deafness", "create or destroy water", "plant growth"], + white: ["gust of wind"], + brass: ["create or destroy water", "speak with animals", "suggestion"], + copper: ["lesser restoration", "phantasmal force", "stone shape"], + }, + 5: { + blue: ["arcane eye", "create or destroy water", "major image"], + red: ["bane", "dominate person", "heat metal", "hypnotic pattern", "suggestion"], + green: ["invisibility", "plant growth", "speak with animals"], + bronze: ["beast sense", "control water", "detect thoughts", "speak with animals"], + gold: ["bless", "commune", "cure wounds", "geas", "slow", "suggestion", "zone of truth"], + silver: ["beacon of hope", "calm emotions", "hold person", "polymorph", "zone of truth"], + }, + 6: { + white: ["gust of wind", "ice storm"], + brass: ["create or destroy water", "locate creature", "speak with animals", "suggestion"], + deep: ["command", "dissonant whispers", "faerie fire", "passwall", "water breathing"], + }, + 7: { + black: ["blindness/deafness", "create or destroy water", "insect plague", "plant growth"], + blue: ["arcane eye", "create or destroy water", "major image", "project image"], + red: ["bane", "dominate person", "heat metal", "hypnotic pattern", "power word stun", "suggestion"], + green: ["invisibility", "mass suggestion", "plant growth", "speak with animals"], + bronze: ["beast sense", "control water", "detect thoughts", "heroes' feast", "speak with animals"], + copper: ["lesser restoration", "move earth", "phantasmal force", "stone shape"], + silver: ["beacon of hope", "calm emotions", "hold person", "polymorph", "teleport", "zone of truth"], + }, + 8: { + gold: ["bless", "commune", "cure wounds", "geas", "plane shift", "slow", "suggestion", "word of recall", "zone of truth"], + }, + }; + + static getAvailableColors () { + const out = new Set(); + + const add = (lookup) => Object.values(lookup).forEach(obj => Object.keys(obj).forEach(k => out.add(k))); + add(Renderer.monster.dragonCasterVariant._LVL_TO_COLOR_TO_SPELLS__UNOFFICIAL); + add(Renderer.monster.dragonCasterVariant._LVL_TO_COLOR_TO_SPELLS__FTD); + + return [...out].sort(SortUtil.ascSortLower); + } + + static hasCastingColorVariant (dragon) { + // if the dragon already has a spellcasting trait specified, don't add a note about adding a spellcasting trait + return dragon.dragonCastingColor && !dragon.spellcasting; + } + + static getMeta (dragon) { + const chaMod = Parser.getAbilityModNumber(dragon.cha); + const pb = Parser.crToPb(dragon.cr); + const maxSpellLevel = Math.floor(Parser.crToNumber(dragon.cr) / 3); + + return { + chaMod, + pb, + maxSpellLevel, + spellSaveDc: pb + chaMod + 8, + spellToHit: pb + chaMod, + exampleSpellsUnofficial: Renderer.monster.dragonCasterVariant._getMeta_getExampleSpells({ + dragon, + maxSpellLevel, + spellLookup: Renderer.monster.dragonCasterVariant._LVL_TO_COLOR_TO_SPELLS__UNOFFICIAL, + }), + exampleSpellsFtd: Renderer.monster.dragonCasterVariant._getMeta_getExampleSpells({ + dragon, + maxSpellLevel, + spellLookup: Renderer.monster.dragonCasterVariant._LVL_TO_COLOR_TO_SPELLS__FTD, + }), + }; + } + + static _getMeta_getExampleSpells ({dragon, maxSpellLevel, spellLookup}) { + if (spellLookup[maxSpellLevel]?.[dragon.dragonCastingColor]) return spellLookup[maxSpellLevel][dragon.dragonCastingColor]; + + // If there's no exact match, try to find the next lowest + const flatKeys = Object.entries(spellLookup) + .map(([lvl, group]) => { + return Object.keys(group) + .map(color => `${lvl}${color}`); + }) + .flat() + .mergeMap(it => ({[it]: true})); + + while (--maxSpellLevel > -1) { + const lookupKey = `${maxSpellLevel}${dragon.dragonCastingColor}`; + if (flatKeys[lookupKey]) return spellLookup[maxSpellLevel][dragon.dragonCastingColor]; + } + return []; + } + + static getSpellcasterDetailsPart ({chaMod, maxSpellLevel, spellSaveDc, spellToHit, isSeeSpellsPageNote = false}) { + const levelString = maxSpellLevel === 0 ? `${chaMod === 1 ? "This" : "These"} spells are Cantrips.` : `${chaMod === 1 ? "The" : "Each"} spell's level can be no higher than ${Parser.spLevelToFull(maxSpellLevel)}.`; + + return `This dragon can innately cast ${Parser.numberToText(chaMod)} spell${chaMod === 1 ? "" : "s"}, once per day${chaMod === 1 ? "" : " each"}, requiring no material components. ${levelString} The dragon's spell save DC is {@dc ${spellSaveDc}}, and it has {@hit ${spellToHit}} to hit with spell attacks.${isSeeSpellsPageNote ? ` See the {@filter spell page|spells|level=${[...new Array(maxSpellLevel + 1)].map((it, i) => i).join(";")}} for a list of spells the dragon is capable of casting.` : ""}`; + } + + static getVariantEntries (dragon) { + if (!Renderer.monster.dragonCasterVariant.hasCastingColorVariant(dragon)) return []; + + const meta = Renderer.monster.dragonCasterVariant.getMeta(dragon); + const {exampleSpellsUnofficial, exampleSpellsFtd} = meta; + + const vFtd = exampleSpellsFtd?.length ? { + type: "variant", + name: "Dragons as Innate Spellcasters", + source: Parser.SRC_FTD, + entries: [ + `${Renderer.monster.dragonCasterVariant.getSpellcasterDetailsPart(meta)}`, + `A suggested spell list is shown below, but you can also choose spells to reflect the dragon's character. A dragon who innately casts {@filter druid|spells|class=druid} spells feels different from one who casts {@filter warlock|spells|class=warlock} spells. You can also give a dragon spells of a higher level than this rule allows, but such a tweak might increase the dragon's challenge rating\u2014especially if those spells deal damage or impose conditions on targets.`, + { + type: "list", + items: exampleSpellsFtd.map(it => `{@spell ${it}}`), + }, + ], + } : null; + + const vBasic = { + type: "variant", + name: "Dragons as Innate Spellcasters", + entries: [ + "Dragons are innately magical creatures that can master a few spells as they age, using this variant.", + `A young or older dragon can innately cast a number of spells equal to its Charisma modifier. Each spell can be cast once per day, requiring no material components, and the spell's level can be no higher than one-third the dragon's challenge rating (rounded down). The dragon's bonus to hit with spell attacks is equal to its proficiency bonus + its Charisma bonus. The dragon's spell save DC equals 8 + its proficiency bonus + its Charisma modifier.`, + `{@note ${Renderer.monster.dragonCasterVariant.getSpellcasterDetailsPart({...meta, isSeeSpellsPageNote: true})}${exampleSpellsUnofficial?.length ? ` A selection of examples are shown below:` : ""}}`, + ], + }; + if (dragon.source !== Parser.SRC_MM) { + vBasic.source = Parser.SRC_MM; + vBasic.page = 86; + } + if (exampleSpellsUnofficial) { + const ls = { + type: "list", + style: "list-italic", + items: exampleSpellsUnofficial.map(it => `{@spell ${it}}`), + }; + vBasic.entries.push(ls); + } + + return [vFtd, vBasic].filter(Boolean); + } + + static getHtml (dragon, {renderer = null} = {}) { + const variantEntrues = Renderer.monster.dragonCasterVariant.getVariantEntries(dragon); + if (!variantEntrues.length) return null; + return variantEntrues.map(it => renderer.render(it)).join(""); + } + }; + + static getCrScaleTarget ( + { + win, + $btnScale, + initialCr, + cbRender, + isCompact, + }, + ) { + const evtName = "click.cr-scaler"; + + let slider; + + const $body = $(win.document.body); + function cleanSliders () { + $body.find(`.mon__cr_slider_wrp`).remove(); + $btnScale.off(evtName); + if (slider) slider.destroy(); + } + + cleanSliders(); + + const $wrp = $(`
    `); + + const cur = Parser.CRS.indexOf(initialCr); + if (cur === -1) throw new Error(`Initial CR ${initialCr} was not valid!`); + + const comp = BaseComponent.fromObject({ + min: 0, + max: Parser.CRS.length - 1, + cur, + }); + slider = new ComponentUiUtil.RangeSlider({ + comp, + propMin: "min", + propMax: "max", + propCurMin: "cur", + fnDisplay: ix => Parser.CRS[ix], + }); + slider.$get().appendTo($wrp); + + $btnScale.off(evtName).on(evtName, (evt) => evt.stopPropagation()); + $wrp.on(evtName, (evt) => evt.stopPropagation()); + $body.off(evtName).on(evtName, cleanSliders); + + comp._addHookBase("cur", () => { + cbRender(Parser.crToNumber(Parser.CRS[comp._state.cur])); + $body.off(evtName); + cleanSliders(); + }); + + $btnScale.after($wrp); + } + + static getSelSummonSpellLevel (mon) { + if (mon.summonedBySpellLevel == null) return; + + return e_({ + tag: "select", + clazz: "input-xs form-control form-control--minimal w-initial inline-block ve-popwindow__hidden", + name: "mon__sel-summon-spell-level", + children: [ + e_({tag: "option", val: "-1", text: "\u2014"}), + ...[...new Array(VeCt.SPELL_LEVEL_MAX + 1 - mon.summonedBySpellLevel)].map((_, i) => e_({ + tag: "option", + val: i + mon.summonedBySpellLevel, + text: i + mon.summonedBySpellLevel, + })), + ], + }); + } + + static getSelSummonClassLevel (mon) { + if (mon.summonedByClass == null) return; + + return e_({ + tag: "select", + clazz: "input-xs form-control form-control--minimal w-initial inline-block ve-popwindow__hidden", + name: "mon__sel-summon-class-level", + children: [ + e_({tag: "option", val: "-1", text: "\u2014"}), + ...[...new Array(VeCt.LEVEL_MAX)].map((_, i) => e_({ + tag: "option", + val: i + 1, + text: i + 1, + })), + ], + }); + } + + static getCompactRenderedStringSection (mon, renderer, title, key, depth) { + if (!mon[key]) return ""; + + const noteKey = `${key}Note`; + + const toRender = key === "lairActions" || key === "regionalEffects" + ? [{type: "entries", entries: mon[key]}] + : mon[key]; + + const ptHeader = mon[key] ? Renderer.monster.getSectionIntro(mon, {prop: key}) : ""; + + return `

    ${title}${mon[noteKey] ? ` (${mon[noteKey]})` : ""}

    + + ${key === "legendary" && mon.legendary ? `

    ${Renderer.monster.getLegendaryActionIntro(mon)}

    ` : ""} + ${ptHeader ? `

    ${ptHeader}

    ` : ""} + ${toRender.map(it => it.rendered || renderer.render(it, depth)).join("")} + `; + } + + static getTypeAlignmentPart (mon) { + const typeObj = Parser.monTypeToFullObj(mon.type); + + return `${mon.level ? `${Parser.getOrdinalForm(mon.level)}-level ` : ""}${typeObj.asTextSidekick ? `${typeObj.asTextSidekick}; ` : ""}${Renderer.utils.getRenderedSize(mon.size)}${mon.sizeNote ? ` ${mon.sizeNote}` : ""} ${typeObj.asText}${mon.alignment ? `, ${mon.alignmentPrefix ? Renderer.get().render(mon.alignmentPrefix) : ""}${Parser.alignmentListToFull(mon.alignment).toTitleCase()}` : ""}`; + } + static getSavesPart (mon) { return `${Object.keys(mon.save || {}).sort(SortUtil.ascSortAtts).map(s => Renderer.monster.getSave(Renderer.get(), s, mon.save[s])).join(", ")}`; } + static getSensesPart (mon) { return `${mon.senses ? `${Renderer.utils.getRenderedSenses(mon.senses)}, ` : ""}passive Perception ${mon.passive || "\u2014"}`; } + + static getRenderWithPlugins ({renderer, mon, fn}) { + return renderer.withPlugin({ + pluginTypes: [ + "dice", + ], + fnPlugin: () => { + if (mon.summonedBySpellLevel == null && mon._summonedByClass_level == null) return null; + if (mon._summonedByClass_level) { + return { + additionalData: { + "data-summoned-by-class-level": mon._summonedByClass_level, + }, + }; + } + return { + additionalData: { + "data-summoned-by-spell-level": mon._summonedBySpell_level ?? mon.summonedBySpellLevel, + }, + }; + }, + fn, + }); + } + + /** + * @param mon + * @param [opts] + * @param [opts.isCompact] + * @param [opts.isEmbeddedEntity] + * @param [opts.isShowScalers] + * @param [opts.isScaledCr] + * @param [opts.isScaledSpellSummon] + * @param [opts.isScaledClassSummon] + */ + static getCompactRenderedString (mon, opts) { + const renderer = Renderer.get(); + return Renderer.monster.getRenderWithPlugins({ + renderer, + mon, + fn: () => Renderer.monster._getCompactRenderedString(mon, renderer, opts), + }); + } + + static _getCompactRenderedString (mon, renderer, opts) { + opts = opts || {}; + if (opts.isCompact === undefined) opts.isCompact = true; + + const renderStack = []; + const legGroup = DataUtil.monster.getMetaGroup(mon); + const hasToken = Renderer.monster.hasToken(mon); + const extraThClasses = !opts.isCompact && hasToken ? ["mon__name--token"] : null; + + const isShowCrScaler = ScaleCreature.isCrInScaleRange(mon); + const isShowSpellLevelScaler = opts.isShowScalers && !isShowCrScaler && mon.summonedBySpellLevel != null; + const isShowClassLevelScaler = opts.isShowScalers && !isShowSpellLevelScaler && mon.summonedByClass != null; + + const fnGetSpellTraits = Renderer.monster.getSpellcastingRenderedTraits.bind(Renderer.monster, renderer); + const allTraits = Renderer.monster.getOrderedTraits(mon, {fnGetSpellTraits}); + const allActions = Renderer.monster.getOrderedActions(mon, {fnGetSpellTraits}); + const allBonusActions = Renderer.monster.getOrderedBonusActions(mon, {fnGetSpellTraits}); + const allReactions = Renderer.monster.getOrderedReactions(mon, {fnGetSpellTraits}); + + let ptCrSpellLevel = `\u2014`; + if (isShowSpellLevelScaler || isShowClassLevelScaler) { + // Note that `outerHTML` ignores the value of the select, so we cannot e.g. select the correct option + // here and expect to return it in the HTML. + const selHtml = isShowSpellLevelScaler ? Renderer.monster.getSelSummonSpellLevel(mon)?.outerHTML : Renderer.monster.getSelSummonClassLevel(mon)?.outerHTML; + ptCrSpellLevel = `${selHtml || ""}`; + } else if (isShowCrScaler) { + ptCrSpellLevel = ` + ${Parser.monCrToFull(mon.cr, {isMythic: !!mon.mythic})} + ${opts.isShowScalers && !opts.isScaledCr && Parser.isValidCr(mon.cr ? (mon.cr.cr || mon.cr) : null) ? ` + + ` : ""} + ${opts.isScaledCr ? ` + + ` : ""} + `; + } + + renderStack.push(` + ${Renderer.utils.getExcludedTr({entity: mon, dataProp: "monster", page: opts.page || UrlUtil.PG_BESTIARY})} + ${Renderer.utils.getNameTr(mon, {page: opts.page || UrlUtil.PG_BESTIARY, extensionData: {_scaledCr: mon._scaledCr, _scaledSpellSummonLevel: mon._scaledSpellSummonLevel, _scaledClassSummonLevel: mon._scaledClassSummonLevel}, extraThClasses, isEmbeddedEntity: opts.isEmbeddedEntity})} + ${Renderer.monster.getTypeAlignmentPart(mon)} +
    + + + + + + + + ${mon.pbNote || Parser.crToNumber(mon.cr) < VeCt.CR_CUSTOM ? `` : ""} + ${hasToken && !opts.isCompact ? `` : ""} + + + + + + ${ptCrSpellLevel} + ${mon.pbNote || Parser.crToNumber(mon.cr) < VeCt.CR_CUSTOM ? `` : ""} + ${hasToken && !opts.isCompact ? `` : ""} + +
    Armor ClassHit PointsSpeed${isShowSpellLevelScaler ? "Spell Level" : isShowClassLevelScaler ? "Class Level" : "Challenge"}PB
    ${mon.ac == null ? "\u2014" : Parser.acToFull(mon.ac)}${mon.hp == null ? "\u2014" : Renderer.monster.getRenderedHp(mon.hp)}${Parser.getSpeedString(mon)}${mon.pbNote ?? UiUtil.intToBonus(Parser.crToPb(mon.cr), {isPretty: true})}
    + +
    + ${Renderer.monster.getRenderedAbilityScores(mon)} +
    + +
    + ${mon.resource ? mon.resource.map(res => `

    ${res.name} ${Renderer.monster.getRenderedResource(res)}

    `).join("") : ""} + ${mon.save ? `

    Saving Throws ${Renderer.monster.getSavesPart(mon)}

    ` : ""} + ${mon.skill ? `

    Skills ${Renderer.monster.getSkillsString(renderer, mon)}

    ` : ""} + ${mon.vulnerable ? `

    Damage Vuln. ${Parser.getFullImmRes(mon.vulnerable)}

    ` : ""} + ${mon.resist ? `

    Damage Res. ${Parser.getFullImmRes(mon.resist)}

    ` : ""} + ${mon.immune ? `

    Damage Imm. ${Parser.getFullImmRes(mon.immune)}

    ` : ""} + ${mon.conditionImmune ? `

    Condition Imm. ${Parser.getFullCondImm(mon.conditionImmune)}

    ` : ""} + ${opts.isHideSenses ? "" : `

    Senses ${Renderer.monster.getSensesPart(mon)}

    `} + ${opts.isHideLanguages ? "" : `

    Languages ${Renderer.monster.getRenderedLanguages(mon.languages)}

    `} +
    + + ${allTraits ? `
    + + ${allTraits.map(it => it.rendered || renderer.render(it, 2)).join("")} + ` : ""} + ${Renderer.monster.getCompactRenderedStringSection({...mon, action: allActions}, renderer, "Actions", "action", 2)} + ${Renderer.monster.getCompactRenderedStringSection({...mon, bonus: allBonusActions}, renderer, "Bonus Actions", "bonus", 2)} + ${Renderer.monster.getCompactRenderedStringSection({...mon, reaction: allReactions}, renderer, "Reactions", "reaction", 2)} + ${Renderer.monster.getCompactRenderedStringSection(mon, renderer, "Legendary Actions", "legendary", 2)} + ${Renderer.monster.getCompactRenderedStringSection(mon, renderer, "Mythic Actions", "mythic", 2)} + ${legGroup && legGroup.lairActions ? Renderer.monster.getCompactRenderedStringSection(legGroup, renderer, "Lair Actions", "lairActions", 1) : ""} + ${legGroup && legGroup.regionalEffects ? Renderer.monster.getCompactRenderedStringSection(legGroup, renderer, "Regional Effects", "regionalEffects", 1) : ""} + ${mon.variant || (mon.dragonCastingColor && !mon.spellcasting) || mon.summonedBySpell ? ` + + ${mon.variant ? mon.variant.map(it => it.rendered || renderer.render(it)).join("") : ""} + ${mon.dragonCastingColor ? Renderer.monster.dragonCasterVariant.getHtml(mon, {renderer}) : ""} + ${mon.footer ? renderer.render({entries: mon.footer}) : ""} + ${mon.summonedBySpell ? `
    Summoned By: ${renderer.render(`{@spell ${mon.summonedBySpell}}`)}
    ` : ""} + + ` : ""} + `); + + return renderStack.join(""); + } + + static _getFormulaMax (formula) { + return Renderer.dice.parseRandomise2(`dmax(${formula})`); + } + + static getRenderedHp (hp, isPlainText) { + if (hp.special != null) return isPlainText ? Renderer.stripTags(hp.special) : Renderer.get().render(hp.special); + + if (/^\d+d1$/.exec(hp.formula)) { + return hp.average; + } + + if (isPlainText) return `${hp.average} (${hp.formula})`; + + const maxVal = Renderer.monster._getFormulaMax(hp.formula); + const maxStr = maxVal ? `Maximum: ${maxVal}` : ""; + return `${maxStr ? `` : ""}${hp.average}${maxStr ? "" : ""} ${Renderer.get().render(`({@dice ${hp.formula}|${hp.formula}|Hit Points})`)}`; + } + + static getRenderedResource (res, isPlainText) { + if (!res.formula) return `${res.value}`; + + if (isPlainText) return `${res.value} (${res.formula})`; + + const maxVal = Renderer.monster._getFormulaMax(res.formula); + const maxStr = maxVal ? `Maximum: ${maxVal}` : ""; + return `${maxStr ? `` : ""}${res.value}${maxStr ? "" : ""} ${Renderer.get().render(`({@dice ${res.formula}|${res.formula}|${res.name}})`)}`; + } + + static getSafeAbilityScore (mon, abil, {isDefaultTen = false} = {}) { + if (!mon || abil == null) return isDefaultTen ? 10 : 0; + if (mon[abil] == null) return isDefaultTen ? 10 : 0; + return typeof mon[abil] === "number" ? mon[abil] : (isDefaultTen ? 10 : 0); + } + + static getRenderedAbilityScores (mon) { + const byAbil = {}; + const byValue = {}; + + Parser.ABIL_ABVS + .forEach(ab => { + if (mon[ab] == null || typeof mon[ab] === "number") return; + + const meta = {abil: ab, value: mon[ab].special}; + byAbil[meta.abil] = meta; + meta.family = (byValue[meta.value] = byValue[meta.value] || []); + meta.family.push(meta); + }); + + const seenAbs = new Set(); + const ptSpecial = Parser.ABIL_ABVS + .map(ab => { + const meta = byAbil[ab]; + if (!meta) return null; + if (seenAbs.has(meta.abil)) return null; + meta.family.forEach(meta => seenAbs.add(meta.abil)); + return `${meta.family.map(meta => meta.abil.toUpperCase()).join(", ")} ${meta.value}`; + }) + .filter(Boolean) + .map(r => `${r}`).join(""); + + if (Parser.ABIL_ABVS.every(ab => mon[ab] != null && typeof mon[ab] !== "number")) return ptSpecial; + + const absRemaining = Parser.ABIL_ABVS.filter(ab => !seenAbs.has(ab)); + + return ` + ${absRemaining.map(ab => `${ab.toUpperCase()}`).join("")} + + + ${absRemaining.map(ab => `${Renderer.utils.getAbilityRoller(mon, ab)}`).join("")} + `; + } + + static getSpellcastingRenderedTraits (renderer, mon, displayAsProp = "trait") { + const out = []; + (mon.spellcasting || []).filter(it => (it.displayAs || "trait") === displayAsProp).forEach(entry => { + entry.type = entry.type || "spellcasting"; + const renderStack = []; + renderer.recursiveRender(entry, renderStack, {depth: 2}); + const rendered = renderStack.join(""); + if (!rendered.length) return; + out.push({name: entry.name, rendered}); + }); + return out; + } + + static getOrderedTraits (mon, {fnGetSpellTraits} = {}) { + let traits = mon.trait ? MiscUtil.copyFast(mon.trait) : null; + + if (fnGetSpellTraits) { + const spellTraits = fnGetSpellTraits(mon, "trait"); + if (spellTraits.length) traits = traits ? traits.concat(spellTraits) : spellTraits; + } + + if (traits?.length) return traits.sort((a, b) => SortUtil.monTraitSort(a, b)); + return null; + } + + static getOrderedActions (mon, {fnGetSpellTraits} = {}) { return Renderer.monster._getOrderedActionsBonusActions({mon, fnGetSpellTraits, prop: "action"}); } + static getOrderedBonusActions (mon, {fnGetSpellTraits} = {}) { return Renderer.monster._getOrderedActionsBonusActions({mon, fnGetSpellTraits, prop: "bonus"}); } + static getOrderedReactions (mon, {fnGetSpellTraits} = {}) { return Renderer.monster._getOrderedActionsBonusActions({mon, fnGetSpellTraits, prop: "reaction"}); } + + static _getOrderedActionsBonusActions ({mon, fnGetSpellTraits, prop} = {}) { + let actions = mon[prop] ? MiscUtil.copyFast(mon[prop]) : null; + + let spellActions; + if (fnGetSpellTraits) { + spellActions = fnGetSpellTraits(mon, prop); + } + + if (!spellActions?.length && !actions?.length) return null; + if (!actions?.length) return spellActions; + if (!spellActions?.length) return actions; + + // Actions are generally ordered as: + // - "Multiattack" + // - Attack actions + // - Other actions (alphabetical) + // Insert our spellcasting section into the "Other actions" part, in an alphabetically-appropriate place. + + const ixLastAttack = actions.findLastIndex(it => it.entries && it.entries.length && typeof it.entries[0] === "string" && it.entries[0].includes(`{@atk `)); + const ixNext = actions.findIndex((act, ix) => ix > ixLastAttack && act.name && SortUtil.ascSortLower(act.name, "Spellcasting") >= 0); + if (~ixNext) actions.splice(ixNext, 0, ...spellActions); + else actions.push(...spellActions); + return actions; + } + + static getSkillsString (renderer, mon) { + if (!mon.skill) return ""; + + function doSortMapJoinSkillKeys (obj, keys, joinWithOr) { + const toJoin = keys.sort(SortUtil.ascSort).map(s => `${renderer.render(`{@skill ${s.toTitleCase()}}`)} ${Renderer.get().render(`{@skillCheck ${s.replace(/ /g, "_")} ${obj[s]}}`)}`); + return joinWithOr ? toJoin.joinConjunct(", ", " or ") : toJoin.join(", "); + } + + const skills = doSortMapJoinSkillKeys(mon.skill, Object.keys(mon.skill).filter(k => k !== "other" && k !== "special")); + if (mon.skill.other || mon.skill.special) { + const others = mon.skill.other && mon.skill.other.map(it => { + if (it.oneOf) { + return `plus one of the following: ${doSortMapJoinSkillKeys(it.oneOf, Object.keys(it.oneOf), true)}`; + } + throw new Error(`Unhandled monster "other" skill properties!`); + }); + const special = mon.skill.special && Renderer.get().render(mon.skill.special); + return [skills, others, special].filter(Boolean).join(", "); + } + return skills; + } + + static hasToken (mon, opts) { + return Renderer.generic.hasToken(mon, opts); + } + + static getTokenUrl (mon, opts) { + return Renderer.generic.getTokenUrl(mon, "bestiary/tokens", opts); + } + + static postProcessFluff (mon, fluff) { + const cpy = MiscUtil.copyFast(fluff); + + // TODO is this good enough? Should additionally check for lair blocks which are not the last, and tag them with + // "data": {"lairRegionals": true}, and insert the lair/regional text there if available (do the current "append" otherwise) + const thisGroup = DataUtil.monster.getMetaGroup(mon); + const handleGroupProp = (prop, name) => { + if (thisGroup && thisGroup[prop]) { + cpy.entries = cpy.entries || []; + cpy.entries.push({ + type: "entries", + entries: [ + { + type: "entries", + name, + entries: MiscUtil.copyFast(thisGroup[prop]), + }, + ], + }); + } + }; + + handleGroupProp("lairActions", "Lair Actions"); + handleGroupProp("regionalEffects", "Regional Effects"); + handleGroupProp("mythicEncounter", `${mon.name} as a Mythic Encounter`); + + return cpy; + } + + static getRenderedLanguages (languages) { + if (typeof languages === "string") languages = [languages]; // handle legacy format + return languages ? languages.map(it => Renderer.get().render(it)).join(", ") : "\u2014"; + } + + static initParsed (mon) { + mon._pTypes = mon._pTypes || Parser.monTypeToFullObj(mon.type); // store the parsed type + if (!mon._pCr) { + if (Parser.crToNumber(mon.cr) === VeCt.CR_CUSTOM) mon._pCr = "Special"; + else if (Parser.crToNumber(mon.cr) === VeCt.CR_UNKNOWN) mon._pCr = "Unknown"; + else mon._pCr = mon.cr == null ? "\u2014" : (mon.cr.cr || mon.cr); + } + if (!mon._fCr) { + mon._fCr = [mon._pCr]; + if (mon.cr) { + if (mon.cr.lair) mon._fCr.push(mon.cr.lair); + if (mon.cr.coven) mon._fCr.push(mon.cr.coven); + } + } + } + + static updateParsed (mon) { + delete mon._pTypes; + delete mon._pCr; + delete mon._fCr; + Renderer.monster.initParsed(mon); + } + + static getRenderedVariants (mon, {renderer = null} = {}) { + renderer = renderer || Renderer.get(); + const dragonVariant = Renderer.monster.dragonCasterVariant.getHtml(mon, {renderer}); + const variants = mon.variant; + if (!variants && !dragonVariant) return null; + + const rStack = []; + (variants || []).forEach(v => renderer.recursiveRender(v, rStack)); + if (dragonVariant) rStack.push(dragonVariant); + return rStack.join(""); + } + + static getRenderedEnvironment (envs) { return (envs || []).sort(SortUtil.ascSortLower).map(it => it.toTitleCase()).join(", "); } + + static getAltArtDisplayName (meta) { return meta.displayName || meta.name || meta.token?.name; } + static getAltArtSource (meta) { return meta.source || meta.token?.source; } + + static getRenderedAltArtEntry (meta, {isPlainText = false} = {}) { + const displayName = Renderer.monster.getAltArtDisplayName(meta); + const source = Renderer.monster.getAltArtSource(meta); + + if (!displayName || !source) return ""; + + return `${isPlainText ? "" : `
    `}${displayName}; ${isPlainText ? "" : ``}${Parser.sourceJsonToAbv(source)}${Renderer.utils.isDisplayPage(meta.page) ? ` p${meta.page}` : ""}${isPlainText ? "" : `
    `}`; + } + + static pGetFluff (mon) { + return Renderer.utils.pGetFluff({ + entity: mon, + pFnPostProcess: Renderer.monster.postProcessFluff.bind(null, mon), + fluffBaseUrl: `data/bestiary/`, + fluffProp: "monsterFluff", + }); + } + + // region Custom hash ID packing/unpacking + static getCustomHashId (mon) { + if (!mon._isScaledCr && !mon._isScaledSpellSummon && !mon._scaledClassSummonLevel) return null; + + const { + name, + source, + _scaledCr: scaledCr, + _scaledSpellSummonLevel: scaledSpellSummonLevel, + _scaledClassSummonLevel: scaledClassSummonLevel, + } = mon; + + return [ + name, + source, + scaledCr ?? "", + scaledSpellSummonLevel ?? "", + scaledClassSummonLevel ?? "", + ].join("__").toLowerCase(); + } + + static getUnpackedCustomHashId (customHashId) { + if (!customHashId) return null; + + const [, , scaledCr, scaledSpellSummonLevel, scaledClassSummonLevel] = customHashId.split("__").map(it => it.trim()); + + if (!scaledCr && !scaledSpellSummonLevel && !scaledClassSummonLevel) return null; + + return { + _scaledCr: scaledCr ? Number(scaledCr) : null, + _scaledSpellSummonLevel: scaledSpellSummonLevel ? Number(scaledSpellSummonLevel) : null, + _scaledClassSummonLevel: scaledClassSummonLevel ? Number(scaledClassSummonLevel) : null, + customHashId, + }; + } + // endregion + + static async pGetModifiedCreature (monRaw, customHashId) { + if (!customHashId) return monRaw; + const {_scaledCr, _scaledSpellSummonLevel, _scaledClassSummonLevel} = Renderer.monster.getUnpackedCustomHashId(customHashId); + if (_scaledCr) return ScaleCreature.scale(monRaw, _scaledCr); + if (_scaledSpellSummonLevel) return ScaleSpellSummonedCreature.scale(monRaw, _scaledSpellSummonLevel); + if (_scaledClassSummonLevel) return ScaleClassSummonedCreature.scale(monRaw, _scaledClassSummonLevel); + throw new Error(`Unhandled custom hash ID "${customHashId}"`); + } + + static _bindListenersScale (mon, ele) { + const page = UrlUtil.PG_BESTIARY; + const source = mon.source; + const hash = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_BESTIARY](mon); + + const fnRender = Renderer.hover.getFnRenderCompact(page); + + const $content = $(ele); + + $content + .find(".mon__btn-scale-cr") + .click(evt => { + evt.stopPropagation(); + const win = (evt.view || {}).window; + + const $btn = $(evt.target).closest("button"); + const initialCr = mon._originalCr != null ? mon._originalCr : mon.cr.cr || mon.cr; + const lastCr = mon.cr.cr || mon.cr; + + Renderer.monster.getCrScaleTarget({ + win, + $btnScale: $btn, + initialCr: lastCr, + isCompact: true, + cbRender: async (targetCr) => { + const original = await DataLoader.pCacheAndGet(page, source, hash); + const toRender = Parser.numberToCr(targetCr) === initialCr + ? original + : await ScaleCreature.scale(original, targetCr); + + $content.empty().append(fnRender(toRender)); + + Renderer.monster._bindListenersScale(toRender, ele); + }, + }); + }); + + $content + .find(".mon__btn-reset-cr") + .click(async () => { + const toRender = await DataLoader.pCacheAndGet(page, source, hash); + $content.empty().append(fnRender(toRender)); + + Renderer.monster._bindListenersScale(toRender, ele); + }); + + const $selSummonSpellLevel = $content + .find(`[name="mon__sel-summon-spell-level"]`) + .change(async () => { + const original = await DataLoader.pCacheAndGet(page, source, hash); + const spellLevel = Number($selSummonSpellLevel.val()); + + const toRender = ~spellLevel + ? await ScaleSpellSummonedCreature.scale(original, spellLevel) + : original; + + $content.empty().append(fnRender(toRender)); + + Renderer.monster._bindListenersScale(toRender, ele); + }) + .val(mon._summonedBySpell_level != null ? `${mon._summonedBySpell_level}` : "-1"); + + const $selSummonClassLevel = $content + .find(`[name="mon__sel-summon-class-level"]`) + .change(async () => { + const original = await DataLoader.pCacheAndGet(page, source, hash); + const classLevel = Number($selSummonClassLevel.val()); + + const toRender = ~classLevel + ? await ScaleClassSummonedCreature.scale(original, classLevel) + : original; + + $content.empty().append(fnRender(toRender)); + + Renderer.monster._bindListenersScale(toRender, ele); + }) + .val(mon._summonedByClass_level != null ? `${mon._summonedByClass_level}` : "-1"); + } + + static bindListenersCompact (mon, ele) { + Renderer.monster._bindListenersScale(mon, ele); + } + + static hover = class { + static bindFluffImageMouseover ({mon, $ele}) { + $ele + .on("mouseover", evt => this._pOnFluffImageMouseover({evt, mon, $ele})); + } + + static async _pOnFluffImageMouseover ({evt, mon, $ele}) { + // We'll rebuild the mouseover handler with whatever we load + $ele.off("mouseover"); + + const fluff = mon ? await Renderer.monster.pGetFluff(mon) : null; + + if (fluff?.images?.length) return this._pOnFluffImageMouseover_hasImage({mon, $ele, fluff}); + return this._pOnFluffImageMouseover_noImage({mon, $ele}); + } + + static _pOnFluffImageMouseover_noImage ({mon, $ele}) { + const hoverMeta = this.getMakePredefinedFluffImageHoverNoImage({name: mon?.name}); + $ele + .on("mouseover", evt => hoverMeta.mouseOver(evt, $ele[0])) + .on("mousemove", evt => hoverMeta.mouseMove(evt, $ele[0])) + .on("mouseleave", evt => hoverMeta.mouseLeave(evt, $ele[0])) + .trigger("mouseover"); + } + + static _pOnFluffImageMouseover_hasImage ({mon, $ele, fluff}) { + const hoverMeta = this.getMakePredefinedFluffImageHoverHasImage({imageHref: fluff.images[0].href, name: mon.name}); + $ele + .on("mouseover", evt => hoverMeta.mouseOver(evt, $ele[0])) + .on("mousemove", evt => hoverMeta.mouseMove(evt, $ele[0])) + .on("mouseleave", evt => hoverMeta.mouseLeave(evt, $ele[0])) + .trigger("mouseover"); + } + + static getMakePredefinedFluffImageHoverNoImage ({name}) { + return Renderer.hover.getMakePredefinedHover( + { + type: "entries", + entries: [ + Renderer.utils.HTML_NO_IMAGES, + ], + data: { + hoverTitle: name ? `Image \u2014 ${name}` : "Image", + }, + }, + {isBookContent: true}, + ); + } + + static getMakePredefinedFluffImageHoverHasImage ({imageHref, name}) { + return Renderer.hover.getMakePredefinedHover( + { + type: "image", + href: imageHref, + data: { + hoverTitle: name ? `Image \u2014 ${name}` : "Image", + }, + }, + {isBookContent: true}, + ); + } + }; +}; +Renderer.monster.CHILD_PROPS_EXTENDED = [...Renderer.monster.CHILD_PROPS, "lairActions", "regionalEffects"]; + +Renderer.monster.CHILD_PROPS_EXTENDED.forEach(prop => { + const propFull = `monster${prop.uppercaseFirst()}`; + Renderer[propFull] = { + getCompactRenderedString (ent) { + return Renderer.generic.getCompactRenderedString(ent); + }, + }; +}); + +Renderer.monsterAction.getWeaponLookupName = act => { + return (act.name || "") + .replace(/\(.*\)$/, "") // remove parenthetical text (e.g. "(Humanoid or Hybrid Form Only)" off the end + .trim() + .toLowerCase() + ; +}; + +Renderer.legendaryGroup = class { + static getCompactRenderedString (legGroup, opts) { + opts = opts || {}; + + const ent = Renderer.legendaryGroup.getSummaryEntry(legGroup); + if (!ent) return ""; + + return ` + ${Renderer.utils.getNameTr(legGroup, {isEmbeddedEntity: opts.isEmbeddedEntity})} + + ${Renderer.get().setFirstSection(true).render(ent)} + + ${Renderer.utils.getPageTr(legGroup)}`; + } + + static getSummaryEntry (legGroup) { + if (!legGroup || (!legGroup.lairActions && !legGroup.regionalEffects && !legGroup.mythicEncounter)) return null; + + return { + type: "section", + entries: [ + legGroup.lairActions ? {name: "Lair Actions", type: "entries", entries: legGroup.lairActions} : null, + legGroup.regionalEffects ? {name: "Regional Effects", type: "entries", entries: legGroup.regionalEffects} : null, + legGroup.mythicEncounter ? {name: "As a Mythic Encounter", type: "entries", entries: legGroup.mythicEncounter} : null, + ].filter(Boolean), + }; + } +}; + +Renderer.item = class { + static _sortProperties (a, b) { + return SortUtil.ascSort(Renderer.item.getProperty(a, {isIgnoreMissing: true})?.name || "", Renderer.item.getProperty(b, {isIgnoreMissing: true})?.name || ""); + } + + static _getPropertiesText (item, {renderer = null} = {}) { + renderer = renderer || Renderer.get(); + + if (!item.property) { + const parts = []; + if (item.dmg2) parts.push(`alt. ${Renderer.item._renderDamage(item.dmg2, {renderer})}`); + if (item.range) parts.push(`range ${item.range} ft.`); + return `${item.dmg1 && parts.length ? " - " : ""}${parts.join(", ")}`; + } + + let renderedDmg2 = false; + + const renderedProperties = item.property + .sort(Renderer.item._sortProperties) + .map(p => { + const pFull = Renderer.item.getProperty(p); + + if (pFull.template) { + const toRender = Renderer.utils.applyTemplate( + item, + pFull.template, + { + fnPreApply: (fullMatch, variablePath) => { + if (variablePath === "item.dmg2") renderedDmg2 = true; + }, + mapCustom: {"prop_name": pFull.name}, + }, + ); + + return renderer.render(toRender); + } else return pFull.name; + }); + + if (!renderedDmg2 && item.dmg2) renderedProperties.unshift(`alt. ${Renderer.item._renderDamage(item.dmg2, {renderer})}`); + + return `${item.dmg1 && renderedProperties.length ? " - " : ""}${renderedProperties.join(", ")}`; + } + + static _getTaggedDamage (dmg, {renderer = null} = {}) { + if (!dmg) return ""; + + renderer = renderer || Renderer.get(); + + Renderer.stripTags(dmg.trim()); + + return renderer.render(`{@damage ${dmg}}`); + } + + static _renderDamage (dmg, {renderer = null} = {}) { + renderer = renderer || Renderer.get(); + return renderer.render(Renderer.item._getTaggedDamage(dmg, {renderer})); + } + + static getDamageAndPropertiesText (item, {renderer = null} = {}) { + renderer = renderer || Renderer.get(); + + const damagePartsPre = []; + const damageParts = []; + + if (item.mastery) damagePartsPre.push(`Mastery: ${item.mastery.map(it => renderer.render(`{@itemMastery ${it}}`)).join(", ")}`); + + // armor + if (item.ac != null) { + const itemType = item.bardingType || item.type; + const dexterityMax = (itemType === "MA" && item.dexterityMax == null) + ? 2 + : item.dexterityMax; + const isAddDex = item.dexterityMax != null || itemType !== "HA"; + + const prefix = item.type === "S" ? "+" : ""; + const suffix = isAddDex ? ` + Dex${dexterityMax ? ` (max ${dexterityMax})` : ""}` : ""; + + damageParts.push(`AC ${prefix}${item.ac}${suffix}`); + } + if (item.acSpecial != null) damageParts.push(item.ac != null ? item.acSpecial : `AC ${item.acSpecial}`); + + // damage + if (item.dmg1) damageParts.push(Renderer.item._renderDamage(item.dmg1, {renderer})); + + // mounts + if (item.speed != null) damageParts.push(`Speed: ${item.speed}`); + if (item.carryingCapacity) damageParts.push(`Carrying Capacity: ${item.carryingCapacity} lb.`); + + // vehicles + if (item.vehSpeed || item.capCargo || item.capPassenger || item.crew || item.crewMin || item.crewMax || item.vehAc || item.vehHp || item.vehDmgThresh || item.travelCost || item.shippingCost) { + const vehPartUpper = item.vehSpeed ? `Speed: ${Parser.numberToVulgar(item.vehSpeed)} mph` : null; + + const vehPartMiddle = item.capCargo || item.capPassenger ? `Carrying Capacity: ${[item.capCargo ? `${Parser.numberToFractional(item.capCargo)} ton${item.capCargo === 0 || item.capCargo > 1 ? "s" : ""} cargo` : null, item.capPassenger ? `${item.capPassenger} passenger${item.capPassenger === 1 ? "" : "s"}` : null].filter(Boolean).join(", ")}` : null; + + const {travelCostFull, shippingCostFull} = Parser.itemVehicleCostsToFull(item); + + // These may not be present in homebrew + const vehPartLower = [ + item.crew ? `Crew ${item.crew}` : null, + item.crewMin && item.crewMax ? `Crew ${item.crewMin}-${item.crewMax}` : null, + item.vehAc ? `AC ${item.vehAc}` : null, + item.vehHp ? `HP ${item.vehHp}${item.vehDmgThresh ? `, Damage Threshold ${item.vehDmgThresh}` : ""}` : null, + ].filter(Boolean).join(", "); + + damageParts.push([ + vehPartUpper, + vehPartMiddle, + + // region ~~Dammit Mercer~~ Additional fields present in EGW + travelCostFull ? `Personal Travel Cost: ${travelCostFull} per mile per passenger` : null, + shippingCostFull ? `Shipping Cost: ${shippingCostFull} per 100 pounds per mile` : null, + // endregion + + vehPartLower, + ].filter(Boolean).join(renderer.getLineBreak())); + } + + const damage = [ + damagePartsPre.join(", "), + damageParts.join(", "), + ] + .filter(Boolean) + .join(renderer.getLineBreak()); + const damageType = item.dmgType ? Parser.dmgTypeToFull(item.dmgType) : ""; + const propertiesTxt = Renderer.item._getPropertiesText(item, {renderer}); + + return [damage, damageType, propertiesTxt]; + } + + static getTypeRarityAndAttunementText (item) { + const typeRarity = [ + item._typeHtml === "other" ? "" : item._typeHtml, + (item.rarity && Renderer.item.doRenderRarity(item.rarity) ? item.rarity : ""), + ].filter(Boolean).join(", "); + + return [ + item.reqAttune ? `${typeRarity} ${item._attunement}` : typeRarity, + item._subTypeHtml || "", + item.tier ? `${item.tier} tier` : "", + ]; + } + + static getAttunementAndAttunementCatText (item, prop = "reqAttune") { + let attunement = null; + let attunementCat = VeCt.STR_NO_ATTUNEMENT; + if (item[prop] != null && item[prop] !== false) { + if (item[prop] === true) { + attunementCat = "Requires Attunement"; + attunement = "(requires attunement)"; + } else if (item[prop] === "optional") { + attunementCat = "Attunement Optional"; + attunement = "(attunement optional)"; + } else if (item[prop].toLowerCase().startsWith("by")) { + attunementCat = "Requires Attunement By..."; + attunement = `(requires attunement ${Renderer.get().render(item[prop])})`; + } else { + attunementCat = "Requires Attunement"; // throw any weird ones in the "Yes" category (e.g. "outdoors at night") + attunement = `(requires attunement ${Renderer.get().render(item[prop])})`; + } + } + return [attunement, attunementCat]; + } + + static getHtmlAndTextTypes (item) { + const typeHtml = []; + const typeListText = []; + const subTypeHtml = []; + + let showingBase = false; + if (item.wondrous) { + typeHtml.push(`wondrous item${item.tattoo ? ` (tattoo)` : ""}`); + typeListText.push("wondrous item"); + } + if (item.tattoo) { + typeListText.push("tattoo"); + } + if (item.staff) { + typeHtml.push("staff"); + typeListText.push("staff"); + } + if (item.ammo) { + typeHtml.push(`ammunition`); + typeListText.push("ammunition"); + } + if (item.firearm) { + subTypeHtml.push("firearm"); + typeListText.push("firearm"); + } + if (item.age) { + subTypeHtml.push(item.age); + typeListText.push(item.age); + } + if (item.weaponCategory) { + typeHtml.push(`weapon${item.baseItem ? ` (${Renderer.get().render(`{@item ${item.baseItem}}`)})` : ""}`); + subTypeHtml.push(`${item.weaponCategory} weapon`); + typeListText.push(`${item.weaponCategory} weapon`); + showingBase = true; + } + if (item.staff && (item.type !== "M" && item.typeAlt !== "M")) { // DMG p140: "Unless a staff's description says otherwise, a staff can be used as a quarterstaff." + subTypeHtml.push("melee weapon"); + typeListText.push("melee weapon"); + } + if (item.type) Renderer.item._getHtmlAndTextTypes_type({type: item.type, typeHtml, typeListText, subTypeHtml, showingBase, item}); + if (item.typeAlt) Renderer.item._getHtmlAndTextTypes_type({type: item.typeAlt, typeHtml, typeListText, subTypeHtml, showingBase, item}); + if (item.poison) { + typeHtml.push(`poison${item.poisonTypes ? ` (${item.poisonTypes.joinConjunct(", ", " or ")})` : ""}`); + typeListText.push("poison"); + } + return [typeListText, typeHtml.join(", "), subTypeHtml.join(", ")]; + } + + static _getHtmlAndTextTypes_type ({type, typeHtml, typeListText, subTypeHtml, showingBase, item}) { + const fullType = Renderer.item.getItemTypeName(type); + + const isSub = (typeListText.some(it => it.includes("weapon")) && fullType.includes("weapon")) + || (typeListText.some(it => it.includes("armor")) && fullType.includes("armor")); + + if (!showingBase && !!item.baseItem) (isSub ? subTypeHtml : typeHtml).push(`${fullType} (${Renderer.get().render(`{@item ${item.baseItem}}`)})`); + else if (type === "S") (isSub ? subTypeHtml : typeHtml).push(Renderer.get().render(`armor ({@item shield|phb})`)); + else (isSub ? subTypeHtml : typeHtml).push(fullType); + + typeListText.push(fullType); + } + + static _GET_RENDERED_ENTRIES_WALKER = null; + + /** + * @param item + * @param isCompact + * @param wrappedTypeAllowlist An optional set of: `"note", "type", "property", "variant"` + */ + static getRenderedEntries (item, {isCompact = false, wrappedTypeAllowlist = null} = {}) { + const renderer = Renderer.get(); + + Renderer.item._GET_RENDERED_ENTRIES_WALKER = Renderer.item._GET_RENDERED_ENTRIES_WALKER || MiscUtil.getWalker({ + keyBlocklist: new Set([ + ...MiscUtil.GENERIC_WALKER_ENTRIES_KEY_BLOCKLIST, + "data", + ]), + }); + + const handlersName = { + string: (str) => Renderer.item._getRenderedEntries_handlerConvertNamesToItalics.bind(Renderer.item, item, item.name)(str), + }; + + const handlersVariantName = item._variantName == null ? null : { + string: (str) => Renderer.item._getRenderedEntries_handlerConvertNamesToItalics.bind(Renderer.item, item, item._variantName)(str), + }; + + const renderStack = []; + if (item._fullEntries || item.entries?.length) { + const entry = MiscUtil.copyFast({type: "entries", entries: item._fullEntries || item.entries}); + let procEntry = Renderer.item._GET_RENDERED_ENTRIES_WALKER.walk(entry, handlersName); + if (handlersVariantName) procEntry = Renderer.item._GET_RENDERED_ENTRIES_WALKER.walk(entry, handlersVariantName); + if (wrappedTypeAllowlist) procEntry.entries = procEntry.entries.filter(it => !it?.data?.[VeCt.ENTDATA_ITEM_MERGED_ENTRY_TAG] || wrappedTypeAllowlist.has(it?.data?.[VeCt.ENTDATA_ITEM_MERGED_ENTRY_TAG])); + renderer.recursiveRender(procEntry, renderStack, {depth: 1}); + } + + if (item._fullAdditionalEntries || item.additionalEntries) { + const additionEntries = MiscUtil.copyFast({type: "entries", entries: item._fullAdditionalEntries || item.additionalEntries}); + let procAdditionEntries = Renderer.item._GET_RENDERED_ENTRIES_WALKER.walk(additionEntries, handlersName); + if (handlersVariantName) procAdditionEntries = Renderer.item._GET_RENDERED_ENTRIES_WALKER.walk(additionEntries, handlersVariantName); + if (wrappedTypeAllowlist) procAdditionEntries.entries = procAdditionEntries.entries.filter(it => !it?.data?.[VeCt.ENTDATA_ITEM_MERGED_ENTRY_TAG] || wrappedTypeAllowlist.has(it?.data?.[VeCt.ENTDATA_ITEM_MERGED_ENTRY_TAG])); + renderer.recursiveRender(procAdditionEntries, renderStack, {depth: 1}); + } + + if (!isCompact && item.lootTables) { + renderStack.push(`
    Found On: ${item.lootTables.sort(SortUtil.ascSortLower).map(tbl => renderer.render(`{@table ${tbl}}`)).join(", ")}
    `); + } + + return renderStack.join("").trim(); + } + + static _getRenderedEntries_handlerConvertNamesToItalics (item, baseName, str) { + if (Renderer.item.isMundane(item)) return str; + + const stack = []; + let depth = 0; + + const tgtLen = baseName.length; + // Only accept title-case names for sentient items (e.g. Wave) + const tgtName = item.sentient ? baseName : baseName.toLowerCase(); + + const tgtNamePlural = tgtName.toPlural(); + const tgtLenPlural = tgtNamePlural.length; + + // e.g. "Orb of Shielding (Fernian Basalt)" -> "Orb of Shielding" + const tgtNameNoBraces = tgtName.replace(/ \(.*$/, ""); + const tgtLenNoBraces = tgtNameNoBraces.length; + + const len = str.length; + for (let i = 0; i < len; ++i) { + const c = str[i]; + + switch (c) { + case "{": { + if (str[i + 1] === "@") depth++; + stack.push(c); + break; + } + case "}": { + if (depth) depth--; + stack.push(c); + break; + } + default: stack.push(c); break; + } + + if (depth) continue; + + if (stack.slice(-tgtLen).join("")[item.sentient ? "toString" : "toLowerCase"]() === tgtName) { + stack.splice(stack.length - tgtLen, tgtLen, `{@i ${stack.slice(-tgtLen).join("")}}`); + } else if (stack.slice(-tgtLenPlural).join("")[item.sentient ? "toString" : "toLowerCase"]() === tgtNamePlural) { + stack.splice(stack.length - tgtLenPlural, tgtLenPlural, `{@i ${stack.slice(-tgtLenPlural).join("")}}`); + } else if (stack.slice(-tgtLenNoBraces).join("")[item.sentient ? "toString" : "toLowerCase"]() === tgtNameNoBraces) { + stack.splice(stack.length - tgtLenNoBraces, tgtLenNoBraces, `{@i ${stack.slice(-tgtLenNoBraces).join("")}}`); + } + } + + return stack.join(""); + } + + static getCompactRenderedString (item, opts) { + opts = opts || {}; + + const [damage, damageType, propertiesTxt] = Renderer.item.getDamageAndPropertiesText(item); + const [typeRarityText, subTypeText, tierText] = Renderer.item.getTypeRarityAndAttunementText(item); + + return ` + ${Renderer.utils.getExcludedTr({entity: item, dataProp: "item", page: UrlUtil.PG_ITEMS})} + ${Renderer.utils.getNameTr(item, {page: UrlUtil.PG_ITEMS, isEmbeddedEntity: opts.isEmbeddedEntity})} + ${Renderer.item.getTypeRarityAndAttunementHtml(typeRarityText, subTypeText, tierText)} + + ${[Parser.itemValueToFullMultiCurrency(item), Parser.itemWeightToFull(item)].filter(Boolean).join(", ").uppercaseFirst()} + ${damage} ${damageType} ${propertiesTxt} + + ${Renderer.item.hasEntries(item) ? `${Renderer.utils.getDividerTr()}${Renderer.item.getRenderedEntries(item, {isCompact: true})}` : ""}`; + } + + static hasEntries (item) { + return item._fullAdditionalEntries?.length || item._fullEntries?.length || item.entries?.length; + } + + static getTypeRarityAndAttunementHtml (typeRarityText, subTypeText, tierText) { + return `
    + ${typeRarityText || tierText ? `
    +
    ${(typeRarityText || "").uppercaseFirst()}
    +
    ${(tierText || "").uppercaseFirst()}
    +
    ` : ""} + ${subTypeText ? `
    ${subTypeText.uppercaseFirst()}
    ` : ""} +
    `; + } + + static _hiddenRarity = new Set(["none", "unknown", "unknown (magic)", "varies"]); + static doRenderRarity (rarity) { + return !Renderer.item._hiddenRarity.has(rarity); + } + + // --- + + static _propertyMap = {}; + static _addProperty (prt) { + if (Renderer.item._propertyMap[prt.abbreviation]) return; + const cpy = MiscUtil.copyFast(prt); + Renderer.item._propertyMap[prt.abbreviation] = prt.name ? cpy : { + ...cpy, + name: (prt.entries || prt.entriesTemplate)[0].name.toLowerCase(), + }; + } + + static getProperty (abbv, {isIgnoreMissing = false} = {}) { + if (!isIgnoreMissing && !Renderer.item._propertyMap[abbv]) throw new Error(`Item property ${abbv} not found. You probably meant to load the property reference first.`); + return Renderer.item._propertyMap[abbv]; + } + + // --- + + static _typeMap = {}; + static _addType (typ) { + if (Renderer.item._typeMap[typ.abbreviation]?.entries || Renderer.item._typeMap[typ.abbreviation]?.entriesTemplate) return; + const cpy = MiscUtil.copyFast(typ); + + // Merge in data from existing version, if it exists + Object.entries(Renderer.item._typeMap[typ.abbreviation] || {}) + .forEach(([k, v]) => { + if (cpy[k]) return; + cpy[k] = v; + }); + + cpy.name = cpy.name || (cpy.entries || cpy.entriesTemplate)[0].name.toLowerCase(); + + Renderer.item._typeMap[typ.abbreviation] = cpy; + } + + static getType (abbv) { + if (!Renderer.item._typeMap[abbv]) throw new Error(`Item type ${abbv} not found. You probably meant to load the type reference first.`); + return Renderer.item._typeMap[abbv]; + } + + // --- + + static entryMap = {}; + static _addEntry (ent) { + if (Renderer.item.entryMap[ent.source]?.[ent.name]) return; + MiscUtil.set(Renderer.item.entryMap, ent.source, ent.name, ent); + } + + // --- + + static _additionalEntriesMap = {}; + static _addAdditionalEntries (ent) { + if (Renderer.item._additionalEntriesMap[ent.appliesTo]) return; + Renderer.item._additionalEntriesMap[ent.appliesTo] = MiscUtil.copyFast(ent.entries); + } + + // --- + + static _masteryMap = {}; + static _addMastery (ent) { + const lookupSource = ent.source.toLowerCase(); + const lookupName = ent.name.toLowerCase(); + if (Renderer.item._masteryMap[lookupSource]?.[lookupName]) return; + MiscUtil.set(Renderer.item._masteryMap, lookupSource, lookupName, ent); + } + + static _getMastery (uid) { + const {name, source} = DataUtil.proxy.unpackUid("itemMastery", uid, "itemMastery", {isLower: true}); + const out = MiscUtil.get(Renderer.item._masteryMap, source, name); + if (!out) throw new Error(`Item mastry ${uid} not found. You probably meant to load the mastery reference first.`); + return out; + } + + // --- + + static async _pAddPrereleaseBrewPropertiesAndTypes () { + if (typeof PrereleaseUtil !== "undefined") Renderer.item.addPrereleaseBrewPropertiesAndTypesFrom({data: await PrereleaseUtil.pGetBrewProcessed()}); + if (typeof BrewUtil2 !== "undefined") Renderer.item.addPrereleaseBrewPropertiesAndTypesFrom({data: await BrewUtil2.pGetBrewProcessed()}); + } + + static addPrereleaseBrewPropertiesAndTypesFrom ({data}) { + (data.itemProperty || []) + .forEach(it => Renderer.item._addProperty(it)); + (data.itemType || []) + .forEach(it => Renderer.item._addType(it)); + (data.itemEntry || []) + .forEach(it => Renderer.item._addEntry(it)); + (data.itemTypeAdditionalEntries || []) + .forEach(it => Renderer.item._addAdditionalEntries(it)); + (data.itemMastery || []) + .forEach(it => Renderer.item._addMastery(it)); + } + + static _addBasePropertiesAndTypes (baseItemData) { + Object.entries(Parser.ITEM_TYPE_JSON_TO_ABV).forEach(([abv, name]) => Renderer.item._addType({abbreviation: abv, name})); + + // Convert the property and type list JSONs into look-ups, i.e. use the abbreviation as a JSON property name + (baseItemData.itemProperty || []).forEach(it => Renderer.item._addProperty(it)); + (baseItemData.itemType || []).forEach(it => Renderer.item._addType(it)); + (baseItemData.itemEntry || []).forEach(it => Renderer.item._addEntry(it)); + (baseItemData.itemTypeAdditionalEntries || []).forEach(it => Renderer.item._addAdditionalEntries(it)); + (baseItemData.itemMastery || []).forEach(it => Renderer.item._addMastery(it)); + + baseItemData.baseitem.forEach(it => it._isBaseItem = true); + } + + static async _pGetSiteUnresolvedRefItems_pLoadItems () { + const itemData = await DataUtil.loadJSON(`${Renderer.get().baseUrl}data/items.json`); + const items = itemData.item; + itemData.itemGroup.forEach(it => it._isItemGroup = true); + return [...items, ...itemData.itemGroup]; + } + + static async pGetSiteUnresolvedRefItems () { + const itemList = await Renderer.item._pGetSiteUnresolvedRefItems_pLoadItems(); + const baseItemsJson = await DataUtil.loadJSON(`${Renderer.get().baseUrl}data/items-base.json`); + const baseItems = await Renderer.item._pGetAndProcBaseItems(baseItemsJson); + const {genericVariants, linkedLootTables} = await Renderer.item._pGetCacheSiteGenericVariants(); + const specificVariants = Renderer.item._createSpecificVariants(baseItems, genericVariants, {linkedLootTables}); + const allItems = [...itemList, ...baseItems, ...genericVariants, ...specificVariants]; + Renderer.item._enhanceItems(allItems); + + return { + item: allItems, + itemEntry: baseItemsJson.itemEntry, + }; + } + + static _pGettingSiteGenericVariants = null; + static async _pGetCacheSiteGenericVariants () { + Renderer.item._pGettingSiteGenericVariants = Renderer.item._pGettingSiteGenericVariants || (async () => { + const [genericVariants, linkedLootTables] = Renderer.item._getAndProcGenericVariants(await DataUtil.loadJSON(`${Renderer.get().baseUrl}data/magicvariants.json`)); + return {genericVariants, linkedLootTables}; + })(); + return Renderer.item._pGettingSiteGenericVariants; + } + + static async pBuildList () { + return DataLoader.pCacheAndGetAllSite(UrlUtil.PG_ITEMS); + } + + static async _pGetAndProcBaseItems (baseItemData) { + Renderer.item._addBasePropertiesAndTypes(baseItemData); + await Renderer.item._pAddPrereleaseBrewPropertiesAndTypes(); + return baseItemData.baseitem; + } + + static _getAndProcGenericVariants (variantData) { + variantData.magicvariant.forEach(Renderer.item._genericVariants_addInheritedPropertiesToSelf); + return [variantData.magicvariant, variantData.linkedLootTables]; + } + + static _initFullEntries (item) { + Renderer.utils.initFullEntries_(item); + } + + static _initFullAdditionalEntries (item) { + Renderer.utils.initFullEntries_(item, {propEntries: "additionalEntries", propFullEntries: "_fullAdditionalEntries"}); + } + + /** + * @param baseItems + * @param genericVariants + * @param [opts] + * @param [opts.linkedLootTables] + */ + static _createSpecificVariants (baseItems, genericVariants, opts) { + opts = opts || {}; + + const genericAndSpecificVariants = []; + baseItems.forEach((curBaseItem) => { + curBaseItem._category = "Basic"; + if (curBaseItem.entries == null) curBaseItem.entries = []; + + if (curBaseItem.packContents) return; // e.g. "Arrows (20)" + + genericVariants.forEach((curGenericVariant) => { + if (!Renderer.item._createSpecificVariants_hasRequiredProperty(curBaseItem, curGenericVariant)) return; + if (Renderer.item._createSpecificVariants_hasExcludedProperty(curBaseItem, curGenericVariant)) return; + + genericAndSpecificVariants.push(Renderer.item._createSpecificVariants_createSpecificVariant(curBaseItem, curGenericVariant, opts)); + }); + }); + return genericAndSpecificVariants; + } + + static _createSpecificVariants_hasRequiredProperty (baseItem, genericVariant) { + return genericVariant.requires.some(req => Renderer.item._createSpecificVariants_isRequiresExcludesMatch(baseItem, req, "every")); + } + + static _createSpecificVariants_hasExcludedProperty (baseItem, genericVariant) { + const curExcludes = genericVariant.excludes || {}; + return Renderer.item._createSpecificVariants_isRequiresExcludesMatch(baseItem, genericVariant.excludes, "some"); + } + + static _createSpecificVariants_isRequiresExcludesMatch (candidate, requirements, method) { + if (candidate == null || requirements == null) return false; + + return Object.entries(requirements)[method](([reqKey, reqVal]) => { + if (reqVal instanceof Array) { + return candidate[reqKey] instanceof Array + ? candidate[reqKey].some(it => reqVal.includes(it)) + : reqVal.includes(candidate[reqKey]); + } + + // Recurse for e.g. `"customProperties": { ... }` + if (reqVal != null && typeof reqVal === "object") { + return Renderer.item._createSpecificVariants_isRequiresExcludesMatch(candidate[reqKey], reqVal, method); + } + + return candidate[reqKey] instanceof Array + ? candidate[reqKey].some(it => reqVal === it) + : reqVal === candidate[reqKey]; + }); + } + + /** + * @param baseItem + * @param genericVariant + * @param [opts] + * @param [opts.linkedLootTables] + */ + static _createSpecificVariants_createSpecificVariant (baseItem, genericVariant, opts) { + const inherits = genericVariant.inherits; + const specificVariant = MiscUtil.copyFast(baseItem); + + // Update prop + specificVariant.__prop = "item"; + + // Remove "base item" flag + delete specificVariant._isBaseItem; + + // Reset enhancements/entry cache + specificVariant._isEnhanced = false; + delete specificVariant._fullEntries; + + specificVariant._baseName = baseItem.name; + specificVariant._baseSrd = baseItem.srd; + specificVariant._baseBasicRules = baseItem.basicRules; + if (baseItem.source !== inherits.source) specificVariant._baseSource = baseItem.source; + + specificVariant._variantName = genericVariant.name; + + // Magic items do not inherit the value of the non-magical item + delete specificVariant.value; + + // Magic variants apply their own SRD info; page info + delete specificVariant.srd; + delete specificVariant.basicRules; + delete specificVariant.page; + + // Remove fluff specifiers + delete specificVariant.hasFluff; + delete specificVariant.hasFluffImages; + + specificVariant._category = "Specific Variant"; + Object.entries(inherits) + .forEach(([inheritedProperty, val]) => { + switch (inheritedProperty) { + case "namePrefix": specificVariant.name = `${val}${specificVariant.name}`; break; + case "nameSuffix": specificVariant.name = `${specificVariant.name}${val}`; break; + case "entries": { + Renderer.item._initFullEntries(specificVariant); + + const appliedPropertyEntries = Renderer.applyAllProperties(val, Renderer.item._getInjectableProps(baseItem, inherits)); + appliedPropertyEntries.forEach((ent, i) => specificVariant._fullEntries.splice(i, 0, ent)); + break; + } + case "vulnerable": + case "resist": + case "immune": { + // Handled below + break; + } + case "conditionImmune": { + specificVariant[inheritedProperty] = [...specificVariant[inheritedProperty] || [], ...val].unique(); + break; + } + case "nameRemove": { + specificVariant.name = specificVariant.name.replace(new RegExp(val.escapeRegexp(), "g"), ""); + + break; + } + case "weightExpression": + case "valueExpression": { + const exp = Renderer.item._createSpecificVariants_evaluateExpression(baseItem, specificVariant, inherits, inheritedProperty); + + const result = Renderer.dice.parseRandomise2(exp); + if (result != null) { + switch (inheritedProperty) { + case "weightExpression": specificVariant.weight = result; break; + case "valueExpression": specificVariant.value = result; break; + } + } + + break; + } + case "barding": { + specificVariant.bardingType = baseItem.type; + break; + } + case "propertyAdd": { + specificVariant.property = [ + ...(specificVariant.property || []), + ...val.filter(it => !specificVariant.property || !specificVariant.property.includes(it)), + ]; + break; + } + case "propertyRemove": { + if (specificVariant.property) { + specificVariant.property = specificVariant.property.filter(it => !val.includes(it)); + if (!specificVariant.property.length) delete specificVariant.property; + } + break; + } + default: specificVariant[inheritedProperty] = val; + } + }); + + Renderer.item._createSpecificVariants_mergeVulnerableResistImmune({specificVariant, inherits}); + + // track the specific variant on the parent generic, to later render as part of the stats + genericVariant.variants = genericVariant.variants || []; + if (!genericVariant.variants.some(it => it.base?.name === baseItem.name && it.base?.source === baseItem.source)) genericVariant.variants.push({base: baseItem, specificVariant}); + + // add reverse link to get generic from specific--primarily used for indexing + specificVariant.genericVariant = { + name: genericVariant.name, + source: genericVariant.source, + }; + + // add linked loot tables + if (opts.linkedLootTables && opts.linkedLootTables[specificVariant.source] && opts.linkedLootTables[specificVariant.source][specificVariant.name]) { + (specificVariant.lootTables = specificVariant.lootTables || []).push(...opts.linkedLootTables[specificVariant.source][specificVariant.name]); + } + + if (baseItem.source !== Parser.SRC_PHB && baseItem.source !== Parser.SRC_DMG) { + Renderer.item._initFullEntries(specificVariant); + specificVariant._fullEntries.unshift({ + type: "wrapper", + wrapped: `{@note The {@item ${baseItem.name}|${baseItem.source}|base item} can be found in ${Parser.sourceJsonToFull(baseItem.source)}${baseItem.page ? `, page ${baseItem.page}` : ""}.}`, + data: { + [VeCt.ENTDATA_ITEM_MERGED_ENTRY_TAG]: "note", + }, + }); + } + + return specificVariant; + } + + static _createSpecificVariants_evaluateExpression (baseItem, specificVariant, inherits, inheritedProperty) { + return inherits[inheritedProperty].replace(/\[\[([^\]]+)]]/g, (...m) => { + const propPath = m[1].split("."); + return propPath[0] === "item" + ? MiscUtil.get(specificVariant, ...propPath.slice(1)) + : propPath[0] === "baseItem" + ? MiscUtil.get(baseItem, ...propPath.slice(1)) + : MiscUtil.get(specificVariant, ...propPath); + }); + } + + static _PROPS_VULN_RES_IMMUNE = [ + "vulnerable", + "resist", + "immune", + ]; + static _createSpecificVariants_mergeVulnerableResistImmune ({specificVariant, inherits}) { + const fromBase = {}; + Renderer.item._PROPS_VULN_RES_IMMUNE + .filter(prop => specificVariant[prop]) + .forEach(prop => fromBase[prop] = [...specificVariant[prop]]); + + // For each `inherits` prop, remove matching values from non-matching props in base item (i.e., a value should be + // unique across all three arrays). + Renderer.item._PROPS_VULN_RES_IMMUNE + .forEach(prop => { + const val = inherits[prop]; + + // Retain existing from base item + if (val === undefined) return; + + // Delete from base item + if (val == null) return delete fromBase[prop]; + + const valSet = new Set(); + val.forEach(it => { + if (typeof it === "string") valSet.add(it); + if (!it?.[prop]?.length) return; + it?.[prop].forEach(itSub => { + if (typeof itSub === "string") valSet.add(itSub); + }); + }); + + Renderer.item._PROPS_VULN_RES_IMMUNE + .filter(it => it !== prop) + .forEach(propOther => { + if (!fromBase[propOther]) return; + + fromBase[propOther] = fromBase[propOther] + .filter(it => { + if (typeof it === "string") return !valSet.has(it); + + if (it?.[propOther]?.length) { + it[propOther] = it[propOther].filter(itSub => { + if (typeof itSub === "string") return !valSet.has(itSub); + return true; + }); + } + + return true; + }); + + if (!fromBase[propOther].length) delete fromBase[propOther]; + }); + }); + + Renderer.item._PROPS_VULN_RES_IMMUNE + .forEach(prop => { + if (fromBase[prop] || inherits[prop]) specificVariant[prop] = [...(fromBase[prop] || []), ...(inherits[prop] || [])].unique(); + else delete specificVariant[prop]; + }); + } + + static _enhanceItems (allItems) { + allItems.forEach((item) => Renderer.item.enhanceItem(item)); + return allItems; + } + + /** + * @param genericVariants + * @param opts + * @param [opts.additionalBaseItems] + * @param [opts.baseItems] + * @param [opts.isSpecificVariantsOnly] + */ + static async pGetGenericAndSpecificVariants (genericVariants, opts) { + opts = opts || {}; + + let baseItems; + if (opts.baseItems) { + baseItems = opts.baseItems; + } else { + const baseItemData = await DataUtil.loadJSON(`${Renderer.get().baseUrl}data/items-base.json`); + Renderer.item._addBasePropertiesAndTypes(baseItemData); + baseItems = [...baseItemData.baseitem, ...(opts.additionalBaseItems || [])]; + } + + await Renderer.item._pAddPrereleaseBrewPropertiesAndTypes(); + genericVariants.forEach(Renderer.item._genericVariants_addInheritedPropertiesToSelf); + const specificVariants = Renderer.item._createSpecificVariants(baseItems, genericVariants); + const outSpecificVariants = Renderer.item._enhanceItems(specificVariants); + + if (opts.isSpecificVariantsOnly) return outSpecificVariants; + + const outGenericVariants = Renderer.item._enhanceItems(genericVariants); + return [...outGenericVariants, ...outSpecificVariants]; + } + + static _getInjectableProps (baseItem, inherits) { + return { + baseName: baseItem.name, + dmgType: baseItem.dmgType ? Parser.dmgTypeToFull(baseItem.dmgType) : null, + bonusAc: inherits.bonusAc, + bonusWeapon: inherits.bonusWeapon, + bonusWeaponAttack: inherits.bonusWeaponAttack, + bonusWeaponDamage: inherits.bonusWeaponDamage, + bonusWeaponCritDamage: inherits.bonusWeaponCritDamage, + bonusSpellAttack: inherits.bonusSpellAttack, + bonusSpellSaveDc: inherits.bonusSpellSaveDc, + bonusSavingThrow: inherits.bonusSavingThrow, + }; + } + + static _INHERITED_PROPS_BLOCKLIST = new Set([ + // region Specific merge strategy + "entries", + // endregion + + // region Meaningless on merged item + "namePrefix", + "nameSuffix", + // endregion + ]); + static _genericVariants_addInheritedPropertiesToSelf (genericVariant) { + if (genericVariant._isInherited) return; + genericVariant._isInherited = true; + + for (const prop in genericVariant.inherits) { + if (Renderer.item._INHERITED_PROPS_BLOCKLIST.has(prop)) continue; + + const val = genericVariant.inherits[prop]; + + if (val == null) delete genericVariant[prop]; + else if (genericVariant[prop]) { + if (genericVariant[prop] instanceof Array && val instanceof Array) genericVariant[prop] = MiscUtil.copyFast(genericVariant[prop]).concat(val); + else genericVariant[prop] = val; + } else genericVariant[prop] = genericVariant.inherits[prop]; + } + + if (!genericVariant.entries && genericVariant.inherits.entries) { + genericVariant.entries = MiscUtil.copyFast(Renderer.applyAllProperties(genericVariant.inherits.entries, genericVariant.inherits)); + } + + if (genericVariant.requires.armor) genericVariant.armor = genericVariant.requires.armor; + } + + static getItemTypeName (t) { + return Renderer.item.getType(t).name?.toLowerCase() || t; + } + + static enhanceItem (item) { + if (item._isEnhanced) return; + item._isEnhanced = true; + if (item.noDisplay) return; + if (item.type === "GV") item._category = "Generic Variant"; + if (item._category == null) item._category = "Other"; + if (item.entries == null) item.entries = []; + if (item.type && (Renderer.item.getType(item.type)?.entries || Renderer.item.getType(item.type)?.entriesTemplate)) { + Renderer.item._initFullEntries(item); + + const propetyEntries = Renderer.item._enhanceItem_getItemPropertyTypeEntries({item, ent: Renderer.item.getType(item.type)}); + propetyEntries.forEach(e => item._fullEntries.push({type: "wrapper", wrapped: e, data: {[VeCt.ENTDATA_ITEM_MERGED_ENTRY_TAG]: "type"}})); + } + if (item.property) { + item.property.forEach(p => { + const entProperty = Renderer.item.getProperty(p); + if (!entProperty.entries && !entProperty.entriesTemplate) return; + + Renderer.item._initFullEntries(item); + + const propetyEntries = Renderer.item._enhanceItem_getItemPropertyTypeEntries({item, ent: entProperty}); + propetyEntries.forEach(e => item._fullEntries.push({type: "wrapper", wrapped: e, data: {[VeCt.ENTDATA_ITEM_MERGED_ENTRY_TAG]: "property"}})); + }); + } + // The following could be encoded in JSON, but they depend on more than one JSON property; maybe fix if really bored later + if (item.type === "LA" || item.type === "MA" || item.type === "HA") { + if (item.stealth) { + Renderer.item._initFullEntries(item); + item._fullEntries.push({type: "wrapper", wrapped: "The wearer has disadvantage on Dexterity ({@skill Stealth}) checks.", data: {[VeCt.ENTDATA_ITEM_MERGED_ENTRY_TAG]: "type"}}); + } + if (item.type === "HA" && item.strength) { + Renderer.item._initFullEntries(item); + item._fullEntries.push({type: "wrapper", wrapped: `If the wearer has a Strength score lower than ${item.strength}, their speed is reduced by 10 feet.`, data: {[VeCt.ENTDATA_ITEM_MERGED_ENTRY_TAG]: "type"}}); + } + } + if (item.type === "SCF") { + if (item._isItemGroup) { + if (item.scfType === "arcane" && item.source !== Parser.SRC_ERLW) { + Renderer.item._initFullEntries(item); + item._fullEntries.push({type: "wrapper", wrapped: "An arcane focus is a special item\u2014an orb, a crystal, a rod, a specially constructed staff, a wand-like length of wood, or some similar item\u2014designed to channel the power of arcane spells. A sorcerer, warlock, or wizard can use such an item as a spellcasting focus.", data: {[VeCt.ENTDATA_ITEM_MERGED_ENTRY_TAG]: "type.SCF"}}); + } + if (item.scfType === "druid") { + Renderer.item._initFullEntries(item); + item._fullEntries.push({type: "wrapper", wrapped: "A druidic focus might be a sprig of mistletoe or holly, a wand or scepter made of yew or another special wood, a staff drawn whole out of a living tree, or a totem object incorporating feathers, fur, bones, and teeth from sacred animals. A druid can use such an object as a spellcasting focus.", data: {[VeCt.ENTDATA_ITEM_MERGED_ENTRY_TAG]: "type.SCF"}}); + } + if (item.scfType === "holy") { + Renderer.item._initFullEntries(item); + item._fullEntries.push({type: "wrapper", wrapped: "A holy symbol is a representation of a god or pantheon. It might be an amulet depicting a symbol representing a deity, the same symbol carefully engraved or inlaid as an emblem on a shield, or a tiny box holding a fragment of a sacred relic. A cleric or paladin can use a holy symbol as a spellcasting focus. To use the symbol in this way, the caster must hold it in hand, wear it visibly, or bear it on a shield.", data: {[VeCt.ENTDATA_ITEM_MERGED_ENTRY_TAG]: "type.SCF"}}); + } + } else { + if (item.scfType === "arcane") { + Renderer.item._initFullEntries(item); + item._fullEntries.push({type: "wrapper", wrapped: "An arcane focus is a special item designed to channel the power of arcane spells. A sorcerer, warlock, or wizard can use such an item as a spellcasting focus.", data: {[VeCt.ENTDATA_ITEM_MERGED_ENTRY_TAG]: "type.SCF"}}); + } + if (item.scfType === "druid") { + Renderer.item._initFullEntries(item); + item._fullEntries.push({type: "wrapper", wrapped: "A druid can use this object as a spellcasting focus.", data: {[VeCt.ENTDATA_ITEM_MERGED_ENTRY_TAG]: "type.SCF"}}); + } + if (item.scfType === "holy") { + Renderer.item._initFullEntries(item); + + item._fullEntries.push({type: "wrapper", wrapped: "A holy symbol is a representation of a god or pantheon.", data: {[VeCt.ENTDATA_ITEM_MERGED_ENTRY_TAG]: "type.SCF"}}); + item._fullEntries.push({type: "wrapper", wrapped: "A cleric or paladin can use a holy symbol as a spellcasting focus. To use the symbol in this way, the caster must hold it in hand, wear it visibly, or bear it on a shield.", data: {[VeCt.ENTDATA_ITEM_MERGED_ENTRY_TAG]: "type.SCF"}}); + } + } + } + + (item.mastery || []) + .forEach(uid => { + const mastery = Renderer.item._getMastery(uid); + + if (!mastery) throw new Error(`Item mastery ${uid} not found. You probably meant to load the property/type reference first; see \`Renderer.item.pPopulatePropertyAndTypeReference()\`.`); + if (!mastery.entries && !mastery.entriesTemplate) return; + + Renderer.item._initFullEntries(item); + + item._fullEntries.push({ + type: "wrapper", + wrapped: { + type: "entries", + name: `Mastery: ${mastery.name}`, + source: mastery.source, + page: mastery.page, + entries: Renderer.item._enhanceItem_getItemPropertyTypeEntries({item, ent: mastery}), + }, + data: { + [VeCt.ENTDATA_ITEM_MERGED_ENTRY_TAG]: "mastery", + }, + }); + }); + + // add additional entries based on type (e.g. XGE variants) + if (item.type === "T" || item.type === "AT" || item.type === "INS" || item.type === "GS") { // tools, artisan's tools, instruments, gaming sets + Renderer.item._initFullAdditionalEntries(item); + item._fullAdditionalEntries.push({type: "wrapper", wrapped: {type: "hr"}, data: {[VeCt.ENTDATA_ITEM_MERGED_ENTRY_TAG]: "type"}}); + item._fullAdditionalEntries.push({type: "wrapper", wrapped: `{@note See the {@variantrule Tool Proficiencies|XGE} entry for more information.}`, data: {[VeCt.ENTDATA_ITEM_MERGED_ENTRY_TAG]: "type"}}); + } + + // Add additional sources for all instruments and gaming sets + if (item.type === "INS" || item.type === "GS") item.additionalSources = item.additionalSources || []; + if (item.type === "INS") { + if (!item.additionalSources.find(it => it.source === "XGE" && it.page === 83)) item.additionalSources.push({"source": "XGE", "page": 83}); + } else if (item.type === "GS") { + if (!item.additionalSources.find(it => it.source === "XGE" && it.page === 81)) item.additionalSources.push({"source": "XGE", "page": 81}); + } + + if (item.type && Renderer.item._additionalEntriesMap[item.type]) { + Renderer.item._initFullAdditionalEntries(item); + const additional = Renderer.item._additionalEntriesMap[item.type]; + item._fullAdditionalEntries.push({type: "wrapper", wrapped: {type: "entries", entries: additional}, data: {[VeCt.ENTDATA_ITEM_MERGED_ENTRY_TAG]: "type"}}); + } + + // bake in types + const [typeListText, typeHtml, subTypeHtml] = Renderer.item.getHtmlAndTextTypes(item); + item._typeListText = typeListText; + item._typeHtml = typeHtml; + item._subTypeHtml = subTypeHtml; + + // bake in attunement + const [attune, attuneCat] = Renderer.item.getAttunementAndAttunementCatText(item); + item._attunement = attune; + item._attunementCategory = attuneCat; + + if (item.reqAttuneAlt) { + const [attuneAlt, attuneCatAlt] = Renderer.item.getAttunementAndAttunementCatText(item, "reqAttuneAlt"); + item._attunementCategory = [attuneCat, attuneCatAlt]; + } + + // handle item groups + if (item._isItemGroup && item.items?.length) { + Renderer.item._initFullEntries(item); + item._fullEntries.push({type: "wrapper", wrapped: "Multiple variations of this item exist, as listed below:", data: {[VeCt.ENTDATA_ITEM_MERGED_ENTRY_TAG]: "magicvariant"}}); + item._fullEntries.push({ + type: "wrapper", + wrapped: { + type: "list", + items: item.items.map(it => typeof it === "string" ? `{@item ${it}}` : `{@item ${it.name}|${it.source}}`), + }, + data: {[VeCt.ENTDATA_ITEM_MERGED_ENTRY_TAG]: "magicvariant"}, + }); + } + + // region Add base items list + // item.variants was added during generic variant creation + if (item.variants && item.variants.length) { + item.variants.sort((a, b) => SortUtil.ascSortLower(a.base.name, b.base.name) || SortUtil.ascSortLower(a.base.source, b.base.source)); + + Renderer.item._initFullEntries(item); + item._fullEntries.push({ + type: "wrapper", + wrapped: { + type: "entries", + name: "Base items", + entries: [ + "This item variant can be applied to the following base items:", + { + type: "list", + items: item.variants.map(({base, specificVariant}) => { + return `{@item ${base.name}|${base.source}} ({@item ${specificVariant.name}|${specificVariant.source}})`; + }), + }, + ], + }, + data: {[VeCt.ENTDATA_ITEM_MERGED_ENTRY_TAG]: "magicvariant"}, + }); + } + // endregion + } + + static _enhanceItem_getItemPropertyTypeEntries ({item, ent}) { + if (!ent.entriesTemplate) return MiscUtil.copyFast(ent.entries); + return MiscUtil + .getWalker({ + keyBlocklist: MiscUtil.GENERIC_WALKER_ENTRIES_KEY_BLOCKLIST, + }) + .walk( + MiscUtil.copyFast(ent.entriesTemplate), + { + string: (str) => { + return Renderer.utils.applyTemplate( + item, + str, + ); + }, + }, + ); + } + + static unenhanceItem (item) { + if (!item._isEnhanced) return; + delete item._isEnhanced; + delete item._fullEntries; + } + + static async pGetSiteUnresolvedRefItemsFromPrereleaseBrew ({brewUtil, brew = null}) { + if (brewUtil == null && brew == null) return []; + + brew = brew || await brewUtil.pGetBrewProcessed(); + + (brew.itemProperty || []).forEach(p => Renderer.item._addProperty(p)); + (brew.itemType || []).forEach(t => Renderer.item._addType(t)); + (brew.itemEntry || []).forEach(it => Renderer.item._addEntry(it)); + (brew.itemTypeAdditionalEntries || []).forEach(it => Renderer.item._addAdditionalEntries(it)); + + let items = [...(brew.baseitem || []), ...(brew.item || [])]; + + if (brew.itemGroup) { + const itemGroups = MiscUtil.copyFast(brew.itemGroup); + itemGroups.forEach(it => it._isItemGroup = true); + items = [...items, ...itemGroups]; + } + + Renderer.item._enhanceItems(items); + + let isReEnhanceVariants = false; + + // Get specific variants for brew base items, using official generic variants + if (brew.baseitem && brew.baseitem.length) { + isReEnhanceVariants = true; + + const {genericVariants} = await Renderer.item._pGetCacheSiteGenericVariants(); + + const variants = await Renderer.item.pGetGenericAndSpecificVariants( + genericVariants, + {baseItems: brew.baseitem || [], isSpecificVariantsOnly: true}, + ); + items = [...items, ...variants]; + } + + // Get specific and generic variants for official and brew base items, using brew generic variants + if (brew.magicvariant && brew.magicvariant.length) { + isReEnhanceVariants = true; + + const variants = await Renderer.item.pGetGenericAndSpecificVariants( + brew.magicvariant, + {additionalBaseItems: brew.baseitem || []}, + ); + items = [...items, ...variants]; + } + + // Regenerate the full entries for the generic variants, as there may be more specific variants to add to their + // specific variant lists. + if (isReEnhanceVariants) { + const {genericVariants} = await Renderer.item._pGetCacheSiteGenericVariants(); + genericVariants.forEach(item => { + Renderer.item.unenhanceItem(item); + Renderer.item.enhanceItem(item); + }); + } + + return items; + } + + static async pGetItemsFromPrerelease () { + return DataLoader.pCacheAndGetAllPrerelease(UrlUtil.PG_ITEMS); + } + + static async pGetItemsFromBrew () { + return DataLoader.pCacheAndGetAllBrew(UrlUtil.PG_ITEMS); + } + + static _pPopulatePropertyAndTypeReference = null; + static pPopulatePropertyAndTypeReference () { + return Renderer.item._pPopulatePropertyAndTypeReference || (async () => { + const data = await DataUtil.loadJSON(`${Renderer.get().baseUrl}data/items-base.json`); + + Object.entries(Parser.ITEM_TYPE_JSON_TO_ABV).forEach(([abv, name]) => Renderer.item._addType({abbreviation: abv, name})); + data.itemProperty.forEach(p => Renderer.item._addProperty(p)); + data.itemType.forEach(t => Renderer.item._addType(t)); + data.itemEntry.forEach(it => Renderer.item._addEntry(it)); + data.itemTypeAdditionalEntries.forEach(e => Renderer.item._addAdditionalEntries(e)); + + await Renderer.item._pAddPrereleaseBrewPropertiesAndTypes(); + })(); + } + + // fetch every possible indexable item from official data + static async getAllIndexableItems (rawVariants, rawBaseItems) { + const basicItems = await Renderer.item._pGetAndProcBaseItems(rawBaseItems); + const [genericVariants, linkedLootTables] = await Renderer.item._getAndProcGenericVariants(rawVariants); + const specificVariants = Renderer.item._createSpecificVariants(basicItems, genericVariants, {linkedLootTables}); + + [...genericVariants, ...specificVariants].forEach(item => { + if (item.variants) delete item.variants; // prevent circular references + }); + + return specificVariants; + } + + static isMundane (item) { return item.rarity === "none" || item.rarity === "unknown" || item._category === "Basic"; } + + static isExcluded (item, {hash = null} = {}) { + const name = item.name; + const source = item.source || item.inherits?.source; + + hash = hash || UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_ITEMS]({name, source}); + + if (ExcludeUtil.isExcluded(hash, "item", source)) return true; + + if (item._isBaseItem) return ExcludeUtil.isExcluded(hash, "baseitem", source); + if (item._isItemGroup) return ExcludeUtil.isExcluded(hash, "itemGroup", source); + if (item._variantName) { + if (ExcludeUtil.isExcluded(hash, "_specificVariant", source)) return true; + + const baseHash = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_ITEMS]({name: item._baseName, source: item._baseSource || source}); + if (ExcludeUtil.isExcluded(baseHash, "baseitem", item._baseSource || source)) return true; + + const variantHash = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_ITEMS]({name: item._variantName, source: source}); + return ExcludeUtil.isExcluded(variantHash, "magicvariant", source); + } + if (item.type === "GV") return ExcludeUtil.isExcluded(hash, "magicvariant", source); + + return false; + } + + static pGetFluff (item) { + return Renderer.utils.pGetFluff({ + entity: item, + fnGetFluffData: DataUtil.itemFluff.loadJSON.bind(DataUtil.itemFluff), + fluffProp: "itemFluff", + }); + } +}; + +Renderer.psionic = class { + static enhanceMode (mode) { + if (mode._isEnhanced) return; + + mode.name = [mode.name, Renderer.psionic._enhanceMode_getModeTitleBracketPart({mode: mode})].filter(Boolean).join(" "); + + if (mode.submodes) { + mode.submodes.forEach(sm => { + sm.name = [sm.name, Renderer.psionic._enhanceMode_getModeTitleBracketPart({mode: sm})].filter(Boolean).join(" "); + }); + } + + mode._isEnhanced = true; + } + + static _enhanceMode_getModeTitleBracketPart ({mode}) { + const modeTitleBracketArray = []; + + if (mode.cost) modeTitleBracketArray.push(Renderer.psionic._enhanceMode_getModeTitleCost({mode})); + if (mode.concentration) modeTitleBracketArray.push(Renderer.psionic._enhanceMode_getModeTitleConcentration({mode})); + + if (modeTitleBracketArray.length === 0) return null; + return `(${modeTitleBracketArray.join("; ")})`; + } + + static _enhanceMode_getModeTitleCost ({mode}) { + const costMin = mode.cost.min; + const costMax = mode.cost.max; + const costString = costMin === costMax ? costMin : `${costMin}-${costMax}`; + return `${costString} psi`; + } + + static _enhanceMode_getModeTitleConcentration ({mode}) { + return `conc., ${mode.concentration.duration} ${mode.concentration.unit}.`; + } + + /* -------------------------------------------- */ + + static getPsionicRenderableEntriesMeta (ent) { + const entriesContent = []; + + return { + entryTypeOrder: `{@i ${Renderer.psionic.getTypeOrderString(ent)}}`, + entryContent: ent.entries ? {entries: ent.entries, type: "entries"} : null, + entryFocus: ent.focus ? `{@b {@i Psychic Focus.}} ${ent.focus}` : null, + entriesModes: ent.modes + ? ent.modes + .flatMap(mode => Renderer.psionic._getModeEntries(mode)) + : null, + }; + } + + static _getModeEntries (mode, renderer) { + Renderer.psionic.enhanceMode(mode); + + return [ + { + type: mode.type || "entries", + name: mode.name, + entries: mode.entries, + }, + mode.submodes ? Renderer.psionic._getSubModesEntry(mode.submodes) : null, + ] + .filter(Boolean); + } + + static _getSubModesEntry (subModes) { + return { + type: "list", + style: "list-hang-notitle", + items: subModes + .map(sm => ({ + type: "item", + name: sm.name, + entries: sm.entries, + })), + }; + } + + static getTypeOrderString (psi) { + const typeMeta = Parser.psiTypeToMeta(psi.type); + // if "isAltDisplay" is true, render as e.g. "Greater Discipline (Awakened)" rather than "Awakened Greater Discipline" + return typeMeta.hasOrder + ? typeMeta.isAltDisplay ? `${typeMeta.full} (${psi.order})` : `${psi.order} ${typeMeta.full}` + : typeMeta.full; + } + + static getBodyHtml (ent, {renderer = null, entriesMeta = null} = {}) { + renderer ||= Renderer.get().setFirstSection(true); + entriesMeta ||= Renderer.psionic.getPsionicRenderableEntriesMeta(ent); + + return `${entriesMeta.entryContent ? renderer.render(entriesMeta.entryContent) : ""} + ${entriesMeta.entryFocus ? `

    ${renderer.render(entriesMeta.entryFocus)}

    ` : ""} + ${entriesMeta.entriesModes ? entriesMeta.entriesModes.map(entry => renderer.render(entry, 2)).join("") : ""}`; + } + + static getCompactRenderedString (ent) { + const renderer = Renderer.get().setFirstSection(true); + const entriesMeta = Renderer.psionic.getPsionicRenderableEntriesMeta(ent); + + return ` + ${Renderer.utils.getExcludedTr({entity: ent, dataProp: "psionic", page: UrlUtil.PG_PSIONICS})} + ${Renderer.utils.getNameTr(ent, {page: UrlUtil.PG_PSIONICS})} + +

    ${renderer.render(entriesMeta.entryTypeOrder)}

    + ${Renderer.psionic.getBodyHtml(ent, {renderer, entriesMeta})} + + `; + } +}; + +Renderer.rule = class { + static getCompactRenderedString (rule) { + return ` + + ${Renderer.get().setFirstSection(true).render(rule)} + + `; + } +}; + +Renderer.variantrule = class { + static getCompactRenderedString (rule) { + const cpy = MiscUtil.copyFast(rule); + delete cpy.name; + return ` + ${Renderer.utils.getExcludedTr({entity: rule, dataProp: "variantrule", page: UrlUtil.PG_VARIANTRULES})} + ${Renderer.utils.getNameTr(rule, {page: UrlUtil.PG_VARIANTRULES})} + + ${Renderer.get().setFirstSection(true).render(cpy)} + + `; + } +}; + +Renderer.table = class { + static getCompactRenderedString (it) { + it.type = it.type || "table"; + const cpy = MiscUtil.copyFast(it); + delete cpy.name; + return ` + ${Renderer.utils.getExcludedTr({entity: it, dataProp: "table", page: UrlUtil.PG_TABLES})} + ${Renderer.utils.getNameTr(it, {page: UrlUtil.PG_TABLES})} + + ${Renderer.get().setFirstSection(true).render(it)} + + `; + } + + static getConvertedEncounterOrNamesTable ({group, tableRaw, fnGetNameCaption, colLabel1}) { + const getPadded = (number) => { + if (tableRaw.diceExpression === "d100") return String(number).padStart(2, "0"); + return String(number); + }; + + const nameCaption = fnGetNameCaption(group, tableRaw); + return { + name: nameCaption, + type: "table", + source: group?.source, + page: group?.page, + caption: nameCaption, + colLabels: [ + `{@dice ${tableRaw.diceExpression}}`, + colLabel1, + tableRaw.rollAttitude ? `Attitude` : null, + ].filter(Boolean), + colStyles: [ + "col-2 text-center", + tableRaw.rollAttitude ? "col-8" : "col-10", + tableRaw.rollAttitude ? `col-2 text-center` : null, + ].filter(Boolean), + rows: tableRaw.table.map(it => [ + `${getPadded(it.min)}${it.max != null && it.max !== it.min ? `-${getPadded(it.max)}` : ""}`, + it.result, + tableRaw.rollAttitude ? it.resultAttitude || "\u2014" : null, + ].filter(Boolean)), + footnotes: tableRaw.footnotes, + }; + } + + static getConvertedEncounterTableName (group, tableRaw) { + return `${group.name}${tableRaw.caption ? ` ${tableRaw.caption}` : ""}${/\bencounters?\b/i.test(group.name) ? "" : " Encounters"}${tableRaw.minlvl && tableRaw.maxlvl ? ` (Levels ${tableRaw.minlvl}\u2014${tableRaw.maxlvl})` : ""}`; + } + + static getConvertedNameTableName (group, tableRaw) { + return `${group.name} Names \u2013 ${tableRaw.option}`; + } + + static getHeaderRowMetas (ent) { + if (!ent.colLabels?.length && !ent.colLabelGroups?.length) return null; + + if (ent.colLabels?.length) return [ent.colLabels]; + + const maxHeight = Math.max(...ent.colLabelGroups.map(clg => clg.colLabels?.length || 0)); + + const padded = ent.colLabelGroups + .map(clg => { + const out = [...(clg.colLabels || [])]; + while (out.length < maxHeight) out.unshift(""); + return out; + }); + + return [...new Array(maxHeight)] + .map((_, i) => padded.map(lbls => lbls[i])); + } + + static _RE_TABLE_ROW_DASHED_NUMBERS = /^\d+([-\u2012\u2013]\d+)?/; + static getAutoConvertedRollMode (table, {headerRowMetas} = {}) { + if (headerRowMetas === undefined) headerRowMetas = Renderer.table.getHeaderRowMetas(table); + + if (!headerRowMetas || headerRowMetas.last().length < 2) return RollerUtil.ROLL_COL_NONE; + + const rollColMode = RollerUtil.getColRollType(headerRowMetas.last()[0]); + if (!rollColMode) return RollerUtil.ROLL_COL_NONE; + + if (!Renderer.table.isEveryRowRollable(table.rows)) return RollerUtil.ROLL_COL_NONE; + + return rollColMode; + } + + static isEveryRowRollable (rows) { + // scan the first column to ensure all rollable + return rows + .every(row => { + if (!row) return false; + const [cell] = row; + return Renderer.table.isRollableCell(cell); + }); + } + + static isRollableCell (cell) { + if (cell == null) return false; + if (cell?.roll) return true; + + if (typeof cell === "number") return Number.isInteger(cell); + + // u2012 = figure dash; u2013 = en-dash + return typeof cell === "string" && Renderer.table._RE_TABLE_ROW_DASHED_NUMBERS.test(cell); + } +}; + +Renderer.vehicle = class { + static CHILD_PROPS = ["movement", "weapon", "other", "action", "trait", "reaction", "control", "actionStation"]; + + static getVehicleRenderableEntriesMeta (ent) { + return { + entryDamageImmunities: ent.immune + ? `{@b Damage Immunities} ${Parser.getFullImmRes(ent.immune)}` + : null, + entryConditionImmunities: ent.conditionImmune + ? `{@b Condition Immunities} ${Parser.getFullCondImm(ent.conditionImmune, {isEntry: true})}` + : null, + }; + } + + static getCompactRenderedString (veh, opts) { + return Renderer.vehicle.getRenderedString(veh, {...opts, isCompact: true}); + } + + static getRenderedString (ent, opts) { + opts = opts || {}; + + if (ent.upgradeType) return Renderer.vehicleUpgrade.getCompactRenderedString(ent, opts); + + ent.vehicleType ||= "SHIP"; + switch (ent.vehicleType) { + case "SHIP": return Renderer.vehicle._getRenderedString_ship(ent, opts); + case "SPELLJAMMER": return Renderer.vehicle._getRenderedString_spelljammer(ent, opts); + case "INFWAR": return Renderer.vehicle._getRenderedString_infwar(ent, opts); + case "CREATURE": return Renderer.monster.getCompactRenderedString(ent, {...opts, isHideLanguages: true, isHideSenses: true, isCompact: opts.isCompact ?? false, page: UrlUtil.PG_VEHICLES}); + case "OBJECT": return Renderer.object.getCompactRenderedString(ent, {...opts, isCompact: opts.isCompact ?? false, page: UrlUtil.PG_VEHICLES}); + default: throw new Error(`Unhandled vehicle type "${ent.vehicleType}"`); + } + } + + static ship = class { + static PROPS_RENDERABLE_ENTRIES_ATTRIBUTES = [ + "entryCreatureCapacity", + "entryCargoCapacity", + "entryTravelPace", + "entryTravelPaceNote", + ]; + + static getVehicleShipRenderableEntriesMeta (ent) { + // Render UA ship actions at the top, to match later printed layout + const entriesOtherActions = (ent.other || []).filter(it => it.name === "Actions"); + const entriesOtherOthers = (ent.other || []).filter(it => it.name !== "Actions"); + + return { + entrySizeDimensions: `{@i ${Parser.sizeAbvToFull(ent.size)} vehicle${ent.dimensions ? ` (${ent.dimensions.join(" by ")})` : ""}}`, + entryCreatureCapacity: ent.capCrew != null || ent.capPassenger != null + ? `{@b Creature Capacity} ${Renderer.vehicle.getShipCreatureCapacity(ent)}` + : null, + entryCargoCapacity: ent.capCargo != null + ? `{@b Cargo Capacity} ${Renderer.vehicle.getShipCargoCapacity(ent)}` + : null, + entryTravelPace: ent.pace != null + ? `{@b Travel Pace} ${ent.pace} miles per hour (${ent.pace * 24} miles per day)` + : null, + entryTravelPaceNote: ent.pace != null + ? `[{@b Speed} ${ent.pace * 10} ft.]` + : null, + entryTravelPaceNoteTitle: ent.pace != null + ? `Based on "Special Travel Pace," DMG p242` + : null, + + entriesOtherActions: entriesOtherActions.length ? entriesOtherActions : null, + entriesOtherOthers: entriesOtherOthers.length ? entriesOtherOthers : null, + }; + } + + static getLocomotionEntries (loc) { + return { + type: "list", + style: "list-hang-notitle", + items: [ + { + type: "item", + name: `Locomotion (${loc.mode})`, + entries: loc.entries, + }, + ], + }; + } + + static getSpeedEntries (spd) { + return { + type: "list", + style: "list-hang-notitle", + items: [ + { + type: "item", + name: `Speed (${spd.mode})`, + entries: spd.entries, + }, + ], + }; + } + + static getActionPart_ (renderer, veh) { + return renderer.render({entries: veh.action}); + } + + static getSectionTitle_ (title) { + return `

    ${title}

    `; + } + + static getSectionHpEntriesMeta_ ({entry, isEach = false}) { + return { + entryArmorClass: entry.ac + ? `{@b Armor Class} ${entry.ac}` + : null, + entryHitPoints: entry.hp + ? `{@b Hit Points} ${entry.hp}${isEach ? ` each` : ""}${entry.dt ? ` (damage threshold ${entry.dt})` : ""}${entry.hpNote ? `; ${entry.hpNote}` : ""}` + : null, + }; + } + + static getSectionHpPart_ (renderer, entry, isEach) { + const entriesMetaSection = Renderer.vehicle.ship.getSectionHpEntriesMeta_({entry, isEach}); + + const props = [ + "entryArmorClass", + "entryHitPoints", + ]; + + if (!props.some(prop => entriesMetaSection[prop])) return ""; + + return props + .map(prop => `
    ${renderer.render(entriesMetaSection[prop])}
    `) + .join(""); + } + + static getControlSection_ (renderer, control) { + if (!control) return ""; + return ` +

    Control: ${control.name}

    + + ${Renderer.vehicle.ship.getSectionHpPart_(renderer, control)} +
    ${renderer.render({entries: control.entries})}
    + + `; + } + + static _getMovementSection_getLocomotionSection ({renderer, entry}) { + const asList = Renderer.vehicle.ship.getLocomotionEntries(entry); + return `
    ${renderer.render(asList)}
    `; + } + + static _getMovementSection_getSpeedSection ({renderer, entry}) { + const asList = Renderer.vehicle.ship.getSpeedEntries(entry); + return `
    ${renderer.render(asList)}
    `; + } + + static getMovementSection_ (renderer, move) { + if (!move) return ""; + + return ` +

    ${move.isControl ? `Control and ` : ""}Movement: ${move.name}

    + + ${Renderer.vehicle.ship.getSectionHpPart_(renderer, move)} + ${(move.locomotion || []).map(entry => Renderer.vehicle.ship._getMovementSection_getLocomotionSection({renderer, entry})).join("")} + ${(move.speed || []).map(entry => Renderer.vehicle.ship._getMovementSection_getSpeedSection({renderer, entry})).join("")} + + `; + } + + static getWeaponSection_ (renderer, weap) { + return ` +

    Weapons: ${weap.name}${weap.count ? ` (${weap.count})` : ""}

    + + ${Renderer.vehicle.ship.getSectionHpPart_(renderer, weap, !!weap.count)} + ${renderer.render({entries: weap.entries})} + + `; + } + + static getOtherSection_ (renderer, oth) { + return ` +

    ${oth.name}

    + + ${Renderer.vehicle.ship.getSectionHpPart_(renderer, oth)} + ${renderer.render({entries: oth.entries})} + + `; + } + + static getCrewCargoPaceSection_ (ent, {entriesMetaShip = null} = {}) { + entriesMetaShip ||= Renderer.vehicle.ship.getVehicleShipRenderableEntriesMeta(ent); + if (!Renderer.vehicle.ship.PROPS_RENDERABLE_ENTRIES_ATTRIBUTES.some(prop => entriesMetaShip[prop])) return ""; + + return ` + ${entriesMetaShip.entryCreatureCapacity ? `
    ${Renderer.get().render(entriesMetaShip.entryCreatureCapacity)}
    ` : ""} + ${entriesMetaShip.entryCargoCapacity ? `
    ${Renderer.get().render(entriesMetaShip.entryCargoCapacity)}
    ` : ""} + ${entriesMetaShip.entryTravelPace ? `
    ${Renderer.get().render(entriesMetaShip.entryTravelPace)}
    ` : ""} + ${entriesMetaShip.entryTravelPaceNote ? `
    ${Renderer.get().render(entriesMetaShip.entryTravelPaceNote)}
    ` : ""} + `; + } + }; + + static spelljammer = class { + static getVehicleSpelljammerRenderableEntriesMeta (ent) { + const ptAc = ent.hull?.ac + ? `${ent.hull.ac}${ent.hull.acFrom ? ` (${ent.hull.acFrom.join(", ")})` : ""}` + : "\u2014"; + + const ptSpeed = ent.speed != null + ? Parser.getSpeedString(ent, {isSkipZeroWalk: true}) + : ""; + const ptPace = Renderer.vehicle.spelljammer._getVehicleSpelljammerRenderableEntriesMeta_getPtPace({ent}); + + const ptSpeedPace = [ptSpeed, ptPace].filter(Boolean).join(" "); + + return { + entryTableSummary: { + type: "table", + style: "summary", + colStyles: ["col-6", "col-6"], + rows: [ + [ + `{@b Armor Class:} ${ptAc}`, + `{@b Cargo:} ${ent.capCargo ? `${ent.capCargo} ton${ent.capCargo === 1 ? "" : "s"}` : "\u2014"}`, + ], + [ + `{@b Hit Points:} ${ent.hull?.hp ?? "\u2014"}`, + `{@b Crew:} ${ent.capCrew ?? "\u2014"}${ent.capCrewNote ? ` ${ent.capCrewNote}` : ""}`, + ], + [ + `{@b Damage Threshold:} ${ent.hull?.dt ?? "\u2014"}`, + `{@b Keel/Beam:} ${(ent.dimensions || ["\u2014"]).join("/")}`, + ], + [ + `{@b Speed:} ${ptSpeedPace}`, + `{@b Cost:} ${ent.cost != null ? Parser.vehicleCostToFull(ent) : "\u2014"}`, + ], + ], + }, + }; + } + + static _getVehicleSpelljammerRenderableEntriesMeta_getPtPace (ent) { + if (!ent.pace) return ""; + + const isMulti = Object.keys(ent.pace).length > 1; + + const out = Parser.SPEED_MODES + .map(mode => { + const pace = ent.pace[mode]; + if (!pace) return null; + + const asNum = Parser.vulgarToNumber(pace); + return `{@tip ${isMulti && mode !== "walk" ? `${mode} ` : ""}${pace} mph|${asNum * 24} miles per day}`; + }) + .filter(Boolean) + .join(", "); + + return `(${out})`; + } + + static getSummarySection_ (renderer, ent) { + const entriesMetaSpelljammer = Renderer.vehicle.spelljammer.getVehicleSpelljammerRenderableEntriesMeta(ent); + + return `${renderer.render(entriesMetaSpelljammer.entryTableSummary)}`; + } + + static getSectionWeaponEntriesMeta (entry) { + const isMultiple = entry.count != null && entry.count > 1; + + return { + entryName: `${isMultiple ? `${entry.count} ` : ""}${entry.name}${entry.crew ? ` (Crew: ${entry.crew}${isMultiple ? " each" : ""})` : ""}`, + }; + } + + static getWeaponSection_ (renderer, entry) { + const entriesMetaSectionWeapon = Renderer.vehicle.spelljammer.getSectionWeaponEntriesMeta(entry); + + const ptAction = entry.action?.length + ? entry.action.map(act => `
    ${renderer.render(act, 2)}
    `).join("") + : ""; + return ` +

    ${entriesMetaSectionWeapon.entryName}

    + + ${Renderer.vehicle.spelljammer.getSectionHpCostPart_(renderer, entry)} + ${entry.entries?.length ? `
    ${renderer.render({entries: entry.entries})}
    ` : ""} + ${ptAction} + + `; + } + + static getSectionHpCostEntriesMeta (entry) { + const ptCosts = entry.costs?.length + ? entry.costs.map(cost => { + return `${Parser.vehicleCostToFull(cost) || "\u2014"}${cost.note ? ` (${cost.note})` : ""}`; + }).join(", ") + : "\u2014"; + + return { + entryArmorClass: `{@b Armor Class:} ${entry.ac == null ? "\u2014" : entry.ac}`, + entryHitPoints: `{@b Hit Points:} ${entry.hp == null ? "\u2014" : entry.hp}`, + entryCost: `{@b Cost:} ${ptCosts}`, + }; + } + + static getSectionHpCostPart_ (renderer, entry) { + const entriesMetaSectionHpCost = Renderer.vehicle.spelljammer.getSectionHpCostEntriesMeta(entry); + + return ` +
    ${renderer.render(entriesMetaSectionHpCost.entryArmorClass)}
    +
    ${renderer.render(entriesMetaSectionHpCost.entryHitPoints)}
    +
    ${renderer.render(entriesMetaSectionHpCost.entryCost)}
    + `; + } + }; + + static _getAbilitySection (veh) { + return Parser.ABIL_ABVS.some(it => veh[it] != null) ? ` + + + + + + + + + + + + + + + + + +
    STRDEXCONINTWISCHA
    ${Renderer.utils.getAbilityRoller(veh, "str")}${Renderer.utils.getAbilityRoller(veh, "dex")}${Renderer.utils.getAbilityRoller(veh, "con")}${Renderer.utils.getAbilityRoller(veh, "int")}${Renderer.utils.getAbilityRoller(veh, "wis")}${Renderer.utils.getAbilityRoller(veh, "cha")}
    + ` : ""; + } + + static _getResImmVulnSection (ent, {entriesMeta = null} = {}) { + entriesMeta ||= Renderer.vehicle.getVehicleRenderableEntriesMeta(ent); + + const props = [ + "entryDamageImmunities", + "entryConditionImmunities", + ]; + + if (!props.some(prop => entriesMeta[prop])) return ""; + + return ` + ${props.filter(prop => entriesMeta[prop]).map(prop => `
    ${Renderer.get().render(entriesMeta[prop])}
    `).join("")} + `; + } + + static _getTraitSection (renderer, veh) { + return veh.trait ? `

    Traits

    +
    + + ${Renderer.monster.getOrderedTraits(veh, renderer).map(it => it.rendered || renderer.render(it, 2)).join("")} + ` : ""; + } + + static _getRenderedString_ship (ent, opts) { + const renderer = Renderer.get(); + const entriesMeta = Renderer.vehicle.getVehicleRenderableEntriesMeta(ent); + const entriesMetaShip = Renderer.vehicle.ship.getVehicleShipRenderableEntriesMeta(ent); + + const hasToken = Renderer.vehicle.hasToken(ent); + const extraThClasses = !opts.isCompact && hasToken ? ["veh__name--token"] : null; + + return ` + ${Renderer.utils.getExcludedTr({entity: ent, dataProp: "vehicle", page: UrlUtil.PG_VEHICLES})} + ${Renderer.utils.getNameTr(ent, {extraThClasses, page: UrlUtil.PG_VEHICLES})} + ${Renderer.get().render(entriesMetaShip.entrySizeDimensions)} + ${Renderer.vehicle.ship.getCrewCargoPaceSection_(ent, {entriesMetaShip})} + ${Renderer.vehicle._getAbilitySection(ent)} + ${Renderer.vehicle._getResImmVulnSection(ent, {entriesMeta})} + ${ent.action ? Renderer.vehicle.ship.getSectionTitle_("Actions") : ""} + ${ent.action ? `${Renderer.vehicle.ship.getActionPart_(renderer, ent)}` : ""} + ${(entriesMetaShip.entriesOtherActions || []).map(Renderer.vehicle.ship.getOtherSection_.bind(this, renderer)).join("")} + ${ent.hull ? `${Renderer.vehicle.ship.getSectionTitle_("Hull")} + + ${Renderer.vehicle.ship.getSectionHpPart_(renderer, ent.hull)} + ` : ""} + ${Renderer.vehicle._getTraitSection(renderer, ent)} + ${(ent.control || []).map(Renderer.vehicle.ship.getControlSection_.bind(this, renderer)).join("")} + ${(ent.movement || []).map(Renderer.vehicle.ship.getMovementSection_.bind(this, renderer)).join("")} + ${(ent.weapon || []).map(Renderer.vehicle.ship.getWeaponSection_.bind(this, renderer)).join("")} + ${(entriesMetaShip.entriesOtherOthers || []).map(Renderer.vehicle.ship.getOtherSection_.bind(this, renderer)).join("")} + `; + } + + static getShipCreatureCapacity (veh) { + return [ + veh.capCrew ? `${veh.capCrew} crew` : null, + veh.capPassenger ? `${veh.capPassenger} passenger${veh.capPassenger === 1 ? "" : "s"}` : null, + ].filter(Boolean).join(", "); + } + + static getShipCargoCapacity (veh) { + return typeof veh.capCargo === "string" ? veh.capCargo : `${veh.capCargo} ton${veh.capCargo === 1 ? "" : "s"}`; + } + + static _getRenderedString_spelljammer (veh, opts) { + const renderer = Renderer.get(); + + const hasToken = Renderer.vehicle.hasToken(veh); + const extraThClasses = !opts.isCompact && hasToken ? ["veh__name--token"] : null; + + return ` + ${Renderer.utils.getExcludedTr({entity: veh, dataProp: "vehicle", page: UrlUtil.PG_VEHICLES})} + ${Renderer.utils.getNameTr(veh, {extraThClasses, page: UrlUtil.PG_VEHICLES})} + ${Renderer.vehicle.spelljammer.getSummarySection_(renderer, veh)} + ${(veh.weapon || []).map(Renderer.vehicle.spelljammer.getWeaponSection_.bind(this, renderer)).join("")} + `; + } + + static infwar = class { + static PROPS_RENDERABLE_ENTRIES_ATTRIBUTES = [ + "entryCreatureCapacity", + "entryCargoCapacity", + "entryArmorClass", + "entryHitPoints", + "entrySpeed", + ]; + + static getVehicleInfwarRenderableEntriesMeta (ent) { + const dexMod = Parser.getAbilityModNumber(ent.dex); + + return { + entrySizeWeight: `{@i ${Parser.sizeAbvToFull(ent.size)} vehicle (${ent.weight.toLocaleString()} lb.)}`, + entryCreatureCapacity: `{@b Creature Capacity} ${Renderer.vehicle.getInfwarCreatureCapacity(ent)}`, + entryCargoCapacity: `{@b Cargo Capacity} ${Parser.weightToFull(ent.capCargo)}`, + entryArmorClass: `{@b Armor Class} ${dexMod === 0 ? `19` : `${19 + dexMod} (19 while motionless)`}`, + entryHitPoints: `{@b Hit Points} ${ent.hp.hp} (damage threshold ${ent.hp.dt}, mishap threshold ${ent.hp.mt})`, + entrySpeed: `{@b Speed} ${ent.speed} ft.`, + entrySpeedNote: `[{@b Travel Pace} ${Math.floor(ent.speed / 10)} miles per hour (${Math.floor(ent.speed * 24 / 10)} miles per day)]`, + entrySpeedNoteTitle: `Based on "Special Travel Pace," DMG p242`, + }; + } + }; + + static _getRenderedString_infwar (ent, opts) { + const renderer = Renderer.get(); + const entriesMeta = Renderer.vehicle.getVehicleRenderableEntriesMeta(ent); + const entriesMetaInfwar = Renderer.vehicle.infwar.getVehicleInfwarRenderableEntriesMeta(ent); + + const hasToken = Renderer.vehicle.hasToken(ent); + const extraThClasses = !opts.isCompact && hasToken ? ["veh__name--token"] : null; + + return ` + ${Renderer.utils.getExcludedTr({entity: ent, datProp: "vehicle", page: UrlUtil.PG_VEHICLES})} + ${Renderer.utils.getNameTr(ent, {extraThClasses, page: UrlUtil.PG_VEHICLES})} + ${renderer.render(entriesMetaInfwar.entrySizeWeight)} + + ${Renderer.vehicle.infwar.PROPS_RENDERABLE_ENTRIES_ATTRIBUTES.map(prop => `
    ${renderer.render(entriesMetaInfwar[prop])}
    `).join("")} +
    ${renderer.render(entriesMetaInfwar.entrySpeedNote)}
    + + ${Renderer.vehicle._getAbilitySection(ent)} + ${Renderer.vehicle._getResImmVulnSection(ent, {entriesMeta})} + ${Renderer.vehicle._getTraitSection(renderer, ent)} + ${Renderer.monster.getCompactRenderedStringSection(ent, renderer, "Action Stations", "actionStation", 2)} + ${Renderer.monster.getCompactRenderedStringSection(ent, renderer, "Reactions", "reaction", 2)} + `; + } + + static getInfwarCreatureCapacity (veh) { + return `${veh.capCreature} Medium creatures`; + } + + static pGetFluff (veh) { + return Renderer.utils.pGetFluff({ + entity: veh, + fnGetFluffData: DataUtil.vehicleFluff.loadJSON.bind(DataUtil.vehicleFluff), + fluffProp: "vehicleFluff", + }); + } + + static hasToken (veh, opts) { + return Renderer.generic.hasToken(veh, opts); + } + + static getTokenUrl (veh, opts) { + return Renderer.generic.getTokenUrl(veh, "vehicles/tokens", opts); + } +}; + +Renderer.vehicleUpgrade = class { + static getUpgradeSummary (ent) { + return [ + ent.upgradeType ? ent.upgradeType.map(t => Parser.vehicleTypeToFull(t)) : null, + ent.prerequisite ? Renderer.utils.prerequisite.getHtml(ent.prerequisite) : null, + ] + .filter(Boolean) + .join(", "); + } + + static getCompactRenderedString (ent, opts) { + return `${Renderer.utils.getExcludedTr({entity: ent, dataProp: "vehicleUpgrade", page: UrlUtil.PG_VEHICLES})} + ${Renderer.utils.getNameTr(ent, {page: UrlUtil.PG_VEHICLES})} + ${Renderer.vehicleUpgrade.getUpgradeSummary(ent)} +
    + ${Renderer.get().render({entries: ent.entries}, 1)}`; + } +}; + +Renderer.action = class { + static getCompactRenderedString (it) { + const cpy = MiscUtil.copyFast(it); + delete cpy.name; + return `${Renderer.utils.getExcludedTr({entity: it, dataProp: "action", page: UrlUtil.PG_ACTIONS})} + ${Renderer.utils.getNameTr(it, {page: UrlUtil.PG_ACTIONS})} + ${Renderer.get().setFirstSection(true).render(cpy)}`; + } +}; + +Renderer.language = class { + static getLanguageRenderableEntriesMeta (ent) { + const hasMeta = ent.typicalSpeakers || ent.script; + + const entriesContent = []; + + if (ent.entries) entriesContent.push(...ent.entries); + if (ent.dialects) { + entriesContent.push(`This language is a family which includes the following dialects: ${ent.dialects.sort(SortUtil.ascSortLower).join(", ")}. Creatures that speak different dialects of the same language can communicate with one another.`); + } + + if (!entriesContent.length && !hasMeta) entriesContent.push("{@i No information available.}"); + + return { + entryType: ent.type ? `{@i ${ent.type.toTitleCase()} language}` : null, + entryTypicalSpeakers: ent.typicalSpeakers ? `{@b Typical Speakers:} ${ent.typicalSpeakers.join(", ")}` : null, + entryScript: ent.script ? `{@b Script:} ${ent.script}` : null, + entriesContent: entriesContent.length ? entriesContent : null, + }; + } + + static getCompactRenderedString (ent) { + return Renderer.language.getRenderedString(ent); + } + + static getRenderedString (ent, {isSkipNameRow = false} = {}) { + const entriesMeta = Renderer.language.getLanguageRenderableEntriesMeta(ent); + + return ` + ${Renderer.utils.getExcludedTr({entity: ent, dataProp: "language", page: UrlUtil.PG_LANGUAGES})} + ${isSkipNameRow ? "" : Renderer.utils.getNameTr(ent, {page: UrlUtil.PG_LANGUAGES})} + ${entriesMeta.entryType ? `${Renderer.get().render(entriesMeta.entryType)}` : ""} + ${entriesMeta.entryTypicalSpeakers || entriesMeta.entryScript ? ` + ${[entriesMeta.entryTypicalSpeakers, entriesMeta.entryScript].filter(Boolean).map(entry => `
    ${Renderer.get().render(entry)}
    `).join("")} + ` : ""} + ${entriesMeta.entriesContent ? ` + ${Renderer.get().setFirstSection(true).render({entries: entriesMeta.entriesContent})} + ` : ""}`; + } + + static pGetFluff (it) { + return Renderer.utils.pGetFluff({ + entity: it, + fnGetFluffData: DataUtil.languageFluff.loadJSON.bind(DataUtil.languageFluff), + fluffProp: "languageFluff", + }); + } +}; + +Renderer.adventureBook = class { + static getEntryIdLookup (bookData, doThrowError = true) { + const out = {}; + const titlesRel = {}; + const titlesRelChapter = {}; + + let chapIx; + const depthStack = []; + const handlers = { + object: (obj) => { + Renderer.ENTRIES_WITH_ENUMERATED_TITLES + .forEach(meta => { + if (obj.type !== meta.type) return; + + const curDepth = depthStack.length ? depthStack.last() : 0; + const nxtDepth = meta.depth ? meta.depth : meta.depthIncrement ? curDepth + meta.depthIncrement : curDepth; + + depthStack.push( + Math.min( + nxtDepth, + 2, + ), + ); + }); + + if (!obj.id) return obj; + + if (out[obj.id]) { + (out.__BAD = out.__BAD || []).push(obj.id); + return obj; + } + + out[obj.id] = { + chapter: chapIx, + entry: obj, + depth: depthStack.last(), + }; + + if (obj.name) { + out[obj.id].name = obj.name; + + const cleanName = obj.name.toLowerCase(); + out[obj.id].nameClean = cleanName; + + // Relative title index for full-book mode + titlesRel[cleanName] = titlesRel[cleanName] || 0; + out[obj.id].ixTitleRel = titlesRel[cleanName]++; + + // Relative title index per-chapter + MiscUtil.getOrSet(titlesRelChapter, chapIx, cleanName, -1); + out[obj.id].ixTitleRelChapter = ++titlesRelChapter[chapIx][cleanName]; + } + + return obj; + }, + postObject: (obj) => { + Renderer.ENTRIES_WITH_ENUMERATED_TITLES + .forEach(meta => { + if (obj.type !== meta.type) return; + + depthStack.pop(); + }); + }, + }; + + bookData.forEach((chap, _chapIx) => { + chapIx = _chapIx; + MiscUtil.getWalker({ + isNoModification: true, + keyBlocklist: new Set(["mapParent"]), + }) + .walk(chap, handlers); + }); + + if (doThrowError) if (out.__BAD) throw new Error(`IDs were already in storage: ${out.__BAD.map(it => `"${it}"`).join(", ")}`); + + return out; + } + + static _isAltMissingCoverUsed = false; + static getCoverUrl (contents) { + if (contents.cover) { + return UrlUtil.link(Renderer.utils.getEntryMediaUrl(contents, "cover", "img")); + } + + // TODO(Future) remove as deprecated; remove from schema; remove from proporder + if (contents.coverUrl) { + if (/^https?:\/\//.test(contents.coverUrl)) return contents.coverUrl; + return UrlUtil.link(Renderer.get().getMediaUrl("img", contents.coverUrl.replace(/^img\//, ""))); + } + + return UrlUtil.link(Renderer.get().getMediaUrl("img", `covers/blank${Math.random() <= 0.05 && !Renderer.adventureBook._isAltMissingCoverUsed && (Renderer.adventureBook._isAltMissingCoverUsed = true) ? "-alt" : ""}.webp`)); + } +}; + +Renderer.charoption = class { + static getCompactRenderedString (ent) { + const prerequisite = Renderer.utils.prerequisite.getHtml(ent.prerequisite); + const preText = Renderer.charoption.getOptionTypePreText(ent); + return ` + ${Renderer.utils.getExcludedTr({entity: ent, dataProp: "charoption", page: UrlUtil.PG_CHAR_CREATION_OPTIONS})} + ${Renderer.utils.getNameTr(ent, {page: UrlUtil.PG_CHAR_CREATION_OPTIONS})} + + ${prerequisite ? `

    ${prerequisite}

    ` : ""} + ${preText || ""}${Renderer.get().setFirstSection(true).render({type: "entries", entries: ent.entries})} + + `; + } + + /* -------------------------------------------- */ + + static getCharoptionRenderableEntriesMeta (ent) { + const optsMapped = ent.optionType + .map(it => Renderer.charoption._OPTION_TYPE_ENTRIES[it]) + .filter(Boolean); + if (!optsMapped.length) return null; + + return { + entryOptionType: {type: "entries", entries: optsMapped}, + }; + } + + static _OPTION_TYPE_ENTRIES = { + "RF:B": `{@note You may replace the standard feature of your background with this feature.}`, + "CS": `{@note See the {@adventure Character Secrets|IDRotF|0|character secrets} section for more information.}`, + }; + + static getOptionTypePreText (ent) { + const meta = Renderer.charoption.getCharoptionRenderableEntriesMeta(ent); + if (!meta) return ""; + return Renderer.get().render(meta.entryOptionType); + } + + /* -------------------------------------------- */ + + static pGetFluff (it) { + return Renderer.utils.pGetFluff({ + entity: it, + fnGetFluffData: DataUtil.charoptionFluff.loadJSON.bind(DataUtil.charoptionFluff), + fluffProp: "charoptionFluff", + }); + } +}; + +Renderer.recipe = class { + static _getEntryMetasTime (ent) { + if (!Object.keys(ent.time || {}).length) return null; + + return [ + "total", + "preparation", + "cooking", + ...Object.keys(ent.time), + ] + .unique() + .filter(prop => ent.time[prop]) + .map((prop, i, arr) => { + const val = ent.time[prop]; + + const ptsTime = ( + val.min != null && val.max != null + ? [ + Parser.getMinutesToFull(val.min), + Parser.getMinutesToFull(val.max), + ] + : [Parser.getMinutesToFull(val)] + ); + + const suffix = MiscUtil.findCommonSuffix(ptsTime, {isRespectWordBoundaries: true}); + const ptTime = ptsTime + .map(it => !suffix.length ? it : it.slice(0, -suffix.length)) + .join(" to "); + + return { + entryName: `{@b {@style ${prop.toTitleCase()} Time:|small-caps}}`, + entryContent: `${ptTime}${suffix}`, + }; + }); + } + + static getRecipeRenderableEntriesMeta (ent) { + return { + entryMakes: ent.makes + ? `{@b {@style Makes|small-caps}} ${ent._scaleFactor ? `${ent._scaleFactor}ร— ` : ""}${ent.makes}` + : null, + entryServes: ent.serves + ? `{@b {@style Serves|small-caps}} ${ent.serves.min ?? ent.serves.exact}${ent.serves.min != null ? " to " : ""}${ent.serves.max ?? ""}` + : null, + entryMetasTime: Renderer.recipe._getEntryMetasTime(ent), + entryIngredients: {entries: ent._fullIngredients}, + entryEquipment: ent._fullEquipment?.length + ? {entries: ent._fullEquipment} + : null, + entryCooksNotes: ent.noteCook + ? {entries: ent.noteCook} + : null, + entryInstructions: {entries: ent.instructions}, + }; + } + + static getCompactRenderedString (ent) { + return `${Renderer.utils.getExcludedTr({entity: ent, dataProp: "recipe", page: UrlUtil.PG_RECIPES})} + ${Renderer.utils.getNameTr(ent, {page: UrlUtil.PG_RECIPES})} + + ${Renderer.recipe.getBodyHtml(ent)} + `; + } + + static getBodyHtml (ent) { + const entriesMeta = Renderer.recipe.getRecipeRenderableEntriesMeta(ent); + + const ptTime = Renderer.recipe.getTimeHtml(ent, {entriesMeta}); + const {ptMakes, ptServes} = Renderer.recipe.getMakesServesHtml(ent, {entriesMeta}); + + return `
    +
    + ${ptTime || ""} + + ${ptMakes || ""} + ${ptServes || ""} + +
    ${Renderer.get().render(entriesMeta.entryIngredients, 0)}
    + + ${entriesMeta.entryEquipment ? `
    Equipment
    ${Renderer.get().render(entriesMeta.entryEquipment)}
    ` : ""} + + ${entriesMeta.entryCooksNotes ? `
    Cook's Notes
    ${Renderer.get().render(entriesMeta.entryCooksNotes)}
    ` : ""} +
    + +
    + ${Renderer.get().setFirstSection(true).render(entriesMeta.entryInstructions, 2)} +
    +
    `; + } + + static getMakesServesHtml (ent, {entriesMeta = null} = {}) { + entriesMeta ||= Renderer.recipe.getRecipeRenderableEntriesMeta(ent); + const ptMakes = entriesMeta.entryMakes ? `
    ${Renderer.get().render(entriesMeta.entryMakes)}
    ` : null; + const ptServes = entriesMeta.entryServes ? `
    ${Renderer.get().render(entriesMeta.entryServes)}
    ` : null; + return {ptMakes, ptServes}; + } + + static getTimeHtml (ent, {entriesMeta = null} = {}) { + entriesMeta ||= Renderer.recipe.getRecipeRenderableEntriesMeta(ent); + if (!entriesMeta.entryMetasTime) return ""; + + return entriesMeta.entryMetasTime + .map(({entryName, entryContent}, i, arr) => { + return `
    + ${Renderer.get().render(entryName)} + ${Renderer.get().render(entryContent)} +
    `; + }) + .join(""); + } + + static pGetFluff (it) { + return Renderer.utils.pGetFluff({ + entity: it, + fnGetFluffData: DataUtil.recipeFluff.loadJSON.bind(DataUtil.recipeFluff), + fluffProp: "recipeFluff", + }); + } + + static populateFullIngredients (r) { + r._fullIngredients = Renderer.applyAllProperties(MiscUtil.copyFast(r.ingredients)); + if (r.equipment) r._fullEquipment = Renderer.applyAllProperties(MiscUtil.copyFast(r.equipment)); + } + + static _RE_AMOUNT = /(?{=amount\d+(?:\/[^}]+)?})/g; + static _SCALED_PRECISION_LIMIT = 10 ** 6; + static getScaledRecipe (r, scaleFactor) { + const cpyR = MiscUtil.copyFast(r); + + ["ingredients", "equipment"] + .forEach(prop => { + if (!cpyR[prop]) return; + + MiscUtil.getWalker({keyBlocklist: MiscUtil.GENERIC_WALKER_ENTRIES_KEY_BLOCKLIST}).walk( + cpyR[prop], + { + object: (obj) => { + if (obj.type !== "ingredient") return obj; + + const objOriginal = MiscUtil.copyFast(obj); + + Object.keys(obj) + .filter(k => /^amount\d+/.test(k)) + .forEach(k => { + let base = obj[k]; + + if (Math.round(base) !== base && base < 20) { + const divOneSixth = obj[k] / 0.166; + if (Math.abs(divOneSixth - Math.round(divOneSixth)) < 0.05) base = (1 / 6) * Math.round(divOneSixth); + } + + let scaled = base * scaleFactor; + obj[k] = Math.round(base * scaleFactor * Renderer.recipe._SCALED_PRECISION_LIMIT) / Renderer.recipe._SCALED_PRECISION_LIMIT; + }); + + // region Attempt to singleize/pluralize units + const amountsOriginal = Object.keys(objOriginal).filter(k => /^amount\d+$/.test(k)).map(k => objOriginal[k]); + const amountsScaled = Object.keys(obj).filter(k => /^amount\d+$/.test(k)).map(k => obj[k]); + + const entryParts = obj.entry.split(Renderer.recipe._RE_AMOUNT).filter(Boolean); + const entryPartsOut = entryParts.slice(0, entryParts.findIndex(it => Renderer.recipe._RE_AMOUNT.test(it)) + 1); + let ixAmount = 0; + for (let i = entryPartsOut.length; i < entryParts.length; ++i) { + let pt = entryParts[i]; + + if (Renderer.recipe._RE_AMOUNT.test(pt)) { + ixAmount++; + entryPartsOut.push(pt); + continue; + } + + if (amountsOriginal[ixAmount] == null || amountsScaled[ixAmount] == null) { + entryPartsOut.push(pt); + continue; + } + + const isSingleToPlural = amountsOriginal[ixAmount] <= 1 && amountsScaled[ixAmount] > 1; + const isPluralToSingle = amountsOriginal[ixAmount] > 1 && amountsScaled[ixAmount] <= 1; + + if (!isSingleToPlural && !isPluralToSingle) { + entryPartsOut.push(pt); + continue; + } + + if (isSingleToPlural) pt = Renderer.recipe._getPluralizedUnits(pt); + else if (isPluralToSingle) pt = Renderer.recipe._getSingleizedUnits(pt); + entryPartsOut.push(pt); + } + + obj.entry = entryPartsOut.join(""); + // endregion + + Renderer.recipe._mutWrapOriginalAmounts({obj, objOriginal}); + + return obj; + }, + }, + ); + }); + + Renderer.recipe.populateFullIngredients(cpyR); + + if (cpyR.serves) { + if (cpyR.serves.min) cpyR.serves.min *= scaleFactor; + if (cpyR.serves.max) cpyR.serves.max *= scaleFactor; + if (cpyR.serves.exact) cpyR.serves.exact *= scaleFactor; + } + + cpyR._displayName = `${cpyR.name} (ร—${scaleFactor})`; + cpyR._scaleFactor = scaleFactor; + + return cpyR; + } + + static _UNITS_SINGLE_TO_PLURAL_S = [ + "bundle", + "cup", + "handful", + "ounce", + "packet", + "piece", + "pound", + "slice", + "sprig", + "square", + "strip", + "tablespoon", + "teaspoon", + "wedge", + ]; + static _UNITS_SINGLE_TO_PLURAL_ES = [ + "dash", + "inch", + ]; + static _FNS_SINGLE_TO_PLURAL = []; + static _FNS_PLURAL_TO_SINGLE = []; + + static _getSingleizedUnits (str) { + if (!Renderer.recipe._FNS_PLURAL_TO_SINGLE.length) { + Renderer.recipe._FNS_PLURAL_TO_SINGLE = [ + ...Renderer.recipe._UNITS_SINGLE_TO_PLURAL_S.map(word => str => str.replace(new RegExp(`\\b${word.escapeRegexp()}s\\b`, "gi"), (...m) => m[0].slice(0, -1))), + ...Renderer.recipe._UNITS_SINGLE_TO_PLURAL_ES.map(word => str => str.replace(new RegExp(`\\b${word.escapeRegexp()}es\\b`, "gi"), (...m) => m[0].slice(0, -2))), + ]; + } + + Renderer.recipe._FNS_PLURAL_TO_SINGLE.forEach(fn => str = fn(str)); + + return str; + } + + static _getPluralizedUnits (str) { + if (!Renderer.recipe._FNS_SINGLE_TO_PLURAL.length) { + Renderer.recipe._FNS_SINGLE_TO_PLURAL = [ + ...Renderer.recipe._UNITS_SINGLE_TO_PLURAL_S.map(word => str => str.replace(new RegExp(`\\b${word.escapeRegexp()}\\b`, "gi"), (...m) => `${m[0]}s`)), + ...Renderer.recipe._UNITS_SINGLE_TO_PLURAL_ES.map(word => str => str.replace(new RegExp(`\\b${word.escapeRegexp()}\\b`, "gi"), (...m) => `${m[0]}es`)), + ]; + } + + Renderer.recipe._FNS_SINGLE_TO_PLURAL.forEach(fn => str = fn(str)); + + return str; + } + + /** Only apply the `@help` note to standalone amounts, i.e. those not in other tags. */ + static _mutWrapOriginalAmounts ({obj, objOriginal}) { + const parts = []; + let stack = ""; + let depth = 0; + for (let i = 0; i < obj.entry.length; ++i) { + const c = obj.entry[i]; + switch (c) { + case "{": { + if (!depth && stack) { + parts.push(stack); + stack = ""; + } + depth++; + stack += c; + break; + } + case "}": { + depth--; + stack += c; + if (!depth && stack) { + parts.push(stack); + stack = ""; + } + break; + } + default: stack += c; + } + } + if (stack) parts.push(stack); + obj.entry = parts + .map(pt => pt.replace(Renderer.recipe._RE_AMOUNT, (...m) => { + const ixStart = m.slice(-3, -2)[0]; + if (ixStart !== 0 || m[0].length !== pt.length) return m[0]; + + const originalValue = Renderer.applyProperties(m.last().tagAmount, objOriginal); + return `{@help ${m.last().tagAmount}|In the original recipe: ${originalValue}}`; + })) + .join(""); + } + + // region Custom hash ID packing/unpacking + static getCustomHashId (it) { + if (!it._scaleFactor) return null; + + const { + name, + source, + _scaleFactor: scaleFactor, + } = it; + + return [ + name, + source, + scaleFactor ?? "", + ].join("__").toLowerCase(); + } + + static getUnpackedCustomHashId (customHashId) { + if (!customHashId) return null; + + const [, , scaleFactor] = customHashId.split("__").map(it => it.trim()); + + if (!scaleFactor) return null; + + return { + _scaleFactor: scaleFactor ? Number(scaleFactor) : null, + customHashId, + }; + } + // endregion + + static async pGetModifiedRecipe (ent, customHashId) { + if (!customHashId) return ent; + const {_scaleFactor} = Renderer.recipe.getUnpackedCustomHashId(customHashId); + if (_scaleFactor == null) return ent; + return Renderer.recipe.getScaledRecipe(ent, _scaleFactor); + } +}; + +Renderer.card = class { + static getFullEntries (ent, {backCredit = null} = {}) { + const entries = [...ent.entries || []]; + if (ent.suit && (ent.valueName || ent.value)) { + const suitAndValue = `${((ent.valueName || "") || Parser.numberToText(ent.value)).toTitleCase()} of ${ent.suit.toTitleCase()}`; + if (suitAndValue.toLowerCase() !== ent.name.toLowerCase()) entries.unshift(`{@i ${suitAndValue}}`); + } + + const ptCredits = [ + ent.face?.credit ? `art credit: ${ent.face?.credit}` : null, + (backCredit || ent.back?.credit) ? `art credit (reverse): ${backCredit || ent.back?.credit}` : null, + ] + .filter(Boolean) + .join(", ") + .uppercaseFirst(); + if (ptCredits) entries.push(`{@note {@style ${ptCredits}|small}}`); + + return entries; + } + + static getCompactRenderedString (ent) { + const fullEntries = Renderer.card.getFullEntries(ent); + return ` + ${Renderer.utils.getNameTr(ent)} + + ${Renderer.get().setFirstSection(true).render({...ent.face, maxHeight: 40, maxHeightUnits: "vh"})} + ${fullEntries?.length ? `
    + ${Renderer.get().setFirstSection(true).render({type: "entries", entries: fullEntries}, 1)}` : ""} + + `; + } +}; + +Renderer.deck = class { + static getCompactRenderedString (ent) { + const lstCards = { + name: "Cards", + entries: [ + { + type: "list", + columns: 3, + items: ent.cards.map(card => `{@card ${card.name}|${card.set}|${card.source}}`), + }, + ], + }; + + return ` + ${Renderer.utils.getNameTr(ent)} + + ${Renderer.get().setFirstSection(true).render({type: "entries", entries: ent.entries}, 1)} +
    + ${Renderer.get().setFirstSection(true).render(lstCards, 1)} + + `; + } +}; + +Renderer.skill = class { + static getCompactRenderedString (ent) { + return Renderer.generic.getCompactRenderedString(ent); + } +}; + +Renderer.sense = class { + static getCompactRenderedString (ent) { + return Renderer.generic.getCompactRenderedString(ent); + } +}; + +Renderer.itemMastery = class { + static getCompactRenderedString (ent) { + return Renderer.generic.getCompactRenderedString(ent); + } +}; + +Renderer.generic = class { + /** + * @param ent + * @param [opts] + * @param [opts.isSkipNameRow] + * @param [opts.isSkipPageRow] + * @param [opts.dataProp] + * @param [opts.page] + */ + static getCompactRenderedString (ent, opts) { + opts = opts || {}; + const prerequisite = Renderer.utils.prerequisite.getHtml(ent.prerequisite); + + return ` + ${opts.dataProp && opts.page ? Renderer.utils.getExcludedTr({entity: ent, dataProp: opts.dataProp, page: opts.page}) : ""} + ${opts.isSkipNameRow ? "" : Renderer.utils.getNameTr(ent, {page: opts.page})} + + ${prerequisite ? `

    ${prerequisite}

    ` : ""} + ${Renderer.get().setFirstSection(true).render({entries: ent.entries})} + + ${opts.isSkipPageRow ? "" : Renderer.utils.getPageTr(ent)}`; + } + + /* -------------------------------------------- */ + + // region Mirror the schema + static FEATURE__SKILLS_ALL = Object.keys(Parser.SKILL_TO_ATB_ABV).sort(SortUtil.ascSortLower); + + static FEATURE__TOOLS_ARTISANS = [ + "alchemist's supplies", + "brewer's supplies", + "calligrapher's supplies", + "carpenter's tools", + "cartographer's tools", + "cobbler's tools", + "cook's utensils", + "glassblower's tools", + "jeweler's tools", + "leatherworker's tools", + "mason's tools", + "painter's supplies", + "potter's tools", + "smith's tools", + "tinker's tools", + "weaver's tools", + "woodcarver's tools", + ]; + static FEATURE__TOOLS_MUSICAL_INSTRUMENTS = [ + "bagpipes", + "drum", + "dulcimer", + "flute", + "horn", + "lute", + "lyre", + "pan flute", + "shawm", + "viol", + ]; + static FEATURE__TOOLS_ALL = [ + "artisan's tools", + + ...this.FEATURE__TOOLS_ARTISANS, + ...this.FEATURE__TOOLS_MUSICAL_INSTRUMENTS, + + "disguise kit", + "forgery kit", + "gaming set", + "herbalism kit", + "musical instrument", + "navigator's tools", + "thieves' tools", + "poisoner's kit", + "vehicles (land)", + "vehicles (water)", + "vehicles (air)", + "vehicles (space)", + ]; + + static FEATURE__LANGUAGES_ALL = Parser.LANGUAGES_ALL.map(it => it.toLowerCase()); + static FEATURE__LANGUAGES_STANDARD__CHOICE_OBJ = { + from: [ + ...Parser.LANGUAGES_STANDARD + .map(it => ({ + name: it.toLowerCase(), + prop: "languageProficiencies", + group: "languagesStandard", + })), + ...Parser.LANGUAGES_EXOTIC + .map(it => ({ + name: it.toLowerCase(), + prop: "languageProficiencies", + group: "languagesExotic", + })), + ...Parser.LANGUAGES_SECRET + .map(it => ({ + name: it.toLowerCase(), + prop: "languageProficiencies", + group: "languagesSecret", + })), + ], + groups: { + languagesStandard: { + name: "Standard Languages", + }, + languagesExotic: { + name: "Exotic Languages", + hint: "With your DM's permission, you can choose an exotic language.", + }, + languagesSecret: { + name: "Secret Languages", + hint: "With your DM's permission, you can choose a secret language.", + }, + }, + }; + + static FEATURE__SAVING_THROWS_ALL = [...Parser.ABIL_ABVS]; + // endregion + + /* -------------------------------------------- */ + + // region Should mirror the schema + static _SKILL_TOOL_LANGUAGE_KEYS__SKILL_ANY = new Set(["anySkill"]); + static _SKILL_TOOL_LANGUAGE_KEYS__TOOL_ANY = new Set(["anyTool", "anyArtisansTool"]); + static _SKILL_TOOL_LANGUAGE_KEYS__LANGAUGE_ANY = new Set(["anyLanguage", "anyStandardLanguage", "anyExoticLanguage"]); + // endregion + + static getSkillSummary ({skillProfs, skillToolLanguageProfs, isShort = false}) { + return this._summariseProfs({ + profGroupArr: skillProfs, + skillToolLanguageProfs, + setValid: new Set(this.FEATURE__SKILLS_ALL), + setValidAny: this._SKILL_TOOL_LANGUAGE_KEYS__SKILL_ANY, + isShort, + hoverTag: "skill", + }); + } + + static getToolSummary ({toolProfs, skillToolLanguageProfs, isShort = false}) { + return this._summariseProfs({ + profGroupArr: toolProfs, + skillToolLanguageProfs, + setValid: new Set(this.FEATURE__TOOLS_ALL), + setValidAny: this._SKILL_TOOL_LANGUAGE_KEYS__TOOL_ANY, + isShort, + }); + } + + static getLanguageSummary ({languageProfs, skillToolLanguageProfs, isShort = false}) { + return this._summariseProfs({ + profGroupArr: languageProfs, + skillToolLanguageProfs, + setValid: new Set(this.FEATURE__LANGUAGES_ALL), + setValidAny: this._SKILL_TOOL_LANGUAGE_KEYS__LANGAUGE_ANY, + isShort, + }); + } + + static _summariseProfs ({profGroupArr, skillToolLanguageProfs, setValid, setValidAny, isShort, hoverTag}) { + if (!profGroupArr?.length && !skillToolLanguageProfs?.length) return {summary: "", collection: []}; + + const collectionSet = new Set(); + + const handleProfGroup = (profGroup, {isValidate = true} = {}) => { + let sep = ", "; + + const toJoin = Object.entries(profGroup) + .sort(([kA], [kB]) => this._summariseProfs_sortKeys(kA, kB)) + .filter(([k, v]) => v && (!isValidate || setValid.has(k) || setValidAny.has(k))) + .map(([k, v], i) => { + const vMapped = this.getMappedAnyProficiency({keyAny: k, countRaw: v}) ?? v; + + if (k === "choose") { + sep = "; "; + + const chooseProfs = vMapped.from + .filter(s => !isValidate || setValid.has(s)) + .map(s => { + collectionSet.add(s); + return this._summariseProfs_getEntry({str: s, isShort, hoverTag}); + }); + return `${isShort ? `${i === 0 ? "C" : "c"}hoose ` : ""}${v.count || 1} ${isShort ? `of` : `from`} ${chooseProfs.joinConjunct(", ", " or ")}`; + } + + collectionSet.add(k); + return this._summariseProfs_getEntry({str: k, isShort, hoverTag}); + }); + + return toJoin.join(sep); + }; + + const summary = [ + ...(profGroupArr || []) + // Skip validation (i.e. allow homebrew/etc.) for the specific proficiency array + .map(profGroup => handleProfGroup(profGroup, {isValidate: false})), + ...(skillToolLanguageProfs || []) + .map(profGroup => handleProfGroup(profGroup)), + ] + .filter(Boolean) + .join(` or `); + + return {summary, collection: [...collectionSet].sort(SortUtil.ascSortLower)}; + } + + static _summariseProfs_sortKeys (a, b, {setValidAny = null} = {}) { + if (a === b) return 0; + if (a === "choose") return 2; + if (b === "choose") return -2; + if (setValidAny) { + if (setValidAny.has(a)) return 1; + if (setValidAny.has(b)) return -1; + } + return SortUtil.ascSort(a, b); + } + + static _summariseProfs_getEntry ({str, isShort, hoverTag}) { + return isShort ? str.toTitleCase() : hoverTag ? `{@${hoverTag} ${str.toTitleCase()}}` : str.toTitleCase(); + } + + /* -------------------------------------------- */ + + static getMappedAnyProficiency ({keyAny, countRaw}) { + const mappedCount = !isNaN(countRaw) ? Number(countRaw) : 1; + if (mappedCount <= 0) return null; + + switch (keyAny) { + case "anySkill": return { + name: mappedCount === 1 ? `Any Skill` : `Any ${mappedCount} Skills`, + from: this.FEATURE__SKILLS_ALL + .map(it => ({name: it, prop: "skillProficiencies"})), + count: mappedCount, + }; + case "anyTool": return { + name: mappedCount === 1 ? `Any Tool` : `Any ${mappedCount} Tools`, + from: this.FEATURE__TOOLS_ALL + .map(it => ({name: it, prop: "toolProficiencies"})), + count: mappedCount, + }; + case "anyArtisansTool": return { + name: mappedCount === 1 ? `Any Artisan's Tool` : `Any ${mappedCount} Artisan's Tools`, + from: this.FEATURE__TOOLS_ARTISANS + .map(it => ({name: it, prop: "toolProficiencies"})), + count: mappedCount, + }; + case "anyMusicalInstrument": return { + name: mappedCount === 1 ? `Any Musical Instrument` : `Any ${mappedCount} Musical Instruments`, + from: this.FEATURE__TOOLS_MUSICAL_INSTRUMENTS + .map(it => ({name: it, prop: "toolProficiencies"})), + count: mappedCount, + }; + case "anyLanguage": return { + name: mappedCount === 1 ? `Any Language` : `Any ${mappedCount} Languages`, + from: this.FEATURE__LANGUAGES_ALL + .map(it => ({name: it, prop: "languageProficiencies"})), + count: mappedCount, + }; + case "anyStandardLanguage": return { + name: mappedCount === 1 ? `Any Standard Language` : `Any ${mappedCount} Standard Languages`, + ...MiscUtil.copyFast(this.FEATURE__LANGUAGES_STANDARD__CHOICE_OBJ), // Use a generic choice object, as rules state DM can allow choosing any + count: mappedCount, + }; + case "anyExoticLanguage": return { + name: mappedCount === 1 ? `Any Exotic Language` : `Any ${mappedCount} Exotic Languages`, + ...MiscUtil.copyFast(this.FEATURE__LANGUAGES_STANDARD__CHOICE_OBJ), // Use a generic choice object, as rules state DM can allow choosing any + count: mappedCount, + }; + case "anySavingThrow": return { + name: mappedCount === 1 ? `Any Saving Throw` : `Any ${mappedCount} Saving Throws`, + from: this.FEATURE__SAVING_THROWS_ALL + .map(it => ({name: it, prop: "savingThrowProficiencies"})), + count: mappedCount, + }; + + case "anyWeapon": throw new Error(`Property handling for "anyWeapon" is unimplemented!`); + case "anyArmor": throw new Error(`Property handling for "anyArmor" is unimplemented!`); + + default: return null; + } + } + + /* -------------------------------------------- */ + + static hasToken (ent, {isIgnoreImplicit = false} = {}) { + const fromEntity = ent.tokenUrl // TODO(Future) legacy; remove + || ent.token // An explicit token + || ent.tokenHref // An explicit token URL (local or external) + ; + if (fromEntity || isIgnoreImplicit) return !!fromEntity; + return ent.hasToken; // An implicit token + } + + static getTokenUrl (ent, mediaDir, {isIgnoreImplicit = false} = {}) { + if (ent.tokenUrl) return ent.tokenUrl; // TODO(Future) legacy; remove + if (ent.token) return Renderer.get().getMediaUrl("img", `${mediaDir}/${Parser.sourceJsonToAbv(ent.token.source)}/${Parser.nameToTokenName(ent.token.name)}.webp`); + if (ent.tokenHref) return Renderer.utils.getEntryMediaUrl(ent, "tokenHref", "img"); + if (isIgnoreImplicit) return null; + return Renderer.get().getMediaUrl("img", `${mediaDir}/${Parser.sourceJsonToAbv(ent.source)}/${Parser.nameToTokenName(ent.name)}.webp`); + } +}; + +Renderer.hover = class { + static LinkMeta = class { + constructor () { + this.isHovered = false; + this.isLoading = false; + this.isPermanent = false; + this.windowMeta = null; + } + }; + + static _BAR_HEIGHT = 16; + + static _linkCache = {}; + static _eleCache = new Map(); + static _entryCache = {}; + static _isInit = false; + static _dmScreen = null; + static _lastId = 0; + static _contextMenu = null; + static _contextMenuLastClicked = null; + + static bindDmScreen (screen) { this._dmScreen = screen; } + + static _getNextId () { return ++Renderer.hover._lastId; } + + static _doInit () { + if (!Renderer.hover._isInit) { + Renderer.hover._isInit = true; + + $(document.body).on("click", () => Renderer.hover.cleanTempWindows()); + + Renderer.hover._contextMenu = ContextUtil.getMenu([ + new ContextUtil.Action( + "Maximize All", + () => { + const $permWindows = $(`.hoverborder[data-perm="true"]`); + $permWindows.attr("data-display-title", "false"); + }, + ), + new ContextUtil.Action( + "Minimize All", + () => { + const $permWindows = $(`.hoverborder[data-perm="true"]`); + $permWindows.attr("data-display-title", "true"); + }, + ), + null, + new ContextUtil.Action( + "Close Others", + () => { + const hoverId = Renderer.hover._contextMenuLastClicked?.hoverId; + Renderer.hover._doCloseAllWindows({hoverIdBlocklist: new Set([hoverId])}); + }, + ), + new ContextUtil.Action( + "Close All", + () => Renderer.hover._doCloseAllWindows(), + ), + ]); + } + } + + static cleanTempWindows () { + for (const [key, meta] of Renderer.hover._eleCache.entries()) { + // If this is an element-less "permanent" show which has been closed + if (!meta.isPermanent && meta.windowMeta && typeof key === "number") { + meta.windowMeta.doClose(); + Renderer.hover._eleCache.delete(key); + return; + } + + if (!meta.isPermanent && meta.windowMeta && !document.body.contains(key)) { + meta.windowMeta.doClose(); + return; + } + + if (!meta.isPermanent && meta.isHovered && meta.windowMeta) { + // Check if any elements have failed to clear their hovering status on mouse move + const bounds = key.getBoundingClientRect(); + if (EventUtil._mouseX < bounds.x + || EventUtil._mouseY < bounds.y + || EventUtil._mouseX > bounds.x + bounds.width + || EventUtil._mouseY > bounds.y + bounds.height) { + meta.windowMeta.doClose(); + } + } + } + } + + static _doCloseAllWindows ({hoverIdBlocklist = null} = {}) { + Object.entries(Renderer.hover._WINDOW_METAS) + .filter(([hoverId, meta]) => hoverIdBlocklist == null || !hoverIdBlocklist.has(Number(hoverId))) + .forEach(([, meta]) => meta.doClose()); + } + + static _getSetMeta (ele) { + if (!Renderer.hover._eleCache.has(ele)) Renderer.hover._eleCache.set(ele, new Renderer.hover.LinkMeta()); + return Renderer.hover._eleCache.get(ele); + } + + static _handleGenericMouseOverStart ({evt, ele}) { + // Don't open on small screens unless forced + if (Renderer.hover.isSmallScreen(evt) && !evt.shiftKey) return; + + Renderer.hover.cleanTempWindows(); + + const meta = Renderer.hover._getSetMeta(ele); + if (meta.isHovered || meta.isLoading) return; // Another hover is already in progress + + // Set the cursor to a waiting spinner + ele.style.cursor = "progress"; + + meta.isHovered = true; + meta.isLoading = true; + meta.isPermanent = evt.shiftKey; + + return meta; + } + + static _doPredefinedShowStart ({entryId}) { + Renderer.hover.cleanTempWindows(); + + const meta = Renderer.hover._getSetMeta(entryId); + + meta.isPermanent = true; + + return meta; + } + + static getLinkElementData (ele) { + return { + page: ele.dataset.vetPage, + source: ele.dataset.vetSource, + hash: ele.dataset.vetHash, + preloadId: ele.dataset.vetPreloadId, + isFauxPage: ele.dataset.vetIsFauxPage, + }; + } + + // (Baked into render strings) + static async pHandleLinkMouseOver (evt, ele, opts) { + Renderer.hover._doInit(); + + let page, source, hash, preloadId, customHashId, isFauxPage; + if (opts) { + page = opts.page; + source = opts.source; + hash = opts.hash; + preloadId = opts.preloadId; + customHashId = opts.customHashId; + isFauxPage = !!opts.isFauxPage; + } else { + ({ + page, + source, + hash, + preloadId, + isFauxPage, + } = Renderer.hover.getLinkElementData(ele)); + } + + let meta = Renderer.hover._handleGenericMouseOverStart({evt, ele}); + if (meta == null) return; + + if ((EventUtil.isCtrlMetaKey(evt)) && Renderer.hover._pageToFluffFn(page)) meta.isFluff = true; + + let toRender; + if (preloadId != null) { // FIXME(Future) remove in favor of `customHashId` + switch (page) { + case UrlUtil.PG_BESTIARY: { + const {_scaledCr: scaledCr, _scaledSpellSummonLevel: scaledSpellSummonLevel, _scaledClassSummonLevel: scaledClassSummonLevel} = Renderer.monster.getUnpackedCustomHashId(preloadId); + + const baseMon = await DataLoader.pCacheAndGet(page, source, hash); + if (scaledCr != null) { + toRender = await ScaleCreature.scale(baseMon, scaledCr); + } else if (scaledSpellSummonLevel != null) { + toRender = await ScaleSpellSummonedCreature.scale(baseMon, scaledSpellSummonLevel); + } else if (scaledClassSummonLevel != null) { + toRender = await ScaleClassSummonedCreature.scale(baseMon, scaledClassSummonLevel); + } + break; + } + } + } else if (customHashId) { + toRender = await DataLoader.pCacheAndGet(page, source, hash); + toRender = await Renderer.hover.pApplyCustomHashId(page, toRender, customHashId); + } else { + if (meta.isFluff) toRender = await Renderer.hover.pGetHoverableFluff(page, source, hash); + else toRender = await DataLoader.pCacheAndGet(page, source, hash); + } + + meta.isLoading = false; + + if (opts?.isDelay) { + meta.isDelayed = true; + ele.style.cursor = "help"; + await MiscUtil.pDelay(1100); + meta.isDelayed = false; + } + + // Reset cursor + ele.style.cursor = ""; + + // Check if we're still hovering the entity + if (!meta || (!meta.isHovered && !meta.isPermanent)) return; + + const tmpEvt = meta._tmpEvt; + delete meta._tmpEvt; + + // TODO(Future) avoid rendering e.g. creature scaling controls if `win?._IS_POPOUT` + const win = (evt.view || {}).window; + + const $content = meta.isFluff + ? Renderer.hover.$getHoverContent_fluff(page, toRender) + : Renderer.hover.$getHoverContent_stats(page, toRender); + + // FIXME(Future) replace this with something maintainable + const compactReferenceData = { + page, + source, + hash, + }; + + if (meta.windowMeta && !meta.isPermanent) { + meta.windowMeta.doClose(); + meta.windowMeta = null; + } + + meta.windowMeta = Renderer.hover.getShowWindow( + $content, + Renderer.hover.getWindowPositionFromEvent(tmpEvt || evt, {isPreventFlicker: !meta.isPermanent}), + { + title: toRender ? toRender.name : "", + isPermanent: meta.isPermanent, + pageUrl: isFauxPage ? null : `${Renderer.get().baseUrl}${page}#${hash}`, + cbClose: () => meta.isHovered = meta.isPermanent = meta.isLoading = meta.isFluff = false, + isBookContent: page === UrlUtil.PG_RECIPES, + compactReferenceData, + sourceData: toRender, + }, + ); + + if (!meta.isFluff && !win?._IS_POPOUT) { + const fnBind = Renderer.hover.getFnBindListenersCompact(page); + if (fnBind) fnBind(toRender, $content); + } + } + + // (Baked into render strings) + static handleInlineMouseOver (evt, ele, entry, opts) { + Renderer.hover._doInit(); + + entry = entry || JSON.parse(ele.dataset.vetEntry); + + let meta = Renderer.hover._handleGenericMouseOverStart({evt, ele}); + if (meta == null) return; + + meta.isLoading = false; + + // Reset cursor + ele.style.cursor = ""; + + // Check if we're still hovering the entity + if (!meta || (!meta.isHovered && !meta.isPermanent)) return; + + const tmpEvt = meta._tmpEvt; + delete meta._tmpEvt; + + const win = (evt.view || {}).window; + + const $content = Renderer.hover.$getHoverContent_generic(entry, opts); + + if (meta.windowMeta && !meta.isPermanent) { + meta.windowMeta.doClose(); + meta.windowMeta = null; + } + + meta.windowMeta = Renderer.hover.getShowWindow( + $content, + Renderer.hover.getWindowPositionFromEvent(tmpEvt || evt, {isPreventFlicker: !meta.isPermanent}), + { + title: entry?.name || "", + isPermanent: meta.isPermanent, + pageUrl: null, + cbClose: () => meta.isHovered = meta.isPermanent = meta.isLoading = false, + isBookContent: true, + sourceData: entry, + }, + ); + } + + static async pGetHoverableFluff (page, source, hash, opts) { + // Try to fetch the fluff directly + let toRender = await DataLoader.pCacheAndGet(`${page}Fluff`, source, hash, opts); + + if (!toRender) { + // Fall back on fluff attached to the object itself + const entity = await DataLoader.pCacheAndGet(page, source, hash, opts); + + const pFnGetFluff = Renderer.hover._pageToFluffFn(page); + if (!pFnGetFluff && opts?.isSilent) return null; + + toRender = await pFnGetFluff(entity); + } + + if (!toRender) return toRender; + + // For inline homebrew fluff, populate the name/source + if (toRender && (!toRender.name || !toRender.source)) { + const toRenderParent = await DataLoader.pCacheAndGet(page, source, hash, opts); + toRender = MiscUtil.copyFast(toRender); + toRender.name = toRenderParent.name; + toRender.source = toRenderParent.source; + } + + return toRender; + } + + // (Baked into render strings) + static handleLinkMouseLeave (evt, ele) { + const meta = Renderer.hover._eleCache.get(ele); + ele.style.cursor = ""; + + if (!meta || meta.isPermanent) return; + + if (evt.shiftKey) { + meta.isPermanent = true; + meta.windowMeta.setIsPermanent(true); + return; + } + + meta.isHovered = false; + if (meta.windowMeta) { + meta.windowMeta.doClose(); + meta.windowMeta = null; + } + } + + // (Baked into render strings) + static handleLinkMouseMove (evt, ele) { + const meta = Renderer.hover._eleCache.get(ele); + if (!meta || meta.isPermanent) return; + + // If loading has finished, but we're not displaying the element yet (e.g. because it has been delayed) + if (meta.isDelayed) { + meta._tmpEvt = evt; + return; + } + + if (!meta.windowMeta) return; + + meta.windowMeta.setPosition(Renderer.hover.getWindowPositionFromEvent(evt, {isPreventFlicker: !evt.shiftKey && !meta.isPermanent})); + + if (evt.shiftKey && !meta.isPermanent) { + meta.isPermanent = true; + meta.windowMeta.setIsPermanent(true); + } + } + + /** + * (Baked into render strings) + * @param evt + * @param ele + * @param entryId + * @param [opts] + * @param [opts.isBookContent] + * @param [opts.isLargeBookContent] + */ + static handlePredefinedMouseOver (evt, ele, entryId, opts) { + opts = opts || {}; + + const meta = Renderer.hover._handleGenericMouseOverStart({evt, ele}); + if (meta == null) return; + + Renderer.hover.cleanTempWindows(); + + const toRender = Renderer.hover._entryCache[entryId]; + + meta.isLoading = false; + // Check if we're still hovering the entity + if (!meta.isHovered && !meta.isPermanent) return; + + const $content = Renderer.hover.$getHoverContent_generic(toRender, opts); + meta.windowMeta = Renderer.hover.getShowWindow( + $content, + Renderer.hover.getWindowPositionFromEvent(evt, {isPreventFlicker: !meta.isPermanent}), + { + title: toRender.data && toRender.data.hoverTitle != null ? toRender.data.hoverTitle : toRender.name, + isPermanent: meta.isPermanent, + cbClose: () => meta.isHovered = meta.isPermanent = meta.isLoading = false, + sourceData: toRender, + }, + ); + + // Reset cursor + ele.style.cursor = ""; + } + + // (Baked into render strings) + static handleLinkDragStart (evt, ele) { + // Close the window + Renderer.hover.handleLinkMouseLeave(evt, ele); + + const {page, source, hash} = Renderer.hover.getLinkElementData(ele); + const meta = { + type: VeCt.DRAG_TYPE_IMPORT, + page, + source, + hash, + }; + evt.dataTransfer.setData("application/json", JSON.stringify(meta)); + } + + static doPredefinedShow (entryId, opts) { + opts = opts || {}; + + const meta = Renderer.hover._doPredefinedShowStart({entryId}); + if (meta == null) return; + + Renderer.hover.cleanTempWindows(); + + const toRender = Renderer.hover._entryCache[entryId]; + + const $content = Renderer.hover.$getHoverContent_generic(toRender, opts); + meta.windowMeta = Renderer.hover.getShowWindow( + $content, + Renderer.hover.getWindowPositionExact((window.innerWidth / 2) - (Renderer.hover._DEFAULT_WIDTH_PX / 2), 100), + { + title: toRender.data && toRender.data.hoverTitle != null ? toRender.data.hoverTitle : toRender.name, + isPermanent: meta.isPermanent, + cbClose: () => meta.isHovered = meta.isPermanent = meta.isLoading = false, + sourceData: toRender, + }, + ); + } + + // (Baked into render strings) + static handlePredefinedMouseLeave (evt, ele) { return Renderer.hover.handleLinkMouseLeave(evt, ele); } + + // (Baked into render strings) + static handlePredefinedMouseMove (evt, ele) { return Renderer.hover.handleLinkMouseMove(evt, ele); } + + static _WINDOW_POSITION_PROPS_FROM_EVENT = [ + "isFromBottom", + "isFromRight", + "clientX", + "window", + "isPreventFlicker", + "bcr", + ]; + + static getWindowPositionFromEvent (evt, {isPreventFlicker = false} = {}) { + const ele = evt.target; + const win = evt?.view?.window || window; + + const bcr = ele.getBoundingClientRect().toJSON(); + + const isFromBottom = bcr.top > win.innerHeight / 2; + const isFromRight = bcr.left > win.innerWidth / 2; + + return { + mode: "autoFromElement", + isFromBottom, + isFromRight, + clientX: EventUtil.getClientX(evt), + window: win, + isPreventFlicker, + bcr, + }; + } + + static getWindowPositionExact (x, y, evt = null) { + return { + window: evt?.view?.window || window, + mode: "exact", + x, + y, + }; + } + + static getWindowPositionExactVisibleBottom (x, y, evt = null) { + return { + ...Renderer.hover.getWindowPositionExact(x, y, evt), + mode: "exactVisibleBottom", + }; + } + + static _WINDOW_METAS = {}; + static MIN_Z_INDEX = 200; + static _MAX_Z_INDEX = 300; + static _DEFAULT_WIDTH_PX = 600; + static _BODY_SCROLLER_WIDTH_PX = 15; + + static _getZIndex () { + const zIndices = Object.values(Renderer.hover._WINDOW_METAS).map(it => it.zIndex); + if (!zIndices.length) return Renderer.hover.MIN_Z_INDEX; + return Math.max(...zIndices); + } + + static _getNextZIndex (hoverId) { + const cur = Renderer.hover._getZIndex(); + // If we're already the highest index, continue to use this index + if (hoverId != null && Renderer.hover._WINDOW_METAS[hoverId].zIndex === cur) return cur; + // otherwise, go one higher + const out = cur + 1; + + // If we've broken through the max z-index, try to free up some z-indices + if (out > Renderer.hover._MAX_Z_INDEX) { + const sortedWindowMetas = Object.entries(Renderer.hover._WINDOW_METAS) + .sort(([kA, vA], [kB, vB]) => SortUtil.ascSort(vA.zIndex, vB.zIndex)); + + if (sortedWindowMetas.length >= (Renderer.hover._MAX_Z_INDEX - Renderer.hover.MIN_Z_INDEX)) { + // If we have too many window open, collapse them into one z-index + sortedWindowMetas.forEach(([k, v]) => { + v.setZIndex(Renderer.hover.MIN_Z_INDEX); + }); + } else { + // Otherwise, ensure one consistent run from min to max z-index + sortedWindowMetas.forEach(([k, v], i) => { + v.setZIndex(Renderer.hover.MIN_Z_INDEX + i); + }); + } + + return Renderer.hover._getNextZIndex(hoverId); + } else return out; + } + + static _isIntersectRect (r1, r2) { + return r1.left <= r2.right + && r2.left <= r1.right + && r1.top <= r2.bottom + && r2.top <= r1.bottom; + } + + /** + * @param $content Content to append to the window. + * @param position The position of the window. Can be specified in various formats. + * @param [opts] Options object. + * @param [opts.isPermanent] If the window should have the expanded toolbar of a "permanent" window. + * @param [opts.title] The window title. + * @param [opts.isBookContent] If the hover window contains book content. Affects the styling of borders. + * @param [opts.pageUrl] A page URL which is navigable via a button in the window header + * @param [opts.cbClose] Callback to run on window close. + * @param [opts.width] An initial width for the window. + * @param [opts.height] An initial height fot the window. + * @param [opts.$pFnGetPopoutContent] A function which loads content for this window when it is popped out. + * @param [opts.fnGetPopoutSize] A function which gets a `{width: ..., height: ...}` object with dimensions for a + * popout window. + * @param [opts.isPopout] If the window should be immediately popped out. + * @param [opts.compactReferenceData] Reference (e.g. page/source/hash/others) which can be used to load the contents into the DM screen. + * @param [opts.sourceData] Source JSON (as raw as possible) used to construct this popout. + */ + static getShowWindow ($content, position, opts) { + opts = opts || {}; + + Renderer.hover._doInit(); + + const initialWidth = opts.width == null ? Renderer.hover._DEFAULT_WIDTH_PX : opts.width; + const initialZIndex = Renderer.hover._getNextZIndex(); + + const $body = $(position.window.document.body); + const $hov = $(`
    `) + .css({ + "right": -initialWidth, + "width": initialWidth, + "zIndex": initialZIndex, + }); + const $wrpContent = $(`
    `); + if (opts.height != null) $wrpContent.css("height", opts.height); + const $hovTitle = $(`${opts.title || ""}`); + + const hoverWindow = {}; + const hoverId = Renderer.hover._getNextId(); + Renderer.hover._WINDOW_METAS[hoverId] = hoverWindow; + const mouseUpId = `mouseup.${hoverId} touchend.${hoverId}`; + const mouseMoveId = `mousemove.${hoverId} touchmove.${hoverId}`; + const resizeId = `resize.${hoverId}`; + const drag = {}; + + const $brdrTopRightResize = $(`
    `) + .on("mousedown touchstart", (evt) => Renderer.hover._getShowWindow_handleDragMousedown({hoverWindow, hoverId, $hov, drag, $wrpContent}, {evt, type: 1})); + + const $brdrRightResize = $(`
    `) + .on("mousedown touchstart", (evt) => Renderer.hover._getShowWindow_handleDragMousedown({hoverWindow, hoverId, $hov, drag, $wrpContent}, {evt, type: 2})); + + const $brdrBottomRightResize = $(`
    `) + .on("mousedown touchstart", (evt) => Renderer.hover._getShowWindow_handleDragMousedown({hoverWindow, hoverId, $hov, drag, $wrpContent}, {evt, type: 3})); + + const $brdrBtm = $(`
    `) + .on("mousedown touchstart", (evt) => Renderer.hover._getShowWindow_handleDragMousedown({hoverWindow, hoverId, $hov, drag, $wrpContent}, {evt, type: 4})); + + const $brdrBtmLeftResize = $(`
    `) + .on("mousedown touchstart", (evt) => Renderer.hover._getShowWindow_handleDragMousedown({hoverWindow, hoverId, $hov, drag, $wrpContent}, {evt, type: 5})); + + const $brdrLeftResize = $(`
    `) + .on("mousedown touchstart", (evt) => Renderer.hover._getShowWindow_handleDragMousedown({hoverWindow, hoverId, $hov, drag, $wrpContent}, {evt, type: 6})); + + const $brdrTopLeftResize = $(`
    `) + .on("mousedown touchstart", (evt) => Renderer.hover._getShowWindow_handleDragMousedown({hoverWindow, hoverId, $hov, drag, $wrpContent}, {evt, type: 7})); + + const $brdrTopResize = $(`
    `) + .on("mousedown touchstart", (evt) => Renderer.hover._getShowWindow_handleDragMousedown({hoverWindow, hoverId, $hov, drag, $wrpContent}, {evt, type: 8})); + + const $brdrTop = $(`
    `) + .on("mousedown touchstart", (evt) => Renderer.hover._getShowWindow_handleDragMousedown({hoverWindow, hoverId, $hov, drag, $wrpContent}, {evt, type: 9})) + .on("contextmenu", (evt) => { + Renderer.hover._contextMenuLastClicked = { + hoverId, + }; + ContextUtil.pOpenMenu(evt, Renderer.hover._contextMenu); + }); + + $(position.window.document) + .on(mouseUpId, (evt) => { + if (drag.type) { + if (drag.type < 9) { + $wrpContent.css("max-height", ""); + $hov.css("max-width", ""); + } + Renderer.hover._getShowWindow_adjustPosition({$hov, $wrpContent, position}); + + if (drag.type === 9) { + // handle mobile button touches + if (EventUtil.isUsingTouch() && evt.target.classList.contains("hwin__top-border-icon")) { + evt.preventDefault(); + drag.type = 0; + $(evt.target).click(); + return; + } + + // handle DM screen integration + if (this._dmScreen && opts.compactReferenceData) { + const panel = this._dmScreen.getPanelPx(EventUtil.getClientX(evt), EventUtil.getClientY(evt)); + if (!panel) return; + this._dmScreen.setHoveringPanel(panel); + const target = panel.getAddButtonPos(); + + if (Renderer.hover._getShowWindow_isOverHoverTarget({evt, target})) { + panel.doPopulate_Stats(opts.compactReferenceData.page, opts.compactReferenceData.source, opts.compactReferenceData.hash); + Renderer.hover._getShowWindow_doClose({$hov, position, mouseUpId, mouseMoveId, resizeId, hoverId, opts, hoverWindow}); + } + this._dmScreen.resetHoveringButton(); + } + } + drag.type = 0; + } + }) + .on(mouseMoveId, (evt) => { + const args = {$wrpContent, $hov, drag, evt}; + switch (drag.type) { + case 1: Renderer.hover._getShowWindow_handleNorthDrag(args); Renderer.hover._getShowWindow_handleEastDrag(args); break; + case 2: Renderer.hover._getShowWindow_handleEastDrag(args); break; + case 3: Renderer.hover._getShowWindow_handleSouthDrag(args); Renderer.hover._getShowWindow_handleEastDrag(args); break; + case 4: Renderer.hover._getShowWindow_handleSouthDrag(args); break; + case 5: Renderer.hover._getShowWindow_handleSouthDrag(args); Renderer.hover._getShowWindow_handleWestDrag(args); break; + case 6: Renderer.hover._getShowWindow_handleWestDrag(args); break; + case 7: Renderer.hover._getShowWindow_handleNorthDrag(args); Renderer.hover._getShowWindow_handleWestDrag(args); break; + case 8: Renderer.hover._getShowWindow_handleNorthDrag(args); break; + case 9: { + const diffX = drag.startX - EventUtil.getClientX(evt); + const diffY = drag.startY - EventUtil.getClientY(evt); + $hov.css("left", drag.baseLeft - diffX) + .css("top", drag.baseTop - diffY); + drag.startX = EventUtil.getClientX(evt); + drag.startY = EventUtil.getClientY(evt); + drag.baseTop = parseFloat($hov.css("top")); + drag.baseLeft = parseFloat($hov.css("left")); + + // handle DM screen integration + if (this._dmScreen) { + const panel = this._dmScreen.getPanelPx(EventUtil.getClientX(evt), EventUtil.getClientY(evt)); + if (!panel) return; + this._dmScreen.setHoveringPanel(panel); + const target = panel.getAddButtonPos(); + + if (Renderer.hover._getShowWindow_isOverHoverTarget({evt, target})) this._dmScreen.setHoveringButton(panel); + else this._dmScreen.resetHoveringButton(); + } + break; + } + } + }); + $(position.window).on(resizeId, () => Renderer.hover._getShowWindow_adjustPosition({$hov, $wrpContent, position})); + + $brdrTop.attr("data-display-title", false); + $brdrTop.on("dblclick", () => Renderer.hover._getShowWindow_doToggleMinimizedMaximized({$brdrTop, $hov})); + $brdrTop.append($hovTitle); + const $brdTopRhs = $(`
    `).appendTo($brdrTop); + + if (opts.pageUrl && !position.window._IS_POPOUT && !Renderer.get().isInternalLinksDisabled()) { + const $btnGotoPage = $(``) + .appendTo($brdTopRhs); + } + + if (!position.window._IS_POPOUT && !opts.isPopout) { + const $btnPopout = $(``) + .on("click", evt => { + evt.stopPropagation(); + return Renderer.hover._getShowWindow_pDoPopout({$hov, position, mouseUpId, mouseMoveId, resizeId, hoverId, opts, hoverWindow, $content}, {evt}); + }) + .appendTo($brdTopRhs); + } + + if (opts.sourceData) { + const btnPopout = e_({ + tag: "span", + clazz: `hwin__top-border-icon hwin__top-border-icon--text`, + title: "Show Source Data", + text: "{}", + click: evt => { + evt.stopPropagation(); + evt.preventDefault(); + + const $content = Renderer.hover.$getHoverContent_statsCode(opts.sourceData); + Renderer.hover.getShowWindow( + $content, + Renderer.hover.getWindowPositionFromEvent(evt), + { + title: [opts.sourceData._displayName || opts.sourceData.name, "Source Data"].filter(Boolean).join(" \u2014 "), + isPermanent: true, + isBookContent: true, + }, + ); + }, + }); + $brdTopRhs.append(btnPopout); + } + + const $btnClose = $(``) + .on("click", (evt) => { + evt.stopPropagation(); + + if (EventUtil.isCtrlMetaKey(evt)) { + Renderer.hover._doCloseAllWindows(); + return; + } + + Renderer.hover._getShowWindow_doClose({$hov, position, mouseUpId, mouseMoveId, resizeId, hoverId, opts, hoverWindow}); + }).appendTo($brdTopRhs); + + $wrpContent.append($content); + + $hov.append($brdrTopResize).append($brdrTopRightResize).append($brdrRightResize).append($brdrBottomRightResize) + .append($brdrBtmLeftResize).append($brdrLeftResize).append($brdrTopLeftResize) + + .append($brdrTop) + .append($wrpContent) + .append($brdrBtm); + + $body.append($hov); + + Renderer.hover._getShowWindow_setPosition({$hov, $wrpContent, position}, position); + + hoverWindow.$windowTitle = $hovTitle; + hoverWindow.zIndex = initialZIndex; + hoverWindow.setZIndex = Renderer.hover._getNextZIndex.bind(this, {$hov, hoverWindow}); + + hoverWindow.setPosition = Renderer.hover._getShowWindow_setPosition.bind(this, {$hov, $wrpContent, position}); + hoverWindow.setIsPermanent = Renderer.hover._getShowWindow_setIsPermanent.bind(this, {opts, $brdrTop}); + hoverWindow.doClose = Renderer.hover._getShowWindow_doClose.bind(this, {$hov, position, mouseUpId, mouseMoveId, resizeId, hoverId, opts, hoverWindow}); + hoverWindow.doMaximize = Renderer.hover._getShowWindow_doMaximize.bind(this, {$brdrTop, $hov}); + hoverWindow.doZIndexToFront = Renderer.hover._getShowWindow_doZIndexToFront.bind(this, {$hov, hoverWindow, hoverId}); + + hoverWindow.getPosition = Renderer.hover._getShowWindow_getPosition.bind(this, {$hov, $wrpContent, position}); + + hoverWindow.$setContent = ($contentNxt) => $wrpContent.empty().append($contentNxt); + + if (opts.isPopout) Renderer.hover._getShowWindow_pDoPopout({$hov, position, mouseUpId, mouseMoveId, resizeId, hoverId, opts, hoverWindow, $content}); + + return hoverWindow; + } + + static _getShowWindow_doClose ({$hov, position, mouseUpId, mouseMoveId, resizeId, hoverId, opts, hoverWindow}) { + $hov.remove(); + $(position.window.document).off(mouseUpId); + $(position.window.document).off(mouseMoveId); + $(position.window).off(resizeId); + + delete Renderer.hover._WINDOW_METAS[hoverId]; + + if (opts.cbClose) opts.cbClose(hoverWindow); + } + + static _getShowWindow_handleDragMousedown ({hoverWindow, hoverId, $hov, drag, $wrpContent}, {evt, type}) { + if (evt.which === 0 || evt.which === 1) evt.preventDefault(); + hoverWindow.zIndex = Renderer.hover._getNextZIndex(hoverId); + $hov.css({ + "z-index": hoverWindow.zIndex, + "animation": "initial", + }); + drag.type = type; + drag.startX = EventUtil.getClientX(evt); + drag.startY = EventUtil.getClientY(evt); + drag.baseTop = parseFloat($hov.css("top")); + drag.baseLeft = parseFloat($hov.css("left")); + drag.baseHeight = $wrpContent.height(); + drag.baseWidth = parseFloat($hov.css("width")); + if (type < 9) { + $wrpContent.css({ + "height": drag.baseHeight, + "max-height": "initial", + }); + $hov.css("max-width", "initial"); + } + } + + static _getShowWindow_isOverHoverTarget ({evt, target}) { + return EventUtil.getClientX(evt) >= target.left + && EventUtil.getClientX(evt) <= target.left + target.width + && EventUtil.getClientY(evt) >= target.top + && EventUtil.getClientY(evt) <= target.top + target.height; + } + + static _getShowWindow_handleNorthDrag ({$wrpContent, $hov, drag, evt}) { + const diffY = Math.max(drag.startY - EventUtil.getClientY(evt), 80 - drag.baseHeight); // prevent <80 height, as this will cause the box to move downwards + $wrpContent.css("height", drag.baseHeight + diffY); + $hov.css("top", drag.baseTop - diffY); + drag.startY = EventUtil.getClientY(evt); + drag.baseHeight = $wrpContent.height(); + drag.baseTop = parseFloat($hov.css("top")); + } + + static _getShowWindow_handleEastDrag ({$wrpContent, $hov, drag, evt}) { + const diffX = drag.startX - EventUtil.getClientX(evt); + $hov.css("width", drag.baseWidth - diffX); + drag.startX = EventUtil.getClientX(evt); + drag.baseWidth = parseFloat($hov.css("width")); + } + + static _getShowWindow_handleSouthDrag ({$wrpContent, $hov, drag, evt}) { + const diffY = drag.startY - EventUtil.getClientY(evt); + $wrpContent.css("height", drag.baseHeight - diffY); + drag.startY = EventUtil.getClientY(evt); + drag.baseHeight = $wrpContent.height(); + } + + static _getShowWindow_handleWestDrag ({$wrpContent, $hov, drag, evt}) { + const diffX = Math.max(drag.startX - EventUtil.getClientX(evt), 150 - drag.baseWidth); + $hov.css("width", drag.baseWidth + diffX) + .css("left", drag.baseLeft - diffX); + drag.startX = EventUtil.getClientX(evt); + drag.baseWidth = parseFloat($hov.css("width")); + drag.baseLeft = parseFloat($hov.css("left")); + } + + static _getShowWindow_doToggleMinimizedMaximized ({$brdrTop, $hov}) { + const curState = $brdrTop.attr("data-display-title"); + const isNextMinified = curState === "false"; + $brdrTop.attr("data-display-title", isNextMinified); + $brdrTop.attr("data-perm", true); + $hov.toggleClass("hwin--minified", isNextMinified); + } + + static _getShowWindow_doMaximize ({$brdrTop, $hov}) { + $brdrTop.attr("data-display-title", false); + $hov.toggleClass("hwin--minified", false); + } + + static async _getShowWindow_pDoPopout ({$hov, position, mouseUpId, mouseMoveId, resizeId, hoverId, opts, hoverWindow, $content}, {evt} = {}) { + const dimensions = opts.fnGetPopoutSize ? opts.fnGetPopoutSize() : {width: 600, height: $content.height()}; + const win = window.open( + "", + opts.title || "", + `width=${dimensions.width},height=${dimensions.height}location=0,menubar=0,status=0,titlebar=0,toolbar=0`, + ); + + // If this is a new window, bootstrap general page elements/variables. + // Otherwise, we can skip straight to using the window. + if (!win._IS_POPOUT) { + win._IS_POPOUT = true; + win.document.write(` + + + + ${opts.title} + ${$(`link[rel="stylesheet"][href]`).map((i, e) => e.outerHTML).get().join("\n")} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + + `); + + win.Renderer = Renderer; + + let ticks = 50; + while (!win.document.body && ticks-- > 0) await MiscUtil.pDelay(5); + + win.$wrpHoverContent = $(win.document).find(`.hoverbox--popout`); + } + + let $cpyContent; + if (opts.$pFnGetPopoutContent) { + $cpyContent = await opts.$pFnGetPopoutContent(); + } else { + $cpyContent = $content.clone(true, true); + } + + $cpyContent.appendTo(win.$wrpHoverContent.empty()); + + Renderer.hover._getShowWindow_doClose({$hov, position, mouseUpId, mouseMoveId, resizeId, hoverId, opts, hoverWindow}); + } + + static _getShowWindow_setPosition ({$hov, $wrpContent, position}, positionNxt) { + switch (positionNxt.mode) { + case "autoFromElement": { + const bcr = $hov[0].getBoundingClientRect(); + + if (positionNxt.isFromBottom) $hov.css("top", positionNxt.bcr.top - (bcr.height + 10)); + else $hov.css("top", positionNxt.bcr.top + positionNxt.bcr.height + 10); + + if (positionNxt.isFromRight) $hov.css("left", (positionNxt.clientX || positionNxt.bcr.left) - (bcr.width + 10)); + else $hov.css("left", (positionNxt.clientX || (positionNxt.bcr.left + positionNxt.bcr.width)) + 10); + + // region Sync position info when updating + if (position !== positionNxt) { + Renderer.hover._WINDOW_POSITION_PROPS_FROM_EVENT + .forEach(prop => { + position[prop] = positionNxt[prop]; + }); + } + // endregion + + break; + } + case "exact": { + $hov.css({ + "left": positionNxt.x, + "top": positionNxt.y, + }); + break; + } + case "exactVisibleBottom": { + $hov.css({ + "left": positionNxt.x, + "top": positionNxt.y, + "animation": "initial", // Briefly remove the animation so we can calculate the height + }); + + let yPos = positionNxt.y; + + const {bottom: posBottom, height: winHeight} = $hov[0].getBoundingClientRect(); + const height = position.window.innerHeight; + if (posBottom > height) { + yPos = position.window.innerHeight - winHeight; + $hov.css({ + "top": yPos, + "animation": "", + }); + } + + break; + } + default: throw new Error(`Positiong mode unimplemented: "${positionNxt.mode}"`); + } + + Renderer.hover._getShowWindow_adjustPosition({$hov, $wrpContent, position}); + } + + static _getShowWindow_adjustPosition ({$hov, $wrpContent, position}) { + const eleHov = $hov[0]; + const wrpContent = $wrpContent[0]; + + const bcr = eleHov.getBoundingClientRect().toJSON(); + const screenHeight = position.window.innerHeight; + const screenWidth = position.window.innerWidth; + + // readjust position... + // ...if vertically clipping off screen + if (bcr.top < 0) { + bcr.top = 0; + bcr.bottom = bcr.top + bcr.height; + eleHov.style.top = `${bcr.top}px`; + } else if (bcr.top >= screenHeight - Renderer.hover._BAR_HEIGHT) { + bcr.top = screenHeight - Renderer.hover._BAR_HEIGHT; + bcr.bottom = bcr.top + bcr.height; + eleHov.style.top = `${bcr.top}px`; + } + + // ...if horizontally clipping off screen + if (bcr.left < 0) { + bcr.left = 0; + bcr.right = bcr.left + bcr.width; + eleHov.style.left = `${bcr.left}px`; + } else if (bcr.left + bcr.width + Renderer.hover._BODY_SCROLLER_WIDTH_PX > screenWidth) { + bcr.left = Math.max(screenWidth - bcr.width - Renderer.hover._BODY_SCROLLER_WIDTH_PX, 0); + bcr.right = bcr.left + bcr.width; + eleHov.style.left = `${bcr.left}px`; + } + + // Prevent window "flickering" when hovering a link + if ( + position.isPreventFlicker + && Renderer.hover._isIntersectRect(bcr, position.bcr) + ) { + if (position.isFromBottom) { + bcr.height = position.bcr.top - 5; + wrpContent.style.height = `${bcr.height}px`; + } else { + bcr.height = screenHeight - position.bcr.bottom - 5; + wrpContent.style.height = `${bcr.height}px`; + } + } + } + + static _getShowWindow_getPosition ({$hov, $wrpContent}) { + return { + wWrpContent: $wrpContent.width(), + hWrapContent: $wrpContent.height(), + }; + } + + static _getShowWindow_setIsPermanent ({opts, $brdrTop}, isPermanent) { + opts.isPermanent = isPermanent; + $brdrTop.attr("data-perm", isPermanent); + } + + static _getShowWindow_setZIndex ({$hov, hoverWindow}, zIndex) { + $hov.css("z-index", zIndex); + hoverWindow.zIndex = zIndex; + } + + static _getShowWindow_doZIndexToFront ({$hov, hoverWindow, hoverId}) { + const nxtZIndex = Renderer.hover._getNextZIndex(hoverId); + Renderer.hover._getShowWindow_setZIndex({$hov, hoverWindow}, nxtZIndex); + } + + /** + * @param entry + * @param [opts] + * @param [opts.isBookContent] + * @param [opts.isLargeBookContent] + * @param [opts.depth] + * @param [opts.id] + */ + static getMakePredefinedHover (entry, opts) { + opts = opts || {}; + + const id = opts.id ?? Renderer.hover._getNextId(); + Renderer.hover._entryCache[id] = entry; + return { + id, + html: `onmouseover="Renderer.hover.handlePredefinedMouseOver(event, this, ${id}, ${JSON.stringify(opts).escapeQuotes()})" onmousemove="Renderer.hover.handlePredefinedMouseMove(event, this)" onmouseleave="Renderer.hover.handlePredefinedMouseLeave(event, this)" ${Renderer.hover.getPreventTouchString()}`, + mouseOver: (evt, ele) => Renderer.hover.handlePredefinedMouseOver(evt, ele, id, opts), + mouseMove: (evt, ele) => Renderer.hover.handlePredefinedMouseMove(evt, ele), + mouseLeave: (evt, ele) => Renderer.hover.handlePredefinedMouseLeave(evt, ele), + touchStart: (evt, ele) => Renderer.hover.handleTouchStart(evt, ele), + show: () => Renderer.hover.doPredefinedShow(id, opts), + }; + } + + static updatePredefinedHover (id, entry) { + Renderer.hover._entryCache[id] = entry; + } + + static getInlineHover (entry, opts) { + return { + // Re-use link handlers, as the inline version is a simplified version + html: `onmouseover="Renderer.hover.handleInlineMouseOver(event, this)" onmouseleave="Renderer.hover.handleLinkMouseLeave(event, this)" onmousemove="Renderer.hover.handleLinkMouseMove(event, this)" data-vet-entry="${JSON.stringify(entry).qq()}" ${opts ? `data-vet-opts="${JSON.stringify(opts).qq()}"` : ""} ${Renderer.hover.getPreventTouchString()}`, + }; + } + + static getPreventTouchString () { + return `ontouchstart="Renderer.hover.handleTouchStart(event, this)"`; + } + + static handleTouchStart (evt, ele) { + // on large touchscreen devices only (e.g. iPads) + if (!Renderer.hover.isSmallScreen(evt)) { + // cache the link location and redirect it to void + $(ele).data("href", $(ele).data("href") || $(ele).attr("href")); + $(ele).attr("href", "javascript:void(0)"); + // restore the location after 100ms; if the user long-presses the link will be restored by the time they + // e.g. attempt to open a new tab + setTimeout(() => { + const data = $(ele).data("href"); + if (data) { + $(ele).attr("href", data); + $(ele).data("href", null); + } + }, 100); + } + } + + // region entry fetching + static getEntityLink ( + ent, + { + displayText = null, + prop = null, + isLowerCase = false, + isTitleCase = false, + } = {}, + ) { + if (isLowerCase && isTitleCase) throw new Error(`"isLowerCase" and "isTitleCase" are mutually exclusive!`); + + const name = isLowerCase ? ent.name.toLowerCase() : isTitleCase ? ent.name.toTitleCase() : ent.name; + + let parts = [ + name, + ent.source, + displayText || "", + ]; + + switch (prop || ent.__prop) { + case "monster": { + if (ent._isScaledCr) { + parts.push(`${VeCt.HASH_SCALED}=${Parser.numberToCr(ent._scaledCr)}`); + } + + if (ent._isScaledSpellSummon) { + parts.push(`${VeCt.HASH_SCALED_SPELL_SUMMON}=${ent._scaledSpellSummonLevel}`); + } + + if (ent._isScaledClassSummon) { + parts.push(`${VeCt.HASH_SCALED_CLASS_SUMMON}=${ent._scaledClassSummonLevel}`); + } + + break; + } + + // TODO recipe? + + case "deity": { + parts.splice(1, 0, ent.pantheon); + break; + } + } + + while (parts.length && !parts.last()?.length) parts.pop(); + + return Renderer.get().render(`{@${Parser.getPropTag(prop || ent.__prop)} ${parts.join("|")}}`); + } + + static getRefMetaFromTag (str) { + // convert e.g. `"{#itemEntry Ring of Resistance|DMG}"` + // to `{type: "refItemEntry", "itemEntry": "Ring of Resistance|DMG"}` + str = str.slice(2, -1); + const [tag, ...refParts] = str.split(" "); + const ref = refParts.join(" "); + const type = `ref${tag.uppercaseFirst()}`; + return {type, [tag]: ref}; + } + // endregion + + // region Apply custom hash IDs + static async pApplyCustomHashId (page, ent, customHashId) { + switch (page) { + case UrlUtil.PG_BESTIARY: { + const out = await Renderer.monster.pGetModifiedCreature(ent, customHashId); + Renderer.monster.updateParsed(out); + return out; + } + + case UrlUtil.PG_RECIPES: return Renderer.recipe.pGetModifiedRecipe(ent, customHashId); + + default: return ent; + } + } + // endregion + + static getGenericCompactRenderedString (entry, depth = 0) { + return ` + + ${Renderer.get().setFirstSection(true).render(entry, depth)} + + `; + } + + static getFnRenderCompact (page, {isStatic = false} = {}) { + switch (page) { + case "generic": + case "hover": return Renderer.hover.getGenericCompactRenderedString; + case UrlUtil.PG_QUICKREF: return Renderer.hover.getGenericCompactRenderedString; + case UrlUtil.PG_CLASSES: return Renderer.class.getCompactRenderedString; + case UrlUtil.PG_SPELLS: return Renderer.spell.getCompactRenderedString; + case UrlUtil.PG_ITEMS: return Renderer.item.getCompactRenderedString; + case UrlUtil.PG_BESTIARY: return it => Renderer.monster.getCompactRenderedString(it, {isShowScalers: !isStatic, isScaledCr: it._originalCr != null, isScaledSpellSummon: it._isScaledSpellSummon, isScaledClassSummon: it._isScaledClassSummon}); + case UrlUtil.PG_CONDITIONS_DISEASES: return Renderer.condition.getCompactRenderedString; + case UrlUtil.PG_BACKGROUNDS: return Renderer.background.getCompactRenderedString; + case UrlUtil.PG_FEATS: return Renderer.feat.getCompactRenderedString; + case UrlUtil.PG_OPT_FEATURES: return Renderer.optionalfeature.getCompactRenderedString; + case UrlUtil.PG_PSIONICS: return Renderer.psionic.getCompactRenderedString; + case UrlUtil.PG_REWARDS: return Renderer.reward.getCompactRenderedString; + case UrlUtil.PG_RACES: return it => Renderer.race.getCompactRenderedString(it, {isStatic}); + case UrlUtil.PG_DEITIES: return Renderer.deity.getCompactRenderedString; + case UrlUtil.PG_OBJECTS: return Renderer.object.getCompactRenderedString; + case UrlUtil.PG_TRAPS_HAZARDS: return Renderer.traphazard.getCompactRenderedString; + case UrlUtil.PG_VARIANTRULES: return Renderer.variantrule.getCompactRenderedString; + case UrlUtil.PG_CULTS_BOONS: return Renderer.cultboon.getCompactRenderedString; + case UrlUtil.PG_TABLES: return Renderer.table.getCompactRenderedString; + case UrlUtil.PG_VEHICLES: return Renderer.vehicle.getCompactRenderedString; + case UrlUtil.PG_ACTIONS: return Renderer.action.getCompactRenderedString; + case UrlUtil.PG_LANGUAGES: return Renderer.language.getCompactRenderedString; + case UrlUtil.PG_CHAR_CREATION_OPTIONS: return Renderer.charoption.getCompactRenderedString; + case UrlUtil.PG_RECIPES: return Renderer.recipe.getCompactRenderedString; + case UrlUtil.PG_CLASS_SUBCLASS_FEATURES: return Renderer.hover.getGenericCompactRenderedString; + case UrlUtil.PG_CREATURE_FEATURES: return Renderer.hover.getGenericCompactRenderedString; + case UrlUtil.PG_DECKS: return Renderer.deck.getCompactRenderedString; + // region props + case "classfeature": + case "classFeature": + return Renderer.hover.getGenericCompactRenderedString; + case "subclassfeature": + case "subclassFeature": + return Renderer.hover.getGenericCompactRenderedString; + case "citation": return Renderer.hover.getGenericCompactRenderedString; + // endregion + default: + if (Renderer[page]?.getCompactRenderedString) return Renderer[page].getCompactRenderedString; + return null; + } + } + + static getFnBindListenersCompact (page) { + switch (page) { + case UrlUtil.PG_BESTIARY: return Renderer.monster.bindListenersCompact; + case UrlUtil.PG_RACES: return Renderer.race.bindListenersCompact; + default: return null; + } + } + + static _pageToFluffFn (page) { + switch (page) { + case UrlUtil.PG_BESTIARY: return Renderer.monster.pGetFluff; + case UrlUtil.PG_ITEMS: return Renderer.item.pGetFluff; + case UrlUtil.PG_CONDITIONS_DISEASES: return Renderer.condition.pGetFluff; + case UrlUtil.PG_SPELLS: return Renderer.spell.pGetFluff; + case UrlUtil.PG_RACES: return Renderer.race.pGetFluff; + case UrlUtil.PG_BACKGROUNDS: return Renderer.background.pGetFluff; + case UrlUtil.PG_FEATS: return Renderer.feat.pGetFluff; + case UrlUtil.PG_OPT_FEATURES: return Renderer.optionalfeature.pGetFluff; + case UrlUtil.PG_LANGUAGES: return Renderer.language.pGetFluff; + case UrlUtil.PG_VEHICLES: return Renderer.vehicle.pGetFluff; + case UrlUtil.PG_CHAR_CREATION_OPTIONS: return Renderer.charoption.pGetFluff; + case UrlUtil.PG_RECIPES: return Renderer.recipe.pGetFluff; + default: return null; + } + } + + static isSmallScreen (evt) { + if (typeof window === "undefined") return false; + + evt = evt || {}; + const win = (evt.view || {}).window || window; + return win.innerWidth <= 768; + } + + /** + * @param page + * @param toRender + * @param [opts] + * @param [opts.isBookContent] + * @param [opts.isStatic] If this content is to be "static," i.e. display only, containing minimal interactive UI. + * @param [opts.fnRender] + * @param [renderFnOpts] + */ + static $getHoverContent_stats (page, toRender, opts, renderFnOpts) { + opts = opts || {}; + if (page === UrlUtil.PG_RECIPES) opts = {...MiscUtil.copyFast(opts), isBookContent: true}; + + const fnRender = opts.fnRender || Renderer.hover.getFnRenderCompact(page, {isStatic: opts.isStatic}); + const $out = $$`${fnRender(toRender, renderFnOpts)}
    `; + + if (!opts.isStatic) { + const fnBind = Renderer.hover.getFnBindListenersCompact(page); + if (fnBind) fnBind(toRender, $out[0]); + } + + return $out; + } + + /** + * @param page + * @param toRender + * @param [opts] + * @param [opts.isBookContent] + * @param [renderFnOpts] + */ + static $getHoverContent_fluff (page, toRender, opts, renderFnOpts) { + opts = opts || {}; + if (page === UrlUtil.PG_RECIPES) opts = {...MiscUtil.copyFast(opts), isBookContent: true}; + + if (!toRender) { + return $$`
    ${Renderer.utils.HTML_NO_INFO}
    `; + } + + toRender = MiscUtil.copyFast(toRender); + + if (toRender.images && toRender.images.length) { + const cachedImages = MiscUtil.copyFast(toRender.images); + delete toRender.images; + + toRender.entries = toRender.entries || []; + const hasText = toRender.entries.length > 0; + // Add the first image at the top + if (hasText) toRender.entries.unshift({type: "hr"}); + cachedImages[0].maxHeight = 33; + cachedImages[0].maxHeightUnits = "vh"; + toRender.entries.unshift(cachedImages[0]); + + // Add any other images at the bottom + if (cachedImages.length > 1) { + if (hasText) toRender.entries.push({type: "hr"}); + toRender.entries.push(...cachedImages.slice(1)); + } + } + + return $$`${Renderer.generic.getCompactRenderedString(toRender, renderFnOpts)}
    `; + } + + static $getHoverContent_statsCode (toRender, {isSkipClean = false, title = null} = {}) { + const cleanCopy = isSkipClean ? toRender : DataUtil.cleanJson(MiscUtil.copyFast(toRender)); + return Renderer.hover.$getHoverContent_miscCode( + title || [cleanCopy.name, "Source Data"].filter(Boolean).join(" \u2014 "), + JSON.stringify(cleanCopy, null, "\t"), + ); + } + + static $getHoverContent_miscCode (name, code) { + const toRenderCode = { + type: "code", + name, + preformatted: code, + }; + return $$`${Renderer.get().render(toRenderCode)}
    `; + } + + /** + * @param toRender + * @param [opts] + * @param [opts.isBookContent] + * @param [opts.isLargeBookContent] + * @param [opts.depth] + */ + static $getHoverContent_generic (toRender, opts) { + opts = opts || {}; + + return $$`${Renderer.hover.getGenericCompactRenderedString(toRender, opts.depth || 0)}
    `; + } + + /** + * @param evt + * @param entity + */ + static doPopoutCurPage (evt, entity) { + const page = UrlUtil.getCurrentPage(); + const $content = Renderer.hover.$getHoverContent_stats(page, entity); + Renderer.hover.getShowWindow( + $content, + Renderer.hover.getWindowPositionFromEvent(evt), + { + pageUrl: `#${UrlUtil.autoEncodeHash(entity)}`, + title: entity._displayName || entity.name, + isPermanent: true, + isBookContent: page === UrlUtil.PG_RECIPES, + sourceData: entity, + }, + ); + } +}; + +/** + * Recursively find all the names of entries, useful for indexing + * @param nameStack an array to append the names to + * @param entry the base entry + * @param [opts] Options object. + * @param [opts.maxDepth] Maximum depth to search for + * @param [opts.depth] Start depth (used internally when recursing) + * @param [opts.typeBlocklist] A set of entry types to avoid. + */ +Renderer.getNames = function (nameStack, entry, opts) { + opts = opts || {}; + if (opts.maxDepth == null) opts.maxDepth = false; + if (opts.depth == null) opts.depth = 0; + + if (opts.typeBlocklist && entry.type && opts.typeBlocklist.has(entry.type)) return; + + if (opts.maxDepth !== false && opts.depth > opts.maxDepth) return; + if (entry.name) nameStack.push(Renderer.stripTags(entry.name)); + if (entry.entries) { + let nextDepth = entry.type === "section" ? -1 : entry.type === "entries" ? opts.depth + 1 : opts.depth; + for (const eX of entry.entries) { + const nxtOpts = {...opts}; + nxtOpts.depth = nextDepth; + Renderer.getNames(nameStack, eX, nxtOpts); + } + } else if (entry.items) { + for (const eX of entry.items) { + Renderer.getNames(nameStack, eX, opts); + } + } +}; + +Renderer.getNumberedNames = function (entry) { + const renderer = new Renderer().setTrackTitles(true); + renderer.render(entry); + const titles = renderer.getTrackedTitles(); + const out = {}; + Object.entries(titles).forEach(([k, v]) => { + v = Renderer.stripTags(v); + out[v] = Number(k); + }); + return out; +}; + +// dig down until we find a name, as feature names can be nested +Renderer.findName = function (entry) { return CollectionUtil.dfs(entry, {prop: "name"}); }; +Renderer.findSource = function (entry) { return CollectionUtil.dfs(entry, {prop: "source"}); }; +Renderer.findEntry = function (entry) { return CollectionUtil.dfs(entry, {fnMatch: obj => obj.name && obj?.entries?.length}); }; + +Renderer.stripTags = function (str) { + if (!str) return str; + let nxtStr = Renderer._stripTagLayer(str); + while (nxtStr.length !== str.length) { + str = nxtStr; + nxtStr = Renderer._stripTagLayer(str); + } + return nxtStr; +}; + +Renderer._stripTagLayer = function (str) { + if (str.includes("{@")) { + const tagSplit = Renderer.splitByTags(str); + return tagSplit.filter(it => it).map(it => { + if (it.startsWith("{@")) { + let [tag, text] = Renderer.splitFirstSpace(it.slice(1, -1)); + const tagInfo = Renderer.tag.TAG_LOOKUP[tag]; + if (!tagInfo) throw new Error(`Unhandled tag: "${tag}"`); + return tagInfo.getStripped(tag, text); + } else return it; + }).join(""); + } return str; +}; + +/** + * This assumes validation has been done in advance. + * @param row + * @param [opts] + * @param [opts.cbErr] + * @param [opts.isForceInfiniteResults] + * @param [opts.isFirstRow] Used it `isForceInfiniteResults` is specified. + * @param [opts.isLastRow] Used it `isForceInfiniteResults` is specified. + */ +Renderer.getRollableRow = function (row, opts) { + opts = opts || {}; + + if ( + row[0]?.type === "cell" + && ( + row[0]?.roll?.exact != null + || (row[0]?.roll?.min != null && row[0]?.roll?.max != null) + ) + ) return row; + + row = MiscUtil.copyFast(row); + try { + const cleanRow = String(row[0]).trim(); + + // format: "20 or lower"; "99 or higher" + const mLowHigh = /^(\d+) or (lower|higher)$/i.exec(cleanRow); + if (mLowHigh) { + row[0] = {type: "cell", entry: cleanRow}; // Preseve the original text + + if (mLowHigh[2].toLowerCase() === "lower") { + row[0].roll = { + min: -Renderer.dice.POS_INFINITE, + max: Number(mLowHigh[1]), + }; + } else { + row[0].roll = { + min: Number(mLowHigh[1]), + max: Renderer.dice.POS_INFINITE, + }; + } + + return row; + } + + // format: "95-00" or "12" + // u2012 = figure dash; u2013 = en-dash + const m = /^(\d+)([-\u2012\u2013](\d+))?$/.exec(cleanRow); + if (m) { + if (m[1] && !m[2]) { + row[0] = { + type: "cell", + roll: { + exact: Number(m[1]), + }, + }; + if (m[1][0] === "0") row[0].roll.pad = true; + Renderer.getRollableRow._handleInfiniteOpts(row, opts); + } else { + row[0] = { + type: "cell", + roll: { + min: Number(m[1]), + max: Number(m[3]), + }, + }; + if (m[1][0] === "0" || m[3][0] === "0") row[0].roll.pad = true; + Renderer.getRollableRow._handleInfiniteOpts(row, opts); + } + } else { + // format: "12+" + const m = /^(\d+)\+$/.exec(row[0]); + row[0] = { + type: "cell", + roll: { + min: Number(m[1]), + max: Renderer.dice.POS_INFINITE, + }, + }; + } + } catch (e) { + if (opts.cbErr) opts.cbErr(row[0], e); + } + return row; +}; +Renderer.getRollableRow._handleInfiniteOpts = function (row, opts) { + if (!opts.isForceInfiniteResults) return; + + const isExact = row[0].roll.exact != null; + + if (opts.isFirstRow) { + if (!isExact) row[0].roll.displayMin = row[0].roll.min; + row[0].roll.min = -Renderer.dice.POS_INFINITE; + } + + if (opts.isLastRow) { + if (!isExact) row[0].roll.displayMax = row[0].roll.max; + row[0].roll.max = Renderer.dice.POS_INFINITE; + } +}; + +Renderer.initLazyImageLoaders = function () { + const images = document.querySelectorAll(`img[data-src]`); + + Renderer.utils.lazy.destroyObserver({observerId: "images"}); + + const observer = Renderer.utils.lazy.getCreateObserver({ + observerId: "images", + fnOnObserve: ({entry}) => { + const $img = $(entry.target); + $img.attr("src", $img.attr("data-src")).removeAttr("data-src"); + }, + }); + + images.forEach(img => observer.track(img)); +}; + +Renderer.HEAD_NEG_1 = "rd__b--0"; +Renderer.HEAD_0 = "rd__b--1"; +Renderer.HEAD_1 = "rd__b--2"; +Renderer.HEAD_2 = "rd__b--3"; +Renderer.HEAD_2_SUB_VARIANT = "rd__b--4"; +Renderer.DATA_NONE = "data-none"; diff --git a/charbuilder/css/charbuilder.css b/charbuilder/css/charbuilder.css new file mode 100644 index 0000000..4bc59fc --- /dev/null +++ b/charbuilder/css/charbuilder.css @@ -0,0 +1,341 @@ +.ve-flex-col { + display: flex !important; + flex-direction: column !important; +} +body { + font-family: 'Arial', sans-serif; + background-color: #f0f0f0; + color: #333; + margin: 20px; + line-height: normal !important; +} + +h1 { + color: #0066cc; +} + +p { + line-height: 1.6; +} + +.ve-hidden { + display: none !important; +} +.w-100 { + width: 100% !important; +} +.ve-flex-col { + display: flex !important; + flex-direction: column !important; +} +.vr-1 { + width: 1px; + height: 100%; + border: 0; + border-left: 1px solid #b5b3a4; + border-right: 1px solid #f0f0e0; + margin-right: 0.25rem !important; + margin-left: 0.25rem !important; +} +.ve-flex { + display: flex !important; +} +input.form-control, textarea.form-control { + border: 1px solid #aaa; + border-radius: 0; + padding: 3px; + background: #d3d1c5; +} +.form-control { + width: 100%; +} +.ui-sel2__ipt-display { + padding-right: 20px; +} +.ui-sel2__ipt-search { + top: 0; + right: 0; + left: 0; + opacity: 0; + background: transparent; +} +.block { + display: block !important; +} +.ve-small { + font-size: 85% !important; +} +.min-h-0 { + min-height: 0 !important; +} +.inline-block { + display: inline-block !important; +} +.bold { + font-weight: bold !important; +} +.mr-1 { + margin-right: 0.25rem !important; +} +.ml-1 { + margin-left: 0.25rem !important; +} +.py-1 { + padding-top: 0.25rem !important; + padding-bottom: 0.25rem !important; +} +.ve-flex-v-center { + display: flex !important; + align-items: center !important; +} +.ve-small { + font-size: 85% !important; +} +.ve-inline-flex-v-center { + display: inline-flex !important; + align-items: center !important; +} +.veapp__list { + transform: translateZ(0); + overflow-y: auto; + overflow-x: hidden; +} +.veapp__list-row { + border-bottom: 1px solid #aaa8; + margin-top: -1px; +} +.col-1 { + width: 8.3333333333% !important; +} +.ve-flex-vh-center { + display: flex !important; + align-items: center !important; + justify-content: center !important; +} +input[type="radio"] { + position: relative; + top: 2px; + margin: 0 0 0 5px; +} +.ve-window input[type=radio], .ui-modal__overlay input[type=radio] { + top: 0; + margin: 0; +} +.col-1-5 { + width: 12.5% !important; +} +.ve-text-center { + text-align: center !important; +} +.ve-flex-v-stretch { + display: flex !important; + align-items: stretch !important; +} +.no-shrink { + flex-shrink: 0 !important; +} +.mb-1 { + margin-bottom: 0.25rem !important; +} +.col-9-5 { + width: 79.1666666667% !important; +} +.btn-5et { + margin: 0; +} +.col-1-5 { + width: 12.5% !important; +} +.ui-tab__wrp-tab-body--border { + padding: 3px 0; +} +.ui-tab__wrp-tab-body { + width: 100%; + height: 100%; + overflow-y: auto; + overflow-x: hidden; +} +.h-100 { + height: 100% !important; +} +.ve-grow { + flex-grow: 1 !important; +} +.overflow-y-auto { + overflow-y: auto; +} +.px-1 { + padding-right: 0.25rem !important; + padding-left: 0.25rem !important; +} +.pt-1 { + padding-top: 0.25rem !important; +} +.split-v-center { + display: flex !important; + justify-content: space-between !important; + align-items: center !important; +} +.mt-2 { + margin-top: 0.5rem !important; +} +.btn-xs, a.btn-xs, label.btn-xs { + padding: 1px 5px; + font-size: 12px; + line-height: 1.5; +} +.glyphicon { + position: relative; + top: 1px; + display: inline-block; + font-family: "Glyphicons Halflings"; + font-style: normal; + font-weight: 400; + line-height: 1; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} +.relative { + position: relative !important; +} +input.form-control, textarea.form-control { + border: 1px solid #aaa; + border-radius: 0; + padding: 3px; + background: #d3d1c5; +} +.form-control { + width: 100%; +} +.italic { + font-style: italic !important; +} +.ve-muted { + color: #929292 !important; +} +.input-xs, input[type=text].input-xs { + height: 22px; +} +.bl-0 { + border-left-width: 0 !important; +} +.ui-sel2__wrp-options { + z-index: 1; + top: 22px; + right: 0; + left: 0; + display: none; + flex-direction: column; + background: #dedcd0; + border: 1px solid #aaa; + border-top: 0; + max-height: 200px; +} +.absolute { + position: absolute !important; +} +.overflow-y-scroll { + overflow-y: scroll; +} +.list-multi-selected { + box-shadow: inset 0 0 0 5000px #00000018; +} +.non-interactive { + pointer-events: none; + } +.window-app * { + overflow-x: hidden; +} +.overflow-x-vis{ + overflow-x: visible; +} +.btn.active{ + border: 2px solid #111e; +} +.hugRight{ + float:right; + margin-right: 2px; +} +.character-builder-header{ + flex-direction: row; + display: flex; + -moz-box-align: center; + align-items: center; +} +.character-builder-header > h1{ + margin: 0px; + font-family: Tiamat Condensed SC; + font-weight: 400; + font-style: normal; + font-stretch: normal; + letter-spacing: 1px; + font-size: 40px; + line-height: 1.4; + flex: 1 1 0%; +} +.character-builder-header > button{ + display: inline-flex; + -moz-box-align: center; + align-items: center; + -moz-box-pack: center; + justify-content: center; + position: relative; + box-sizing: border-box; + outline: 0px; + border: 0px; + margin: 0px; + cursor: pointer; + user-select: none; + vertical-align: middle; + appearance: none; + text-decoration: none; + font-family: "Roboto", "Helvetica", "Arial", sans-serif; + font-weight: 500; + text-transform: uppercase; + min-width: 64px; + padding: 8px 22px; + border-radius: 4px; + transition: background-color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms, box-shadow 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms, border-color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms, color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms; + color: rgb(255, 255, 255); + background-color: rgb(55, 64, 69); + box-shadow: rgba(0, 0, 0, 0.2) 0px 3px 1px -2px, rgba(0, 0, 0, 0.14) 0px 2px 2px 0px, rgba(0, 0, 0, 0.12) 0px 1px 5px 0px; + font-size: 15px; + line-height: 1.73333; + letter-spacing: 0.45px; + max-height: 42px; + width: auto; + margin: 0px 0px 0px 16px; + color: white; +} +.character-builder-description { + padding-right: 20px; +} +.character-builder-description-textwrp{ + padding-right: 20px; +} +.character-builder-description > ul{ + display: flex; + flex-wrap: wrap; + padding: 0px; +} +.character-builder-description > ul > li{ + display: flex; + flex-direction: column-reverse; + margin-left: 8px; + margin-right: 8px; + margin-bottom: 10px; +} +.character-builder-description .inputul > li { + width: 40%; +} +.character-builder-description .inputtext > li { + width: 100%; + flex-direction: column; +} + +.manc__list-row-button { + padding: 0 2px !important; + font-size: 12px !important; + line-height: 14px !important; + border: 2px outset rgb(227,227,227) !important; +} \ No newline at end of file diff --git a/charbuilder/css/charselect.css b/charbuilder/css/charselect.css new file mode 100644 index 0000000..7b4eaf9 --- /dev/null +++ b/charbuilder/css/charselect.css @@ -0,0 +1,171 @@ +.character-screen-header{ + flex-direction: row; + display: flex; + -moz-box-align: center; + align-items: center; +} +.character-screen-header > h1{ + margin: 0px; + font-family: Tiamat Condensed SC; + font-weight: 400; + font-style: normal; + font-stretch: normal; + letter-spacing: 1px; + font-size: 40px; + line-height: 1.4; + flex: 1 1 0%; +} +.character-screen-header > button{ + display: inline-flex; + -moz-box-align: center; + align-items: center; + -moz-box-pack: center; + justify-content: center; + position: relative; + box-sizing: border-box; + outline: 0px; + border: 0px; + margin: 0px; + cursor: pointer; + user-select: none; + vertical-align: middle; + appearance: none; + text-decoration: none; + font-family: "Roboto", "Helvetica", "Arial", sans-serif; + font-weight: 500; + text-transform: uppercase; + min-width: 64px; + padding: 8px 22px; + border-radius: 4px; + transition: background-color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms, box-shadow 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms, border-color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms, color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms; + color: rgb(255, 255, 255); + background-color: rgb(55, 64, 69); + box-shadow: rgba(0, 0, 0, 0.2) 0px 3px 1px -2px, rgba(0, 0, 0, 0.14) 0px 2px 2px 0px, rgba(0, 0, 0, 0.12) 0px 1px 5px 0px; + font-size: 15px; + line-height: 1.73333; + letter-spacing: 0.45px; + max-height: 42px; + width: auto; + margin: 0px 0px 0px 16px; + color: white; +} +.my-characters-listing { + list-style: none; + padding: 0; + list-style-image: none; +} +.character-listing { + display: flex; + flex-wrap: wrap; + justify-content: flex-start; +} +.character-card-wrapper +{ + width: 33.3333333333%; + padding: 10px; +} +.character-card{ + border: 1px solid rgb(209, 205, 202); +} +.character-card-header { + padding: 30px 20px; + position: relative; +} +.character-card-background-image{ + bottom: 0; + left: 0; + position: absolute; + right: 0; + top: 0; + background-position: 50%; + background-repeat: no-repeat; + background-size: cover; + background-color: #eaeaea; +} +.character-card-header-upper{ + align-items: center; + display: flex; + position: relative; + z-index: 1; +} +.character-card-header-upper-portrait{ + flex: 0 0 auto; +} +.character-card-header-upper-portrait .image{ + border-radius: 3px; + height: 60px; + margin-right: 10px; + width: 60px; +} +.character-card-header-upper-portrait .user-selected-avatar.image +{ + background-position: 50%; + background-size: cover; +} +.character-card-header-upper-info{ + flex: 1 1 100%; + min-width: 0; + padding-right: 10px; +} +.character-card-header-upper-info-header{ + margin: 0px; + font-size: 26px; + font-weight: 400; + font-style: normal; + font-stretch: normal; + line-height: 32.5px; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + font-family: Roboto Condensed; + letter-spacing: normal; +} +.character-card-header-upper-info-secondary{ + color: #b4b4b4; + display: block; + font-family: Roboto Condensed,Roboto,Helvetica,sans-serif; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.character-card-footer{ + border-top: 1px solid #dedede; + padding: 0 20px; +} +.character-card-footer-links{ + align-items: center; + display: flex; + height: 50px; + justify-content: space-between; +} +.character-card-footer-links-button{ + display: inline-flex; + -moz-box-align: center; + align-items: center; + -moz-box-pack: center; + justify-content: center; + position: relative; + box-sizing: border-box; + background-color: transparent; + outline: 0px; + border: 0px; + margin: 0px; + cursor: pointer; + user-select: none; + vertical-align: middle; + appearance: none; + text-decoration: none; + font-family: "Roboto", "Helvetica", "Arial", sans-serif; + font-weight: bold; + text-transform: uppercase; + min-width: 64px; + padding: 4px 5px; + border-radius: 4px; + transition: background-color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms, box-shadow 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms, border-color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms, color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms; + font-size: 15px; + line-height: 1.73333; + letter-spacing: 0.45px; +} +.character-card-footer-links-button.btn-dangerous{ + color: rgb(237, 108, 2); +} \ No newline at end of file diff --git a/charbuilder/css/plutonium.css b/charbuilder/css/plutonium.css new file mode 100644 index 0000000..0cb1943 --- /dev/null +++ b/charbuilder/css/plutonium.css @@ -0,0 +1,7421 @@ +๏ปฟ.col-0-1, +.col-0-2, +.col-0-3, +.col-0-4, +.col-0-5, +.col-0-6, +.col-0-7, +.col-0-8, +.col-0-9, +.col-1-1, +.col-1-2, +.col-1-3, +.col-1-4, +.col-1-5, +.col-1-6, +.col-1-7, +.col-1-8, +.col-1-9, +.col-1, +.col-2-1, +.col-2-2, +.col-2-3, +.col-2-4, +.col-2-5, +.col-2-6, +.col-2-7, +.col-2-8, +.col-2-9, +.col-2, +.col-3-1, +.col-3-2, +.col-3-3, +.col-3-4, +.col-3-5, +.col-3-6, +.col-3-7, +.col-3-8, +.col-3-9, +.col-3, +.col-4-1, +.col-4-2, +.col-4-3, +.col-4-4, +.col-4-5, +.col-4-6, +.col-4-7, +.col-4-8, +.col-4-9, +.col-4, +.col-5-1, +.col-5-2, +.col-5-3, +.col-5-4, +.col-5-5, +.col-5-6, +.col-5-7, +.col-5-8, +.col-5-9, +.col-5, +.col-6-1, +.col-6-2, +.col-6-3, +.col-6-4, +.col-6-5, +.col-6-6, +.col-6-7, +.col-6-8, +.col-6-9, +.col-6, +.col-7-1, +.col-7-2, +.col-7-3, +.col-7-4, +.col-7-5, +.col-7-6, +.col-7-7, +.col-7-8, +.col-7-9, +.col-7, +.col-8-1, +.col-8-2, +.col-8-3, +.col-8-4, +.col-8-5, +.col-8-6, +.col-8-7, +.col-8-8, +.col-8-9, +.col-8, +.col-9-1, +.col-9-2, +.col-9-3, +.col-9-4, +.col-9-5, +.col-9-6, +.col-9-7, +.col-9-8, +.col-9-9, +.col-9, +.col-10-1, +.col-10-2, +.col-10-3, +.col-10-4, +.col-10-5, +.col-10-6, +.col-10-7, +.col-10-8, +.col-10-9, +.col-10, +.col-11-1, +.col-11-2, +.col-11-3, +.col-11-4, +.col-11-5, +.col-11-6, +.col-11-7, +.col-11-8, +.col-11-9, +.col-11, +.col-12 { + position: relative; + min-height: 1px; +} +.col-12 { + width: 100% !important; +} +.col-11 { + width: 91.6666666667% !important; +} +.col-11-9 { + width: 99.1666666667% !important; +} +.col-11-8 { + width: 98.3333333333% !important; +} +.col-11-7 { + width: 97.5% !important; +} +.col-11-6 { + width: 96.6666666667% !important; +} +.col-11-5 { + width: 95.8333333333% !important; +} +.col-11-4 { + width: 95% !important; +} +.col-11-3 { + width: 94.1666666667% !important; +} +.col-11-2 { + width: 93.3333333333% !important; +} +.col-11-1 { + width: 92.5% !important; +} +.col-10 { + width: 83.3333333333% !important; +} +.col-10-9 { + width: 90.8333333333% !important; +} +.col-10-8 { + width: 90% !important; +} +.col-10-7 { + width: 89.1666666667% !important; +} +.col-10-6 { + width: 88.3333333333% !important; +} +.col-10-5 { + width: 87.5% !important; +} +.col-10-4 { + width: 86.6666666667% !important; +} +.col-10-3 { + width: 85.8333333333% !important; +} +.col-10-2 { + width: 85% !important; +} +.col-10-1 { + width: 84.1666666667% !important; +} +.col-9 { + width: 75% !important; +} +.col-9-9 { + width: 82.5% !important; +} +.col-9-8 { + width: 81.6666666667% !important; +} +.col-9-7 { + width: 80.8333333333% !important; +} +.col-9-6 { + width: 80% !important; +} +.col-9-5 { + width: 79.1666666667% !important; +} +.col-9-4 { + width: 78.3333333333% !important; +} +.col-9-3 { + width: 77.5% !important; +} +.col-9-2 { + width: 76.6666666667% !important; +} +.col-9-1 { + width: 75.8333333333% !important; +} +.col-8 { + width: 66.6666666667% !important; +} +.col-8-9 { + width: 74.1666666667% !important; +} +.col-8-8 { + width: 73.3333333333% !important; +} +.col-8-7 { + width: 72.5% !important; +} +.col-8-6 { + width: 71.6666666667% !important; +} +.col-8-5 { + width: 70.8333333333% !important; +} +.col-8-4 { + width: 70% !important; +} +.col-8-3 { + width: 69.1666666667% !important; +} +.col-8-2 { + width: 68.3333333333% !important; +} +.col-8-1 { + width: 67.5% !important; +} +.col-7 { + width: 58.3333333333% !important; +} +.col-7-9 { + width: 65.8333333333% !important; +} +.col-7-8 { + width: 65% !important; +} +.col-7-7 { + width: 64.1666666667% !important; +} +.col-7-6 { + width: 63.3333333333% !important; +} +.col-7-5 { + width: 62.5% !important; +} +.col-7-4 { + width: 61.6666666667% !important; +} +.col-7-3 { + width: 60.8333333333% !important; +} +.col-7-2 { + width: 60% !important; +} +.col-7-1 { + width: 59.1666666667% !important; +} +.col-6 { + width: 50% !important; +} +.col-6-9 { + width: 57.5% !important; +} +.col-6-8 { + width: 56.6666666667% !important; +} +.col-6-7 { + width: 55.8333333333% !important; +} +.col-6-6 { + width: 55% !important; +} +.col-6-5 { + width: 54.1666666667% !important; +} +.col-6-4 { + width: 53.3333333333% !important; +} +.col-6-3 { + width: 52.5% !important; +} +.col-6-2 { + width: 51.6666666667% !important; +} +.col-6-1 { + width: 50.8333333333% !important; +} +.col-5 { + width: 41.6666666667% !important; +} +.col-5-9 { + width: 49.1666666667% !important; +} +.col-5-8 { + width: 48.3333333333% !important; +} +.col-5-7 { + width: 47.5% !important; +} +.col-5-6 { + width: 46.6666666667% !important; +} +.col-5-5 { + width: 45.8333333333% !important; +} +.col-5-4 { + width: 45% !important; +} +.col-5-3 { + width: 44.1666666667% !important; +} +.col-5-2 { + width: 43.3333333333% !important; +} +.col-5-1 { + width: 42.5% !important; +} +.col-4 { + width: 33.3333333333% !important; +} +.col-4-9 { + width: 40.8333333333% !important; +} +.col-4-8 { + width: 40% !important; +} +.col-4-7 { + width: 39.1666666667% !important; +} +.col-4-6 { + width: 38.3333333333% !important; +} +.col-4-5 { + width: 37.5% !important; +} +.col-4-4 { + width: 36.6666666667% !important; +} +.col-4-3 { + width: 35.8333333333% !important; +} +.col-4-2 { + width: 35% !important; +} +.col-4-1 { + width: 34.1666666667% !important; +} +.col-3 { + width: 25% !important; +} +.col-3-9 { + width: 32.5% !important; +} +.col-3-8 { + width: 31.6666666667% !important; +} +.col-3-7 { + width: 30.8333333333% !important; +} +.col-3-6 { + width: 30% !important; +} +.col-3-5 { + width: 29.1666666667% !important; +} +.col-3-4 { + width: 28.3333333333% !important; +} +.col-3-3 { + width: 27.5% !important; +} +.col-3-2 { + width: 26.6666666667% !important; +} +.col-3-1 { + width: 25.8333333333% !important; +} +.col-2 { + width: 16.6666666667% !important; +} +.col-2-9 { + width: 24.1666666667% !important; +} +.col-2-8 { + width: 23.3333333333% !important; +} +.col-2-7 { + width: 22.5% !important; +} +.col-2-6 { + width: 21.6666666667% !important; +} +.col-2-5 { + width: 20.8333333333% !important; +} +.col-2-4 { + width: 20% !important; +} +.col-2-3 { + width: 19.1666666667% !important; +} +.col-2-2 { + width: 18.3333333333% !important; +} +.col-2-1 { + width: 17.5% !important; +} +.col-1 { + width: 8.3333333333% !important; +} +.col-1-9 { + width: 15.8333333333% !important; +} +.col-1-8 { + width: 15% !important; +} +.col-1-7 { + width: 14.1666666667% !important; +} +.col-1-6 { + width: 13.3333333333% !important; +} +.col-1-5 { + width: 12.5% !important; +} +.col-1-4 { + width: 11.6666666667% !important; +} +.col-1-3 { + width: 10.8333333333% !important; +} +.col-1-2 { + width: 10% !important; +} +.col-1-1 { + width: 9.1666666667% !important; +} +.col-0-9 { + width: 7.5% !important; +} +.col-0-8 { + width: 6.6666666667% !important; +} +.col-0-7 { + width: 5.8333333333% !important; +} +.col-0-6 { + width: 5% !important; +} +.col-0-5 { + width: 4.1666666667% !important; +} +.col-0-4 { + width: 3.3333333333% !important; +} +.col-0-3 { + width: 2.5% !important; +} +.col-0-2 { + width: 1.6666666667% !important; +} +.col-0-1 { + width: 0.8333333333% !important; +} +.b-0 { + border: 0 !important; +} +.b-1 { + border-width: 0.25rem !important; +} +.b-2 { + border-width: 0.5rem !important; +} +.b-3 { + border-width: 1rem !important; +} +.b-4 { + border-width: 1.5rem !important; +} +.b-5 { + border-width: 3rem !important; +} +.b-1p { + border: 1px solid #aaa !important; +} +.bt-0 { + border-top-width: 0 !important; +} +.bt-1 { + border-top-width: 0.25rem !important; +} +.bt-2 { + border-top-width: 0.5rem !important; +} +.bt-3 { + border-top-width: 1rem !important; +} +.bt-4 { + border-top-width: 1.5rem !important; +} +.bt-5 { + border-top-width: 3rem !important; +} +.bt-1p { + border-top: 1px solid #aaa !important; +} +.br-0 { + border-right-width: 0 !important; +} +.br-1 { + border-right-width: 0.25rem !important; +} +.br-2 { + border-right-width: 0.5rem !important; +} +.br-3 { + border-right-width: 1rem !important; +} +.br-4 { + border-right-width: 1.5rem !important; +} +.br-5 { + border-right-width: 3rem !important; +} +.br-1p { + border-right: 1px solid #aaa !important; +} +.bb-0 { + border-bottom-width: 0 !important; +} +.bb-1 { + border-bottom-width: 0.25rem !important; +} +.bb-2 { + border-bottom-width: 0.5rem !important; +} +.bb-3 { + border-bottom-width: 1rem !important; +} +.bb-4 { + border-bottom-width: 1.5rem !important; +} +.bb-5 { + border-bottom-width: 3rem !important; +} +.bb-1p { + border-bottom: 1px solid #aaa !important; +} +.bb-1p-trans { + border-bottom: 1px solid #aaa8 !important; +} +.bl-0 { + border-left-width: 0 !important; +} +.bl-1 { + border-left-width: 0.25rem !important; +} +.bl-2 { + border-left-width: 0.5rem !important; +} +.bl-3 { + border-left-width: 1rem !important; +} +.bl-4 { + border-left-width: 1.5rem !important; +} +.bl-5 { + border-left-width: 3rem !important; +} +.bl-1p { + border-left: 1px solid #aaa !important; +} +.by-0 { + border-top-width: 0 !important; + border-bottom-width: 0 !important; +} +.by-1 { + border-top-width: 0.25rem !important; + border-bottom-width: 0.25rem !important; +} +.by-2 { + border-top-width: 0.5rem !important; + border-bottom-width: 0.5rem !important; +} +.by-3 { + border-top-width: 1rem !important; + border-bottom-width: 1rem !important; +} +.by-4 { + border-top-width: 1.5rem !important; + border-bottom-width: 1.5rem !important; +} +.by-5 { + border-top-width: 3rem !important; + border-bottom-width: 3rem !important; +} +.bx-0 { + border-right-width: 0 !important; + border-left-width: 0 !important; +} +.bx-1 { + border-right-width: 0.25rem !important; + border-left-width: 0.25rem !important; +} +.bx-2 { + border-right-width: 0.5rem !important; + border-left-width: 0.5rem !important; +} +.bx-3 { + border-right-width: 1rem !important; + border-left-width: 1rem !important; +} +.bx-4 { + border-right-width: 1.5rem !important; + border-left-width: 1.5rem !important; +} +.bx-5 { + border-right-width: 3rem !important; + border-left-width: 3rem !important; +} +.btl-0 { + border-top-left-radius: 0 !important; +} +.btl-5p { + border-top-left-radius: 5px !important; +} +.btr-0 { + border-top-right-radius: 0 !important; +} +.btr-5p { + border-top-right-radius: 5px !important; +} +.bbr-0 { + border-bottom-right-radius: 0 !important; +} +.bbr-5p { + border-bottom-right-radius: 5px !important; +} +.bbl-0 { + border-bottom-left-radius: 0 !important; +} +.bbl-5p { + border-bottom-left-radius: 5px !important; +} +.hr-0 { + margin-top: 0 !important; + margin-bottom: 0 !important; + width: 100%; +} +.hr-1 { + margin-top: 0.25rem !important; + margin-bottom: 0.25rem !important; + width: 100%; +} +.hr-2 { + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; + width: 100%; +} +.hr-3 { + margin-top: 1rem; + margin-bottom: 1rem; + width: 100%; +} +.hr-4 { + margin-top: 1.5rem !important; + margin-bottom: 1.5rem !important; + width: 100%; +} +.hr-5 { + margin-top: 3rem !important; + margin-bottom: 3rem !important; + width: 100%; +} +.vr-0 { + width: 1px; + height: 100%; + border: 0; + border-left: 1px solid #b5b3a4; + border-right: 1px solid #f0f0e0; + margin-right: 0 !important; + margin-left: 0 !important; +} +.vr-1 { + width: 1px; + height: 100%; + border: 0; + border-left: 1px solid #b5b3a4; + border-right: 1px solid #f0f0e0; + margin-right: 0.25rem !important; + margin-left: 0.25rem !important; +} +.vr-2 { + width: 1px; + height: 100%; + border: 0; + border-left: 1px solid #b5b3a4; + border-right: 1px solid #f0f0e0; + margin-right: 0.5rem !important; + margin-left: 0.5rem !important; +} +.vr-3 { + width: 1px; + height: 100%; + border: 0; + border-left: 1px solid #b5b3a4; + border-right: 1px solid #f0f0e0; + margin-right: 1rem !important; + margin-left: 1rem !important; +} +.vr-4 { + width: 1px; + height: 100%; + border: 0; + border-left: 1px solid #b5b3a4; + border-right: 1px solid #f0f0e0; + margin-right: 1.5rem !important; + margin-left: 1.5rem !important; +} +.vr-5 { + width: 1px; + height: 100%; + border: 0; + border-left: 1px solid #b5b3a4; + border-right: 1px solid #f0f0e0; + margin-right: 3rem !important; + margin-left: 3rem !important; +} +.m-auto { + margin: auto !important; +} +.m-0 { + margin: 0 !important; +} +.m-1 { + margin: 0.25rem !important; +} +.m-2 { + margin: 0.5rem !important; +} +.m-3 { + margin: 1rem !important; +} +.m-4 { + margin: 1.5rem !important; +} +.m-5 { + margin: 3rem !important; +} +.my-auto { + margin-top: auto !important; + margin-bottom: auto !important; +} +.my-0 { + margin-top: 0 !important; + margin-bottom: 0 !important; +} +.my-1 { + margin-top: 0.25rem !important; + margin-bottom: 0.25rem !important; +} +.my-2 { + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; +} +.my-3 { + margin-top: 1rem !important; + margin-bottom: 1rem !important; +} +.my-4 { + margin-top: 1.5rem !important; + margin-bottom: 1.5rem !important; +} +.my-5 { + margin-top: 3rem !important; + margin-bottom: 3rem !important; +} +.mx-auto { + margin-right: auto !important; + margin-left: auto !important; +} +.mx-0 { + margin-right: 0 !important; + margin-left: 0 !important; +} +.mx-1 { + margin-right: 0.25rem !important; + margin-left: 0.25rem !important; +} +.mx-2 { + margin-right: 0.5rem !important; + margin-left: 0.5rem !important; +} +.mx-3 { + margin-right: 1rem !important; + margin-left: 1rem !important; +} +.mx-4 { + margin-right: 1.5rem !important; + margin-left: 1.5rem !important; +} +.mx-5 { + margin-right: 3rem !important; + margin-left: 3rem !important; +} +.my-1p { + margin-top: 1px; + margin-bottom: 1px; +} +.mx-2p { + margin-right: 2px !important; + margin-left: 2px !important; +} +.mt-auto { + margin-top: auto !important; +} +.mt-0 { + margin-top: 0 !important; +} +.mt-1 { + margin-top: 0.25rem !important; +} +.mt-n1 { + margin-top: -0.25rem !important; +} +.mt-2 { + margin-top: 0.5rem !important; +} +.mt-n2 { + margin-top: -0.5rem !important; +} +.mt-3 { + margin-top: 1rem !important; +} +.mt-4 { + margin-top: 1.5rem !important; +} +.mt-5 { + margin-top: 3rem !important; +} +.mt-1p { + margin-top: 1px !important; +} +.mr-auto { + margin-right: auto !important; +} +.mr-0 { + margin-right: 0 !important; +} +.mr-1 { + margin-right: 0.25rem !important; +} +.mr-n1 { + margin-right: -0.25rem !important; +} +.mr-2 { + margin-right: 0.5rem !important; +} +.mr-n2 { + margin-right: -0.5rem !important; +} +.mr-3 { + margin-right: 1rem !important; +} +.mr-4 { + margin-right: 1.5rem !important; +} +.mr-5 { + margin-right: 3rem !important; +} +.mr-3p { + margin-right: 3px !important; +} +.mb-auto { + margin-bottom: auto !important; +} +.mb-0 { + margin-bottom: 0 !important; +} +.mb-1 { + margin-bottom: 0.25rem !important; +} +.mb-n1 { + margin-bottom: -0.25rem !important; +} +.mb-2 { + margin-bottom: 0.5rem !important; +} +.mb-n2 { + margin-bottom: -0.5rem !important; +} +.mb-3 { + margin-bottom: 1rem !important; +} +.mb-4 { + margin-bottom: 1.5rem !important; +} +.mb-5 { + margin-bottom: 3rem !important; +} +.ml-auto { + margin-left: auto !important; +} +.ml-0 { + margin-left: 0 !important; +} +.ml-1 { + margin-left: 0.25rem !important; +} +.ml-n1 { + margin-left: -0.25rem !important; +} +.ml-2 { + margin-left: 0.5rem !important; +} +.ml-n2 { + margin-left: -0.5rem !important; +} +.ml-3 { + margin-left: 1rem !important; +} +.ml-4 { + margin-left: 1.5rem !important; +} +.ml-5 { + margin-left: 3rem !important; +} +.p-0 { + padding: 0 !important; +} +.p-1 { + padding: 0.25rem !important; +} +.p-2 { + padding: 0.5rem !important; +} +.p-3 { + padding: 1rem !important; +} +.p-4 { + padding: 1.5rem !important; +} +.p-5 { + padding: 3rem !important; +} +.p-1p { + padding: 1px !important; +} +.py-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; +} +.py-1 { + padding-top: 0.25rem !important; + padding-bottom: 0.25rem !important; +} +.py-2 { + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; +} +.py-3 { + padding-top: 1rem !important; + padding-bottom: 1rem !important; +} +.py-4 { + padding-top: 1.5rem !important; + padding-bottom: 1.5rem !important; +} +.py-5 { + padding-top: 3rem !important; + padding-bottom: 3rem !important; +} +.px-0 { + padding-right: 0 !important; + padding-left: 0 !important; +} +.px-1 { + padding-right: 0.25rem !important; + padding-left: 0.25rem !important; +} +.px-2 { + padding-right: 0.5rem !important; + padding-left: 0.5rem !important; +} +.px-3 { + padding-right: 1rem !important; + padding-left: 1rem !important; +} +.px-4 { + padding-right: 1.5rem !important; + padding-left: 1.5rem !important; +} +.px-5 { + padding-right: 3rem !important; + padding-left: 3rem !important; +} +.py-1p { + padding-top: 1px !important; + padding-bottom: 1px !important; +} +.py-2p { + padding-top: 2px !important; + padding-bottom: 2px !important; +} +.px-1p { + padding-right: 1px !important; + padding-left: 1px !important; +} +.px-2p { + padding-right: 2px !important; + padding-left: 2px !important; +} +.pt-0 { + padding-top: 0 !important; +} +.pt-1 { + padding-top: 0.25rem !important; +} +.pt-2 { + padding-top: 0.5rem !important; +} +.pt-3 { + padding-top: 1rem !important; +} +.pt-4 { + padding-top: 1.5rem !important; +} +.pt-5 { + padding-top: 3rem !important; +} +.pt-1p { + padding-top: 1px !important; +} +.pr-0 { + padding-right: 0 !important; +} +.pr-1 { + padding-right: 0.25rem !important; +} +.pr-2 { + padding-right: 0.5rem !important; +} +.pr-3 { + padding-right: 1rem !important; +} +.pr-4 { + padding-right: 1.5rem !important; +} +.pr-5 { + padding-right: 3rem !important; +} +.pr-1p { + padding-right: 1px !important; +} +.pb-0 { + padding-bottom: 0 !important; +} +.pb-1 { + padding-bottom: 0.25rem !important; +} +.pb-2 { + padding-bottom: 0.5rem !important; +} +.pb-3 { + padding-bottom: 1rem !important; +} +.pb-4 { + padding-bottom: 1.5rem !important; +} +.pb-5 { + padding-bottom: 3rem !important; +} +.pb-1p { + padding-bottom: 1px !important; +} +.pl-0 { + padding-left: 0 !important; +} +.pl-1 { + padding-left: 0.25rem !important; +} +.pl-2 { + padding-left: 0.5rem !important; +} +.pl-3 { + padding-left: 1rem !important; +} +.pl-4 { + padding-left: 1.5rem !important; +} +.pl-5 { + padding-left: 3rem !important; +} +.pl-1p { + padding-left: 1px !important; +} +.z-index-1 { + z-index: 1 !important; +} +.top-n1p { + top: -1px; +} +.right-0 { + right: 0 !important; +} +@keyframes anim-btn-pulse { + 0% { + background-color: rgba(255, 255, 240, 0.8); + border-color: #b5b3a4; + box-shadow: 0 0 0 #0000; + } + 40% { + background-color: rgba(255, 203, 160, 0.85); + border-color: #c89f7b; + box-shadow: 0 0 5px #ff6400; + } + 60% { + background-color: rgba(255, 203, 160, 0.85); + border-color: #c89f7b; + box-shadow: 0 0 5px #ff6400; + } + 100% { + background-color: rgba(255, 255, 240, 0.8); + border-color: #b5b3a4; + box-shadow: 0 0 0 #0000; + } +} +a.btn, +label.btn, +div.btn { + text-rendering: auto; + color: buttontext; + letter-spacing: normal; + word-spacing: normal; + text-transform: none; + text-indent: 0; + text-shadow: none; + align-items: flex-start; + background: rgba(255, 255, 240, 0.8); + border: 1px solid #b5b3a4; + border-radius: 3px; + font-family: "Signika", sans-serif; +} +a.btn:hover, +a.btn:focus, +label.btn:hover, +label.btn:focus, +div.btn:hover, +div.btn:focus { + box-shadow: 0 0 5px red; +} +a.btn:focus, +label.btn:focus, +div.btn:focus { + outline: none; +} +.btn, +.btn-5et { + margin: 0; +} +.btn, +a.btn, +label.btn { + width: initial; + display: inline-block; + padding: 6px 12px; + font-size: 14px; + line-height: 1.42857143; + text-align: center; + white-space: nowrap; + cursor: pointer; + text-decoration: none; +} +.btn-sm, +a.btn-sm, +label.btn-sm { + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; +} +.btn-xs, +a.btn-xs, +label.btn-xs { + padding: 1px 5px; + font-size: 12px; + line-height: 1.5; +} +.btn-xxs, +a.btn-xxs, +label.btn-xxs { + padding: 0 2px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} +.btn.active, +.btn--active, +a.btn.active, +a.btn--active, +label.btn.active, +label.btn--active { + box-shadow: inset 0 3px 7px #111e; +} +.btn.active:hover, +.btn.active:focus, +.btn--active:hover, +.btn--active:focus, +a.btn.active:hover, +a.btn.active:focus, +a.btn--active:hover, +a.btn--active:focus, +label.btn.active:hover, +label.btn.active:focus, +label.btn--active:hover, +label.btn--active:focus { + box-shadow: inset 0 5px 10px #111e; +} +.btn-primary, +a.btn-primary, +label.btn-primary { + filter: saturate(2) hue-rotate(-10deg) contrast(200%); +} +.btn-danger, +a.btn-danger, +label.btn-danger { + color: #fff; + background-color: #ca5454; + border-color: #a00000ee; +} +.btn-danger:hover, +.btn-danger:focus, +a.btn-danger:hover, +a.btn-danger:focus, +label.btn-danger:hover, +label.btn-danger:focus { + border-color: #a00000ee; + box-shadow: 0 0 5px #a00000ee; +} +.btn-danger.disabled, +.btn-danger[disabled], +a.btn-danger.disabled, +a.btn-danger[disabled], +label.btn-danger.disabled, +label.btn-danger[disabled] { + color: #929292; + cursor: not-allowed; +} +.btn-danger.disabled:hover, +.btn-danger.disabled:focus, +.btn-danger[disabled]:hover, +.btn-danger[disabled]:focus, +a.btn-danger.disabled:hover, +a.btn-danger.disabled:focus, +a.btn-danger[disabled]:hover, +a.btn-danger[disabled]:focus, +label.btn-danger.disabled:hover, +label.btn-danger.disabled:focus, +label.btn-danger[disabled]:hover, +label.btn-danger[disabled]:focus { + border-color: #a00000ee; + box-shadow: none; +} +.btn-success, +a.btn-success, +label.btn-success { + color: #fff; + background-color: #6fb56f; + border-color: #398439ee; +} +.btn-success:hover, +.btn-success:focus, +a.btn-success:hover, +a.btn-success:focus, +label.btn-success:hover, +label.btn-success:focus { + border-color: #398439ee; + box-shadow: 0 0 5px #398439ee; +} +.btn-success.disabled, +.btn-success[disabled], +a.btn-success.disabled, +a.btn-success[disabled], +label.btn-success.disabled, +label.btn-success[disabled] { + color: #929292; + cursor: not-allowed; +} +.btn-success.disabled:hover, +.btn-success.disabled:focus, +.btn-success[disabled]:hover, +.btn-success[disabled]:focus, +a.btn-success.disabled:hover, +a.btn-success.disabled:focus, +a.btn-success[disabled]:hover, +a.btn-success[disabled]:focus, +label.btn-success.disabled:hover, +label.btn-success.disabled:focus, +label.btn-success[disabled]:hover, +label.btn-success[disabled]:focus { + border-color: #398439ee; + box-shadow: none; +} +.btn-pulse, +a.btn-pulse, +label.btn-pulse { + animation: anim-btn-pulse 1.4s infinite; +} +.btn[disabled], +.btn.disabled, +a.btn[disabled], +a.btn.disabled, +label.btn[disabled], +label.btn.disabled { + cursor: not-allowed; + color: #929292; +} +.btn[disabled]:hover, +.btn[disabled]:focus, +.btn.disabled:hover, +.btn.disabled:focus, +a.btn[disabled]:hover, +a.btn[disabled]:focus, +a.btn.disabled:hover, +a.btn.disabled:focus, +label.btn[disabled]:hover, +label.btn[disabled]:focus, +label.btn.disabled:hover, +label.btn.disabled:focus { + box-shadow: none; +} +.btn[disabled]:hover.active, +.btn[disabled]:focus.active, +.btn.disabled:hover.active, +.btn.disabled:focus.active, +a.btn[disabled]:hover.active, +a.btn[disabled]:focus.active, +a.btn.disabled:hover.active, +a.btn.disabled:focus.active, +label.btn[disabled]:hover.active, +label.btn[disabled]:focus.active, +label.btn.disabled:hover.active, +label.btn.disabled:focus.active { + box-shadow: inset 0 3px 7px #111e; +} +.btn[disabled].disabled, +.btn[disabled][disabled], +.btn.disabled.disabled, +.btn.disabled[disabled], +a.btn[disabled].disabled, +a.btn[disabled][disabled], +a.btn.disabled.disabled, +a.btn.disabled[disabled], +label.btn[disabled].disabled, +label.btn[disabled][disabled], +label.btn.disabled.disabled, +label.btn.disabled[disabled] { + cursor: not-allowed; + color: #929292; +} +.button.btn.active, +.button.btn--active { + box-shadow: inset 0 3px 7px #111e; +} +.button.btn.active:hover, +.button.btn.active:focus, +.button.btn--active:hover, +.button.btn--active:focus { + box-shadow: inset 0 5px 10px #111e; +} +input.form-control, +textarea.form-control { + border: 1px solid #aaa; + border-radius: 0; + padding: 3px; + background: #d3d1c5; +} +input.form-control:hover, +input.form-control:focus, +input.form-control:active, +textarea.form-control:hover, +textarea.form-control:focus, +textarea.form-control:active { + outline: 0; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(255, 0, 0, 0.6); +} +input.form-control--error, +input.form-control--error[readonly], +input.form-control--error[disabled], +textarea.form-control--error, +textarea.form-control--error[readonly], +textarea.form-control--error[disabled] { + background-color: #ff000018 !important; + border: 1px solid #843534 !important; +} +input.form-control--error:focus, +input.form-control--error[readonly]:focus, +input.form-control--error[disabled]:focus, +textarea.form-control--error:focus, +textarea.form-control--error[readonly]:focus, +textarea.form-control--error[disabled]:focus { + border-color: #843534 !important; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px #ce8483 !important; +} +.input-xs, +input[type="text"].input-xs { + height: 22px; +} +select.form-control { + border: 1px solid #aaa; + border-radius: 0; +} +.btn-group > select, +.input-group > select { + border-color: #b5b3a4; +} +.btn-group > input.form-control, +.input-group > input.form-control { + background: #fff; +} +.btn-group > input.form-control, +.btn-group > label, +.btn-group > button, +.btn-group > select, +.btn-group > a.btn, +.btn-group > div.btn, +.input-group > input.form-control, +.input-group > label, +.input-group > button, +.input-group > select, +.input-group > a.btn, +.input-group > div.btn { + border-radius: 0; + border-right: 0; +} +.btn-group > input.form-control:first-child, +.btn-group > label:first-child, +.btn-group > button:first-child, +.btn-group > select:first-child, +.btn-group > a.btn:first-child, +.btn-group > div.btn:first-child, +.input-group > input.form-control:first-child, +.input-group > label:first-child, +.input-group > button:first-child, +.input-group > select:first-child, +.input-group > a.btn:first-child, +.input-group > div.btn:first-child { + border-top-left-radius: 3px; + border-bottom-left-radius: 3px; +} +.btn-group > input.form-control:last-child, +.btn-group > label:last-child, +.btn-group > button:last-child, +.btn-group > select:last-child, +.btn-group > a.btn:last-child, +.btn-group > div.btn:last-child, +.input-group > input.form-control:last-child, +.input-group > label:last-child, +.input-group > button:last-child, +.input-group > select:last-child, +.input-group > a.btn:last-child, +.input-group > div.btn:last-child { + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; + border-right: 1px solid #b5b3a4; +} +.btn-group--top > input.form-control:first-child, +.btn-group--top > label:first-child, +.btn-group--top > button:first-child, +.btn-group--top > select:first-child, +.btn-group--top > a.btn:first-child, +.btn-group--top > div.btn:first-child, +.input-group--top > input.form-control:first-child, +.input-group--top > label:first-child, +.input-group--top > button:first-child, +.input-group--top > select:first-child, +.input-group--top > a.btn:first-child, +.input-group--top > div.btn:first-child { + border-bottom-left-radius: 0; +} +.btn-group--top > input.form-control:last-child, +.btn-group--top > label:last-child, +.btn-group--top > button:last-child, +.btn-group--top > select:last-child, +.btn-group--top > a.btn:last-child, +.btn-group--top > div.btn:last-child, +.input-group--top > input.form-control:last-child, +.input-group--top > label:last-child, +.input-group--top > button:last-child, +.input-group--top > select:last-child, +.input-group--top > a.btn:last-child, +.input-group--top > div.btn:last-child { + border-bottom-right-radius: 0; +} +.btn-group--middle > input.form-control, +.btn-group--middle > label, +.btn-group--middle > button, +.btn-group--middle > select, +.btn-group--middle > a.btn, +.btn-group--middle > div.btn, +.input-group--middle > input.form-control, +.input-group--middle > label, +.input-group--middle > button, +.input-group--middle > select, +.input-group--middle > a.btn, +.input-group--middle > div.btn { + border-top: 0; +} +.btn-group--middle > input.form-control:first-child, +.btn-group--middle > label:first-child, +.btn-group--middle > button:first-child, +.btn-group--middle > select:first-child, +.btn-group--middle > a.btn:first-child, +.btn-group--middle > div.btn:first-child, +.input-group--middle > input.form-control:first-child, +.input-group--middle > label:first-child, +.input-group--middle > button:first-child, +.input-group--middle > select:first-child, +.input-group--middle > a.btn:first-child, +.input-group--middle > div.btn:first-child { + border-radius: 0; +} +.btn-group--middle > input.form-control:last-child, +.btn-group--middle > label:last-child, +.btn-group--middle > button:last-child, +.btn-group--middle > select:last-child, +.btn-group--middle > a.btn:last-child, +.btn-group--middle > div.btn:last-child, +.input-group--middle > input.form-control:last-child, +.input-group--middle > label:last-child, +.input-group--middle > button:last-child, +.input-group--middle > select:last-child, +.input-group--middle > a.btn:last-child, +.input-group--middle > div.btn:last-child { + border-radius: 0; +} +.btn-group--bottom > input.form-control, +.btn-group--bottom > label, +.btn-group--bottom > button, +.btn-group--bottom > select, +.btn-group--bottom > a.btn, +.btn-group--bottom > div.btn, +.input-group--bottom > input.form-control, +.input-group--bottom > label, +.input-group--bottom > button, +.input-group--bottom > select, +.input-group--bottom > a.btn, +.input-group--bottom > div.btn { + border-top: 0; +} +.btn-group--bottom > input.form-control:first-child, +.btn-group--bottom > label:first-child, +.btn-group--bottom > button:first-child, +.btn-group--bottom > select:first-child, +.btn-group--bottom > a.btn:first-child, +.btn-group--bottom > div.btn:first-child, +.input-group--bottom > input.form-control:first-child, +.input-group--bottom > label:first-child, +.input-group--bottom > button:first-child, +.input-group--bottom > select:first-child, +.input-group--bottom > a.btn:first-child, +.input-group--bottom > div.btn:first-child { + border-top-left-radius: 0; +} +.btn-group--bottom > input.form-control:last-child, +.btn-group--bottom > label:last-child, +.btn-group--bottom > button:last-child, +.btn-group--bottom > select:last-child, +.btn-group--bottom > a.btn:last-child, +.btn-group--bottom > div.btn:last-child, +.input-group--bottom > input.form-control:last-child, +.input-group--bottom > label:last-child, +.input-group--bottom > button:last-child, +.input-group--bottom > select:last-child, +.input-group--bottom > a.btn:last-child, +.input-group--bottom > div.btn:last-child { + border-top-right-radius: 0; +} +.btn-group-vertical > select, +.input-group-vertical > select { + border-color: #b5b3a4; +} +.btn-group-vertical > input.form-control, +.input-group-vertical > input.form-control { + background: #fff; +} +.btn-group-vertical > input.form-control, +.btn-group-vertical > label, +.btn-group-vertical > button, +.btn-group-vertical > select, +.btn-group-vertical > a.btn, +.btn-group-vertical > div.btn, +.input-group-vertical > input.form-control, +.input-group-vertical > label, +.input-group-vertical > button, +.input-group-vertical > select, +.input-group-vertical > a.btn, +.input-group-vertical > div.btn { + border-radius: 0; + border-bottom: 0; +} +.btn-group-vertical > input.form-control:first-child, +.btn-group-vertical > label:first-child, +.btn-group-vertical > button:first-child, +.btn-group-vertical > select:first-child, +.btn-group-vertical > a.btn:first-child, +.btn-group-vertical > div.btn:first-child, +.input-group-vertical > input.form-control:first-child, +.input-group-vertical > label:first-child, +.input-group-vertical > button:first-child, +.input-group-vertical > select:first-child, +.input-group-vertical > a.btn:first-child, +.input-group-vertical > div.btn:first-child { + border-top-left-radius: 3px; + border-top-right-radius: 3px; +} +.btn-group-vertical > input.form-control:last-child, +.btn-group-vertical > label:last-child, +.btn-group-vertical > button:last-child, +.btn-group-vertical > select:last-child, +.btn-group-vertical > a.btn:last-child, +.btn-group-vertical > div.btn:last-child, +.input-group-vertical > input.form-control:last-child, +.input-group-vertical > label:last-child, +.input-group-vertical > button:last-child, +.input-group-vertical > select:last-child, +.input-group-vertical > a.btn:last-child, +.input-group-vertical > div.btn:last-child { + border-bottom-left-radius: 3px; + border-bottom-right-radius: 3px; + border-bottom: 1px solid #b5b3a4; +} +input[type="checkbox"]:checked { + filter: grayscale(100%); +} +input[type="radio"]:checked { + filter: grayscale(100%); +} +.code { + font-family: monospace !important; +} +.dnd-font { + font-family: "Times New Roman", serif; + font-variant: small-caps; + font-weight: 500; +} +.ve-small { + font-size: 85% !important; +} +.font-size-24p { + font-size: 24px !important; +} +.ve-muted { + color: #929292 !important; +} +.bold { + font-weight: bold !important; +} +.ve-bolder { + font-weight: bolder !important; +} +.italic { + font-style: italic !important; +} +i > i { + font-style: initial; +} +.underline { + text-decoration: underline !important; +} +.no-underline { + text-decoration: none !important; +} +.help { + cursor: help !important; + text-decoration: underline !important; + text-decoration-style: dotted !important; +} +.help:hover, +.help:active, +.help:focus { + text-decoration: underline !important; + text-decoration-style: dotted !important; +} +.help-subtle { + cursor: help !important; +} +.no-wrap { + white-space: nowrap !important; +} +.text-clip-ellipsis { + white-space: nowrap !important; + text-overflow: ellipsis !important; + overflow: hidden !important; +} +.whitespace-normal { + white-space: normal; +} +.whitespace-pre { + white-space: pre; +} +.word-break-all { + word-break: break-all; +} +.small-caps { + font-variant: small-caps; +} +.capitalize { + text-transform: capitalize; +} +.no-breaks { + break-before: auto; + break-after: auto; + break-inside: avoid; +} +.text-left { + text-align: left !important; +} +.text-right { + text-align: right !important; +} +.ve-text-center { + text-align: center !important; +} +.text-rtl { + direction: rtl; +} +.trans-x-flip { + transform: scaleX(-1) !important; +} +.clickable { + cursor: pointer !important; +} +.not-clickable { + cursor: default !important; +} +.copyable { + cursor: copy !important; +} +.ve-draggable { + cursor: grab; +} +.no-events { + pointer-events: none !important; +} +.events-initial { + pointer-events: initial !important; +} +.no-select { + user-select: none !important; +} +.user-select-text { + user-select: text !important; +} +.user-select-all { + user-select: all !important; +} +.smooth-scroll { + transform: translateZ(0) !important; +} +.scrollbar-stable { + scrollbar-gutter: stable; +} +.overflow-auto { + overflow-x: auto; + overflow-y: auto; +} +.overflow-y-auto { + overflow-y: auto; +} +.overflow-y-scroll { + overflow-y: scroll; +} +.overflow-y-hidden { + overflow-y: hidden; +} +.overflow-x-auto { + overflow-x: auto; +} +.overflow-x-scroll { + overflow-x: scroll; +} +.overflow-x-hidden { + overflow-x: hidden; +} +.overflow-hidden { + overflow: hidden; +} +.overflow-ellipsis { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.overflow-overlay { + overflow: auto; + overflow: overlay; +} +.resize-vertical { + resize: vertical; +} +.resize-none { + resize: none; +} +.w-10 { + width: 10% !important; +} +.w-15 { + width: 15% !important; +} +.w-20 { + width: 20% !important; +} +.w-25 { + width: 25% !important; +} +.w-30 { + width: 30% !important; +} +.w-33 { + width: 33.3333333% !important; +} +.w-40 { + width: 40% !important; +} +.w-50 { + width: 50% !important; +} +.w-50--mr-2 { + width: calc(50% - 0.5rem) !important; +} +.w-60 { + width: 60% !important; +} +.w-66 { + width: 66.6666666% !important; +} +.w-70 { + width: 70% !important; +} +.w-75 { + width: 75% !important; +} +.w-80 { + width: 80% !important; +} +.w-90 { + width: 90% !important; +} +.w-100 { + width: 100% !important; +} +.w-100w { + width: 100vw !important; +} +.w-initial { + width: initial !important; +} +.w-20p { + width: 20px !important; +} +.w-24p { + width: 24px !important; +} +.w-30p { + width: 30px !important; +} +.w-40p { + width: 40px !important; +} +.w-48p { + width: 48px !important; +} +.w-50p { + width: 50px !important; +} +.w-70p { + width: 70px !important; +} +.w-80p { + width: 80px !important; +} +.w-90p { + width: 90px !important; +} +.w-100p { + width: 100px !important; +} +.w-140p { + width: 140px !important; +} +.w-200p { + width: 200px !important; +} +.w-640p { + width: 640px !important; +} +.min-w-0 { + min-width: 0 !important; +} +.min-w-80 { + min-width: 80% !important; +} +.min-w-100 { + min-width: 100% !important; +} +.min-w-20p { + min-width: 20px !important; +} +.min-w-100p { + min-width: 100px !important; +} +.min-w-200p { + min-width: 200px !important; +} +.max-w-25 { + max-width: 25% !important; +} +.max-w-33 { + max-width: 33.3333333% !important; +} +.max-w-80 { + max-width: 80% !important; +} +.max-w-100 { + max-width: 100% !important; +} +.max-w-80p { + max-width: 80px !important; +} +.max-w-100p { + max-width: 100px !important; +} +.max-w-200p { + max-width: 200px !important; +} +.max-w-300p { + max-width: 300px !important; +} +.max-w-640p { + max-width: 640px !important; +} +.h-initial { + height: initial !important; +} +.h-50 { + height: 50% !important; +} +.h-100 { + height: 100% !important; +} +.h-100h { + height: 100vh !important; +} +.h-20p { + height: 20px !important; +} +.h-21p { + height: 21px !important; +} +.h-25p { + height: 25px !important; +} +.h-27p { + height: 27px !important; +} +.h-30p { + height: 30px !important; +} +.h-100p { + height: 100px !important; +} +.h-120p { + height: 120px !important; +} +.h-ipt-xs { + height: 22px; +} +.min-h-0 { + min-height: 0 !important; +} +.min-h-100 { + min-height: 100% !important; +} +.min-h-24p { + min-height: 24px !important; +} +.min-h-100p { + min-height: 100px !important; +} +.max-h-40 { + max-height: 40% !important; +} +.max-h-unset { + max-height: unset !important; +} +.relative { + position: relative !important; +} +.absolute { + position: absolute !important; +} +.sticky { + position: sticky !important; +} +.ve-grid { + display: grid !important; +} +.block { + display: block !important; +} +.ve-block { + display: block !important; +} +.inline-block { + display: inline-block !important; +} +.ve-inline-block { + display: inline-block !important; +} +.inline { + display: inline !important; +} +.ve-inline-flex { + display: inline-flex !important; +} +.ve-flex { + display: flex !important; +} +.ve-flex-col { + display: flex !important; + flex-direction: column !important; +} +.ve-flex-v-center { + display: flex !important; + align-items: center !important; +} +.ve-inline-flex-v-center { + display: inline-flex !important; + align-items: center !important; +} +.ve-flex-v-top { + display: flex; + align-items: flex-start; +} +.ve-flex-v-baseline { + display: flex !important; + align-items: baseline !important; +} +.ve-flex-v-end { + display: flex !important; + align-items: flex-end !important; +} +.ve-flex-v-stretch { + display: flex !important; + align-items: stretch !important; +} +.ve-flex-h-center { + display: flex !important; + justify-content: center !important; +} +.ve-flex-h-right { + display: flex !important; + justify-content: flex-end !important; +} +.ve-flex-vh-center { + display: flex !important; + align-items: center !important; + justify-content: center !important; +} +.ve-flex-vh-center-around { + display: flex; + align-items: center; + justify-content: space-around; +} +.ve-flex-inline-col { + display: inline-flex !important; + flex-direction: column !important; +} +.ve-flex-inline-v-center { + display: inline-flex !important; + align-items: center !important; + justify-content: center !important; +} +.ve-self-flex-start { + align-self: flex-start !important; +} +.ve-self-flex-center { + align-self: center !important; +} +.ve-self-flex-end { + align-self: flex-end !important; +} +.ve-self-flex-stretch { + align-self: stretch !important; +} +.ve-flex-fill { + flex-basis: 100%; +} +.ve-grow { + flex-grow: 1 !important; +} +.no-shrink { + flex-shrink: 0 !important; +} +.no-grow { + flex-grow: 0 !important; +} +.ve-flex-1 { + flex: 1 !important; +} +.ve-flex-2 { + flex: 2 !important; +} +.ve-flex-3 { + flex: 3 !important; +} +.ve-flex-4 { + flex: 4 !important; +} +.ve-flex-5 { + flex: 5 !important; +} +.ve-flex-6 { + flex: 6 !important; +} +.ve-flex-7 { + flex: 7 !important; +} +.ve-shrink-10 { + flex-shrink: 10 !important; +} +.ve-flex-wrap { + display: flex !important; + flex-wrap: wrap !important; +} +.split { + display: flex !important; + justify-content: space-between !important; +} +.split-v-center { + display: flex !important; + justify-content: space-between !important; + align-items: center !important; +} +.inline-split-v-center { + display: inline-flex !important; + justify-content: space-between; + align-items: center; +} +.split-v-end { + display: flex !important; + justify-content: space-between !important; + align-items: flex-end !important; +} +.split-child { + width: 50%; + flex-shrink: 0; + flex-grow: 0; +} +.split-column { + display: flex; + justify-content: space-between; + flex-direction: column; +} +.split-column--inline { + display: inline-flex; +} +.columns-2 { + column-count: 2; + break-inside: avoid-column; + column-gap: 1.75rem; +} +.columns-2 > * { + break-inside: avoid-column; +} +@media (max-width: 768px) { + .columns-2 { + column-count: 1; + } +} +.columns-3 { + column-count: 3; + break-inside: avoid-column; + column-gap: 1.75rem; +} +.columns-3 > * { + break-inside: avoid-column; +} +@media (max-width: 768px) { + .columns-3 { + column-count: 2; + } +} +@media (max-width: 480px) { + .columns-3 { + column-count: 1; + } +} +.columns-4 { + column-count: 4; + break-inside: avoid-column; + column-gap: 1.75rem; +} +.columns-4 > * { + break-inside: avoid-column; +} +@media (max-width: 768px) { + .columns-4 { + column-count: 3; + } +} +@media (max-width: 480px) { + .columns-4 { + column-count: 2; + } +} +.columns-5 { + column-count: 5; + break-inside: avoid-column; + column-gap: 1.75rem; +} +.columns-5 > * { + break-inside: avoid-column; +} +@media (max-width: 768px) { + .columns-5 { + column-count: 3; + } +} +@media (max-width: 480px) { + .columns-5 { + column-count: 2; + } +} +.columns-6 { + column-count: 6; + break-inside: avoid-column; + column-gap: 1.75rem; +} +.columns-6 > * { + break-inside: avoid-column; +} +@media (max-width: 768px) { + .columns-6 { + column-count: 3; + } +} +@media (max-width: 480px) { + .columns-6 { + column-count: 2; + } +} +.table-layout-fixed { + table-layout: fixed !important; +} +.hr--dotted { + border-style: dashed; + border-left: 0; + border-right: 0; +} +.hr--heavy { + border-bottom-width: 2px; + border-top-width: 3px; + border-style: outset; +} +.border-dotted { + border-style: dotted !important; +} +.opacity-50 { + opacity: 0.5 !important; +} +.ve-hidden { + display: none !important; +} +.ve-muted a[href] { + opacity: 0.6; + filter: saturate(0); +} +.dropdown-menu { + background: url("../media/img/parchment.jpg") repeat local; + position: absolute; + top: 100%; + left: 0; + margin: 2px 0 0; + padding: 5px 0; + display: none; + float: left; + min-width: 160px; + font-size: 14px; + text-align: left; + list-style: none; + background-clip: padding-box; + border: 1px solid #aaa; + border-radius: 4px; + box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); +} +.form-control { + width: 100%; +} +@media only screen and (min-width: 1201px) { + .mobile-ish__visible { + display: none !important; + } +} +@media only screen and (max-width: 1200px) { + .mobile-ish__hidden { + display: none !important; + } + .mobile-ish__ve-flex-col { + display: flex !important; + flex-direction: column !important; + } + .mobile-ish__ve-flex-ai-start { + align-items: flex-start !important; + } + .mobile-ish__w-100 { + width: 100% !important; + } + .mobile-ish__mr-0 { + margin-right: 0 !important; + } + .mobile-ish__mb-2 { + margin-bottom: 0.5rem !important; + } +} +@media only screen and (min-width: 769px) { + .mobile__visible { + display: none !important; + } +} +@media only screen and (max-width: 768px) { + .mobile__hidden { + display: none !important; + } + .mobile__text-center { + text-align: center !important; + } + .mobile__text-clip-ellipsis { + white-space: nowrap !important; + text-overflow: ellipsis !important; + overflow: hidden !important; + } + .mobile__ve-flex-col { + display: flex !important; + flex-direction: column !important; + } + .mobile__ve-flex-row { + display: flex !important; + flex-direction: row !important; + } + .mobile__ve-flex-col-reverse { + display: flex !important; + flex-direction: column-reverse !important; + } + .mobile__ve-flex-ai-start { + align-items: flex-start !important; + } + .mobile__w-100 { + width: 100% !important; + } + .mobile__max-w-100 { + max-width: 100% !important; + } + .mobile__h-initial { + height: initial !important; + } + .mobile__m-auto { + margin: auto !important; + } + .mobile__m-0 { + margin: 0 !important; + } + .mobile__m-1 { + margin: 0.25rem !important; + } + .mobile__m-2 { + margin: 0.5rem !important; + } + .mobile__m-3 { + margin: 1rem !important; + } + .mobile__m-4 { + margin: 1.5rem !important; + } + .mobile__m-5 { + margin: 3rem !important; + } + .mobile__mt-auto { + margin-top: auto !important; + } + .mobile__mt-0 { + margin-top: 0 !important; + } + .mobile__mt-1 { + margin-top: 0.25rem !important; + } + .mobile__mt-2 { + margin-top: 0.5rem !important; + } + .mobile__mt-3 { + margin-top: 1rem !important; + } + .mobile__mt-4 { + margin-top: 1.5rem !important; + } + .mobile__mt-5 { + margin-top: 3rem !important; + } + .mobile__mr-auto { + margin-right: auto !important; + } + .mobile__mr-0 { + margin-right: 0 !important; + } + .mobile__mr-1 { + margin-right: 0.25rem !important; + } + .mobile__mr-2 { + margin-right: 0.5rem !important; + } + .mobile__mr-3 { + margin-right: 1rem !important; + } + .mobile__mr-4 { + margin-right: 1.5rem !important; + } + .mobile__mr-5 { + margin-right: 3rem !important; + } + .mobile__mb-auto { + margin-bottom: auto !important; + } + .mobile__mb-0 { + margin-bottom: 0 !important; + } + .mobile__mb-1 { + margin-bottom: 0.25rem !important; + } + .mobile__mb-2 { + margin-bottom: 0.5rem !important; + } + .mobile__mb-3 { + margin-bottom: 1rem !important; + } + .mobile__mb-4 { + margin-bottom: 1.5rem !important; + } + .mobile__mb-5 { + margin-bottom: 3rem !important; + } + .mobile__ml-auto { + margin-left: auto !important; + } + .mobile__ml-0 { + margin-left: 0 !important; + } + .mobile__ml-1 { + margin-left: 0.25rem !important; + } + .mobile__ml-2 { + margin-left: 0.5rem !important; + } + .mobile__ml-3 { + margin-left: 1rem !important; + } + .mobile__ml-4 { + margin-left: 1.5rem !important; + } + .mobile__ml-5 { + margin-left: 3rem !important; + } + .mobile__my-auto { + margin-top: auto !important; + margin-bottom: auto !important; + } + .mobile__my-0 { + margin-top: 0 !important; + margin-bottom: 0 !important; + } + .mobile__my-1 { + margin-top: 0.25rem !important; + margin-bottom: 0.25rem !important; + } + .mobile__my-2 { + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; + } + .mobile__my-3 { + margin-top: 1rem !important; + margin-bottom: 1rem !important; + } + .mobile__my-4 { + margin-top: 1.5rem !important; + margin-bottom: 1.5rem !important; + } + .mobile__my-5 { + margin-top: 3rem !important; + margin-bottom: 3rem !important; + } + .mobile__mx-auto { + margin-right: auto !important; + margin-left: auto !important; + } + .mobile__mx-0 { + margin-right: 0 !important; + margin-left: 0 !important; + } + .mobile__mx-1 { + margin-right: 0.25rem !important; + margin-left: 0.25rem !important; + } + .mobile__mx-2 { + margin-right: 0.5rem !important; + margin-left: 0.5rem !important; + } + .mobile__mx-3 { + margin-right: 1rem !important; + margin-left: 1rem !important; + } + .mobile__mx-4 { + margin-right: 1.5rem !important; + margin-left: 1.5rem !important; + } + .mobile__mx-5 { + margin-right: 3rem !important; + margin-left: 3rem !important; + } + .mobile__p-0 { + padding: 0 !important; + } + .mobile__p-1 { + padding: 0.25rem !important; + } + .mobile__p-2 { + padding: 0.5rem !important; + } + .mobile__p-3 { + padding: 1rem !important; + } + .mobile__p-4 { + padding: 1.5rem !important; + } + .mobile__p-5 { + padding: 3rem !important; + } + .mobile__p-1p { + padding: 1px !important; + } + .mobile__pt-0 { + padding-top: 0 !important; + } + .mobile__pt-1 { + padding-top: 0.25rem !important; + } + .mobile__pt-2 { + padding-top: 0.5rem !important; + } + .mobile__pt-3 { + padding-top: 1rem !important; + } + .mobile__pt-4 { + padding-top: 1.5rem !important; + } + .mobile__pt-5 { + padding-top: 3rem !important; + } + .mobile__pt-1p { + padding-top: 1px !important; + } + .mobile__pr-0 { + padding-right: 0 !important; + } + .mobile__pr-1 { + padding-right: 0.25rem !important; + } + .mobile__pr-2 { + padding-right: 0.5rem !important; + } + .mobile__pr-3 { + padding-right: 1rem !important; + } + .mobile__pr-4 { + padding-right: 1.5rem !important; + } + .mobile__pr-5 { + padding-right: 3rem !important; + } + .mobile__pr-1p { + padding-right: 1px !important; + } + .mobile__pb-0 { + padding-bottom: 0 !important; + } + .mobile__pb-1 { + padding-bottom: 0.25rem !important; + } + .mobile__pb-2 { + padding-bottom: 0.5rem !important; + } + .mobile__pb-3 { + padding-bottom: 1rem !important; + } + .mobile__pb-4 { + padding-bottom: 1.5rem !important; + } + .mobile__pb-5 { + padding-bottom: 3rem !important; + } + .mobile__pb-1p { + padding-bottom: 1px !important; + } + .mobile__pl-0 { + padding-left: 0 !important; + } + .mobile__pl-1 { + padding-left: 0.25rem !important; + } + .mobile__pl-2 { + padding-left: 0.5rem !important; + } + .mobile__pl-3 { + padding-left: 1rem !important; + } + .mobile__pl-4 { + padding-left: 1.5rem !important; + } + .mobile__pl-5 { + padding-left: 3rem !important; + } + .mobile__pl-1p { + padding-left: 1px !important; + } + .mobile__py-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; + } + .mobile__py-1 { + padding-top: 0.25rem !important; + padding-bottom: 0.25rem !important; + } + .mobile__py-2 { + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; + } + .mobile__py-3 { + padding-top: 1rem !important; + padding-bottom: 1rem !important; + } + .mobile__py-4 { + padding-top: 1.5rem !important; + padding-bottom: 1.5rem !important; + } + .mobile__py-5 { + padding-top: 3rem !important; + padding-bottom: 3rem !important; + } + .mobile__px-0 { + padding-right: 0 !important; + padding-left: 0 !important; + } + .mobile__px-1 { + padding-right: 0.25rem !important; + padding-left: 0.25rem !important; + } + .mobile__px-2 { + padding-right: 0.5rem !important; + padding-left: 0.5rem !important; + } + .mobile__px-3 { + padding-right: 1rem !important; + padding-left: 1rem !important; + } + .mobile__px-4 { + padding-right: 1.5rem !important; + padding-left: 1.5rem !important; + } + .mobile__px-5 { + padding-right: 3rem !important; + padding-left: 3rem !important; + } + .mobile__py-1p { + padding-top: 1px !important; + padding-bottom: 1px !important; + } +} +.ve-popwindow .ve-popwindow__hidden { + display: none !important; +} +@font-face { + font-family: "Glyphicons Halflings"; + src: url("../fonts/glyphicons-halflings-regular.eot"); + src: url("../fonts/glyphicons-halflings-regular.woff2") format("woff2"), + url("../fonts/glyphicons-halflings-regular.woff") format("woff"), + url("../fonts/glyphicons-halflings-regular.ttf") format("truetype"), + url("../fonts/glyphicons-halflings-regular.svg") format("svg"); +} +.glyphicon { + position: relative; + top: 1px; + display: inline-block; + font-family: "Glyphicons Halflings"; + font-style: normal; + font-weight: 400; + line-height: 1; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} +.glyphicon-asterisk::before { + content: "*"; +} +.glyphicon-plus::before { + content: "+"; +} +.glyphicon-eur::before, +.glyphicon-euro::before { + content: "โ‚ฌ"; +} +.glyphicon-minus::before { + content: "โˆ’"; +} +.glyphicon-cloud::before { + content: "โ˜"; +} +.glyphicon-envelope::before { + content: "โœ‰"; +} +.glyphicon-pencil::before { + content: "โœ"; +} +.glyphicon-glass::before { + content: "๎€"; +} +.glyphicon-music::before { + content: "๎€‚"; +} +.glyphicon-search::before { + content: "๎€ƒ"; +} +.glyphicon-heart::before { + content: "๎€…"; +} +.glyphicon-star::before { + content: "๎€†"; +} +.glyphicon-star-empty::before { + content: "๎€‡"; +} +.glyphicon-user::before { + content: "๎€ˆ"; +} +.glyphicon-film::before { + content: "๎€‰"; +} +.glyphicon-th-large::before { + content: "๎€"; +} +.glyphicon-th::before { + content: "๎€‘"; +} +.glyphicon-th-list::before { + content: "๎€’"; +} +.glyphicon-ok::before { + content: "๎€“"; +} +.glyphicon-remove::before { + content: "๎€”"; +} +.glyphicon-zoom-in::before { + content: "๎€•"; +} +.glyphicon-zoom-out::before { + content: "๎€–"; +} +.glyphicon-off::before { + content: "๎€—"; +} +.glyphicon-signal::before { + content: "๎€˜"; +} +.glyphicon-cog::before { + content: "๎€™"; +} +.glyphicon-trash::before { + content: "๎€ "; +} +.glyphicon-home::before { + content: "๎€ก"; +} +.glyphicon-file::before { + content: "๎€ข"; +} +.glyphicon-time::before { + content: "๎€ฃ"; +} +.glyphicon-road::before { + content: "๎€ค"; +} +.glyphicon-download-alt::before { + content: "๎€ฅ"; +} +.glyphicon-download::before { + content: "๎€ฆ"; +} +.glyphicon-upload::before { + content: "๎€ง"; +} +.glyphicon-inbox::before { + content: "๎€จ"; +} +.glyphicon-play-circle::before { + content: "๎€ฉ"; +} +.glyphicon-repeat::before { + content: "๎€ฐ"; +} +.glyphicon-refresh::before { + content: "๎€ฑ"; +} +.glyphicon-list-alt::before { + content: "๎€ฒ"; +} +.glyphicon-lock::before { + content: "๎€ณ"; +} +.glyphicon-flag::before { + content: "๎€ด"; +} +.glyphicon-headphones::before { + content: "๎€ต"; +} +.glyphicon-volume-off::before { + content: "๎€ถ"; +} +.glyphicon-volume-down::before { + content: "๎€ท"; +} +.glyphicon-volume-up::before { + content: "๎€ธ"; +} +.glyphicon-qrcode::before { + content: "๎€น"; +} +.glyphicon-barcode::before { + content: "๎€"; +} +.glyphicon-tag::before { + content: "๎"; +} +.glyphicon-tags::before { + content: "๎‚"; +} +.glyphicon-book::before { + content: "๎ƒ"; +} +.glyphicon-bookmark::before { + content: "๎„"; +} +.glyphicon-print::before { + content: "๎…"; +} +.glyphicon-camera::before { + content: "๎†"; +} +.glyphicon-font::before { + content: "๎‡"; +} +.glyphicon-bold::before { + content: "๎ˆ"; +} +.glyphicon-italic::before { + content: "๎‰"; +} +.glyphicon-text-height::before { + content: "๎"; +} +.glyphicon-text-width::before { + content: "๎‘"; +} +.glyphicon-align-left::before { + content: "๎’"; +} +.glyphicon-align-center::before { + content: "๎“"; +} +.glyphicon-align-right::before { + content: "๎”"; +} +.glyphicon-align-justify::before { + content: "๎•"; +} +.glyphicon-list::before { + content: "๎–"; +} +.glyphicon-indent-left::before { + content: "๎—"; +} +.glyphicon-indent-right::before { + content: "๎˜"; +} +.glyphicon-facetime-video::before { + content: "๎™"; +} +.glyphicon-picture::before { + content: "๎ "; +} +.glyphicon-map-marker::before { + content: "๎ข"; +} +.glyphicon-adjust::before { + content: "๎ฃ"; +} +.glyphicon-tint::before { + content: "๎ค"; +} +.glyphicon-edit::before { + content: "๎ฅ"; +} +.glyphicon-share::before { + content: "๎ฆ"; +} +.glyphicon-check::before { + content: "๎ง"; +} +.glyphicon-move::before { + content: "๎จ"; +} +.glyphicon-step-backward::before { + content: "๎ฉ"; +} +.glyphicon-fast-backward::before { + content: "๎ฐ"; +} +.glyphicon-backward::before { + content: "๎ฑ"; +} +.glyphicon-play::before { + content: "๎ฒ"; +} +.glyphicon-pause::before { + content: "๎ณ"; +} +.glyphicon-stop::before { + content: "๎ด"; +} +.glyphicon-forward::before { + content: "๎ต"; +} +.glyphicon-fast-forward::before { + content: "๎ถ"; +} +.glyphicon-step-forward::before { + content: "๎ท"; +} +.glyphicon-eject::before { + content: "๎ธ"; +} +.glyphicon-chevron-left::before { + content: "๎น"; +} +.glyphicon-chevron-right::before { + content: "๎‚€"; +} +.glyphicon-plus-sign::before { + content: "๎‚"; +} +.glyphicon-minus-sign::before { + content: "๎‚‚"; +} +.glyphicon-remove-sign::before { + content: "๎‚ƒ"; +} +.glyphicon-ok-sign::before { + content: "๎‚„"; +} +.glyphicon-question-sign::before { + content: "๎‚…"; +} +.glyphicon-info-sign::before { + content: "๎‚†"; +} +.glyphicon-screenshot::before { + content: "๎‚‡"; +} +.glyphicon-remove-circle::before { + content: "๎‚ˆ"; +} +.glyphicon-ok-circle::before { + content: "๎‚‰"; +} +.glyphicon-ban-circle::before { + content: "๎‚"; +} +.glyphicon-arrow-left::before { + content: "๎‚‘"; +} +.glyphicon-arrow-right::before { + content: "๎‚’"; +} +.glyphicon-arrow-up::before { + content: "๎‚“"; +} +.glyphicon-arrow-down::before { + content: "๎‚”"; +} +.glyphicon-share-alt::before { + content: "๎‚•"; +} +.glyphicon-resize-full::before { + content: "๎‚–"; +} +.glyphicon-resize-small::before { + content: "๎‚—"; +} +.glyphicon-exclamation-sign::before { + content: "๎„"; +} +.glyphicon-gift::before { + content: "๎„‚"; +} +.glyphicon-leaf::before { + content: "๎„ƒ"; +} +.glyphicon-fire::before { + content: "๎„„"; +} +.glyphicon-eye-open::before { + content: "๎„…"; +} +.glyphicon-eye-close::before { + content: "๎„†"; +} +.glyphicon-warning-sign::before { + content: "๎„‡"; +} +.glyphicon-plane::before { + content: "๎„ˆ"; +} +.glyphicon-calendar::before { + content: "๎„‰"; +} +.glyphicon-random::before { + content: "๎„"; +} +.glyphicon-comment::before { + content: "๎„‘"; +} +.glyphicon-magnet::before { + content: "๎„’"; +} +.glyphicon-chevron-up::before { + content: "๎„“"; +} +.glyphicon-chevron-down::before { + content: "๎„”"; +} +.glyphicon-retweet::before { + content: "๎„•"; +} +.glyphicon-shopping-cart::before { + content: "๎„–"; +} +.glyphicon-folder-close::before { + content: "๎„—"; +} +.glyphicon-folder-open::before { + content: "๎„˜"; +} +.glyphicon-resize-vertical::before { + content: "๎„™"; +} +.glyphicon-resize-horizontal::before { + content: "๎„ "; +} +.glyphicon-hdd::before { + content: "๎„ก"; +} +.glyphicon-bullhorn::before { + content: "๎„ข"; +} +.glyphicon-bell::before { + content: "๎„ฃ"; +} +.glyphicon-certificate::before { + content: "๎„ค"; +} +.glyphicon-thumbs-up::before { + content: "๎„ฅ"; +} +.glyphicon-thumbs-down::before { + content: "๎„ฆ"; +} +.glyphicon-hand-right::before { + content: "๎„ง"; +} +.glyphicon-hand-left::before { + content: "๎„จ"; +} +.glyphicon-hand-up::before { + content: "๎„ฉ"; +} +.glyphicon-hand-down::before { + content: "๎„ฐ"; +} +.glyphicon-circle-arrow-right::before { + content: "๎„ฑ"; +} +.glyphicon-circle-arrow-left::before { + content: "๎„ฒ"; +} +.glyphicon-circle-arrow-up::before { + content: "๎„ณ"; +} +.glyphicon-circle-arrow-down::before { + content: "๎„ด"; +} +.glyphicon-globe::before { + content: "๎„ต"; +} +.glyphicon-wrench::before { + content: "๎„ถ"; +} +.glyphicon-tasks::before { + content: "๎„ท"; +} +.glyphicon-filter::before { + content: "๎„ธ"; +} +.glyphicon-briefcase::before { + content: "๎„น"; +} +.glyphicon-fullscreen::before { + content: "๎…€"; +} +.glyphicon-dashboard::before { + content: "๎…"; +} +.glyphicon-paperclip::before { + content: "๎…‚"; +} +.glyphicon-heart-empty::before { + content: "๎…ƒ"; +} +.glyphicon-link::before { + content: "๎…„"; +} +.glyphicon-phone::before { + content: "๎……"; +} +.glyphicon-pushpin::before { + content: "๎…†"; +} +.glyphicon-usd::before { + content: "๎…ˆ"; +} +.glyphicon-gbp::before { + content: "๎…‰"; +} +.glyphicon-sort::before { + content: "๎…"; +} +.glyphicon-sort-by-alphabet::before { + content: "๎…‘"; +} +.glyphicon-sort-by-alphabet-alt::before { + content: "๎…’"; +} +.glyphicon-sort-by-order::before { + content: "๎…“"; +} +.glyphicon-sort-by-order-alt::before { + content: "๎…”"; +} +.glyphicon-sort-by-attributes::before { + content: "๎…•"; +} +.glyphicon-sort-by-attributes-alt::before { + content: "๎…–"; +} +.glyphicon-unchecked::before { + content: "๎…—"; +} +.glyphicon-expand::before { + content: "๎…˜"; +} +.glyphicon-collapse-down::before { + content: "๎…™"; +} +.glyphicon-collapse-up::before { + content: "๎… "; +} +.glyphicon-log-in::before { + content: "๎…ก"; +} +.glyphicon-flash::before { + content: "๎…ข"; +} +.glyphicon-log-out::before { + content: "๎…ฃ"; +} +.glyphicon-new-window::before { + content: "๎…ค"; +} +.glyphicon-record::before { + content: "๎…ฅ"; +} +.glyphicon-save::before { + content: "๎…ฆ"; +} +.glyphicon-open::before { + content: "๎…ง"; +} +.glyphicon-saved::before { + content: "๎…จ"; +} +.glyphicon-import::before { + content: "๎…ฉ"; +} +.glyphicon-export::before { + content: "๎…ฐ"; +} +.glyphicon-send::before { + content: "๎…ฑ"; +} +.glyphicon-floppy-disk::before { + content: "๎…ฒ"; +} +.glyphicon-floppy-saved::before { + content: "๎…ณ"; +} +.glyphicon-floppy-remove::before { + content: "๎…ด"; +} +.glyphicon-floppy-save::before { + content: "๎…ต"; +} +.glyphicon-floppy-open::before { + content: "๎…ถ"; +} +.glyphicon-credit-card::before { + content: "๎…ท"; +} +.glyphicon-transfer::before { + content: "๎…ธ"; +} +.glyphicon-cutlery::before { + content: "๎…น"; +} +.glyphicon-header::before { + content: "๎†€"; +} +.glyphicon-compressed::before { + content: "๎†"; +} +.glyphicon-earphone::before { + content: "๎†‚"; +} +.glyphicon-phone-alt::before { + content: "๎†ƒ"; +} +.glyphicon-tower::before { + content: "๎†„"; +} +.glyphicon-stats::before { + content: "๎†…"; +} +.glyphicon-sd-video::before { + content: "๎††"; +} +.glyphicon-hd-video::before { + content: "๎†‡"; +} +.glyphicon-subtitles::before { + content: "๎†ˆ"; +} +.glyphicon-sound-stereo::before { + content: "๎†‰"; +} +.glyphicon-sound-dolby::before { + content: "๎†"; +} +.glyphicon-sound-5-1::before { + content: "๎†‘"; +} +.glyphicon-sound-6-1::before { + content: "๎†’"; +} +.glyphicon-sound-7-1::before { + content: "๎†“"; +} +.glyphicon-copyright-mark::before { + content: "๎†”"; +} +.glyphicon-registration-mark::before { + content: "๎†•"; +} +.glyphicon-cloud-download::before { + content: "๎†—"; +} +.glyphicon-cloud-upload::before { + content: "๎†˜"; +} +.glyphicon-tree-conifer::before { + content: "๎†™"; +} +.glyphicon-tree-deciduous::before { + content: "๎ˆ€"; +} +.glyphicon-cd::before { + content: "๎ˆ"; +} +.glyphicon-save-file::before { + content: "๎ˆ‚"; +} +.glyphicon-open-file::before { + content: "๎ˆƒ"; +} +.glyphicon-level-up::before { + content: "๎ˆ„"; +} +.glyphicon-copy::before { + content: "๎ˆ…"; +} +.glyphicon-paste::before { + content: "๎ˆ†"; +} +.glyphicon-alert::before { + content: "๎ˆ‰"; +} +.glyphicon-equalizer::before { + content: "๎ˆ"; +} +.glyphicon-king::before { + content: "๎ˆ‘"; +} +.glyphicon-queen::before { + content: "๎ˆ’"; +} +.glyphicon-pawn::before { + content: "๎ˆ“"; +} +.glyphicon-bishop::before { + content: "๎ˆ”"; +} +.glyphicon-knight::before { + content: "๎ˆ•"; +} +.glyphicon-baby-formula::before { + content: "๎ˆ–"; +} +.glyphicon-tent::before { + content: "โ›บ"; +} +.glyphicon-blackboard::before { + content: "๎ˆ˜"; +} +.glyphicon-bed::before { + content: "๎ˆ™"; +} +.glyphicon-apple::before { + content: "๏ฃฟ"; +} +.glyphicon-erase::before { + content: "๎ˆก"; +} +.glyphicon-hourglass::before { + content: "โŒ›"; +} +.glyphicon-lamp::before { + content: "๎ˆฃ"; +} +.glyphicon-duplicate::before { + content: "๎ˆค"; +} +.glyphicon-piggy-bank::before { + content: "๎ˆฅ"; +} +.glyphicon-scissors::before { + content: "๎ˆฆ"; +} +.glyphicon-bitcoin::before { + content: "๎ˆง"; +} +.glyphicon-btc::before { + content: "๎ˆง"; +} +.glyphicon-xbt::before { + content: "๎ˆง"; +} +.glyphicon-yen::before { + content: "ยฅ"; +} +.glyphicon-jpy::before { + content: "ยฅ"; +} +.glyphicon-ruble::before { + content: "โ‚ฝ"; +} +.glyphicon-rub::before { + content: "โ‚ฝ"; +} +.glyphicon-scale::before { + content: "๎ˆฐ"; +} +.glyphicon-ice-lolly::before { + content: "๎ˆฑ"; +} +.glyphicon-ice-lolly-tasted::before { + content: "๎ˆฒ"; +} +.glyphicon-education::before { + content: "๎ˆณ"; +} +.glyphicon-option-horizontal::before { + content: "๎ˆด"; +} +.glyphicon-option-vertical::before { + content: "๎ˆต"; +} +.glyphicon-menu-hamburger::before { + content: "๎ˆถ"; +} +.glyphicon-modal-window::before { + content: "๎ˆท"; +} +.glyphicon-oil::before { + content: "๎ˆธ"; +} +.glyphicon-grain::before { + content: "๎ˆน"; +} +.glyphicon-sunglasses::before { + content: "๎‰€"; +} +.glyphicon-text-size::before { + content: "๎‰"; +} +.glyphicon-text-color::before { + content: "๎‰‚"; +} +.glyphicon-text-background::before { + content: "๎‰ƒ"; +} +.glyphicon-object-align-top::before { + content: "๎‰„"; +} +.glyphicon-object-align-bottom::before { + content: "๎‰…"; +} +.glyphicon-object-align-horizontal::before { + content: "๎‰†"; +} +.glyphicon-object-align-left::before { + content: "๎‰‡"; +} +.glyphicon-object-align-vertical::before { + content: "๎‰ˆ"; +} +.glyphicon-object-align-right::before { + content: "๎‰‰"; +} +.glyphicon-triangle-right::before { + content: "๎‰"; +} +.glyphicon-triangle-left::before { + content: "๎‰‘"; +} +.glyphicon-triangle-bottom::before { + content: "๎‰’"; +} +.glyphicon-triangle-top::before { + content: "๎‰“"; +} +.glyphicon-console::before { + content: "๎‰”"; +} +.glyphicon-superscript::before { + content: "๎‰•"; +} +.glyphicon-subscript::before { + content: "๎‰–"; +} +.glyphicon-menu-left::before { + content: "๎‰—"; +} +.glyphicon-menu-right::before { + content: "๎‰˜"; +} +.glyphicon-menu-down::before { + content: "๎‰™"; +} +.glyphicon-menu-up::before { + content: "๎‰ "; +} +@media (max-width: 991px) { + .dropdown-menu-filter { + max-height: 525px; + } +} +.fltr__btn-close { + min-width: 100px; +} +.fltr__minimal-hide { + display: none; +} +.fltr__no-items { + display: none !important; +} +.fltr__h { + display: flex; + justify-content: space-between; + font-size: 15px; + align-items: center; +} +@media only screen and (max-width: 768px) { + .fltr__h { + flex-direction: column; + } + .fltr__h--multi { + flex-direction: initial; + } +} +@media only screen and (max-width: 768px) { + .fltr__h-text { + align-self: flex-start; + } +} +@media only screen and (max-width: 768px) { + .fltr__h-wrp-btns-outer { + width: 100%; + flex-direction: column; + align-items: initial !important; + } + .fltr__h-wrp-btns-outer > * { + width: 100%; + margin: 0.25rem !important; + } +} +@media only screen and (max-width: 768px) { + .fltr__h-wrp-state-btns-outer { + flex-direction: column; + } + .fltr__h-wrp-state-btns-outer > * { + width: 100%; + } +} +.fltr__h-btn-mobile-settings { + min-width: 30px; +} +.fltr__h-btn-logic { + min-width: 46px; + font-weight: bold; +} +.fltr__h-btn-logic.btn-xxs { + min-width: 34px; +} +.fltr__h-btn-logic--blue { + color: #337ab7; +} +.fltr__h-btn-logic--blue:hover { + color: #2a6496; +} +.fltr__h-btn-logic--red { + color: #8a1a1b; +} +.fltr__h-btn-logic--red:hover { + color: #711617; +} +.fltr__h-btn--all, +.fltr__h-btn--all:focus, +.fltr__h-btn--all:hover { + text-decoration: underline; + text-decoration-color: #337ab7; +} +.fltr__h-btn--clear, +.fltr__h-btn--clear:focus, +.fltr__h-btn--clear:hover { + text-decoration: underline; + text-decoration-color: #c3c3c3; +} +.fltr__h-btn--none, +.fltr__h-btn--none:focus, +.fltr__h-btn--none:hover { + text-decoration: underline; + text-decoration-color: #8a1a1b; +} +.fltr__summary_item { + cursor: help; + margin: 0 3px; + font-weight: bold; + font-size: 12px; + line-height: 12px; +} +.fltr__summary_nest { + display: flex; + padding: 2px 0; + font-size: 12px; + align-items: center; +} +.fltr__summary_item--include { + color: #337ab7; + text-shadow: 0 0 1px #337ab7; +} +.fltr__summary_item--exclude { + color: #8a1a1b; + text-shadow: 0 0 1px #8a1a1b; +} +.fltr__summary_item_spacer { + margin: 0 3px; + padding-left: 1px; + cursor: default; + background: #aaa8; + min-height: 12px; +} +.fltr__btn_nest { + margin: 2px; + padding: 2px 6px; + white-space: nowrap; + text-align: center; + font-size: 10.5px; + cursor: pointer; + user-select: none; + background: #f0f0f0; + border: 1px solid #aaa; +} +.fltr__btn_nest:hover { + background-color: #e6e6e6; +} +.fltr__btn_nest--include { + background: repeating-linear-gradient( + 135deg, + #337ab7, + #337ab7 11px, + transparent 11px, + transparent 22px + ); +} +.fltr__btn_nest--include:hover { + background: repeating-linear-gradient( + 135deg, + #2d6da3, + #2d6da3 11px, + transparent 11px, + transparent 22px + ); +} +.fltr__btn_nest--include span { + background: #fff; + padding: 1px 0; +} +.fltr__btn_nest--include-all { + background: #337ab7; + color: #fff; +} +.fltr__btn_nest--include-all:hover { + background: #2d6da3; +} +.fltr__btn_nest--exclude { + background: repeating-linear-gradient( + 135deg, + transparent, + transparent 11px, + #8a1a1b 11px, + #8a1a1b 22px + ); +} +.fltr__btn_nest--exclude:hover { + background: repeating-linear-gradient( + 135deg, + transparent, + transparent 11px, + #751617 11px, + #751617 22px + ); +} +.fltr__btn_nest--exclude span { + background: #fff; + padding: 1px 0; +} +.fltr__btn_nest--exclude-all { + background: #8a1a1b; + color: #fff; +} +.fltr__btn_nest--exclude-all:hover { + background: #751617; +} +.fltr__btn_nest--both { + background: repeating-linear-gradient( + 135deg, + #337ab7, + #337ab7 11px, + #8a1a1b 11px, + #8a1a1b 22px + ); + color: #fff; +} +.fltr__btn_nest--both:hover { + background: repeating-linear-gradient( + 135deg, + #2d6da3, + #2d6da3 11px, + #751617 11px, + #751617 22px + ); +} +.fltr__container-pills { + margin-right: -2px; + margin-left: -2px; +} +.fltr__dropdown-divider { + border-bottom: #aaa 1px dotted; + width: 100%; +} +@media only screen and (max-width: 768px) { + .fltr__dropdown-divider { + box-shadow: inset 0 0 2px 2px #eee; + height: 7px; + flex-shrink: 0; + border: 0; + background: #aaa; + margin-top: 0.5rem; + margin-bottom: 0.75rem !important; + } +} +.fltr__dropdown-divider--indented { + opacity: 0.4; + width: calc(100% - 80px); + margin: 0 auto; +} +.fltr__dropdown-divider--sub { + border-style: dashed; + width: calc(100% - 2rem); + border-color: #aaa8; +} +.fltr__pill { + margin: 2px; + padding: 2px 6px; + background: #f0f0f0; + white-space: nowrap; + text-align: center; + font-size: 10.5px; + cursor: pointer; + user-select: none; + border: 1px solid #aaa; + float: left; +} +.fltr__pill:hover { + background-color: #e6e6e6; +} +.fltr__pill[state="yes"] { + background: #337ab7; + color: #fff; + border-color: #22527b; +} +.fltr__pill[state="yes"]:hover { + background: #2d6da3; +} +.fltr__pill[state="no"] { + background: #8a1a1b; + color: #fff; + border-color: #4a0e0e; +} +.fltr__pill[state="no"]:hover { + background: #751617; +} +.fltr__pill--ability-bonus { + min-width: 26px; + border-right-width: 0; + margin: 0; + flex: 1; +} +.fltr__pill--ability-bonus:last-of-type { + border-right-width: 1px; +} +.fltr__pill--muted { + background-color: #dedede; + color: #a4a4a4; +} +.fltr__pill--muted[state="yes"], +.fltr__pill--muted[state="no"] { + color: #fff; +} +.fltr__wrp-pills, +.fltr__wrp-pills--sub { + flex-wrap: wrap; + margin-bottom: 7px; +} +.fltr__wrp-pills { + display: block; +} +.fltr__wrp-pills::after { + content: ""; + clear: both; + display: block; +} +.fltr__wrp-pills--flex, +.fltr__wrp-pills--sub { + display: flex; +} +.fltr__wrp-subs { + display: block; +} +.fltr__mini-view { + border-left: #aaa 1px solid; + border-right: #aaa 1px solid; + background: linear-gradient(to top, #aaa, #c9c6b2 1px); + display: flex; + flex-wrap: wrap; + flex-shrink: 0; +} +.fltr__mini-view--no-sort-buttons { + border-bottom: 1px solid #aaa; + background: #c9c6b2; + border-bottom-left-radius: 3px; + border-bottom-right-radius: 3px; + min-height: 3px; +} +.fltr__mini-pill { + margin: 1px 2px; + padding: 1px 2px; + white-space: nowrap; + text-align: center; + font-size: 9.4px; + border-radius: 3px; + cursor: pointer; + user-select: none; + display: none; +} +.fltr__mini-pill:hover { + text-decoration: red line-through; +} +.fltr__mini-pill[state="yes"] { + background: #337ab7; + color: #fff; + display: block; +} +.fltr__mini-pill--default-sel[state="yes"] { + background: #48637a; +} +.fltr__mini-pill[state="no"] { + background: #782e22; + color: #fff; + display: block; +} +.fltr__mini-pill--default-desel[state="no"] { + background: #7a564f; +} +.fltr__h-summary { + position: relative; + display: inline-block; + vertical-align: middle; + box-sizing: border-box; + font-size: 11px; + line-height: 22px; + margin-left: auto; +} +.fltr__h-summary-filtering { + color: #333; + text-shadow: 0 0 1px #333; +} +.fltr__h-btn-toggle-display { + min-width: 43px; +} +.fltr__slider { + width: 100%; +} +.fltr__range-inline-label { + margin-left: 15px; + flex-shrink: 0; + min-width: 75px; + text-align: right; + font-style: italic; +} +.fltr__group-comb-toggle { + font-style: italic; + cursor: pointer; + letter-spacing: -1px; + user-select: none; +} +.fltr__label-ability-score { + width: 80px; +} +.fltr__hidden--inactive { + display: none !important; +} +.fltr__hidden--search { + display: none !important; +} +.fltr-search__wrp-search:focus .fltr-search__wrp-values, +.fltr-search__wrp-search:focus-within .fltr-search__wrp-values, +.fltr-search__wrp-search:focus-visible .fltr-search__wrp-values, +.fltr-search__wrp-search:active .fltr-search__wrp-values { + display: flex; +} +.fltr-search__wrp-row:focus, +.fltr-search__wrp-row:hover { + background-color: #c9c6b2; +} +.fltr-search__wrp-values { + max-height: 200px; + background: #dedcd0; + border: 1px solid #aaa; + z-index: 1; + top: 22px; + right: 0; + left: 0; + display: none; + flex-direction: column; +} +.fltr-search__disp-name { + font-size: 10.5px; +} +.fltr-search__btn-activate { + width: 16px; + height: 16px; + border-radius: 3px; +} +.fltr-search__btn-activate--yes { + background: #337ab7; + color: #fff; + border: 1px solid #63a0d4; +} +.fltr-search__btn-activate--yes:hover { + background: #2d6da3; +} +.fltr-search__btn-activate--no { + background: #8a1a1b; + color: #fff; + border: 1px solid #ca2628; +} +.fltr-search__btn-activate--no:hover { + background: #751617; +} +.fltr-src__spc-pill { + color: #929292; +} +.fltr-src__wrp-slider { + background: #f0f0f0; + border-radius: 4px; +} +.fltr-cls__tgl { + width: 16px; + height: 16px; + padding: 0; + flex-shrink: 0; + flex-grow: 0; + display: inline-block; + cursor: pointer; + border: 1px solid #aaa; + border-radius: 4px; + outline: none; + user-select: none; + border-radius: 7px; +} +.fltr-cls__tgl:active { + outline: 0; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(255, 0, 0, 0.6); +} +.fltr-cls__tgl.active { + background: #666; + border-color: #8c8c8c; +} +.fltr-cls__tgl.active.disabled { + background-color: #a6a6a6; +} +.fltr-cls__tgl.disabled { + cursor: default; + box-shadow: none; +} +.fltr__pill[state="yes"] > .fltr-src__spc-pill { + color: #fff9; +} +.fltr__pill[state="no"] > .fltr-src__spc-pill { + color: #fffa; +} +:root { + --sz-font-h0: 1.8em; + --sz-font-h1: 1.5em; + --sz-font-h2: 1.35em; + --h-mb-p: 5px; + --h-mb-p-inline: 0; + --h-mb-quote-line: 5px; + --h-mb-quote-line-last: 5px; + --h-mb-li: 3px; + --w-text-indent-inline-p: 0.7em; + --w-pl-list: 24px; + --w-pl-list-no-bullets: 10px; +} +@keyframes rd__spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} +.rd__b p { + margin-bottom: var(--h-mb-p); +} +.rd__b--0, +.rd__b--1, +.rd__b--2, +.rd__b--3, +.rd__b--4 { + margin-bottom: var(--h-mb-p); +} +.rd__b--0:last-child, +.rd__b--1:last-child, +.rd__b--2:last-child, +.rd__b--3:last-child, +.rd__b--4:last-child { + margin-bottom: 0; +} +.rd__b--0 > *:last-child, +.rd__b--1 > *:last-child, +.rd__b--2 > *:last-child, +.rd__b--3 > *:last-child, +.rd__b--4 > *:last-child { + margin-bottom: 0; +} +.rd__hr { + border-color: #aaa6; + margin: 17px 0 5px; +} +.rd__hr--section { + margin: 30px 0 5px; +} +.rd__list { + margin-top: 0; + margin-bottom: var(--h-mb-p); + padding-left: var(--w-pl-list); +} +.rd__list > .rd__list:last-child { + margin-bottom: 0; +} +.rd__list > .rd__list-name { + margin-left: calc(-1 * var(--w-pl-list)); +} +.rd__list-name { + margin: 0 0 var(--h-mb-li); + font-weight: bold; + list-style-type: none; +} +.rd__li { + margin-bottom: var(--h-mb-p); +} +.rd__compact-stats { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + align-items: flex-start; + align-content: flex-start; +} +.rd__title-link { + opacity: 0.3; + font-size: 12px; + font-weight: normal; +} +.rd__title-link--inset { + font-size: 12px; +} +.rd__wrp-image { + margin: 5px auto 0; + text-align: center; +} +.rd__image { + max-width: 100%; + max-height: 60vh; + cursor: zoom-in; +} +.rd__wrp-map { + max-width: 33%; + margin: 0 auto; +} +.rd__wrp-gallery { + display: flex; + flex-wrap: wrap; + justify-content: center; + align-items: flex-end; +} +.rd__wrp-gallery-image { + padding: 0 10px 10px; + max-width: 33%; +} +.rd__gallery-name { + font-style: italic; + text-decoration: underline; +} +.rd__quote-line { + margin-bottom: var(--h-mb-quote-line); +} +.rd__quote-line--last { + margin-bottom: var(--h-mb-quote-line-last); +} +.rd__quote-by { + width: 100%; + text-align: right; + display: block; +} +.rd__p-list-item { + font-style: initial; +} +.rd__p-cont-indent { + display: block; + text-indent: 1em; +} +.rd__tab-indent { + width: 1em; + display: inline-block; +} +.rd__image-title { + width: 100%; + text-align: center; + font-style: italic; + margin-top: 3px; +} +.rd__image-title-inner { + display: inline-block; + text-decoration: underline; + margin: 2px 0; +} +.rd__image-btn-viewer { + font-style: initial; + white-space: normal; + font-size: inherit; + line-height: 1.7; +} +.rd__image-credit { + font-size: 80%; +} +.rd__scroller-viewer { + scrollbar-width: auto; +} +.rd__scroller-viewer::-webkit-scrollbar { + width: 15px; + height: 15px; +} +.rd__prerequisite { + font-style: italic; + display: block; +} +.rd__li-spell { + margin: 0; +} +.rd__list-hang-notitle { + padding: 0; + list-style: none; +} +.rd__list-hang-notitle > .rd__li { + margin-bottom: var(--h-mb-li); + text-indent: -1.1em; + margin-left: 1.1em; +} +.rd__list-hang-notitle > .rd__li a, +.rd__list-hang-notitle > .rd__li span { + text-indent: initial; +} +.rd__list-hang-notitle > .rd__li > * { + margin: 0 0 var(--h-mb-li); +} +.rd__list-hang-notitle > .rd__li > ul { + text-indent: 0; +} +.rd__list-hang { + list-style: none; +} +.rd__list-hang > .rd__list-name { + margin-left: calc(-1 * var(--w-pl-list)); +} +.rd__list-hang > li > *:not(::marker) { + text-indent: -1.1em; + margin-left: 1.1em; +} +.rd__list-decimal { + list-style: decimal; +} +.rd__list-lower-roman { + list-style: lower-roman; +} +.rd__list-upper-roman { + list-style: upper-roman; +} +.rd__list-no-bullets { + list-style: none; + padding: 0 0 0 var(--w-pl-list-no-bullets); +} +.rd__list-no-bullets > .rd__list-name { + margin-left: calc(-1 * var(--w-pl-list-no-bullets)); +} +.rd__list-italic { + font-style: italic; +} +.rd__quote-pull { + padding: 10px 15px; + text-align: center; + font-size: 125%; +} +.rd__h { + margin: 0; + line-height: inherit; +} +.rd__h--0 { + color: #782e22; + font-family: "Times New Roman", serif; + font-variant: small-caps; + font-weight: 500; + display: flex; + justify-content: space-between; + align-items: center; + font-size: var(--sz-font-h0); +} +.rd__h--1 { + color: #782e22; + font-family: "Times New Roman", serif; + font-variant: small-caps; + font-weight: 500; + display: flex; + justify-content: space-between; + align-items: center; + font-size: var(--sz-font-h1); + border-bottom: 1px solid #782e22; + margin: 0 0 0.2em; +} +.rd__h--2 { + color: #782e22; + font-family: "Times New Roman", serif; + font-variant: small-caps; + font-weight: 500; + display: flex; + justify-content: space-between; + align-items: center; + font-size: var(--sz-font-h2); +} +.rd__h--2-inset { + font-variant: small-caps; + font-weight: bolder; + font-size: 1.1em; + display: flex; + justify-content: space-between; + align-items: center; +} +.rd__h--2-inset-no-name { + justify-content: flex-end; + float: right; +} +.rd__h--2-flow-block { + display: block; + font-variant: small-caps; + font-weight: bolder; + font-size: 1.1em; + text-align: center; +} +.rd__h--2-inset > h4, +.rd__h--2-flow-block > h4 { + font-size: inherit; + font-weight: inherit; + line-height: 1.42857143; + margin: 0; +} +.rd__h--3 { + font-weight: bold; + font-style: italic; +} +.rd__h--4 { + font-style: italic; +} +.rd__h-toggle { + font-family: Arial, sans-serif; + font-size: 12px; + opacity: 0.3; + font-weight: normal; +} +.rd__ele-toggled-hidden { + display: none !important; +} +.rd__b--3 > p, +.rd__b--4 > p { + text-indent: var(--w-text-indent-inline-p); + margin-bottom: var(--h-mb-p-inline); +} +.rd__b--3 > p:first-of-type, +.rd__b--4 > p:first-of-type { + display: inline; +} +.rd__b-inset > p { + text-indent: var(--w-text-indent-inline-p); + margin-bottom: 0; +} +.rd__b-inset > p:first-of-type { + text-indent: 0; +} +.rd__b-inset { + margin: 7px 15px; + padding: 5px 10px; + box-shadow: 0 0 4px 0 #988e7c; + border: 1px solid #656565; + border-top: 2px solid #656565; + border-bottom: 2px solid #656565; + background-color: #e9ecda; +} +.rd__b-inset > *:last-of-type { + margin-bottom: 0; +} +.rd__b-inset--readaloud { + box-shadow: 0 0 4px 0 #988e7c; + border: 1px solid #656565; + border-left: 2px solid #656565; + border-right: 2px solid #656565; + background-color: #eef0f3; +} +.rd__b-inset-inner { + margin-top: 10px; +} +.rd__b-data { + border: 3px solid #b5b3a4; + border-left-width: 1px; + border-right-width: 1px; + margin: 5px; + width: calc(100% - 12px); + table-layout: fixed; +} +.rd__b-data--inset { + box-shadow: 0 0 4px 0 #988e7c; + border: 1px solid #656565; + background-color: rgba(156, 150, 120, 0.1); +} +.rd__li > .rd__b-data { + margin: 0; +} +.rd__data-embed-header { + cursor: pointer; + font-family: "Times New Roman", serif; + font-variant: small-caps; + text-transform: uppercase; + font-weight: bold; +} +.rd__data-embed-header:hover { + background: rgba(100, 100, 100, 0.08); +} +.rd__data-embed-toggle { + font-family: Arial, sans-serif; + float: right; +} +.rd__wrp-loadbrew--ready { + cursor: pointer; + text-decoration: underline; +} +.rd__loadbrew-icon { + text-indent: 0; + margin-left: 2px; + transition-property: transform; + transition-duration: 1s; +} +.rd__loadbrew-icon--active { + animation-name: rd__spin; + animation-duration: 1.2s; + animation-iteration-count: infinite; + animation-timing-function: linear; +} +.rd__table { + width: 100%; + margin-bottom: var(--h-mb-p); +} +.rd__table > caption { + text-align: left; +} +.rd__comic { + font-family: "Blambot Casual", sans-serif; + color: #1942be; +} +.rd__comic--h1 { + font-size: 140%; + font-variant: small-caps; +} +.rd__comic--h2 { + font-size: 130%; +} +.rd__comic--h3 { + font-size: 120%; +} +.rd__comic--h4 { + font-size: 110%; +} +.rd__comic--note { + opacity: 0.7; +} +.rd__comic-img-speaker { + margin-top: -5px; + margin-bottom: -5px; +} +.rd__comic-img-speaker--left { + float: left; + margin-right: 0; + margin-left: -20px; +} +.rd__comic-img-speaker--right { + float: right; + margin-right: -20px; + margin-left: 0; +} +.rd__comic-img-speaker::after { + content: ""; + clear: both; + display: block; +} +.rd__img-small { + max-width: 25vw; + max-height: 25vh; +} +.rd__s-v-flow { + height: 15px; + width: 0; + border-left: 1px solid #656565; + border-right: 1px solid #656565; + margin: 0 auto; +} +.rd__b-flow { + margin: 0 15px; + padding: 5px 10px; + box-shadow: 0 0 4px 0 #988e7c; + border: 1px solid #656565; + border-top: 2px solid #656565; + border-bottom: 2px solid #656565; + background-color: #ece4da; +} +.rd__b-flow > *:last-of-type { + margin-bottom: 0; +} +.rd__stats-name-page { + font-family: "Convergence", Arial, sans-serif; + font-size: 12px; + color: #333; + font-weight: 100; +} +.rd__stats-name-brew-link { + font-size: 13px; + font-weight: initial; +} +.rd__pre-wrap { + white-space: pre-wrap; +} +.rd__highlight { + background-color: #ff0; +} +.rd__color a { + color: inherit !important; +} +.rd-item__type-rarity-attunement { + color: #333; +} +.rd-spell__level-school-ritual { + font-style: italic; + color: #333; +} +.rd-ability-icon { + max-width: 100px; +} +.rd-ability-icon__fill-primary { + fill: #333; +} +.rd-ability-icon__fill-bg { + fill: #dedcd0; +} +.rd-ability-icon__stroke-bg { + stroke: #dedcd0; +} +td > .rd__b:last-child { + margin-bottom: 0; +} +.rd-recipes__wrp-recipe .rd__b--3 > p, +.rd-recipes__wrp-recipe .rd__b--4 > p { + text-indent: 0; +} +.rd-recipes__wrp-instructions .rd__h--3 { + font-style: initial; + font-variant: small-caps; +} +.rd-recipes__wrp-instructions .rd__b--3 > p, +.rd-recipes__wrp-instructions .rd__b--4 > p { + margin-bottom: 10px; +} +.rd-recipes__wrp-instructions .rd__b--3 > p:nth-of-type(2), +.rd-recipes__wrp-instructions .rd__b--4 > p:nth-of-type(2) { + margin-top: 10px; +} +.rd-recipes__wrp-ingredients .rd__h--2 { + font-size: 1em; + font-family: Roboto, Helvetica, sans-serif; + color: inherit; + font-weight: bold; +} +.rd-recipes__wrp-ingredients .rd__b p { + margin-bottom: 0; +} +@keyframes kf-fade-in { + from { + opacity: 0; + } + to { + opacity: 1; + } +} +.hwin { + position: fixed; + width: 600px; + max-width: 92vw; + min-width: 150px; + z-index: 1000000000; + box-shadow: 0 0 12px 0 #000; + animation-name: kf-fade-in; + animation-duration: 150ms; + display: flex; + flex-direction: column; + background: #c9c6b2; +} +.hwin--minified .hoverborder__resize-n, +.hwin--minified .hoverborder__resize-ne, +.hwin--minified .hoverborder__resize-e, +.hwin--minified .hoverborder__resize-se, +.hwin--minified .hoverborder__resize-s, +.hwin--minified .hoverborder__resize-sw, +.hwin--minified .hoverborder__resize-w, +.hwin--minified .hoverborder__resize-nw { + display: none; +} +.hwin--popout { + box-shadow: initial; + width: 100%; + animation-duration: initial; + overflow-y: scroll; + height: 100%; + max-width: initial; + max-height: initial; +} +@media (max-width: 1023px) { + .hwin { + max-width: 95vw; + } +} +.hwin::-webkit-scrollbar-track { + background: #a0a0a0; +} +.hwin::-webkit-scrollbar { + width: 4px; +} +.hwin__wrp-table { + max-height: 92vh; + min-height: 20px; + overflow-y: auto; + background: #c9c6b2; + transform: translateZ(0); +} +.hwin p { + margin-bottom: 5px; +} +.hwin .rnd-name { + font-size: 22.4px; +} +.hwin td div.border { + height: 2px; + background-color: #782e22; + margin: 0 3px; + padding: 0; + border-right: 5px transparent; +} +.hoverborder { + position: relative; + min-height: 3px; + max-height: 16px; + text-align: right; +} +.hoverborder--btm { + cursor: ns-resize; +} +.hoverborder--top { + cursor: move; + user-select: none; + display: flex; + justify-content: space-between; +} +.hoverborder .hwin__top-border-icon { + display: none; +} +.hoverborder[data-perm="true"] .hwin__top-border-icon { + display: block; +} +.hoverborder .window-title { + max-width: calc(100% - 45px); + text-align: left; + margin-left: 4px; + padding: 1px 0; + font-size: 12px; + display: none; + font-family: "Times New Roman", serif; + font-variant: small-caps; + text-transform: uppercase; + font-weight: bold; +} +.hoverborder[data-perm="true"] .window-title { + display: block; +} +.hoverborder__resize-n { + position: absolute; + top: -4px; + right: 4px; + left: 4px; + height: 4px; + cursor: ns-resize; +} +.hoverborder__resize-ne { + position: absolute; + top: -6px; + right: -6px; + height: 10px; + width: 10px; + cursor: ne-resize; +} +.hoverborder__resize-e { + position: absolute; + top: 4px; + right: -4px; + bottom: 4px; + width: 4px; + cursor: ew-resize; +} +.hoverborder__resize-se { + position: absolute; + right: -6px; + bottom: -6px; + height: 10px; + width: 10px; + cursor: se-resize; +} +.hoverborder__resize-s { + position: absolute; + top: 3px; + right: 4px; + left: 4px; + height: 2px; +} +.hoverborder__resize-sw { + position: absolute; + bottom: -6px; + left: -6px; + height: 10px; + width: 10px; + cursor: sw-resize; +} +.hoverborder__resize-w { + position: absolute; + top: 4px; + bottom: 4px; + left: -4px; + width: 4px; + cursor: ew-resize; +} +.hoverborder__resize-nw { + position: absolute; + top: -6px; + left: -6px; + height: 10px; + width: 10px; + cursor: nw-resize; +} +.hoverborder[data-display-title="true"] ~ .hwin__wrp-table, +.hoverborder[data-display-title="true"] ~ .hoverborder { + display: none; +} +.source-category-site { + color: #e50711 !important; + border-color: #e50711 !important; + text-decoration-color: #e50711 !important; +} +.source-category-extras { + color: #9d4c4f !important; + border-color: #9d4c4f !important; + text-decoration-color: #9d4c4f !important; +} +.source-category-homebrew { + color: #8c3b96 !important; + border-color: #8c3b96 !important; + text-decoration-color: #8c3b96 !important; +} +.source-category-homebrew--local { + color: #4b40ed !important; + border-color: #4b40ed !important; + text-decoration-color: #4b40ed !important; +} +.source-category-spicy { + color: #1d965d !important; + border-color: #1d965d !important; + text-decoration-color: #1d965d !important; +} +.source-category-spicy--local { + color: #54ce19 !important; + border-color: #54ce19 !important; + text-decoration-color: #54ce19 !important; +} +.sourcePHB { + color: #4a6898 !important; + border-color: #4a6898 !important; + text-decoration-color: #4a6898 !important; +} +.sourceDMG { + color: purple !important; + border-color: purple !important; + text-decoration-color: purple !important; +} +.sourceMM { + color: green !important; + border-color: green !important; + text-decoration-color: green !important; +} +.sourceSCAG { + color: #76af76 !important; + border-color: #76af76 !important; + text-decoration-color: #76af76 !important; +} +.sourceVGM { + color: gray !important; + border-color: gray !important; + text-decoration-color: gray !important; +} +.sourceOGA { + color: #933d0f !important; + border-color: #933d0f !important; + text-decoration-color: #933d0f !important; +} +.sourceXGE, +.sourceTTP { + color: #ba7c00 !important; + border-color: #ba7c00 !important; + text-decoration-color: #ba7c00 !important; +} +.sourceXMtS { + color: #830051 !important; + border-color: #830051 !important; + text-decoration-color: #830051 !important; +} +.sourceHotDQ { + color: #ad8eba !important; + border-color: #ad8eba !important; + text-decoration-color: #ad8eba !important; +} +.sourceRoT { + color: #ff2900 !important; + border-color: #ff2900 !important; + text-decoration-color: #ff2900 !important; +} +.sourceCoS { + color: purple !important; + border-color: purple !important; + text-decoration-color: purple !important; +} +.sourceOotA { + color: gray !important; + border-color: gray !important; + text-decoration-color: gray !important; +} +.sourceSKT { + color: #008b8b !important; + border-color: #008b8b !important; + text-decoration-color: #008b8b !important; +} +.sourcePotA, +.sourceEEPC { + color: #57b6c6 !important; + border-color: #57b6c6 !important; + text-decoration-color: #57b6c6 !important; +} +.sourceLMoP { + color: #8da851 !important; + border-color: #8da851 !important; + text-decoration-color: #8da851 !important; +} +.sourceTftYP { + color: #c94029 !important; + border-color: #c94029 !important; + text-decoration-color: #c94029 !important; +} +.sourceToA { + color: #666f30 !important; + border-color: #666f30 !important; + text-decoration-color: #666f30 !important; +} +.sourceMTF { + color: #1f6e7b !important; + border-color: #1f6e7b !important; + text-decoration-color: #1f6e7b !important; +} +.sourceWDH { + color: #d4af37 !important; + border-color: #d4af37 !important; + text-decoration-color: #d4af37 !important; +} +.sourceGGR, +.sourceKKW { + color: #bfa76c !important; + border-color: #bfa76c !important; + text-decoration-color: #bfa76c !important; +} +.sourceWDMM { + color: #a2201f !important; + border-color: #a2201f !important; + text-decoration-color: #a2201f !important; +} +.sourceLLK { + color: #6e7a71 !important; + border-color: #6e7a71 !important; + text-decoration-color: #6e7a71 !important; +} +.sourceAZfyT { + color: #4667a7 !important; + border-color: #4667a7 !important; + text-decoration-color: #4667a7 !important; +} +.sourceGoS { + color: #3d695a !important; + border-color: #3d695a !important; + text-decoration-color: #3d695a !important; +} +.sourceAI, +.sourceOoW { + color: #5baf04 !important; + border-color: #5baf04 !important; + text-decoration-color: #5baf04 !important; +} +.sourceESK, +.sourceDIP, +.sourceDC, +.sourceSDW, +.sourceSLW { + color: #6b909a !important; + border-color: #6b909a !important; + text-decoration-color: #6b909a !important; +} +.sourceBGDIA { + color: #752418 !important; + border-color: #752418 !important; + text-decoration-color: #752418 !important; +} +.sourceERLW, +.sourceEFR { + color: #983426 !important; + border-color: #983426 !important; + text-decoration-color: #983426 !important; +} +.sourceRMR, +.sourceRMBRE { + color: #5c7c27 !important; + border-color: #5c7c27 !important; + text-decoration-color: #5c7c27 !important; +} +.sourceMFF { + color: #92817f !important; + border-color: #92817f !important; + text-decoration-color: #92817f !important; +} +.sourceLR { + color: #78613c !important; + border-color: #78613c !important; + text-decoration-color: #78613c !important; +} +.sourceIMR { + color: #a19364 !important; + border-color: #a19364 !important; + text-decoration-color: #a19364 !important; +} +.sourceSADS { + color: #333bab !important; + border-color: #333bab !important; + text-decoration-color: #333bab !important; +} +.sourceEGW, +.sourceFS, +.sourceDD, +.sourceUS, +.sourceToR { + color: #855a6e !important; + border-color: #855a6e !important; + text-decoration-color: #855a6e !important; +} +.sourceMOT { + color: #556b2e !important; + border-color: #556b2e !important; + text-decoration-color: #556b2e !important; +} +.sourceIDRotF { + color: #8fb8c0 !important; + border-color: #8fb8c0 !important; + text-decoration-color: #8fb8c0 !important; +} +.sourceTCE { + color: #a24d08 !important; + border-color: #a24d08 !important; + text-decoration-color: #a24d08 !important; +} +.sourceAL { + color: #e50711 !important; + border-color: #e50711 !important; + text-decoration-color: #e50711 !important; +} +.sourceHF { + color: #ac9544 !important; + border-color: #ac9544 !important; + text-decoration-color: #ac9544 !important; +} +.sourceCM { + color: #e6585e !important; + border-color: #e6585e !important; + text-decoration-color: #e6585e !important; +} +.sourceVRGR, +.sourceHoL { + color: #bd000f !important; + border-color: #bd000f !important; + text-decoration-color: #bd000f !important; +} +.sourceRtG { + color: #8a536a !important; + border-color: #8a536a !important; + text-decoration-color: #8a536a !important; +} +.sourceAitFR { + color: #6e5ab9 !important; + border-color: #6e5ab9 !important; + text-decoration-color: #6e5ab9 !important; +} +.sourceAitFR-ISF, +.sourceAitFR-THP, +.sourceAitFR-AVT, +.sourceAitFR-DN, +.sourceAitFR-FCD { + color: #6e5ab9 !important; + border-color: #6e5ab9 !important; + text-decoration-color: #6e5ab9 !important; +} +.sourceWBtW { + color: #7151b6 !important; + border-color: #7151b6 !important; + text-decoration-color: #7151b6 !important; +} +.sourceDoD { + color: #fe4935 !important; + border-color: #fe4935 !important; + text-decoration-color: #fe4935 !important; +} +.sourceMaBJoV { + color: #7a2854 !important; + border-color: #7a2854 !important; + text-decoration-color: #7a2854 !important; +} +.sourceFTD { + color: #b82a15 !important; + border-color: #b82a15 !important; + text-decoration-color: #b82a15 !important; +} +.sourceNRH { + color: #bd335b !important; + border-color: #bd335b !important; + text-decoration-color: #bd335b !important; +} +.sourceNRH-TCMC, +.sourceNRH-AVitW, +.sourceNRH-ASS, +.sourceNRH-CoI, +.sourceNRH-TLT, +.sourceNRH-AWoL, +.sourceNRH-AT { + color: #bd335b !important; + border-color: #bd335b !important; + text-decoration-color: #bd335b !important; +} +.sourceSCC { + color: #be9c56 !important; + border-color: #be9c56 !important; + text-decoration-color: #be9c56 !important; +} +.sourceSCC-CK, +.sourceSCC-HfMT, +.sourceSCC-TMM, +.sourceSCC-ARiR { + color: #be9c56 !important; + border-color: #be9c56 !important; + text-decoration-color: #be9c56 !important; +} +.sourceMPMM { + color: #5c758d !important; + border-color: #5c758d !important; + text-decoration-color: #5c758d !important; +} +.sourceCRCotN { + color: #ac4a70 !important; + border-color: #ac4a70 !important; + text-decoration-color: #ac4a70 !important; +} +.sourceJttRC { + color: #cf48e2 !important; + border-color: #cf48e2 !important; + text-decoration-color: #cf48e2 !important; +} +.sourceSjA, +.sourceSAiS, +.sourceAAG, +.sourceBAM, +.sourceLoX { + color: #056b97 !important; + border-color: #056b97 !important; + text-decoration-color: #056b97 !important; +} +.sourceDoSI { + color: #478bb8 !important; + border-color: #478bb8 !important; + text-decoration-color: #478bb8 !important; +} +.sourceDSotDQ { + color: #851e20 !important; + border-color: #851e20 !important; + text-decoration-color: #851e20 !important; +} +.sourcePSA { + color: #d76404 !important; + border-color: #d76404 !important; + text-decoration-color: #d76404 !important; +} +.sourcePSD { + color: #5db7da !important; + border-color: #5db7da !important; + text-decoration-color: #5db7da !important; +} +.sourcePSI { + color: #5d4696 !important; + border-color: #5d4696 !important; + text-decoration-color: #5d4696 !important; +} +.sourcePSK { + color: #a27135 !important; + border-color: #a27135 !important; + text-decoration-color: #a27135 !important; +} +.sourcePSX { + color: #bb2722 !important; + border-color: #bb2722 !important; + text-decoration-color: #bb2722 !important; +} +.sourcePSZ { + color: #6f8a2d !important; + border-color: #6f8a2d !important; + text-decoration-color: #6f8a2d !important; +} +.sourceKftGV { + color: #876e38 !important; + border-color: #876e38 !important; + text-decoration-color: #876e38 !important; +} +.sourceHAT-TG, +.sourceHAT-LMI { + color: #a24545 !important; + border-color: #a24545 !important; + text-decoration-color: #a24545 !important; +} +.sourceBGG { + color: #469cb7 !important; + border-color: #469cb7 !important; + text-decoration-color: #469cb7 !important; +} +.sourceTDCSR { + color: #642e4b !important; + border-color: #642e4b !important; + text-decoration-color: #642e4b !important; +} +.sourcePaBTSO { + color: #b2b34e !important; + border-color: #b2b34e !important; + text-decoration-color: #b2b34e !important; +} +.sourcePAitM, +.sourceSatO, +.sourceToFW, +.sourceMPP { + color: #a23087 !important; + border-color: #a23087 !important; + text-decoration-color: #a23087 !important; +} +.sourceCoA { + color: #a13a0f !important; + border-color: #a13a0f !important; + text-decoration-color: #a13a0f !important; +} +.sourceHFFotM { + color: #7b702c !important; + border-color: #7b702c !important; + text-decoration-color: #7b702c !important; +} +.sourceBMT { + color: #694165 !important; + border-color: #694165 !important; + text-decoration-color: #694165 !important; +} +.sourceGHLoE { + color: #c07e4e !important; + border-color: #c07e4e !important; + text-decoration-color: #c07e4e !important; +} +.sourceDoDk { + color: #825494 !important; + border-color: #825494 !important; + text-decoration-color: #825494 !important; +} +.sp__school-A { + color: #00b921; +} +.sp__school-V { + color: #bb0100; +} +.sp__school-E { + color: #b30083; +} +.sp__school-I { + color: #006dbd; +} +.sp__school-D { + color: #00adb3; +} +.sp__school-N { + color: #6c00cc; +} +.sp__school-T { + color: #ccbe00; +} +.sp__school-C { + color: #bd0044; +} +.lst__wrp-search-glass { + position: absolute; + top: 0; + bottom: 2px; + left: 6px; + opacity: 0.5; +} +.lst__wrp-search-visible { + position: absolute; + top: 0; + right: 6px; + bottom: 0; + opacity: 0.5; +} +.lst__caret--active { + display: inline-block; + width: 0; + height: 0; + vertical-align: middle; + border-top: 4px dashed; + border-right: 4px solid transparent; + border-left: 4px solid transparent; + margin-left: 2px; +} +.lst__caret--reverse { + transform: rotate(180deg); +} +input.lst__search { + padding-left: 23px; +} +input.lst__search--no-border-h { + border-radius: 0; + border-right: 0; +} +.ui__btn-xxl-square { + width: 110px; + height: 110px; +} +.ui__ipt-color { + width: 40px; + padding: 0; +} +.ui__ipt-color::-webkit-color-swatch-wrapper { + padding: 3px; +} +.ui__ipt-color::-webkit-color-swatch { + border: 1px solid #aaa; +} +.ui-list__wrp { + transform: translateZ(0); + font-size: 11.2px; +} +.ui-list__wrp-preview { + background: #dedcd0; + font-size: 90%; + border-top-left-radius: 5px; + margin-top: 1px; + margin-bottom: 4px; +} +.ui-list__btn-inline { + cursor: pointer; + color: #929292; +} +.ui-list__btn-inline:hover { + background: rgba(0, 0, 0, 0.175); + color: #525252; +} +.ui-source__row { + margin-left: calc(-96px - 0.5rem); +} +.ui-source__name { + min-width: 96px; + white-space: nowrap; + text-align: right; +} +.ui-source__divider { + height: 1px; + width: 30px; + background: #aaa; + display: inline-block; + margin: 0 3px; +} +.ui-modal__body-active { + overflow-y: hidden !important; +} +.ui-modal__row { + margin-bottom: 5px; + display: flex; + justify-content: space-between; + align-items: center; + font-weight: initial; + min-height: 30px; +} +.ui-modal__row:first-of-type { + margin-top: -1px; +} +.ui-modal__row--cb { + padding: 0 3px; + border-radius: 3px; +} +.ui-modal__row--cb:hover { + background: #c9c6b2; +} +.ui-modal__row--sel { + padding: 0 3px; +} +.ui-modal__row > * { + margin-right: 5px; +} +.ui-modal__row > *:last-child { + margin-right: 0; +} +.ui-modal__header--border { + border-bottom: 1px solid #aaa8; +} +.ui-modal__header--fullscreen { + box-shadow: 0 3px 6px rgba(0, 0, 0, 0.175); +} +.ui-modal__footer { + border-top: 1px solid #aaa8; +} +.ui-modal__footer--fullscreen { + box-shadow: 0 3px 6px rgba(0, 0, 0, 0.175); +} +.ui-modal__overlay { + position: fixed; + z-index: 10000; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + display: flex; + justify-content: center; + align-items: center; + background-color: #45454588; +} +.ui-modal__overlay-blind { + background-color: #fff; +} +.ui-modal__inner { + position: relative; + z-index: 1000001001; + top: initial; + left: initial; + margin: 60px auto; + padding: 5px 10px; + height: 400px; + float: none; + min-width: 600px; + max-height: 400px; + min-height: 400px; + font-size: 14px; + background: #dedcd0; + border: 1px solid #aaa; + border-radius: 4px; + box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); +} +@media (max-width: 767px) { + .ui-modal__inner { + min-width: 0; + } +} +@media (min-width: 768px) { + .ui-modal__inner { + max-width: 750px; + } +} +@media (min-width: 992px) { + .ui-modal__inner { + max-width: 970px; + } +} +@media (min-width: 1200px) { + .ui-modal__inner { + max-width: 1170px; + } +} +.ui-modal__inner--no-min-height { + min-height: 0; + height: initial; +} +.ui-modal__inner--no-min-height { + min-width: 0; + width: initial; +} +.ui-modal__inner--uncap-height { + max-height: calc(100% - 120px); + height: initial; +} +.ui-modal__inner--uncap-width { + max-width: calc(100% - 180px); + width: initial; +} +.ui-modal__inner--max-width-640p { + max-width: 640px; +} +.ui-modal__inner--mode-fullscreen { + max-height: 0; + height: 100vh; + flex-shrink: 0; + min-height: 100vh; + border-radius: 0; + box-shadow: none; + border: 0; +} +.ui-modal__scroller { + height: 100%; + width: 100%; + min-height: 0; + overflow-y: auto; +} +.ui-search__wrp-output { + position: relative; + height: 100%; + width: 100%; + display: flex; + flex-direction: column; +} +.ui-search__wrp-controls { + width: 100%; + display: flex; + z-index: 10000; +} +.ui-search__wrp-controls--in-tabs { + margin-top: -1px; +} +.ui-search__wrp-results { + position: relative; + padding: 3px; + transform: translateZ(0); + height: 100%; + overflow-y: auto; + overflow-x: hidden; + font-size: 11.2px; +} +.ui-search__row { + cursor: pointer; + font-weight: bold; + padding: 1px 2px; + display: flex; + justify-content: space-between; + border-bottom: 1px solid #aaa; +} +.ui-search__row:hover { + background: #d3d3d3; +} +.ui-search__row:focus { + box-shadow: inset 0 0 0 5000px #00000018; +} +.ui-search__sel-category { + border-radius: 0; + max-width: 180px; + flex-shrink: 0; + border-right: 0; +} +.ui-search__ipt-search { + border-radius: 0; + width: 100%; +} +.ui-search__ipt-search-sub-ipt[type="radio"] { + display: inline-block; + margin: 0 3px 0 0; +} +.ui-search__ipt-search-sub-ipt-custom { + max-width: 30px; + border-radius: 0; + border-left: 0; + margin-right: -1px; + border-right-color: #e0e0e0; + border-left-color: #e0e0e0; + padding-left: 0; +} +.ui-search__ipt-search-sub-ipt-custom[type="number"]::-webkit-inner-spin-button { + margin: 0; + -webkit-appearance: none; +} +.ui-search__ipt-search-sub-wrp { + flex-shrink: 0; + margin-bottom: 0; + padding: 5px; + font-weight: normal; + border: 1px solid #aaa; + height: 34px; + border-left: 0; +} +.ui-search__ipt-search-sub-lbl { + display: flex; + align-items: center; + height: 100%; +} +.ui-search__ipt-search-sub-lbl:not(:last-child) { + margin-right: 7px; +} +.ui-search__message { + font-size: 1.4rem; + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + font-family: "Times New Roman", serif; + font-variant: small-caps; + font-weight: 500; +} +.ui-tab__btn-tab-head { + display: inline-block; + padding: 2px 4px 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; + cursor: pointer; + user-select: none; + border-bottom: 0; +} +.ui-tab__btn-tab-head.active { + background-color: #e6e6e6; + border-color: #adadad; +} +.ui-tab__wrp-tab-body { + width: 100%; + height: 100%; + overflow-y: auto; + overflow-x: hidden; +} +.ui-tab__wrp-tab-body--border { + padding: 3px 0; +} +.ui-tab__wrp-tab-body--background { + background: #dedcd0; + border: 1px solid #aaa8; + border-top: 0; + border-bottom-color: #aaa5; +} +.ui-tab__wrp-tab-heads--border { + border-bottom: 1px solid #aaa; +} +.ui-tab-side__disp-active-tab-name { + margin-left: 126px; + font-size: 28px; +} +@media only screen and (max-width: 1200px) { + .ui-tab-side__disp-active-tab-name { + margin-left: 39px; + } +} +.ui-tab-side__btn-tab { + width: 120px; +} +@media only screen and (max-width: 1200px) { + .ui-tab-side__btn-tab { + width: 33px; + height: 30px; + } +} +.ui-tab-side__icon-tab { + min-width: 15px; + min-height: 12px; +} +.ui-tab-side__wrp-tab { + background: #dedcd0; + border: 1px solid #aaa8; + border-bottom: 0; +} +.ui-tab-side__wrp-tab--single { + border: 0; +} +.ui-prof__btn-cycle { + width: 16px; + height: 16px; + padding: 0; + flex-shrink: 0; + flex-grow: 0; + display: inline-block; + cursor: pointer; + border: 1px solid #aaa; + border-radius: 4px; + outline: none; + user-select: none; +} +.ui-prof__btn-cycle:active { + outline: 0; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(255, 0, 0, 0.6); +} +.ui-prof__btn-cycle.active { + background: #666; + border-color: #8c8c8c; +} +.ui-prof__btn-cycle.active.disabled { + background-color: #a6a6a6; +} +.ui-prof__btn-cycle.disabled { + cursor: default; + box-shadow: none; +} +.ui-prof__btn-cycle[data-state="0"] { + background: #fff; +} +.ui-prof__btn-cycle[data-state="1"] { + background: #666; + border-color: #8c8c8c; +} +.ui-prof__btn-cycle[data-state="2"] { + background: #666; + border-color: #8c8c8c; + display: flex; + line-height: 14px; + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + font-style: normal; + font-variant: normal; + text-rendering: auto; + font-family: "Font Awesome 5 Pro"; + font-weight: 900; + color: #fff; + font-size: 12px; +} +.ui-prof__btn-cycle[data-state="2"]::before { + content: "๏€…"; +} +.ui-prof__btn-cycle[data-state="3"] { + background: repeating-linear-gradient( + 135deg, + white, + white 10px, + #666 10px, + #666 20px + ); + border-color: #8c8c8c; +} +.ui-dir__face { + position: relative; + width: 92px; + height: 92px; + border-radius: 46px; + background: #f0f0f0; + border: 1px solid #aaa; + user-select: none; + cursor: grab; +} +.ui-dir__arm { + width: 1px; + height: 40px; + background: #333; + position: absolute; + top: 46px; + left: 46px; + transform: rotate(180deg); + transform-origin: top; + pointer-events: none; + user-select: none; + box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.75); +} +.ui-icn__wrp-icon { + font-size: 24px; +} +.ui-drag__wrp-drag-block { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; +} +.ui-drag__wrp-drag-dummy--highlight { + background: #cfe5ff78; +} +.ui-drag__wrp-drag-dummy--lowlight { + background: transparent; +} +.ui-drag__patch { + cursor: move; + user-select: none; + display: flex; + flex-shrink: 0; + padding: 5px 3px; + width: 14px; + font-size: 14px; +} +.ui-drag__dummy-patch { + width: 14px; +} +.ui-drag__patch-col { + display: flex; + flex-direction: column; + flex-shrink: 0; +} +.ui-drag__patch-col > div { + line-height: 4px; + text-align: center; +} +.ui-tip__parent { + cursor: help; + position: relative; +} +.ui-tip__child { + box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); + display: none; + position: absolute; + border: 1px solid #aaa; + background: #fff; + border-radius: 3px; + z-index: 1; + top: calc(100% + 5px); + padding: 5px; + opacity: 0; + transition: opacity 84ms ease-in-out; + pointer-events: none; +} +.ui-tip__parent:hover .ui-tip__child { + display: flex; + opacity: 1; +} +.ui-ctx__wrp { + box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); + z-index: 1000001100; + font-size: 14px; + background: #dedcd0; + border: 1px solid #aaa8; + border-top-color: #aaa; +} +.ui-ctx__divider { + height: 1px; + width: 100%; + background: #aaa; +} +.ui-ctx__row { + min-width: 160px; +} +.ui-ctx__btn { + cursor: pointer; +} +.ui-ctx__btn:hover { + background: #c9c6b2; +} +.ui-ctx__btn.disabled, +.ui-ctx__btn.disabled:hover { + cursor: default; + background: #dedcd0; +} +.ui-pick__btn-add { + font-weight: bold; + padding: 1px 2px; + line-height: 8px; + font-size: 18px; + display: flex; + height: 16px; +} +.ui-pick__btn-add--sub { + line-height: 11px; + height: 14px; + font-size: 16px; + border-radius: 0; + padding: 0 1px; + font-weight: bold; +} +.ui-pick__btn-remove { + width: 10px; + line-height: 20px; + padding: 0; + border-radius: 0; + font-size: 12px; + flex-shrink: 0; + flex-grow: 0; + cursor: pointer; + font-style: initial; +} +.ui-pick__btn-remove--sub { + height: 18px; + line-height: 16px; +} +.ui-pick__pill { + align-items: stretch; +} +.ui-pick__disp-text { + border: 1px solid #aaa; + border-right: 0; +} +.fa--btn-sm { + position: relative; + top: 1px; + font-size: 15px; +} +.fa--btn-xs { + position: relative; + font-size: 12px; +} +.fa--btn-xs::before { + width: 12px; + height: 14px; + display: inline-block; + text-align: center; +} +.fa--btn-xs.fa-dice { + left: -2px; +} +.clp__wrp-temp { + position: fixed; + top: -10000px; + left: -10000px; + width: 1px; + height: 1px; +} +.clp__disp-copied { + position: fixed; + white-space: nowrap; + width: auto; + transform: translateX(-50%); + pointer-events: none; + user-select: none; + height: 24px; + font-size: 12px; + z-index: 20000; + background: radial-gradient( + ellipse at center, + #dedcd0 0%, + #dedcd0 35%, + transparent 75%, + transparent 100% + ); +} +.ui-ideco__ipt--left { + padding-left: 22px !important; +} +.ui-ideco__ipt--right { + padding-right: 22px !important; +} +.ui-ideco__wrp { + position: absolute; + top: 0; + bottom: 0; + opacity: 0.5; + justify-content: center; +} +.ui-ideco__wrp > .glyphicon { + top: 0; +} +.ui-ideco__wrp--left { + left: 5px; +} +.ui-ideco__wrp--right { + right: 5px; +} +.ui-ideco__btn-ticker { + transition: opacity 34ms; + opacity: 0; + padding: 0; + width: 14px; + height: 10px; + border: 0; + font-size: 14px; + line-height: 10px; + border-radius: 0; + background: #0003; + color: #333; +} +.ui-ideco__btn-ticker:hover, +.ui-ideco__btn-ticker:active, +.ui-ideco__btn-ticker:focus, +.ui-ideco__btn-ticker:active:focus { + box-shadow: none; + outline: none; +} +.ui-ideco__btn-ticker:hover { + background: #0005; + color: #333; +} +.ui-ideco__btn-ticker:active, +.ui-ideco__btn-ticker:focus, +.ui-ideco__btn-ticker:active:focus { + background: #0007; + color: #333; +} +.ui-ideco__ipt:hover + .ui-ideco__wrp .ui-ideco__btn-ticker, +.ui-ideco__wrp:hover .ui-ideco__btn-ticker { + transition: opacity 34ms; + opacity: 1; +} +.ui-sel2__ipt-search { + top: 0; + right: 0; + left: 0; + opacity: 0; + background: transparent; +} +.ui-sel2__ipt-display { + padding-right: 20px; +} +.ui-sel2__wrp:focus > .ui-sel2__ipt-search, +.ui-sel2__wrp:focus-within > .ui-sel2__ipt-search { + opacity: 1; +} +.ui-sel2__wrp:focus > .ui-sel2__ipt-display, +.ui-sel2__wrp:focus-within > .ui-sel2__ipt-display { + text-align: right; + color: #929292; + font-weight: bold; +} +.ui-sel2__wrp:focus > .ui-sel2__wrp-options, +.ui-sel2__wrp:focus-within > .ui-sel2__wrp-options { + display: flex; +} +.ui-sel2__wrp-options { + z-index: 1; + top: 22px; + right: 0; + left: 0; + display: none; + flex-direction: column; + background: #dedcd0; + border: 1px solid #aaa; + border-top: 0; + max-height: 200px; +} +.ui-sel2__wrp-options:hover, +.ui-sel2__wrp-options:active, +.ui-sel2__wrp-options:focus, +.ui-sel2__wrp-options:focus-within { + display: flex; +} +.ui-sel2__disp-option.active, +.ui-sel2__disp-option:focus, +.ui-sel2__disp-option:hover { + background: #c9c6b2; +} +.ui-sel2__disp-option:focus.active, +.ui-sel2__disp-option:hover.active { + background: #b4af94; +} +.ui-sel2__disp-arrow { + top: 4px; + right: 4px; + bottom: 0; + font-size: 12px; +} +.ui-slidr__wrp { + font-size: 14px; +} +.ui-slidr__thumb { + width: 14px; + height: 18px; + top: -5px; + background: #c9c6b2; + border: 1px solid #aaa; + border-radius: 2px; +} +.ui-slidr__thumb--hover, +.ui-slidr__thumb:hover { + background: #b4af94; + border-color: #919191; +} +.ui-slidr__wrp-track { + padding-top: 6px; + padding-bottom: 7px; +} +.ui-slidr__track-outer { + border: 1px solid #aaa; + height: 10px; + border-radius: 3px; +} +.ui-slidr__track-inner { + background: #eee; +} +.ui-slidr__disp-value { + width: 80px; + height: 26px; + border-radius: 4px; +} +.ui-slidr__disp-value--visible { + border: 1px solid #aaa; + background: #dedcd0; +} +.ui-slidr__disp-value--left { + margin-right: 15px; +} +.ui-slidr__disp-value--right { + margin-left: 15px; +} +.ui-slidr__wrp-bottom { + height: 3em; +} +.ui-slidr__wrp-pips { + padding-top: 6px; +} +.ui-slidr__pip { + width: 1px; + height: 4px; + background: #aaa; +} +.ui-slidr__pip--major { + height: 6px; + background: #848484; +} +.ui-slidr__pip-label { + top: 0; + width: 24px; + height: 20px; + padding-top: 20px; +} +.statgen .statgen-shared__btn-reset { + right: -3px; + width: 22px; + height: 22px; +} +.statgen .statgen-shared__ipt { + width: 42px; + height: 24px; +} +.statgen .statgen-shared__ipt[readonly] { + background-color: #eee; +} +.statgen .statgen-shared__ipt--sel { + width: 64px; + text-align-last: center; +} +.statgen .statgen-shared__btn-toggle-tashas-rules { + line-height: 14px; + width: 16px; + height: 16px; +} +.statgen .statgen-shared__btn-toggle-tashas-rules .glyphicon { + left: -0.6px; +} +.statgen .statgen-rolled__wrp-results { + border: 1px solid #aaa8; + border-radius: 3px; +} +.statgen .statgen-rolled__disp-result { + min-width: 24px; +} +.statgen .statgen-pb__ipt-budget { + width: 70px; +} +.statgen .statgen-pb__ipt-budget--error { + border-color: red; +} +.statgen .statgen-pb__header { + height: 22px; +} +.statgen .statgen-pb__header--group { + border-bottom: 1px solid #aaa8; +} +.statgen .statgen-pb__header--choose-from { + min-width: 40px; +} +.statgen .statgen-pb__cell { + height: 24px; + min-width: 36px; +} +.statgen .statgen-pb__col-cost { + width: 90px; +} +.statgen .statgen-pb__col-cost-delete { + width: 26px; +} +.statgen .statgen-pb__row-cost { + min-height: 27px; +} +.statgen .statgen-asi__row { + border-bottom: 1px solid #aaa8; +} +.statgen .statgen-asi__cell { + width: 52px; +} +.statgen .statgen-asi__disp-plus { + left: -1px; +} +.statgen .statgen-asi__cell-feat { + width: 40px; + min-height: 20px; +} +.lootg__wrp-output { + border-radius: 5px; + background: #dedcd0; + border: 1px solid #aaa; +} +.ve-window { + background: url("../media/img/parchment.jpg") repeat local; +} +.ve-window * { + scrollbar-width: thin; +} +.ve-window input[type="checkbox"], +.ui-modal__overlay input[type="checkbox"] { + margin: 0; + transform: scale(1); + height: initial; +} +.ve-window input[type="radio"], +.ui-modal__overlay input[type="radio"] { + top: 0; + margin: 0; +} +#context-menu .context-item .fa-trash, +#context-menu .context-item .fa-dumpster { + color: red; +} +select.ve-foundry-button { + width: 100%; + margin: 0 1px; + background: rgba(255, 255, 240, 0.8); + border: 1px solid #b5b3a4; + border-radius: 3px; + font-size: 14px; + line-height: 28px; + font-family: "Signika", sans-serif; + height: 32px; +} +#settings { + overflow-y: scroll; +} +button:disabled, +input:disabled, +select:disabled { + cursor: not-allowed; +} +table.stats tr:nth-child(even) { + background-color: unset; +} +#loading #context { + white-space: nowrap; +} +.stats { + width: 100%; + table-layout: fixed; + overflow-wrap: break-word; + margin-top: 0; + background: 0; + border: 0; +} +.stats:last-child { + margin-bottom: 0; +} +.hwin__top-border-icon { + top: 0; + margin-left: auto; + padding: 2px; + color: #c9c6b2 !important; + cursor: pointer; + font-size: 12px; + width: 18px; + text-align: center; +} +.hwin__top-border-icon--text { + line-height: 11px; + font-weight: bolder; + font-family: monospace; +} +.hwin__top-border-icon:hover, +.hwin__top-border-icon:active, +.hwin__top-border-icon:visited { + color: #bebba3 !important; + text-decoration: none; +} +.hwin .hoverborder, +th.border { + color: #aaa; + background: #333; +} +.hwin .hoverborder a, +.hwin .hoverborder a:hover, +.hwin .hoverborder a:visited, +.hwin .hoverborder a:focus, +.hwin .hoverborder a:active, +th.border a, +th.border a:hover, +th.border a:visited, +th.border a:focus, +th.border a:active { + text-decoration: none; +} +.hwin__wrp-table { + padding: 0 3px; + color: #333; + background: #dedcd0; +} +.hwin__wrp-table table { + background: 0; + border: 0; + margin: 0 0 5px; +} +td.divider div, +.stats td div.border { + background: #333; + height: 2px; +} +.rnd-name { + position: relative; + font-size: 1.8em; + font-family: "Andada", serif; + font-variant: small-caps; + padding-top: 0; + padding-right: 0; + padding-bottom: 0; + padding-left: 0.2em !important; + font-weight: initial; +} +.rnd-name div.name-inner { + display: flex; + justify-content: space-between; + align-items: flex-end; +} +.rnd-name a { + text-decoration: none; +} +.stats-name { + font-size: unset; + border-bottom: 0; +} +.stats table { + background: initial; + border-top: 0; + border-bottom: 0; + margin: 0 0 5px; +} +.stats table > caption { + text-align: initial; +} +.stats table th { + font-weight: bold; +} +.render-roller { + color: #ff6400; + cursor: pointer; +} +.rd__body-popout { + background: #dedcd0; +} +.stripe-odd:nth-child(odd), +.stripe-even:nth-child(even), +.stripe-odd-table > tbody > tr:nth-child(odd), +.stripe-even-table > tbody > tr:nth-child(even) { + background: #c0c0c080 !important; +} +.help--hover { + cursor: help; + text-decoration: underline; + text-decoration-style: dotted; +} +.rd__h--0 { + color: #333; + border-color: #333; +} +.rd__h--1 { + color: #333; + border-color: #333; +} +.rd__h--2 { + color: #333; + border: 0; +} +.rd__b--3 > p, +.rd__b--4 > p { + text-indent: 0; + margin-bottom: 5px; +} +.rd__b--3 > p:first-of-type, +.rd__b--4 > p:first-of-type { + display: block; +} +.rd__b-inset > p { + text-indent: 0; + margin-bottom: 5px; +} +.rd__table { + background: initial; + border-top: 0; + border-bottom: 0; + margin: 0 0 5px; +} +.rd__table th { + text-align: initial; +} +.rd__table > caption { + font-weight: bold; +} +.list-multi-selected { + box-shadow: inset 0 0 0 5000px #00000018; +} +.lst__wrp-search-visible { + right: 19px; +} +input.lst__search { + background: #fff; + width: 100%; +} +input.lst__search:placeholder-shown ~ .lst__wrp-search-visible { + right: 6px; +} +.ui-list__wrp { + font-size: 13px; +} +.ui-list__wrp > .row > .bold { + font-weight: initial !important; +} +.ui-list__wrp-preview { + background: #dedcd0; +} +.ui-modal__overlay h4 { + font-size: 16px; +} +.ui-tab-side__wrp-tab { + background: url("../media/img/parchment.jpg") repeat local; +} +.mon__stat-header-underline { + font-size: 1.2rem; + font-family: "Andada", serif; + font-variant: small-caps; + border-bottom: 2px solid #822000; + color: #822000; + vertical-align: bottom !important; + padding-left: 0.2rem; +} +.mon__sect-header-inner { + display: block; + margin-top: -5px; + margin-bottom: -8px; + font-weight: 100; +} +.statgen input[type="number"].statgen-shared__ipt { + appearance: initial; +} +.statgen input[type="number"].statgen-shared__ipt::-webkit-inner-spin-button, +.statgen input[type="number"].statgen-shared__ipt::-webkit-outer-spin-button { + appearance: auto; +} +.statgen .statgen-shared__ipt[readonly] { + background-color: #aaaa; +} +.statgen .statgen-shared__ipt[readonly]:active, +.statgen .statgen-shared__ipt[readonly]:focus, +.statgen .statgen-shared__ipt[readonly]:hover { + box-shadow: none; +} +.statgen .statgen-shared__btn-toggle-tashas-rules { + line-height: 14px; + width: 18px; + height: 17px; +} +.statgen .statgen-shared__btn-toggle-tashas-rules .glyphicon { + left: 0.3px; +} +.statgen .statgen-shared__wrp-header { + margin-top: 0.25rem; +} +.statgen .statgen-asi__cell-feat { + min-height: 18px; +} +table.cls-tbl { + font-size: 11.2px; + font-family: Arial, sans-serif; + color: #333; + border-color: #aaa; + background: #dad4b2; + overflow: hidden; +} +table.cls-tbl tr:nth-child(even) { + background: none; +} +table.cls-tbl td, +table.cls-tbl th { + padding: 2px 0; +} +table.cls-tbl .cls-tbl__col-level, +table.cls-tbl .cls-tbl__col-prof-bonus { + width: 70px; +} +table.cls-tbl .cls-tbl__stripe-odd:nth-child(odd) { + background: #ece3d1; +} +table.cls-tbl th.border { + background: #aaa; +} +.mobile-ish__visible { + display: none !important; +} +.mobile__visible { + display: none !important; +} +.artr__wrp .artr__modal__overlay { + color: #fff; +} +.artr__wrp .artr__modal__overlay select { + color: #fff; +} +.cls-tbl { + font-size: 0.8em; + background: #dedcd0; +} +.cls-tbl > tbody > tr > th { + color: #333; + padding: 1px 0.3em; +} +.cls-tbl__disp-name { + font-size: 1.8em; + font-family: "Times New Roman", serif; + font-variant: small-caps; + font-weight: 500; + padding-left: 0.2em; + color: #333; +} +.cls-tbl__col-group { + text-align: center; +} +.cls-tbl__col-group::after { + position: relative; + bottom: 0; + left: 0; + margin: 0 auto; + padding: 0; + display: block; + content: ""; + height: 1px; + width: 96%; + border-bottom: 1px solid #333; +} +.cls-tbl__col-level { + text-align: center; + width: 1.5em; +} +.cls-tbl__col-prof-bonus { + text-align: center; + width: 1.5em; + padding: 1px 0.5em; +} +.cls-tbl__col-generic-center { + text-align: center; + min-width: 1em; + max-width: 5em; +} +.cls-tbl__stripe-odd:nth-child(odd) { + background: #d3d3d3; +} +.cls-tbl__cell-spell-progression--spell-points-enabled { + display: none !important; +} +.cls-tbl__cell-spell-points { + display: none !important; +} +.cls-tbl__cell-spell-points--spell-points-enabled { + display: table-cell !important; +} +.patlog__img-sign-up { + border: 0; + height: 45px; +} +.patlog__btn-log-in { + width: 247px; +} +.veapp__list { + transform: translateZ(0); + overflow-y: auto; + overflow-x: hidden; +} +.veapp__list-row { + border-bottom: 1px solid #aaa8; + margin-top: -1px; +} +.veapp__list-row-hoverable:hover { + background: #00000018; +} +.veapp__list-btn-inline { + color: #929292; +} +.veapp__list-btn-inline:hover { + background: rgba(0, 0, 0, 0.0941176471); + color: #525252; +} +.veapp__btn-filter { + width: 100px; +} +.veapp__btn-list-reset { + width: 100px; +} +.veapp__btn-list-feeling-lucky { + width: 40px; +} +.veapp__msg-warning { + color: #ff6400; +} +.veapp__msg-error { + color: red; +} +.veapp__bg-foundry { + background: url("../media/img/parchment.jpg") repeat local; +} +.veapp__sel-row-inline { + height: 20px; +} +.veapp__lnk { + color: #ff6400; +} +.veapp__ele-hoverable:hover { + background: #00000018; +} +.veapp-ctx-menu__wrp { + box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); + background: url("../media/img/parchment.jpg") repeat local; + position: fixed; + z-index: 1000001100; + border: 1px solid #000; + border-radius: 2px; +} +.veapp-ctx-menu__item { + min-width: 100px; +} +.veapp-ctx-menu__spacer { + border-bottom: 2px groove #eeede0; + margin: 1px 4px; +} +.veapp-ctx-menu__spacer--double { + border-bottom: 4px groove #fffeeb; +} +.veapp-loading__wrp-outer { + background: url("../media/img/parchment.jpg") repeat local; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + display: flex; + justify-content: center; + align-items: center; +} +.veapp-task-runner__wrp-progress-outer { + height: 26px; + border: 1px solid #3c1711; + border-radius: 3px; + box-shadow: inset 0 0 3px #111e; + background: rgba(255, 100, 0, 0.2); +} +.veapp-task-runner__wrp-progress-inner { + height: 100%; + width: 0; + background: #782e22; + border-radius: 3px; + transition: width 67ms; +} +.veapp-task-runner__wrp-progress-inner--active { + border-right: 2px solid #ff6400; +} +.veapp-task-runner__disp-progress-number { + top: 0; + right: 0; + bottom: 0; + left: 0; + color: #fff; + font-size: 14px; + text-shadow: 0 0 2px #3338; +} +.veapp-task-runner__wrp-console { + border: 1px solid #7a7971; + border-radius: 3px; +} +input[type="text"].veapp__ipt-row-inline { + height: 20px; +} +.plutsrv__disp-version { + position: absolute; + z-index: 60; + top: 44px; + left: 10px; + pointer-events: none; + color: #20c20e; + width: 90px; + font-size: 9px; + font-family: "Lucida Console", Monaco, monospace; +} +.rivet__disp-active { + position: absolute; + z-index: 61; + top: 15px; + left: 13px; + pointer-events: none; + font-size: 11px; + color: #aaa; + background: #333; + width: 20px; + height: 18px; + border: 1px solid #222; + border-radius: 3px 0 0 3px; +} +.dir__wrp-header { + padding: 0 4px; +} +.dir__btn-header { + height: 28px; + line-height: 26px; +} +.dir__wrp-footer { + padding: 0 3px; +} +.dir__control-header--alt { + display: none !important; +} +.dir__wrp-btns-import--placeholder { + display: none !important; +} +.cfg__wrp { + margin: -8px; +} +.cfg__wrp-tab-headers { + border-right: 1px solid #aaa; + background: #00000018; +} +.cfg__btn-tab-header { + background: url("../media/img/parchment.jpg") repeat local; + border: 1px solid #aaa; + border-right: 0; + border-top-left-radius: 5px; + border-bottom-left-radius: 5px; + cursor: pointer; +} +.cfg__btn-tab-header--active { + box-shadow: 0 0 10px #ff6400; + border-color: red; +} +.cfg__btn-tab-header--muted { + color: #929292; +} +.cfg__btn-tab-header--muted.cfg__btn-tab-header--active { + box-shadow: 0 0 10px gray; + border-color: gray; +} +.cfg__disp-row-count-tab-header { + position: absolute; + top: calc(50% - 8px); + right: 3px; + height: 16px; + width: 16px; + border-radius: 8px; + border: 1px solid #aaa; + background: #e8e8e8; + font-size: 10px; + line-height: 10px; + opacity: 0.7; +} +.cfg__disp-row-count-tab-header--has-results { + box-shadow: 0 0 5px #ff6400 inset; + opacity: 1; + font-weight: bold; +} +.cfg__disp-name { + max-width: calc(100% - 200px - 20px); +} +.cfg__disp-name--narrow { + max-width: 200px; +} +.cfg__wrp-input { + max-width: 200px; +} +.cfg__wrp-input--wide { + max-width: calc(100% - 200px - 20px); +} +.cfg__disp-requires-refresh { + color: red; +} +.cfg__disp-player-editable { + color: #337ab7; +} +.cfg__hr-tab-section { + flex-shrink: 0; + margin: 3px 0; +} +.cfg__head-tab-section { + font-size: 103%; + font-style: italic; +} +.cfg__wrp-search { + border-bottom: 1px solid #aaa; +} +.cfg__wrp-save { + border-top: 1px solid #aaa; +} +.cfg__row { + min-height: 32px; +} +.cfg__row:nth-child(even) { + background: #3331; +} +.cfg__row--no-match { + opacity: 0.3; +} +.cfg__btn-reset-row { + font-size: 10px; + padding: 1px 2px; +} +.text-sneaky:not(:hover) { + color: transparent; +} +.toku__sect-head { + font-size: 125%; + text-decoration: underline; + text-decoration-color: #aaa; +} +.toku__row { + min-height: 24px; +} +.toku__form select, +.toku__form input:not([type="checkbox"]) { + max-width: 200px; + width: 100%; +} +input[type="checkbox"].toku__cb-head { + transform: scale(1.5); +} +.imp-wiz__panel-1 { + width: 205px; +} +.imp-wiz__head-panel { + font-size: 125%; + text-decoration: underline; + text-decoration-color: #aaa; +} +.imp-wiz__spc-panels { + width: 1px; + background: #aaa; + height: 95%; + align-self: center; +} +.imp-wiz__row-mode { + margin-bottom: 1px; +} +.imp-wiz__row-source { + min-height: 24px; +} +.imp-wiz__btn-package-archive, +.imp-wiz__btn-quick { + font-size: 10px; +} +.imp-wiz__btn-tab-head { + border-bottom: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; + padding-bottom: 3px; +} +.imp-wiz__btn-tab-head--left { + border-right: 0; + border-top-right-radius: 0; +} +.imp-wiz__btn-tab-head--right { + border-top-left-radius: 0; +} +.imp-wiz__tab-config { + border-top: 1px solid #aaa; +} +.imp__disp-filename { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + height: 21px; + line-height: 21px; +} +.imp__btn-import-single { + height: 17px; + width: 19px; + background: rgba(255, 255, 240, 0.8); + border: 1px solid #b5b3a4; + border-radius: 3px; +} +.imp__tbl-stats-asi { + background: 0; + border: 0; +} +.imp__disp-import-from { + min-height: 55px; +} +.imp-folder__row:nth-child(1)::before { + content: ""; + display: flex; + height: 5px; + margin-right: 4px; + margin-left: 3px; + border-bottom: 1px solid #aaa; + border-left: 1px solid #aaa; + border-bottom-left-radius: 2px; + flex-shrink: 0; + flex-grow: 0; + width: 44px; +} +.imp-folder__row:nth-child(2)::before { + content: ""; + display: flex; + height: 5px; + margin-right: 4px; + margin-left: 3px; + border-bottom: 1px solid #aaa; + border-left: 1px solid #aaa; + border-bottom-left-radius: 2px; + flex-shrink: 0; + flex-grow: 0; + width: 33px; + margin-left: 14px; +} +.imp-folder__row:nth-child(3)::before { + content: ""; + display: flex; + height: 5px; + margin-right: 4px; + margin-left: 3px; + border-bottom: 1px solid #aaa; + border-left: 1px solid #aaa; + border-bottom-left-radius: 2px; + flex-shrink: 0; + flex-grow: 0; + width: 22px; + margin-left: 25px; +} +.imp-folder__row:nth-child(4)::before { + content: ""; + display: flex; + height: 5px; + margin-right: 4px; + margin-left: 3px; + border-bottom: 1px solid #aaa; + border-left: 1px solid #aaa; + border-bottom-left-radius: 2px; + flex-shrink: 0; + flex-grow: 0; + width: 11px; + margin-left: 36px; +} +.imp-advbk__header { + font-size: 125%; + text-decoration: underline; + text-decoration-color: #aaa; +} +.imp-advbk__sect-header { + font-size: 110%; +} +.imp-advbk__wrp-cb-source { + border: 1px solid #aaa; + background: #e8e8e8; + border-radius: 3px; + padding: 1px 5px; +} +.imp-cls__wrp-equi-group { + border: 1px solid #aaa; + border-radius: 3px; +} +.imp-cls__disp-equi-choice-key { + min-width: 32px; +} +.imp-cls__wrp-equi-btns { + width: 40px; +} +.imp-cls__disp-equi-count { + width: 50px; +} +.imp-cls__disp-equi-cost { + width: 96px; +} +.imp-cls__btn-sheet-level-up { + display: none; + width: 18px; + height: 18px; + line-height: 15px; + padding-left: 1px; +} +.imp-sp__disp-conc { + color: #0f7ede; +} +.imp-sp__disp-ritual { + color: #d01517; +} +.act-sp-prep__disp-modified { + position: absolute; + top: 2px; + right: 24px; + color: red; + font-weight: bold; + overflow-y: hidden; + height: 10px; +} +.act-sp-prep__row:hover { + background: #00000018; +} +.permu__list { + align-self: baseline; + max-height: initial; +} +.permu__row-head { + padding: 1px 0; +} +.permu__hr-head { + border: 0 solid #848484; + border-bottom-width: 1px; +} +.permu__row-ent { + border-bottom: 1px solid #aaa; +} +.permu__row-ent:hover { + background: #00000018; +} +.permu__wrp-controls-inline-player { + border-right: 1px solid #aaa; +} +.permu__cell-ent-name { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + position: absolute; + z-index: 1; + left: 0; + padding: 0 3px; + width: 200px; + height: 24px; + line-height: 26px; + direction: rtl; + text-align: left; + border-right: 1px solid #aaa; + background: #e8e8e8; +} +.permu__cell-ent-name--header { + background: #dedcd0; +} +.permu__cell-start-spacer { + width: 200px; +} +.permu__cell-player { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + width: 110px; + height: 24px; + border-right: 1px solid #aaa; +} +.permu__cell-player--mass { + border-right: 1px solid #848484; +} +.permu__cell-player--compact { + width: 75px; +} +.permu__cell-player--heavy-border { + border-right-color: #6a6a6a; +} +.permu__sel-permission { + width: 85px; +} +.permu__sel-permission--mass { + color: #888; +} +.permu__sel-permission--mass option { + color: #191813; +} +.permu__sel-permission--compact { + width: 50px; +} +.permu__sel-permission[data-permu-value="-1"] { + color: #191813; +} +.permu__sel-permission[data-permu-value="0"] { + color: #6b1718; + background: #ffbebe; +} +.permu__sel-permission[data-permu-value="1"] { + color: #5f5f13; + background: #ffef9f; +} +.permu__sel-permission[data-permu-value="2"] { + color: #25557d; + background: #a4d9ff; +} +.permu__sel-permission[data-permu-value="3"] { + color: #13521e; + background: #b6f3b6; +} +.tit-menu__btn-open--sheet { + width: 22px; + margin-left: 0; + padding-right: 8px; + padding-left: 8px; + text-align: center; +} +.minimized .tit-menu__btn-open--sheet { + display: none !important; +} +.pop__window { + top: 0 !important; + left: 0 !important; + width: 100% !important; + height: 100% !important; +} +.pop__window .window-header { + display: none !important; +} +.pop__btn-open { + display: none !important; +} +.act-menu__btn-open--sheet { + width: 26px; + text-align: center; + display: none !important; +} +.artr__wrp button { + line-height: normal; + width: initial; +} +.artr__wrp button:hover { + box-shadow: 0 0 2px #fff8; +} +.artr__wrp .artr__item__lnk-view { + display: none; +} +.artr__wrp .artr__btn-lg--search-controls { + min-width: 31px; +} +.jemb__btn-toggle { + height: 21px; + width: 48px; + background: #ddd; + padding: 1px 4px; + border: 1px solid #4b4a44; + color: #4b4a44; + border-radius: 2px; +} +.jemb__wrp-content { + border-bottom: 1px solid #b5b3a4; + background: #3331; +} +.jemb__wrp-content .jemb__wrp-content { + background: transparent; +} +.jemb__wrp-lnk > a { + display: inline-block; + width: 100%; +} +.jemb__img { + max-width: 20vw; + max-height: 20vh; +} +.manc__list { + font-size: 12px; +} +.manc__list-row { + box-shadow: inset 0 -1px 0 0 #aaa; +} +.manc__list-row-button { + padding: 0 2px; + font-size: 12px; + line-height: 14px; +} +.manc__window .ui-tab-side__wrp-tab { + border: 0; +} +.manc-sp__disp-prepared { + margin-top: -7px; +} +.manc-sp__sel-slot-level { + border-radius: 0; + border: 1px solid #aaa; + background: #d3d0c7; + appearance: none; + text-align-last: center; + font-size: 12px; + height: 22px; + width: 76px; +} +.manc-sp__sel-slot-level:not(.ve-hidden) + + * + + .manc-sp__sel-slot-level:not(.ve-hidden) { + border-left: 0; +} +.manc-sp__btn-prepare, +.manc-sp__btn-learn-spell { + display: none !important; +} +.manc-sp__is-prepared-caster .manc-sp__btn-prepare { + display: inline-block !important; +} +.manc-sp__is-learn-caster .manc-sp__btn-learn-spell { + display: inline-block !important; +} +.manc-sp__is-prepared-caster.manc-sp__is-learn-caster + .manc-sp__btn-learn-cantrip, +.manc-sp__is-prepared-caster.manc-sp__is-learn-caster + .manc-sp__btn-learn-spell { + border-right: 0; + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} +.manc-sp__is-prepared-caster.manc-sp__is-learn-caster .manc-sp__btn-prepare { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} +.manc-sp__is-prepared-caster.manc-sp__is-learn-caster.manc-sp__is-max-learned-spells + .manc-sp__btn-learn-spell + + .manc-sp__btn-prepare { + cursor: not-allowed; + color: #929292; +} +.manc-sp__is-prepared-caster.manc-sp__is-learn-caster.manc-sp__is-max-learned-spells + .manc-sp__btn-learn-spell + + .manc-sp__btn-prepare:hover, +.manc-sp__is-prepared-caster.manc-sp__is-learn-caster.manc-sp__is-max-learned-spells + .manc-sp__btn-learn-spell + + .manc-sp__btn-prepare:focus { + box-shadow: none; +} +.manc-sp__is-prepared-caster.manc-sp__is-learn-caster.manc-sp__is-max-learned-spells + .manc-sp__btn-learn-spell + + .manc-sp__btn-prepare:hover.active, +.manc-sp__is-prepared-caster.manc-sp__is-learn-caster.manc-sp__is-max-learned-spells + .manc-sp__btn-learn-spell + + .manc-sp__btn-prepare:focus.active { + box-shadow: inset 0 3px 7px #111e; +} +.manc-sp__is-prepared-caster.manc-sp__is-learn-caster.manc-sp__is-max-learned-spells + .manc-sp__btn-learn-spell + + .manc-sp__btn-prepare.disabled, +.manc-sp__is-prepared-caster.manc-sp__is-learn-caster.manc-sp__is-max-learned-spells + .manc-sp__btn-learn-spell + + .manc-sp__btn-prepare[disabled] { + cursor: not-allowed; + color: #929292; +} +.manc-sp__is-prepared-caster.manc-sp__is-learn-caster.manc-sp__is-max-learned-spells + .manc-sp__btn-learn-spell.active + + .manc-sp__btn-prepare { + cursor: pointer; + color: #000; +} +.manc-sp__is-prepared-caster.manc-sp__is-learn-caster.manc-sp__is-max-learned-spells + .manc-sp__btn-learn-spell.active + + .manc-sp__btn-prepare:hover, +.manc-sp__is-prepared-caster.manc-sp__is-learn-caster.manc-sp__is-max-learned-spells + .manc-sp__btn-learn-spell.active + + .manc-sp__btn-prepare:focus { + box-shadow: 0 0 5px red; +} +.manc-sp__is-prepared-caster.manc-sp__is-learn-caster.manc-sp__is-max-learned-spells + .manc-sp__btn-learn-spell.active + + .manc-sp__btn-prepare:hover.active, +.manc-sp__is-prepared-caster.manc-sp__is-learn-caster.manc-sp__is-max-learned-spells + .manc-sp__btn-learn-spell.active + + .manc-sp__btn-prepare:focus.active { + box-shadow: inset 0 5px 10px #111e; +} +.manc-sp__is-prepared-caster.manc-sp__is-learn-caster.manc-sp__is-max-learned-spells + .manc-sp__btn-learn-spell.active + + .manc-sp__btn-prepare.disabled, +.manc-sp__is-prepared-caster.manc-sp__is-learn-caster.manc-sp__is-max-learned-spells + .manc-sp__btn-learn-spell.active + + .manc-sp__btn-prepare[disabled] { + cursor: not-allowed; + color: #929292; +} +.manc-sp__is-prepared-caster.manc-sp__is-learn-caster.manc-sp__is-max-learned-spells + .manc-sp__btn-learn-spell.active + + .manc-sp__btn-prepare.disabled:hover, +.manc-sp__is-prepared-caster.manc-sp__is-learn-caster.manc-sp__is-max-learned-spells + .manc-sp__btn-learn-spell.active + + .manc-sp__btn-prepare.disabled:focus, +.manc-sp__is-prepared-caster.manc-sp__is-learn-caster.manc-sp__is-max-learned-spells + .manc-sp__btn-learn-spell.active + + .manc-sp__btn-prepare[disabled]:hover, +.manc-sp__is-prepared-caster.manc-sp__is-learn-caster.manc-sp__is-max-learned-spells + .manc-sp__btn-learn-spell.active + + .manc-sp__btn-prepare[disabled]:focus { + box-shadow: none; +} +.manc-sp__is-prepared-caster.manc-sp__is-learn-caster.manc-sp__is-max-learned-spells + .manc-sp__btn-learn-spell.active + + .manc-sp__btn-prepare.disabled:hover.active, +.manc-sp__is-prepared-caster.manc-sp__is-learn-caster.manc-sp__is-max-learned-spells + .manc-sp__btn-learn-spell.active + + .manc-sp__btn-prepare.disabled:focus.active, +.manc-sp__is-prepared-caster.manc-sp__is-learn-caster.manc-sp__is-max-learned-spells + .manc-sp__btn-learn-spell.active + + .manc-sp__btn-prepare[disabled]:hover.active, +.manc-sp__is-prepared-caster.manc-sp__is-learn-caster.manc-sp__is-max-learned-spells + .manc-sp__btn-learn-spell.active + + .manc-sp__btn-prepare[disabled]:focus.active { + box-shadow: inset 0 5px 10px #111e; +} +.manc-sp__is-prepared-caster.manc-sp__is-learn-caster.manc-sp__is-max-learned-spells.manc-sp__is-max-prepared-spells + .manc-sp__btn-prepare:not(.active) { + cursor: not-allowed; + color: #929292; +} +.manc-sp__is-prepared-caster.manc-sp__is-learn-caster.manc-sp__is-max-learned-spells.manc-sp__is-max-prepared-spells + .manc-sp__btn-prepare:not(.active):hover, +.manc-sp__is-prepared-caster.manc-sp__is-learn-caster.manc-sp__is-max-learned-spells.manc-sp__is-max-prepared-spells + .manc-sp__btn-prepare:not(.active):focus { + box-shadow: none; +} +.manc-sp__is-prepared-caster.manc-sp__is-learn-caster.manc-sp__is-max-learned-spells.manc-sp__is-max-prepared-spells + .manc-sp__btn-prepare:not(.active):hover.active, +.manc-sp__is-prepared-caster.manc-sp__is-learn-caster.manc-sp__is-max-learned-spells.manc-sp__is-max-prepared-spells + .manc-sp__btn-prepare:not(.active):focus.active { + box-shadow: inset 0 3px 7px #111e; +} +.manc-sp__is-prepared-caster.manc-sp__is-learn-caster.manc-sp__is-max-learned-spells.manc-sp__is-max-prepared-spells + .manc-sp__btn-prepare:not(.active).disabled, +.manc-sp__is-prepared-caster.manc-sp__is-learn-caster.manc-sp__is-max-learned-spells.manc-sp__is-max-prepared-spells + .manc-sp__btn-prepare:not(.active)[disabled] { + cursor: not-allowed; + color: #929292; +} +.manc-sp__is-prepared-caster.manc-sp__is-learn-caster.manc-sp__is-max-learned-spells.manc-sp__is-max-prepared-spells--is-over-limit + .manc-sp__btn-prepare.active { + color: #fff; + background-color: #ff5050; + box-shadow: inset 0 3px 7px #650000ee; +} +.manc-sp__is-prepared-caster.manc-sp__is-learn-caster.manc-sp__is-max-learned-spells.manc-sp__is-max-prepared-spells--is-over-limit + .manc-sp__btn-prepare.active:hover, +.manc-sp__is-prepared-caster.manc-sp__is-learn-caster.manc-sp__is-max-learned-spells.manc-sp__is-max-prepared-spells--is-over-limit + .manc-sp__btn-prepare.active:focus { + box-shadow: inset 0 5px 10px #650000ee; +} +.manc-sp__is-prepared-caster.manc-sp__is-learn-caster.manc-sp__is-max-learned-spells.manc-sp__is-max-prepared-spells--is-over-limit + .manc-sp__btn-prepare.active.disabled, +.manc-sp__is-prepared-caster.manc-sp__is-learn-caster.manc-sp__is-max-learned-spells.manc-sp__is-max-prepared-spells--is-over-limit + .manc-sp__btn-prepare.active[disabled] { + color: #929292; + cursor: not-allowed; +} +.manc-sp__is-prepared-caster.manc-sp__is-learn-caster.manc-sp__is-max-learned-spells.manc-sp__is-max-prepared-spells--is-over-limit + .manc-sp__btn-prepare.active.disabled:hover, +.manc-sp__is-prepared-caster.manc-sp__is-learn-caster.manc-sp__is-max-learned-spells.manc-sp__is-max-prepared-spells--is-over-limit + .manc-sp__btn-prepare.active.disabled:focus, +.manc-sp__is-prepared-caster.manc-sp__is-learn-caster.manc-sp__is-max-learned-spells.manc-sp__is-max-prepared-spells--is-over-limit + .manc-sp__btn-prepare.active[disabled]:hover, +.manc-sp__is-prepared-caster.manc-sp__is-learn-caster.manc-sp__is-max-learned-spells.manc-sp__is-max-prepared-spells--is-over-limit + .manc-sp__btn-prepare.active[disabled]:focus { + box-shadow: inset 0 3px 7px #650000ee; +} +.manc-sp__is-max-learned-cantrips .manc-sp__btn-learn-cantrip:not(.active) { + cursor: not-allowed; + color: #929292; +} +.manc-sp__is-max-learned-cantrips + .manc-sp__btn-learn-cantrip:not(.active):hover, +.manc-sp__is-max-learned-cantrips + .manc-sp__btn-learn-cantrip:not(.active):focus { + box-shadow: none; +} +.manc-sp__is-max-learned-cantrips + .manc-sp__btn-learn-cantrip:not(.active):hover.active, +.manc-sp__is-max-learned-cantrips + .manc-sp__btn-learn-cantrip:not(.active):focus.active { + box-shadow: inset 0 3px 7px #111e; +} +.manc-sp__is-max-learned-cantrips + .manc-sp__btn-learn-cantrip:not(.active).disabled, +.manc-sp__is-max-learned-cantrips + .manc-sp__btn-learn-cantrip:not(.active)[disabled] { + cursor: not-allowed; + color: #929292; +} +.manc-sp__is-max-learned-cantrips--is-over-limit + .manc-sp__btn-learn-cantrip.active { + color: #fff; + background-color: #ff5050; + box-shadow: inset 0 3px 7px #650000ee; +} +.manc-sp__is-max-learned-cantrips--is-over-limit + .manc-sp__btn-learn-cantrip.active:hover, +.manc-sp__is-max-learned-cantrips--is-over-limit + .manc-sp__btn-learn-cantrip.active:focus { + box-shadow: inset 0 5px 10px #650000ee; +} +.manc-sp__is-max-learned-cantrips--is-over-limit + .manc-sp__btn-learn-cantrip.active.disabled, +.manc-sp__is-max-learned-cantrips--is-over-limit + .manc-sp__btn-learn-cantrip.active[disabled] { + color: #929292; + cursor: not-allowed; +} +.manc-sp__is-max-learned-cantrips--is-over-limit + .manc-sp__btn-learn-cantrip.active.disabled:hover, +.manc-sp__is-max-learned-cantrips--is-over-limit + .manc-sp__btn-learn-cantrip.active.disabled:focus, +.manc-sp__is-max-learned-cantrips--is-over-limit + .manc-sp__btn-learn-cantrip.active[disabled]:hover, +.manc-sp__is-max-learned-cantrips--is-over-limit + .manc-sp__btn-learn-cantrip.active[disabled]:focus { + box-shadow: inset 0 3px 7px #650000ee; +} +.manc-sp__is-max-learned-spells .manc-sp__btn-learn-spell:not(.active) { + cursor: not-allowed; + color: #929292; +} +.manc-sp__is-max-learned-spells .manc-sp__btn-learn-spell:not(.active):hover, +.manc-sp__is-max-learned-spells .manc-sp__btn-learn-spell:not(.active):focus { + box-shadow: none; +} +.manc-sp__is-max-learned-spells + .manc-sp__btn-learn-spell:not(.active):hover.active, +.manc-sp__is-max-learned-spells + .manc-sp__btn-learn-spell:not(.active):focus.active { + box-shadow: inset 0 3px 7px #111e; +} +.manc-sp__is-max-learned-spells .manc-sp__btn-learn-spell:not(.active).disabled, +.manc-sp__is-max-learned-spells + .manc-sp__btn-learn-spell:not(.active)[disabled] { + cursor: not-allowed; + color: #929292; +} +.manc-sp__is-max-learned-spells--is-over-limit + .manc-sp__btn-learn-spell.active { + color: #fff; + background-color: #ff5050; + box-shadow: inset 0 3px 7px #650000ee; +} +.manc-sp__is-max-learned-spells--is-over-limit + .manc-sp__btn-learn-spell.active:hover, +.manc-sp__is-max-learned-spells--is-over-limit + .manc-sp__btn-learn-spell.active:focus { + box-shadow: inset 0 5px 10px #650000ee; +} +.manc-sp__is-max-learned-spells--is-over-limit + .manc-sp__btn-learn-spell.active.disabled, +.manc-sp__is-max-learned-spells--is-over-limit + .manc-sp__btn-learn-spell.active[disabled] { + color: #929292; + cursor: not-allowed; +} +.manc-sp__is-max-learned-spells--is-over-limit + .manc-sp__btn-learn-spell.active.disabled:hover, +.manc-sp__is-max-learned-spells--is-over-limit + .manc-sp__btn-learn-spell.active.disabled:focus, +.manc-sp__is-max-learned-spells--is-over-limit + .manc-sp__btn-learn-spell.active[disabled]:hover, +.manc-sp__is-max-learned-spells--is-over-limit + .manc-sp__btn-learn-spell.active[disabled]:focus { + box-shadow: inset 0 3px 7px #650000ee; +} +.manc-sp__is-max-prepared-spells .manc-sp__btn-prepare:not(.active) { + cursor: not-allowed; + color: #929292; +} +.manc-sp__is-max-prepared-spells .manc-sp__btn-prepare:not(.active):hover, +.manc-sp__is-max-prepared-spells .manc-sp__btn-prepare:not(.active):focus { + box-shadow: none; +} +.manc-sp__is-max-prepared-spells + .manc-sp__btn-prepare:not(.active):hover.active, +.manc-sp__is-max-prepared-spells + .manc-sp__btn-prepare:not(.active):focus.active { + box-shadow: inset 0 3px 7px #111e; +} +.manc-sp__is-max-prepared-spells .manc-sp__btn-prepare:not(.active).disabled, +.manc-sp__is-max-prepared-spells .manc-sp__btn-prepare:not(.active)[disabled] { + cursor: not-allowed; + color: #929292; +} +.manc-sp__is-max-prepared-spells--is-over-limit .manc-sp__btn-prepare.active { + color: #fff; + background-color: #ff5050; + box-shadow: inset 0 3px 7px #650000ee; +} +.manc-sp__is-max-prepared-spells--is-over-limit + .manc-sp__btn-prepare.active:hover, +.manc-sp__is-max-prepared-spells--is-over-limit + .manc-sp__btn-prepare.active:focus { + box-shadow: inset 0 5px 10px #650000ee; +} +.manc-sp__is-max-prepared-spells--is-over-limit + .manc-sp__btn-prepare.active.disabled, +.manc-sp__is-max-prepared-spells--is-over-limit + .manc-sp__btn-prepare.active[disabled] { + color: #929292; + cursor: not-allowed; +} +.manc-sp__is-max-prepared-spells--is-over-limit + .manc-sp__btn-prepare.active.disabled:hover, +.manc-sp__is-max-prepared-spells--is-over-limit + .manc-sp__btn-prepare.active.disabled:focus, +.manc-sp__is-max-prepared-spells--is-over-limit + .manc-sp__btn-prepare.active[disabled]:hover, +.manc-sp__is-max-prepared-spells--is-over-limit + .manc-sp__btn-prepare.active[disabled]:focus { + box-shadow: inset 0 3px 7px #650000ee; +} +.aeff__btn-inline { + width: 27px; + height: 27px; + line-height: 23px; + min-width: 27px; + border: 1px solid #7a7971; +} +.jlnk__entity-link { + background: #ddd; + padding: 1px 4px; + border: 1px solid #4b4a44; + border-radius: 2px; + white-space: nowrap; + word-break: break-all; + text-indent: 0; +} +.jlnk__entity-link .fas { + color: #7a7971; +} +@keyframes folder-anim-pulse { + 0% { + box-shadow: inset 0 0 0 10000px #ff6400; + } + 100% { + box-shadow: inset 0 0 0 0 transparent; + } +} +.jlnk__folder-pulse-1s { + animation: folder-anim-pulse 1s; + animation-timing-function: cubic-bezier(0.86, 0.17, 0.79, 0.98); +} +.wdss__stg-sidebar { + width: 230px; +} +.conu__wrp-details { + background: #dedcd0; + border-radius: 5px; + border: 1px solid #aaa; + border-top-color: #aaa8; + box-shadow: 0 2px 2px rgba(0, 0, 0, 0.175); + font-size: 86%; +} +.conu__disp-diff { + border-left: 1px solid #3333; + border-right: 1px solid #3333; +} +.conu__disp-diff--old { + background: #e1c6bb; + border-bottom: 1px dashed #6c000033; +} +.conu__disp-diff--nu { + background: #c6ddbb; + border-bottom: 1px dashed #006c0b33; +} +.conu__vr-diff { + border-left: 1px solid #3336; + border-right: 1px solid #3336; + background: #b1afa7; + line-height: 1; +} +.conu__row-field-diff:not(.conu__row-field-diff--disabled):hover + > * + > * + > .conu__disp-diff--old { + background-color: #e5aba2; +} +.conu__row-field-diff:not(.conu__row-field-diff--disabled):hover + > * + > * + > .conu__disp-diff--nu { + background-color: #abdea3; +} +.conu__row-field-diff:not(.conu__row-field-diff--disabled):hover + > * + > * + > .conu__vr-diff { + background: #919089; +} +.conu__row-field-diff:first-of-type > * > * > .conu__disp-diff-border-y { + border-top: 1px solid #3333; +} +.conu__row-field-diff:last-of-type > * > * > .conu__disp-diff-border-y { + border-bottom: 1px solid #3333; +} +.conu__wrp-embedded-diff { + background: #dedcd0; + border-radius: 5px; + box-shadow: 0 0 2px 1px rgba(0, 0, 0, 0.175); +} +.secret-gm__block, +.secret-player__block { + display: none !important; +} +.secret-gm__flex, +.secret-player__flex { + display: none !important; +} /*# sourceMappingURL=main.css.map */ diff --git a/charbuilder/css/sheet.css b/charbuilder/css/sheet.css new file mode 100644 index 0000000..1126841 --- /dev/null +++ b/charbuilder/css/sheet.css @@ -0,0 +1,778 @@ +.red { + background: red; + } + + .blue { + background: blue; + } + + .hide { + display: none !important; + } + + textarea { + font-size: 12px; + text-align: left; + width: calc(100% - 20px - 2px); + border-radius: 10px; + padding: 10px; + resize: none; + overflow: hidden; + height: 15em; + } + .textbox { + font-size: 12px; + text-align: left; + width: calc(100% - 20px - 2px); + border-radius: 10px; + padding: 10px; + resize: none; + overflow: hidden; + height: 15em; + } + + input[type=checkbox] { + cursor: pointer; + } + + div.box { + margin-top: 10px; + } + + form.charsheet { + width: 800px; + right: 0; + left: 0; + margin-right: auto; + margin-left: auto; + margin-top: 10px; + } + form.charsheet div.textblock { + display: flex; + flex-direction: column-reverse; + width: 100%; + align-items: center; + } + form.charsheet div.textblock label.footerLabel { + text-align: center; + border: 1px solid black; + border-top: 0; + font-size: 10px; + width: calc(100% - 20px - 2px); + border-radius: 0 0 10px 10px; + padding: 4px 0; + font-weight: bold; + } + form.charsheet div.textblock textarea { + border: 1px solid black; + } + form.charsheet div.textblock div.profTextArea { + border: 1px solid black; + } + form.charsheet ul { + margin: 0; + padding: 0; + } + form.charsheet ul li { + list-style-image: none; + display: block; + } + form.charsheet ::-moz-placeholder { + color: #bbb; + } + form.charsheet :-ms-input-placeholder { + color: #bbb; + } + form.charsheet ::placeholder { + color: #bbb; + } + form.charsheet .upperCase { + text-transform: uppercase; + font-size: 12px; + } + form.charsheet header { + display: flex; + align-contents: stretch; + align-items: stretch; + } + form.charsheet header section.charname { + border: 1px solid black; + border-right: 0; + border-radius: 10px 0 0 10px; + padding: 5px 0; + background-color: #eee; + width: 30%; + display: flex; + flex-direction: column-reverse; + bottom: 0; + top: 0; + margin: auto; + } + form.charsheet header section.charname input { + padding: 0.5em; + margin: 5px; + border: 0; + } + form.charsheet header section.charname .lblResultName { + padding-left: 1em; + border-bottom: 1px solid #ddd; + background-color: #fff; + font-size: 16px; + min-height: 19px; + } + form.charsheet header section.charname label { + padding-left: 1em; + } + form.charsheet header .lblResult { + font-size: 12px; + min-height: 15px; + } + form.charsheet header section.misc { + width: 70%; + border: 1px solid black; + border-radius: 10px; + padding-left: 1em; + padding-right: 1em; + } + form.charsheet header section.misc ul { + padding: 10px 0px 5px 0px; + display: flex; + flex-wrap: wrap; + } + form.charsheet header section.misc ul li { + width: 50%; + display: flex; + flex-direction: column-reverse; + } + form.charsheet header section.misc ul li label.lblTitle { + margin-bottom: 5px; + } + form.charsheet header section.misc ul li input { + border: 0; + border-bottom: 1px solid #ddd; + } + form.charsheet header section.misc ul li label.lblResult { + border: 0; + border-bottom: 1px solid #ddd; + background-color: #fff; + } + form.charsheet main { + display: flex; + justify-content: space-between; + margin-top: 20px; + } + form.charsheet main div.label-container { + position: relative; + width: 100%; + height: 18px; + margin-top: 6px; + border: 1px solid black; + border-left: 0; + text-align: center; + } + form.charsheet main div.label-container > label { + position: absolute; + left: 0; + top: 1px; + transform: translate(0%, 50%); + width: 100%; + font-size: 8px; + } + form.charsheet main > section { + width: 32%; + display: flex; + flex-direction: column; + } + form.charsheet main > section section.attributes { + width: 100%; + display: flex; + flex-direction: row; + justify-content: space-between; + } + form.charsheet main > section section.attributes div.scores { + width: 101px; + background-color: #bbb; + border-radius: 10px; + padding-bottom: 5px; + min-height: 550px; + } + form.charsheet main > section section.attributes div.scores label.ablName { + font-size: 8px; + font-weight: bold; + } + form.charsheet main > section section.attributes div.scores ul { + display: flex; + flex-direction: column; + justify-content: space-around; + align-items: center; + height: 100%; + } + form.charsheet main > section section.attributes div.scores ul li { + height: 80px; + width: 70px; + background-color: white; + border: 1px solid black; + text-align: center; + display: flex; + flex-direction: column; + border-radius: 10px; + } + form.charsheet main > section section.attributes div.scores ul li label { + width: 100%; + padding: 0; + border: 0; + } + form.charsheet main > section section.attributes div.scores ul li div.score label.stat { + text-align: center; + font-size: 40px; + padding: 2px 0px 0px 0px; + background: white; + display: block; + } + form.charsheet main > section section.attributes div.scores ul li div.modifier { + margin-top: 3px; + } + form.charsheet main > section section.attributes div.scores ul li div.modifier label { + background: white; + width: 30px; + height: 20px; + border: 1px inset black; + border-radius: 20px; + margin: -1px; + text-align: center; + display: inline-block; + font-size: 20px; + } + form.charsheet main > section section.attributes div.attr-applications{ + width: 100%; + padding-left: 10px; + } + form.charsheet main > section section.attributes div.attr-applications div.proficiencybonus { + display: flex; + flex-direction: row-reverse; + justify-content: flex-end; + } + form.charsheet main > section section.attributes div.attr-applications div.proficiencybonus label.lblProfScore { + width: 38px; + height: 28px; + line-height: 28px; + border: 1px solid black; + text-align: center; + border-radius: 10px; + font-size: 16px; + margin-left: 4px; + margin-right: 3px; + } + form.charsheet main > section section.attributes div.attr-applications div.list-section { + border: 1px solid black; + border-radius: 10px; + padding: 10px 2px; + } + form.charsheet main > section section.attributes div.attr-applications div.list-section div.label { + margin-top: 10px; + margin-bottom: 2.5px; + text-align: center; + text-transform: uppercase; + font-size: 10px; + font-weight: bold; + } + form.charsheet main > section section.attributes div.attr-applications div.list-section ul li { + display: flex; + align-items: center; + } + form.charsheet main > section section.attributes div.attr-applications div.list-section ul li > * { + margin-left: 5px; + } + form.charsheet main > section section.attributes div.attr-applications div.list-section ul li label { + text-transform: none; + font-size: 12px; + text-align: left; + order: 3; + } + form.charsheet main > section section.attributes div.attr-applications div.list-section ul li label span.skill { + color: #bbb; + } + form.charsheet main > section section.attributes div.attr-applications div.list-section ul li input[type=text] { + width: 30px; + font-size: 12px; + text-align: center; + border: 0; + border-bottom: 1px solid black; + order: 2; + } + form.charsheet main > section section.attributes div.attr-applications div.list-section ul li label.modifier{ + width: 20px; + font-size: 12px; + text-align: center; + border: 0; + border-bottom: 1px solid black; + order: 2; + } + form.charsheet main > section section.attributes div.attr-applications div.list-section ul li input[type=checkbox] { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + width: 10px; + height: 10px; + border: 1px solid black; + border-radius: 10px; + order: 1; + } + form.charsheet main > section section.attributes div.attr-applications div.list-section ul li input[type=checkbox]:checked { + background-color: black; + } + form.charsheet main > section div.passive-perception { + display: flex; + flex-direction: row-reverse; + justify-content: flex-end; + } + form.charsheet main > section div.passive-perception label.score { + width: 30px; + height: 28px; + line-height: 28px; + text-align: center; + border: 1px solid black; + border-radius: 10px; + } + form.charsheet main > section div.otherprofs div.profTextArea { + height: 26em; + } + form.charsheet main section.combat { + background-color: #eee; + display: flex; + flex-wrap: wrap; + border-radius: 10px; + } + form.charsheet main section.combat > div { + overflow: hidden; + } + form.charsheet main section.combat > div.scoreBubble { + flex-basis: 33.333%; + } + form.charsheet main section.combat > div.scoreBubble > div { + display: flex; + flex-direction: column-reverse; + align-items: center; + margin-top: 10px; + } + form.charsheet main section.combat > div.scoreBubble > div label.title { + font-size: 8px; + width: 50px; + border: 1px solid black; + border-top: 0; + background-color: white; + text-align: center; + padding-top: 5px; + padding-bottom: 5px; + border-radius: 0 0 10px 10px; + } + form.charsheet main section.combat > div.scoreBubble > div input { + height: 70px; + width: 70px; + border-radius: 10px; + border: 1px solid black; + text-align: center; + font-size: 30px; + } + form.charsheet main section.combat > div.scoreBubble > div label.score { + height: 70px; + width: 70px; + border-radius: 10px; + border: 1px solid black; + text-align: center; + font-size: 30px; + line-height: 70px; + background-color: #fff; + } + form.charsheet main section.combat > div.scoreBubbleWide { + flex-basis: 50%; + } + form.charsheet main section.combat > div.scoreBubbleWide > div { + display: flex; + flex-direction: column-reverse; + align-items: center; + margin-top: 10px; + } + form.charsheet main section.combat > div.scoreBubbleWide > div label.title { + font-size: 8px; + width: 50px; + border: 1px solid black; + border-top: 0; + background-color: white; + text-align: center; + padding-top: 5px; + padding-bottom: 5px; + border-radius: 0 0 10px 10px; + } + form.charsheet main section.combat > div.scoreBubbleWide > div label.score { + height: 70px; + width: 105px; + border-radius: 10px; + border: 1px solid black; + text-align: center; + font-size: 30px; + line-height: 70px; + background-color: #fff; + } + form.charsheet main section.combat > div.scoreBubbleWide > div label.scoreHitDice { + height: 70px; + width: 105px; + border-radius: 10px; + border: 1px solid black; + text-align: center; + font-size: 18px; + line-height: 18px; + background-color: #fff; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + } + label.scoreHitDice div{ + display:block; + } + form.charsheet .armorWornText { + font-size: 12px; + padding-left: 10px; + padding-top: 5px; + } + form.charsheet .coinage { + font-size: 12px; + padding-left: 10px; + padding-top: 5px; + padding-bottom: 5px; + } + form.charsheet main section.combat > div.hp { + flex-basis: 100%; + } + form.charsheet main section.combat > div.hp > div.regular { + background-color: white; + border: 1px solid black; + margin: 10px; + margin-bottom: 5px; + border-radius: 10px 10px 0 0; + } + form.charsheet main section.combat > div.hp > div.regular > div.max { + display: flex; + justify-content: space-around; + align-items: baseline; + } + form.charsheet main section.combat > div.hp > div.regular > div.max label { + font-size: 10px; + text-transform: none; + color: #bbb; + } + form.charsheet main section.combat > div.hp > div.regular > div.max input { + width: 40%; + border: 0; + border-bottom: 1px solid #ddd; + font-size: 12px; + text-align: center; + } + form.charsheet main section.combat > div.hp > div.regular > div.current { + display: flex; + flex-direction: column-reverse; + } + form.charsheet main section.combat > div.hp > div.regular > div.current input { + border: 0; + width: 100%; + padding: 1em 0; + font-size: 20px; + text-align: center; + } + form.charsheet main section.combat > div.hp > div.regular > div.current label { + font-size: 10px; + padding-bottom: 5px; + text-align: center; + font-weight: bold; + } + form.charsheet main section.combat > div.hp > div.temporary { + margin: 10px; + margin-top: 0; + border: 1px solid black; + border-radius: 0 0 10px 10px; + display: flex; + flex-direction: column-reverse; + background-color: white; + } + form.charsheet main section.combat > div.hp > div.temporary input { + padding: 1em 0; + font-size: 20px; + border: 0; + text-align: center; + } + form.charsheet main section.combat > div.hp > div.temporary label { + font-size: 10px; + padding-bottom: 5px; + text-align: center; + font-weight: bold; + } + form.charsheet main section.combat > div.hitdice, form.charsheet main section.combat > div.deathsaves { + flex: 1 50%; + height: 100px; + } + form.charsheet main section.combat > div.hitdice > div, form.charsheet main section.combat > div.deathsaves > div { + height: 80px; + } + form.charsheet main section.combat > div.hitdice > div { + background-color: white; + margin: 10px; + border: 1px solid black; + border-radius: 10px; + display: flex; + flex-direction: column; + } + form.charsheet main section.combat > div.hitdice > div > div.total { + display: flex; + align-items: baseline; + justify-content: space-around; + padding: 5px 0; + } + form.charsheet main section.combat > div.hitdice > div > div.total label { + font-size: 10px; + color: #bbb; + margin: 0.25em; + text-transform: none; + } + form.charsheet main section.combat > div.hitdice > div > div.total input { + font-size: 12px; + flex-grow: 1; + border: 0; + border-bottom: 1px solid #ddd; + width: 50%; + margin-right: 0.25em; + padding: 0 0.25em; + text-align: center; + } + form.charsheet main section.combat > div.hitdice > div > div.remaining { + flex: 1; + display: flex; + flex-direction: column-reverse; + } + form.charsheet main section.combat > div.hitdice > div > div.remaining label { + text-align: center; + padding: 2px; + font-size: 10px; + } + form.charsheet main section.combat > div.hitdice > div > div.remaining input { + text-align: center; + border: 0; + flex: 1; + } + form.charsheet main section.combat > div.deathsaves > div { + margin: 10px; + background: white; + border: 1px solid black; + border-radius: 10px; + display: flex; + flex-direction: column-reverse; + } + form.charsheet main section.combat > div.deathsaves > div > div.label { + text-align: center; + } + form.charsheet main section.combat > div.deathsaves > div > div.label label { + font-size: 10px; + } + form.charsheet main section.combat > div.deathsaves > div > div.marks { + display: flex; + flex: 1; + flex-direction: column; + justify-content: center; + } + form.charsheet main section.combat > div.deathsaves > div > div.marks div.deathsuccesses, form.charsheet main section.combat > div.deathsaves > div > div.marks div.deathfails { + display: flex; + align-items: center; + } + form.charsheet main section.combat > div.deathsaves > div > div.marks div.deathsuccesses label, form.charsheet main section.combat > div.deathsaves > div > div.marks div.deathfails label { + font-size: 8px; + text-align: right; + flex: 1 50%; + } + form.charsheet main section.combat > div.deathsaves > div div.bubbles { + flex: 1 40%; + margin-left: 5px; + } + form.charsheet main section.combat > div.deathsaves > div div.bubbles input[type=checkbox] { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + width: 10px; + height: 10px; + border: 1px solid black; + border-radius: 10px; + } + form.charsheet main section.combat > div.deathsaves > div div.bubbles input[type=checkbox]:checked { + background-color: black; + } + form.charsheet main section.attacksandspellcasting { + border: 1px solid black; + border-radius: 10px; + margin-top: 10px; + } + form.charsheet main section.attacksandspellcasting > div { + margin: 10px; + display: flex; + flex-direction: column; + } + form.charsheet main section.attacksandspellcasting > div > label { + order: 3; + text-align: center; + } + form.charsheet main section.attacksandspellcasting > div > table { + width: 100%; + } + form.charsheet main section.attacksandspellcasting > div > table th { + font-size: 10px; + color: #ddd; + } + form.charsheet main section.attacksandspellcasting > div > table input { + width: calc(100% - 4px); + border: 0; + background-color: #eee; + font-size: 10px; + padding: 3px; + } + form.charsheet main section.attacksandspellcasting > div textarea { + border: 0; + } + form.charsheet main section.equipment { + border: 1px solid black; + border-radius: 10px; + margin-top: 10px; + } + form.charsheet main section.equipment > div { + padding: 10px; + display: flex; + flex-direction: column; + flex-wrap: wrap; + } + form.charsheet main section.equipment > div > div.money ul { + display: flex; + flex-direction: column; + justify-content: space-between; + height: 100%; + } + form.charsheet main section.equipment > div > div.money ul > li { + display: flex; + align-items: center; + } + form.charsheet main section.equipment > div > div.money ul > li label { + border: 1px solid black; + border-radius: 10px 0 0 10px; + border-right: 0; + width: 20px; + font-size: 8px; + text-align: center; + padding: 3px 0; + } + form.charsheet main section.equipment > div > div.money ul > li input { + border: 1px solid black; + border-radius: 10px; + width: 40px; + padding: 10px 3px; + font-size: 12px; + text-align: center; + } + form.charsheet main section.equipment > div > label { + order: 3; + text-align: center; + flex: 100%; + } + form.charsheet main section.equipment > div > div.equipmentTextArea { + flex: 1; + border: 0; + } + form.charsheet main section.flavor { + padding: 10px; + background: #bbb; + border-radius: 10px; + } + form.charsheet main section.flavor div { + background: white; + display: flex; + flex-direction: column-reverse; + padding: 5px; + border: 1px solid black; + } + form.charsheet main section.flavor div label { + text-align: center; + font-size: 10px; + margin-top: 3px; + } + form.charsheet main section.flavor div textarea { + border: 0; + border-radius: 0; + height: 4em; + } + form.charsheet main section.flavor div:first-child { + border-radius: 10px 10px 0 0; + } + form.charsheet main section.flavor div:not(:first-child) { + margin-top: 10px; + } + form.charsheet main section.flavor div:last-child { + border-radius: 0 0 10px 10px; + } + form.charsheet main .bkFeatures li{ + list-style-type: initial; + margin-left: 10px; + display: list-item; + } + form.charsheet main section.features { + padding: 10px; + } + form.charsheet main section.features > div { + padding: 10px; + border: 1px solid black; + border-radius: 10px; + display: flex; + flex-direction: column; + } + form.charsheet main section.features div label { + text-align: center; + } + form.charsheet main section.features div .featureTextArea { + border: 0; + padding: 5px; + height: 100%; + } + form.charsheet main section.features div .featureTextArea > div { + padding-bottom: 10px; + font-size: 12px; + } + form.charsheet main section.spells { + padding: 10px; + } + form.charsheet main section.spells > div { + padding: 10px; + border: 1px solid black; + border-radius: 10px; + display: flex; + flex-direction: column; + } + form.charsheet main section.spells div label { + text-align: center; + } + form.charsheet main section.spells div .spellsTextArea { + border: 0; + padding: 5px; + height: 100%; + } + form.charsheet main section.spells div .spellsTextArea > div { + padding-bottom: 10px; + font-size: 12px; + } + form.charsheet .ml10 { + margin-left: 10px; + } + form.charsheet .mb10 { + margin-bottom: 10px; + } + form.charsheet .ptb2{ + padding-top: 2px; + padding-bottom: 2px; + font-size: 13px; + width: 10px; + height: 10px; + } + diff --git a/charbuilder/fonts/glyphicons-halflings-regular.eot b/charbuilder/fonts/glyphicons-halflings-regular.eot new file mode 100644 index 0000000000000000000000000000000000000000..87eaa434234e2a984c261e0450a2f4ad837aa7b4 GIT binary patch literal 14079 zcma)jRa_K6^zJUrQcHI&-Agwt-Q6i&BGL^KOLw;{-AD_FG)Q-gGzdrvN-EcX-iP~g z&*b^eH{Y4xyv%PN=0ykqC=mnzkp2}Ez<(I(fA#{~JL1@9|&czbr17 z?0>QUi2(qt040DrzyzQTPzI;~05<^oukZrI|7re*(tmmX7j^o_^aj}eC*Svf zS8xM_|1re@Z~iI2{-^mL9EX2e|B>GY!1r$^_@7M#!2iz^{g+$h|9j_j|IfYw09iey z|2e7uJq%=kUm`%z3m_N(;2I^EK8c@Rz+WzA_5K>K_A~&N-y3An#=6kB0L1`ghg@hn zZl7)JRrzdfN4}^l((rOb8!6cPsFL3<+h>Ko$*N(B`~JnKcb$DjB~XQQFl-maOT7?| z=??-O{TBG@KcAzmSNxsJz-Lt-`@AJr0kN!Di;SF6C_P<|x%6Q{;498Vwc}wHl?UCr z{Q~3fpz|ayjwAvkULRl`8oaqCD1Wz4@8$~fj$UC?mYD}9H~K)mrxoe9!WwG7+6D1~ zu)}%fLgSy{-z-;>e_xUdTzZz=OI{SZWnRf9!Z!c1f25WUO+5X9vri&A$czeCIfk$M z9$(eLNbUdRcqZ=w)1@@tN<^z0pQP-fOfjvjK3hvorqiV%Rl2xSOKU%hzr6ahgV9*$ zJlgSvPU509MBT=C+`yifpkEyy8#9c4UL5|r5gWS_tr}Av>(G)ZhAtjcTRS3?SSA9N z_Kegnh`V2N6RU=69p<{&He6g~O%EZ5+2OH{@ca1ru$Z)c3E&|1G!5~|4CfxK{)bF7rn^i` zwcKpWlzAHWR{;3USb36)e|%;$T55rp9tZ<6==s|-B*BebGk#$IYB|(ZrzrewrIl2Q zcVZsN=FLe{6k5m7YDaR%(#gdFf#BlrKVjI$R-nNKpd*2(T6`_?7Tr%rq~E9(yIypk z15x#%OfK;;uk|PQR~)DEppbSH6DmW;v@k*#ZhaG5{w7e$S`ot*K<^C*oB^co5cNr- z84k3(uHIXMy>++r-IRV%?Vpo$*r`8)jmh{vx(My9BI&4V4t z@q&H_L`zH3p725(a{oTG;rYk3%_{r*|8>5_6G?cTr)|U^XlDg8z zm^W6r3{qR3liJadUw%-DfiMsiV2YTxYOPA_X1lBkNTo&NjbQ(_zP!Rimikpp%G~h_ ztU^LLtxb8e!>D>CG^8eZ_@-EFi+JA&%Ym}4^tY?&sz92_hbFAune34RX{tbjogYXK zb;~ja9%4IE{_iiY6WdJ>_PH&3&@yDo2T(p1E`%?ub^PQ3)diW6ii}#+*!=`BpbGP_1R+t&;29S$UAcpH3h}2^>rGvH){c0jJtjcaSiIpFl?|Ykw|FXrNy% zn~l3m7e4&RgrOCH+jCRW=Ls5PATEyA`J8Ad?TVOG`l@pE({KV)pF3Z7;oa4-Hx3nk z^j1RZ{N?bQZy$cYv6=A&0^)qVweZ{+Bno|~E=9j=k-GDXeQ3qsW?N%I&@}1?wxuHf zA|Ro-_+d*C6M-#@VpM30RTEPdo!APpRrFObUDP^Ic|AJ;)&LVdnWX#RxiFb+zGKCQ zI_Kger%ADWvepR*8TGZ{JN(1K9%&P;^!XU4tSvkgGe_{JR~^f9$<0Tklc96r9x1B=VltaV_PCB77l_0tL3{`BdedCe5j3CF zO*e3HwE9GE<^LnU6k=*E%b)otxd+9+t<9)#+ze$kGPmX41&oF?8tHV!$ntX{*8aX^eeP@F2xMvpFGcra42@FI zDr{tW)yt3)P*7pvoD&$N2UDat?KH#6Zr3Wj1ocGNeW7Gj^2e)tH;o4O)FyAx_b=b8 zd=9(x+S@-Ai=UJC?i@DuZ0CtTtAU!S<4~e$K4CsxC85Tve7fHoj%T!vPv{JHch5_Y zM%K`rC>1Uk_m|u`%z4L~W*R<1JgN zI(cyXr))hytWI9~bat*Gf;?_avFr#*aq=$;3DEl;rBBbSfL&s-CmEN9Z=FWBPq|*w zV=1XfmME`nZtgN@DBWrbTSnz2oWcA9yL*=L#%fP3TXt!c0F%_>FvWM9H}5Urg0WkI zNt&dRN)2J@03gGYXLU}Ws1SoLa(2xNG04O@u`3C?42=UF%K^ZmD2OcrLpkyPD{zkZ zqZSrZ%U#vZMaTD{N9>OdGG?lPL;z?aQq&oxZHacwkYDWEjRc9X)Mg4w1*sqqdytQc z;>DOou1OedrNNb->@o%dNQsBess9-iEOg6MCTz%8RuuTHw%yfj66ap};<tL)BjF!!xYDU^iC@^Rt2BMhA>^Oluv#5vBd^doV(|U*_eW!Fpo^kadb~1qfM1 z-4xV$$`eWJMc%3OjU5A{fCA-11x&T35;A``cBD@_K+AfYp`ItY-nO9GFXyk(6H&gC zgVP-%-^o=btFjCC^slGFm}WC)1Fkw6WT{3uKjkNm`0Q%U67%Y#OLYbxB}u8qEXyBf z+jt?k7GWf9V1;7X7NJF^$kk!j@XFwhY;np}TTfKNM)sdEtVZLgSNz~z0}w_y_MM$P z{7ZPot7f{~deqdkb!?PO@3M6uVpZ)~0PM!uFW*8tGxGouYU+idM&+mch>1YWrfYbw zNHh7S!OA3^0A)hxl7xkSusWMIn}pAG7sVY<1G(8sqQS{%57LmXJp-HiSyD=l$*Riw zY+20T)}-|#pikZ7^U!gc1p%vkX1Q*!C%Ns1AbUha>5MtQHVJ(Q7;^mZrN_`4&gR#d z*GMiPozmbFnk7GQMUfb1z-LiF4xQ67RJ<1As!AEvs7ht4PG7P&xpL)JUK!S%jeUiX ziGEQ1j5YCz%;X#HVS2_}6~%)EQ*SZCzV-TqZo{O6%{r8|Py{vm3>zZHrnDT-D+S?Jo!n<`QZ%7N z6#HY((OAs1v%<)LZ%T1o@hclr9U{s$FY2`$#A222+iwA0^_ZWa}Sp$~Z`tSRz?fYd)Prtgp>DC@x&win* zYx)}AGLxzuz+^6ox_-KQe7OJaF4>UhEn2<^kp=1~zSKf2O8lsvgwt(+%dH&YE^$~{ zmIZuN4KWfnT+eLo`$Ntu+@_4dx-xCn%;H+*qI*rz{Pj+IMWV4q&4&v_vDJ?KnuhT? zp`HFH-{i7G z&cb3tRVzJC2)Aj&v-_2I=-cTnDad;U%gi?|r{%q8M3=JWIA4A_$1xksNX8fGQ0MXv z7jsG@yqP^YVXh~FGG7ztRofbb%v-Y2Oa0c4{DoEW2+ghB#=X?sC)zOnd<$FcA;P}k z!&0wB1tjlcu)sC=F=AuzvQsD3oXvch4Ur;5+K@a2;bjf`X@%InJU~*7p!QXL|3UP=)q(sV!;RVRF4eC( z5w2y7m}t3+flB}{o?fK>I$D|ykMw@kZumiw3J18$_+UA|-{#xqT-R~i?db}=&OhR9(;d>s&5GJ-M zuHl@XB;EHQ^c`j#mM47s|SScy-SD&Q0s(780*ui5*B(NU{ z1JAM6oymA%{(T`Qwoer|4`e4fbXpw=Ujf|X8hmq7E&vxv*}=+Rye%5X2xD0*^}YEf zEGd7~le2mpyS%mw8xl44hIvof|Pxp1T*z47AL}K^XlL>J6(gyYOmc|;VYs(tHAWpG7 znr9Tel(H$KV%()2(VBNVoP!o~|Gd)(^S&Q{PCqTk&dV;xZm_-lB_hr!QE$$#GqKT6 zV~RS4<7x-=tx0m&jE1BDqd(cc2iA@B7Ib0!{b&v`-5`t7XEV6UG7WdVy)z(@VR3p< zDC1lTpXHX3oE}5E3V7yx^8>jVnwr!w1_he&_17RJW+}R?{niZFG|4RyT7ZmC!Y^% zbR{57inS^QNGx!}+P3f7%?Sionp@*#h+8;FTaj1>q z1~X!#NO{YL-6+QR)z_o*SW%A+v-XebXs8&@TRzyDRieHy_t(B}bl)uwdFg%YXZ-^# zMWTYOwIkzv%>xr%$CBM=*m$T9k}!UxqnsS6rl-gw-*rU&V2or^ZkP6vPI|0njAB4O zn5CyBPHvXL)29>zpPkhW{`Qw3B?(G-TWfAV0^+}Ji$*Wob6n`WzRTBhd{);=mfm^% z{;`v`S>9Z(j2Nv-VLKD3~iA$Oj{Dq0(I z8U*-!Po9%GdOD|LVS~3(q-_)biNZxTiT)GN)YVr!4f4IRLNhAD48qw@0S#E{-e>UP z!dWH9**gQ$DqT?TkKNJl#J(f~7r6JAfSveml{UZ6jueeC&zR#Vi@e*Z==rWJgp@xj zDdR~Hd=3W?q0l(VMfRu(XreTXK*$pogtsuagZUmp^U^=wp0PM}Wf8W^Fm9n^8S4AS z7GJfQqzDgu-5C9o_f0zKKx$9L$|nGrE2rf%PLxV|c5LZ}PzELiSVok_zxZdiw78@4 zczsV08yXH>t5P&u(+XYPsiu48SXe7a3yEBGFiS7KFN#T`R)LMID_lZrUwvIx-Jfbw zW&lwFFkZK~+S9BQcb`8iqN%$0O{ zd_R#~i~MUF@fY!H4LxF+H=SJ{%h^?na-7Yogv2T6317oP^NJ}Jbg&)D&P;P^w8oe# zDNHRAqcPe>x zP|B*V4YPfm)deuX7-N@-7Mz4N1KmAfyYI78#jS0>Bkd}i9TWLsIZgXQY}1jqm+pG` zy{JiBImlPiF($3(sE&p7ntgNWLh&&5y{|mea7L8%c);7R2$T z_HrZz(`Nx;xE)NtPgF(IH0m#(y)Npg}NBkIWpJb(OJq&ymq^iBIHfZB+V!qd}3EnxDKf_XvD zT3tuka_2>|KJ_Qr(qpGJAf}w3%5Qo=u)K?~`O2CzZnMD_J96QGYE`74E@)I~ODsKK zH%}vL(dJC~ZUF3t99-z<+)r4yfgnU{Y-RryR^-SYY95;xsg#!aUC-Afy-0t%`Ccv_)YQ)A}F@oIMmu2ZX7PQ72ukwf(Cvsr!%uk z?~fxQtYEo0ehCIE`*_+|rxqV~hPV#FQyC(#HP&p@G#fKOUMp?w>)uN0&^pgnu4xwA z{+=Wo;`6mUi`y&O^6j1|StaDJHzuv-uBNf~cik{Jl#-tM_hJ^k+>c0kMduSMRtVAB zXTfh&yMOb>MNO5I1PZ0o!i;G4!y_^YHKHq6oX4a^KR@ocvM24QDH>)gQ-zdAXg{pR zt7?3h$uSFFv$4~lRcBSlUCKIO9p9VFeN}^EPQrbB!iSk~Ba2aSpMlf7sUnT!2PnKp z*Z0Gpr%sIM*x*BP?6E2Zk^y$a@Bl!Rt4YArYn_Po5M;&@gJz097wEglfz`ESLsIET zBs|I>ZJ0yIG}&DmAFB*@>{;;yJ_vO?f1N3M;xsLT(}SOFekLA$9KWf&-oNL?8X4J4oyU8tKa|1>*wEyh6Ebf)U!Z zYdS#`zoaL-RrPmx!}8501YZ{qj!4m&Y7SrdF&73udbUZylkG?gV+qAaszsvHEe+{D z<45m&hYodO2}g4E7>W2VeQ&n7!#30RJ8KbdK;T;5$lg`8J^y4jw3DP%j^Drg_woO{_t+eT$A)(~X?aCV(oI(=tpI1st*S@&~g6?&k z>s|?NRJcDff1`1?-Jc?K@U3-!Ys+&;g!A9IYGA|)zLH&vmifA**}mdVQFo{e8U~b2 zO2E010oyxaVfzV>!DiaH1em79k8chs%8c=txP&UaPiGwS0WcWl(|%w+^T*t*H|mk8 zz)Ak3o-PR;*!0I#w>D*9!+3J9$A|8=Ap!W>(U}g$h&Z!YOggAp^3=wF!Yaz_P($@? z(n!BM5i+f_^FX8~nrY$)=ZBTKHqm zVdAIS4fs!QL{-!F1~xy(})Hxa6p?Rjwv#-#Pvf zm8TQQeBr%Pn(2S+vFpu&c%{Rrk4#{RycSckZsn7q)i-C?s^e~PurOnw~O zv`sbAk*TMuA3Lo&9S}C+NVe+lL`zRzEuw^L!#*K_R{1j-SsyFUDFnW}3R%$ zis0vASSvzW7Jd2#61)h4#M6URkA_A3SsK4n#`cE2$ zLWp@8V}aGF=zO!}e(^Si*LlMGu3Si8)@_u+nrICpR-ng^i~GNd$UP_6*gd;57I81d zqLuuFat(5+->FEsY>{47M=^M$XX_r^DhHhyoVF&%)642YK9oHn`28XL@oD6zTRCr_ zQj#&uvxDDr@MK}Rs%^cX(zMsDRa3RzUQqW?O#N@x@1442leTwu=(D`c&~bPJX1eJx zR}5A8N$9Bq;W2HP`r4=%i4+)}>MCN-g9+FaIfz4#pX3o%gk8jR#?u%4F3+u2WCA{+7b24rYuJ1 zwW3Y9w-Bt2a(91Hcuj#xdB*q8Hy&$|)<1KPvN*|iiK~tq?ka$u;jeH>1QR}^dUxIFtyRN6z{I4L_o?enJ zFR95EMp$tQTUr!1vOm|XcjELh%@1qHj^++_t7XehC^Kxgs_HUQqFOBndGbf*;KnrP z>1BrQ)f5<&={TbN%QdERb6ljEbbCGjdd@5M#n06;VPP)$ z>chCAA@WK55n7o^L|)RL4<9m6lWth#q>&#GG5)ftZ#UzvbU+$2(jP)!o(zaw#;sdv z^%g(${-K@o670tu4>IZELt3#`+>9j?qf(`5Ch+>S&;~QQKzkSNY)16RqV;^f>T9$m zdqgaB84{#YEI4zWG)0m2{JP4snKf5{q~3>X2#QxOjG=sO9EHimSic@4V^<|@R-5Hy zEp^BF6R52jd09ovYpsaxywq*xnqd^%9fxrz=LFuUgxW6tSBC@dGWefD{H&>5oMjlj z6Ud@Q2;X<$!M}!W1R~uQvtTfS6QH%6nlH&~+q&RAWmVP$rbyZI&7MJD!MWh1sb*t; z&V+sSq(hi;g5~PTh!VqP_4Zlgx`%k?t19FqAJy6{$9?t}qv_oZP(+mjL!&s9hsSi0 z`1hZBgO1QyH=#|A^)bdk-w<5x6J#hivLy8_sDXLZ9cyp#>1cVkuO~R8$$=T!YcnR* z2IK3z=tD9$YM0E;xMYvjGX;DYEKeMPAY0k(Lwzo{Vh7}c15$J|s~_D_e%+RH^Zh!m zk4lp6r#OascmM8jGUcEAXfHU(neLo*wABl3)3I;N>=s`|zJAWwZHZtQNH-HR7WUvwmZrG!N z6@C{M0eWXL%2LZxW5tb=HS-8XP81s4JBB@;v&wkf0l#Qa_S5T7lahYrpP#_4z4ku! z%79{Wf8-DjEOK`d7PC)LJqBs(n-#-j1cvFr54a3Sabtu+VZ|9mz#=H?Or~eqxl$PQ@(j-#K-^vA1?!cVSYHiqjG%wgoo{ z;V>B_%aMBK*fx*zO(E~G2V^Rge0k6DE6)El91p>sh#YPjHEIdf%#qo8d;2q;-PEL# zM$qSYuUAeQ2&IGK;PK6zotMsO$LC!pl>@QKlp--=jQIkEwD||8ke1rQc)#gAZCdSP zbp|sBqb`OyD=c13US7+@&9PO~KE57bfoh^{0jOecez`2lpKQh@(KW*IF9t5p(vD6; zqC<&N{Yb0E4bC_{JpkUsO@rlnQkGCgPZc&=!#+=sq3)AE1cd=a-Lo&kH67=u3f~^x z$gvF;{hY5N=zW-MGNTT=kuvj=Eeje|_OvDefcre>sl=DrFKM*}wkk;l`}4haQL%D& zozLBx7UB^7A2;9x3fXkFDG|nU!vVTV#n;l`sA<8?C44E$S_CvCJyIKcbBTSJm2-dp z+A@d77melYFx?WF=8D}pZGaBq7o{5e+?i$`$d&UL1MLb{9o$$YA(U~As5FJ(o8zOW zjycOOtBY}?CJP+$sVEXp?BZ2aL1i4K0obmwIcc&4(62jbW8swa9f?DjTSetJS_F2B z5Z$cKkvqo(>(e|^<$|2NpV%tz7CM|Ai^m?Kd>Yu-{R!v%f8RBr7rWNtfZ^9vKm!u^dP~TR}A-E{C@XK9TX7!)BcW+IpovW>PA7tEh)jxk?zJUM*2{Y zN?T}i@F{LR5-+vp%IKQlcB3Ym)7}cJ12(U+D}MPeLlGDyvcfbe8%LPEy)G!?=e1L= zDJJoWSy{8;p|+#$)~16&EB2)`e$!tX1y-N{WXm?gwG*OnD!ci3u-9+(iLd7=7;7jR zmcY=*?xB}|#asYF%EX6t2{+RK&4M4{66KihGOAs;ij@mK&3Uu)3^b|?B;3B+z!38I z93x_C6}@3&mJvH)!lIq0oQQL86oWy_A|U@GvyD(NwO$c!`%U{`)TMN_Jau#t*Y0lu z0c4~`*Vxk$tP&+W8%8kVnREOkJevuHD;AI8ltWOEzPR%_#f5(Y$jArOxfd2TY42x( zvdviv@hBSfQLqM3;mpaTz|811VlQ7jQEm?Is1NzX>fhX*)3?iglf#v5#%li7DBSDs z9yr*Son&|AfaSp^FHcK!iyS|rW|~Ho3BGnwfGSacSD-Pd3HZx4^Tn{rw@X)t0G#!L z)6pFajr<=k25R8M>3^D^?Vl5V6+B+5p3Y=}-8meaQr23s5Ci^QiE_I#JND7F{`x)Z z${rPtj&q-)Eg1mQ&R^d8PLmmpTs0_NfM;Ld9p`~M`3B|`d)KSkHhIgWGh4h9V(M!E zprOL?IrlHS-Zj#5YaezY^EfJop++5!6~dG@VczVZsShn@a!H)^)mLap zN-5d|ZA^-9-}C0NQY-(>WWq2>z$nZ#9f)04o}#fdrZX(@%ws*mvWvY{x|!V;M+h(u zc(X?j+n3l}NT?SeX>yk#wP026HlrMO$^jJSY9}JbsQW`La`|uCRVgB?-NUkr!Q62rlZJ0 z4(P@;r`r%R2v%XcY4gwA4RY5cS9^>;1!-;WRHH6?A9H4nS~L6+Erf{kNRARp0%v#mG!BN`{Z0DT(;hL>q2tUur3n4FyKJATTZeC)I7~MlF{vYq zP#u$a?65CY1gX<_^dpm$T93g7cEiaEzJi=f(PP7*$Cf< z3e!q;mMXoy);Hc=X!%VmT-e!^igX6GoDK`Lrz#=>sc zkvcN?I-(oNR%$y<5v;+H$CX{e0F$s;-Dc+ckzFlEF7xK<7+Ij5F~FWrmDWsXraDch zDC0G}@xv|q?bH-m|Mjy0Ms)dZNpHw-DvLp2+c4S+O0)kVJ7zx(o)JrS?zKB>t||@D zeBgbVopB;#ax&umSZS)xCuXSI)HhTG6R!eRH?)QacpQ5#6L!rNa(`x=`VUEj)U|nB z1MMG_Tv{ZK#mpijK)fq&ckNP|V4+@K=S)c}ve;M#Pdu?5l^rr)DvUwV0PT?vKYzR% zGPWilY;hyPpFoR|5JP6?I@iC3Vq6S&sN@s)yy2Kk_{_=#E{tj(A~6Gn2o~=^zMyvs zejH=*na5H)n8DO#XSngd{F-OXphTbN9bu!~RA1@WgFi`~<6C$z-&Eg~>%F!po2S1_ ze(jCXcwQ%!S`|5^h}24Cf%DGYlJ8~b8L?zf;0`mM@)Jd|9&jr#{?*Qg1XJuUM}jTV zML9{SGQW{o>!LsKk$gTo3em@>#xK?}8b9NgS$?dN7ub9st#1lf=`*RfERqiz( z%zTB8hI6(Wpm4#3HbZ{z&OHArOIRM>JR?w6>jxW$d~1R( z8=RTg(0-+#XZ>UEu5%s=xiU`S%_}9ZcU{{C`IHp8yqFeq7L^5hHPf(B>{qz0U zx75z&dEB?!YvH!0%yFPn0dnvtlCDFL)%Bh>h0|%OxMnXF0(`E_T1cWldfPUNA#532 zF_UFlhm*4BwrzGZgWp~l89&g1;$Os_(e;Y|xl=2m@`F6(@A7#Zg$6~4{MITfoS(mY z#oK2mo@6)ugHMq+fCN82iP%cl>0rRR$+U-6UX}VIBZ_N3v^l9y2J@~+nXeeKV5tl_ z58#~`c(ljwfpHzaef#fbnkmRlut=er45g1&uFAxlaV4_Qd(S_*vcPY6fo5V{29CqR zh0CQnCWemD$tb;75jw?v?k%iaE$Zb*lYKU|?cRSJjsw=kp)Q^XpVWYrI2cu!TG~H7n=oNXG9I#<8 z2XoyS^Mf6^!*Rvnvc8xyFfpcXmSrE)F%hEOCa_GWBD#KOV3`AJX5v%eZiII@eMG4w zP{6>u6syX2q59xdCM#LN@M@N#|``%$kWIB0~(ROY~Ve=g* zNO-8sq+gRLR{DVwQ!Jfm!U>SpZI$h+6PlG3&djhh9*Vu$hD=4jV#(`EepWBB)od_U z1z*Wewx!;!ADjqaCwDW1G6@8ht6c*A{M}l8%l0jf?jh`J4b);-n=1;fmgB)4p1;ZG zDDk{q6&;eqX;tp_US%-mWh|)q)i{eHZbo|{^0}=bKxC@sGOV$YXz)91vn7~h<-uH& zQb0dByDZJPD`EGPd`kqAvI?*g=B3fqa9H9Rd{L`va?B=t~Y&l0h{I!^E9pG>!S z#>{UpLngb5T`Uqt6sO=~BOjkJh)+u0qiSo-es@5}f!h*a9Gx*&<5{Eoxc-WF!jSyn zM@qOve{Y;Ok^%FZK{2K;y}YNN_;1tethBv;U%(w z%RNe4t*ldJayql#MMurNnNoO;%!n-U0V4mzVpPdGu`LKf+RWv>l>VJ zh|rXJv9Mk&iDk|e!hBRh$KiV}utL&NkptF@GM$|`tR)5FxIigOLHS7vqDnsGiFl7bTk4baLCJDyHe`hWp4JT~ zxRJRy9oc;pw2eW?wv3s^8AsUEk+&zZY`Ez-Lo@iJt=-gFZhS`U&Ct+KB$VGUar1N* z@v1?8ygBYN+o*ZMCgDHM7MC=Korw86(SB>G1fFAvHmj{-oZNU|ZY7bG?7% za!4;s_~l~@pOTy7Zo^+6AY`23W==`h_ME&XEh#dIqn)Ei1rAP5;j0oaGirRuwQysr zBa#0yNX`7Po5nBsn|`gMKsYvFEKdsi0e?F_b6jl8h=+@ms+m|v$is-!NWtw6(@?$V zl_q&yu*vK7NYkl6M5O+M8>hB}h=2U?wrE48%##YSN^?I=0+$V|M7{IRFWf36;()R* zxJPdQDzTQ8c-0|B0$0G*)swoM=@rL%&=A*ZOgwL>7z1a%8 zFKtztnNhe(UFtdIA>1N=eN!pq;(cN?j@4UgtmpU_OVf+Lt5A!~Q-4!7z4rNbGV*<4 z`3S~~rTA$L`Bs@(J%h0xlX-Cme-na$&VA?CWqV?s!6CpeZMEoe$7DyV^%f(Y$CD^& zqb+UVeb3zQ$3puFCqi%M<_{j4`f>6W>Qts%OZ(sH37e1+(`!sDT=vci2*%*lcnLfGx#FXv!uiQm` zC&DPMh8FaCMRu3k7P2;P<>)CU&Sw8mr%`j%w6%l28(zv})E#p^r{~M)l3_X_Eef#9 z!fgwyX5@Oqx9=Waz>)cTxBx#FRZ7Q4&|@q3fbSjP*Pt|Bw)q1)JAG_&4Bc0~QYI5; z9l5@3gJ7IgX2*bCLz?mlb1Z8!pV-p58bZOp4MrH)-?C4BM%`bn_bw_v8c^mNSm=5N}{I(?E;74 zX%b#E#TsuQAAXq1n>W8vD~|I|L(Aqg?g=aXtg!r5BXJq%+P*yi5*0j^`Ml4I6;HT7 z5db0$wG~_=*tJmS#%smF=#xa&&Jz8fS=qB8x{B|9vz!fwmKbQU8&%pTg}ZM=3#kzV z_ZQ6}eE9}~T4%V0Xs%r}Jw9AwZlZ~)%XtE(9Q39 z5S-nO>sGi>EdT88T`M*cJ-QO2)(J{jpdX2j!noU=B@Ze69N9Z*ygRJ((WnKT=0Xa4 z5>HTd{3T)O`V-xs9(FA8^R$B+<_d`Zg!1rg#WK2+HXS(SR!(O)SwKq@O>%tXdp}KT zpzS>sB$N=B!h1`B*_hr3l_}mcGqYM@5PwPL1j^?PC&BQ_KvG0v0}CmL3|yC_fNyLi zaib~0C!;PY#bDnTXvPWs+Y5`ZCeOAdxX zCQNr*a)lN~1JDbninPT|6#xvPr!u6P!D6j#QGyAlSi+iMZzAA8s4!|Oo;I<&P#87f z1}&8+%t~ev%@`NRwfE8lg1+grWmTX#j0Luf0bat{$*Vv6?Oll&1AW4N=p!AztoBEDh8Zbul!(v09dV^(vw_m;E~n7Ix72vc`pWtfDyKs=Ist`7lb zYP5YlV6WodgY`h z&;}e>0a?Pt@c>>_fJG=UQ(rXrUsV^iQy0~j7nOpEOwo~<;9xV3M&qR&z^trFp|Dga z%#afXVTGYE$^|P&Bhs+bBC)Q+6RvGR*Dzw6Fg8?xZ5*HlD1 zp==t)lZj-JiTHwSbr}Zi=tnw-A&Z3toC4Q#(PpeD$iv(YfbFqpp>$-%VOD!U+gMaL z0Fg03#R`b$j_fdp`mKrB7p7qXn6*PHa>q32r&t2sKcoxsl=5LGrqWU=$$(DfX?Z*- zZDL9~XrfbHDB*7s)JG)=$rjZu)RQU*#d&mL*HpM3ux+Bz<4Qp}-b(Vs)G51Y8=Uo+ z7zZlqTu0xvo&(e>I!;k&;b#AbQzV}1(2(z1y>Fk6KE@waF^Kq{d@b-3Ge{J{jt>gwJni6ufU{X-fc+B2-`YjYGsmBSgS6oO)Aq; zI7J~w=8hx-a2*4z3=5D&uDPO|4O?(UBedeq1L}`~nEDmC0d1YYpF1Hr$ZOS9QLtrp z6nW>C@!SbU@@ZZaznY-{-@R|GhS4I()!-?p@Vi*TJjF`oVea-G1XNzd! y-^Vp%pcMc>T*9)K0*lM!C8AZPg+G7PFFQ7O_Sp6RwD_p|> literal 0 HcmV?d00001 diff --git a/charbuilder/fonts/glyphicons-halflings-regular.svg b/charbuilder/fonts/glyphicons-halflings-regular.svg new file mode 100644 index 0000000..5fee068 --- /dev/null +++ b/charbuilder/fonts/glyphicons-halflings-regular.svg @@ -0,0 +1,228 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/charbuilder/fonts/glyphicons-halflings-regular.ttf b/charbuilder/fonts/glyphicons-halflings-regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..67fa00bf83801d2fa568546b982c80d27f6ef74e GIT binary patch literal 41280 zcmc${2b>$#wLd<0X4JKkMs=IoY9(#guC%-Ix~!LV@5XgawLzwtVoFRi&4B<;Yzzq| z1QHw)z@da0*@PsIyqA!`6G@b6oWOe_b_$P#@)GbXG2Zd-d+unfZAkvV-{LBX3Wc;?Pswd9i3FaAXkSUrx`&zn7GF0_`M^SUUB}0?t9iO6@<@rQX4MYaNTB6W_twTb8q4L*yS58+j!vF z2j3Nh`>lc?ZQXpu)z^G$?&B8=!spQk>+PGb+PGPLztt}YU&eW%aO!9EjS$4lmWxSf0(+a;I;S#pX$!?81r zPxe(ID}q`APM!R3^`f;)g#n@JcY^fY+Km6eDgyYBYd&V!e;1`7xevutA z9r7HC9qK$ZaA-Mx@w`Ku58Zlb*I{&GuRWclsyf4l#;7ri09Ui*6RHTP@wSWT=t=8ZXH=9myY8a)#IAo_0fKca`D z*F~?2UK+h1x;}btbX|01bV+nx^t9+egvQ|i`5yx>jQlJU@$>W=|A&(_6vm%?s-YdZ z;Q!}OV(bZjm;rz1-#tQ;_`j;qrV74A>f+@?>cTDSR3S05S~a&0%~;2e-Lx)tKxMv; z>UNd2#a>sPt?jDVwrIuBoW#0#yDGI^Tpd#fmJh|%fpzVw+(uuGC*n5@{id$Gt`64? z4cEQ9t}YQ*O|3)f+%4<)iFNDnd#1Lkv(9K&&23r(y9;-Z-F4Pkb*g}$v9xK8{LsMY zA#0mgiS=dLRa;x^Cc4QF@cS`UN-jvmR5`U!6_yWe-?)84j5em!#pCPhw)4Fe#va|! zZnVx*=ZWJcj<(n@cz2v_v5abIJ!>cyo0pio;gZ-;tZ<(36Leh_-5IxzZI8{{K6gW6 zdu)4x-!7pFD~8koT#5eCZPkH|w1e-s_?>1Ptd7U)Vh6W_4EWLlv~6{zZD=1ZbGId8 z2P-#E#D*5Ftc$B`-OzS)XhC9oBDQ_O_QVEi33Z3wsXZPV1}}y|p$^c7cTxw?(8S!t zhD+9u?+Ja?*M?4Pzmv$eu#nhpQDe)8rq_KJXZ&sZgaI}%ILH=#(<7WO@OQd+HCi6q zzG5hG9$KFmtiuOO41)3lD~5_fOqg~4V3EZbKGfLxYR$%a-ctNxpiRY5&;@Vp#E_7w zkT-73wkGUcB*ievEJBCIgv|7!MHb)9YG%{FPcKR$HU&+h!zMahw3wx1(~FFb=ajgT z%qfW`HlV-tm%m7{V~3g`k(p2s3i4uku@Dj(1y#tXRXLTFRY#Vo)fv@yP&H*$Z&|fu zwHnqcbawfA;^}-y$tn4eB_4=}ENLa7Skn0dlb+x4dBA$NMe@P+tN3)UA)gG`7`p@g}ksuP_r4esa$Nz(oZ#Y*myhQ zydBZ3YRahfIn`WNYqM$~qdLmPfP*d!c&KGlGHRZ;tf8!hquH$5;L+MytLn+B9c9&> z)%sYg){s}cs-;hDSBj2Uwy&>`sF=@n=M(u{Z@xE|4FyAq?hY~0;1VryOWYj5TSU%f z`^BD|*kB}m6&MwIx%*C_4-Kj)_rGq6J%mIJM#ave| z6W_b;$tSPtXlr}!^3VTT99+%bTYl9u??3I@aP6-itZ}+F;Z~$u6l4`VD`Otmv91d} zER<(S#b#32t`d6j;d0id9}tJcA&h=ofez}MOMLIh@MGecx|6jH@5S#($3Hm!f&3l$ zJD6Q&(h@95us6di-`kyGsRm0GTk_j84vH5XTyyaJs;URwjqa+=zdhYJa8^~?^^8KtwNh&Fei-jtC-6@O7#R52HmK*O{ zb{aZAuyEO0ulKHHb62|T!ydZ}`=7qNxi+xAMLg%B;s5c3YOm_eH`jzt&r4U@9n$wC zpM7|lQe8tUd+7K(@(<((1)oqStP_e*@>*4IMh%tKx(s^5)cTCd4yu8&8t{;8P)(Qv zVE3AU;@u~S9&cl)PcOVYDiH%eQKR|9}_GlobT-NdeEVO-@<}^H#0Y+ z8Q5L)1Y^CPR4l~m!D{tOS)0XjnbmLA4_v#m^vM^Q_j}*d-(&C6IsFf%o!9CIaPl&X zg|#geFV+9@;`eX`hJ?@aA^BN(won6(WNK|j6%Gd{TZs`|W+=eeBozwtMwk^=|gMSwn`IzBM5z3t%CUFVn_xPg)&+-Z}Nm+_k}F^P&%JTTTZ;stRF1+?)Mjd z@9iZ^PjW}`nw`J<%#J^P=9j)n&CF?*>`C{+zjvK zuNOv-VW}N|3CU6jr(;`3FW{u)Z?q=6LBotNQy3JAAabkPmIDEaWZ{fDos*^;yfMJ( zfi(x~V>RAAS`5<>L~AaqQ?lA=oNs!R?p{dTU_il`#v4*K7~%2z>|@S{!3BYEIG}H) z_pxnpX#C#z?d;e^VeztYJHy`@w=?040O^T8t{05-eVK5saD{M-a1YjMP6ciHrCKltrL=JU^%w? z%G&%P`t)e)acuLg*uJ=|U3XVDtKG{fM{{8sGiF08Ye*?QAHB~$=KSRE|D)H310@=Q zQ@pWVr#!_^eBAl$=-)<^As zJhjCaXt;)F)BDM{$J2alXh-S%@f4-CE-W<2@5?O&s9@VPh1%VaGs>!k%%NCOX!q7hU38p|b zovTxd{u+j_eYEZ&L7wLVxj-V2==n%JWNx8UD3m@%8`0O%MTNo`?Y_YEs;F@G1lm<7 z6B|dFie`mXi)&WTk!DpN9@opsy47=}Th&KCR=bk0jD2*^NKaw!Rn)8<*XyrZg3!aP zBWl)*%=02T#&ty@BtHoKp$@D49Dxi+JJ#tozAjnHMJVYQMGK5M)#A~d7;9g-==9M+ zC+sLPnKY*bgA}T+PoUvsAa#550cf*+sDeG+sdP`!3k^+d=n$DPfw7($6FBsXCobH2 zl%02U>xEDJ;>?F$edpDO&Sbv{2MRQk@FosD&zkxl&zG*#jvm#nE9D>W*MI%|7F>mk znUk(EmLpgb1%W{>X`^~fr%;5k(W+UUxg1kH8C5<=T0J^pMJF6Ela21U%bLQaO&%6D zgK<3auK;7Dt%RX3F)~Ql5#33aHxvaxlcG>7)XBT$-NHQKbm2UK)a&JCbx}s`1@%^N z>dh~!^F7)U+zkubO3-P(KsMA2u>BHcpF5E2BUWhiYBd=cmfCW#yk>y{qb^eRN%8a? zI@{~jT2CW}_xYn@Fv={!P(BpIW-dEZ?48L%z4>&$7n?oZ88MY%`Bd7HPGK|A;1YEiG@Keut^O%am$rsLQ0x9U0T7rgScss@?4KCe!Dc zCnPOzoBkzKkurMPR~sJlqu6;PIcA{-F)-Vx|?r? z`d|?X$B)aZ$q&7MOasjecMHWhX;F=^_B*??Sm@K4VoSC+2X&#Y3>A}<3RfGBXENMw zg?V3lkXD^WkCwy`019a$&9s)?Cn=eC2St6RCAO;o}h)=XB2SH>r+jiH(R9}{

    PBK;&Wcg|NX{>QR@W3{K zY;bp3^^^Hp4EgCcp#a7O7KV(e2E!07sKTguG(W~^?4lZ66!OsI#=Iw^QS(LZUvY)|-*On%Um?5>WA zl?50LJ%&XEbBcfmH}zOz=!^;alP6P=Rtc7q@Q=l%gyhRfi2{4}=YdE4KV#1hzuEkL zQ`e!oCxJ!)KmnXWYrzo%_u;5NbadmMK<}VRv{vp06NK?w7^1Q$Tj1RM!76dG8csvB z!8uB~T2M}Lf-thpE(M7RjA_gX6%1j2BB6X0eI$mNZ8{a1K44Q>^W@3P_G84KehO22 zJG-|8&J9&`rg~weKrl1JkCIVq&`ucl7;DHYw@0%Zyc$6}?KFTU+2;?{&=A`cEfAzN zU!jp_g3S-`18T6M@<#h3A_2$=zd4rj5XfwaD;BKizzZu%((a@Bm!J{db@_d4*S%kS z85)uJ6H=aVdJ9w~XjG@unH$c0h>vFo<4HQ6M~DkI2t|eFJmy!hTnt8Ojt6To$AMXy z%Ec-Z9jL;jXKDjiV*u!Qj44=K))MH9htwFwi|JpZJZ~{M?9ff()c#tpX0uYaf>A6l zaV{Qgbe)MnbW#laMf4`G#PjHlIUp%<3ly2&o*d>RpmOTnmY2VHufF-SoA1<)E?~R( z=WgS$I7Euy4Rm(-QH_=+`sBw1ta=csoM*|uG8xBOE~wUwTAd@51j zuy`QZW4sK^2*CTH5tN8z;Mj{$CxYdT<=Hw1#U3GNO1s#SIAVG`KswTTkWM*}C5vDY4%wW!qp-T+P zjiH`H`Pj08wXN8~6_I0Gp}9bcbE~-^4mD3Jt=O_gbB3QV zH@0hfXH~q;wCr?tu*vs1?)CViBPBqx&5q{6GO8C#^wH0-chR_FWDrbUXgQ%zxOyH_!jd8*jbwmGetZ z>mI90oWQ{QRn`etwI7z}UM6U%>aS8Ge=hn7*WU)BCt>J`RFVl82?Fd<+Sqyf4cQeRYe?3g$5AO038R??pu*~f{I-;y@--*Usl#4Re< zL0XHkkYPBDUr**?V_4F#Mn-@8g*jJTGHZ?Tt9?CpKKr#hdN1F8-^loVTRu^_1Pm+j5TO#%nF7n|JOqvwP95V~0xY6*TP0JMx!rzqf3C;CtWMZ5^~0 zfB$CDI*O00kSYqexd!cwb5wk$FblTdB4HV028U~%vtf*Q%f;rdIV3Y`GsSf4V#7cw zCfk?Lv4)H$nsHSE3V9aY)Liqi7Y81?fbh=cWVC3e2(E;^A(2-yY~Y<$WZLA)Y7gE$ zT8E=mZQ+p1K(^Syah8q-KrYPTrn>-c$%9<8=VNnP74)pTvUR)I5b;omxX3DD3l3;dW|5Dauo)5oQzd4%ke=n%?~M z83VJpFzJdbi5`Mmay@YZ(+%OsARvLo1SC=ifx8=s3|(X#g#d^XKyO?vL1Z#q?Zb;5 zA-fy+dO>$`EsG3s{LwJd8U9DwWodXXebC_2=_AG&D82jX5Lrq30g|WU3-n9;qCyE< z1?eqPcW{p*(2a2s325o|LSc9|Aw45lHu+UfTu(L|)=yFP*VE`$m9;=Po8=Y}R!}aM z;WRW529hmKs7+7^%Bl}03PuiYIM^lC*n;I+XCVHGG6`wTL(U9~xvx*FgS6)E49qQ% zC;{JnAPtIzXtlv-0G~aTPufS%E41M&N2w&e_2F_XBhp*Ps!L~{dD73yyf)TNi=pdT zNP@zwBc%)LA(R5GyG`y`07Vhif3$W;Z9geJw zgy{`K@NafEbUml^`&HpcBusC(FOTyw{RZ@<`_@2y18KsYLzqEybJdUOVAyuJKY9E# zy8nLMKS(N6XIC9}f=p~dGDqksgTh&9$ghkW;;y0tOrSfn>_uvl!!@Z%D(&MWjXlLx z7&NiNe`EN*;PWEA7v?n9Fnd|GPcWzL5Jg4N0^J9*27q z7YoDQg7}`yo;_9#7Azd&p?6FG5Qp_rgBBy82SCT5LYo66_9A;R95{9;5N0pvbL5-- zkqE^(jjVfQ!-e3bgNHXsw1b5N%MmuCoqMP$v;wgoMTy5;j9QS;YtRL7CxS8nfe{!6 zYy=iEL9Hy%fV~2X0 z#O3|xh#tG%Z}*6UDbZ(VN9;Z^B|7ZGd+js^n6tA>CGoYbTiF@3mVJ2J=j|?+o!-zl z880I~AS@(>cJRd&JQ@M$a&ty)hnfb@Dh49Udl4-cqa2@%X3*EDM@yqOtz|8Tu0$~m zYE7Tknnsu6jma2wNo#M$UbG=W7NHtfw2m$aG@p0Bqoy_kFC!^NMs$OLQFh2!z+Ix7 zM>z-tp#eb?{XvR;XdvZpTC?;Pp)|W?cP_uOrPRD)YKOzQ8=6vKS83O-lDU7Vzki5< zI&>8&P1d?OJ+0UY_@_0)6vj2XSd1>}KL?^m6nZ%CJqw$-0WX955Z4na7eyyYccvyX z2oy84(4K}4Hj~9e7zP9&q!4U^wJrfm(Z$@1`9i)Pc3E?Oqwg$s=L%125BqXMlQ&{E z>$jY(Us+x6Y;n8Ureeo6gTdamKflqw7Liabz7AKF^yV>dXPvVae))f8uY5-TK6nmu zLi#@DYYY})m#|SN#)#+QW#bcJM;M=$vf9P1p(+nJjE@pf*Lay0t2mY|j1H`cWbB{< zX62)l?7%1mF)+<>Y}EIuEedwkE&~6dBlb|JM0baj?lBR1Nh1-F@yQZtvKvTG?J+hI z&{0KOurbPhb=|i^@dk$zgzj$L^7yjSm)G5T(>afPdhw-uA6jS0HA&OzL*Xj7Wgb&M zlRrD(WVJ}n+-Y0puDW+gX~U{BZY$ilWW@%sA>;t&rE~??y=UgvhIy`es<9(OlyR{j0uR*$h-@{gKz7%1**%k? zlOYRapLB|@$Dc5IS1`Kn&y01wBjCvqRq&F2I@d%%3V$1Q2;S z`7-d2?uP^NVzR_O+)wXPjNWMt!S-8xyPDp`A$lL)3)O{|74C5YGP5#~nRMds7vZ5&8wZ(r^v{u0f2-j0|9Z zip8kJTaaIQyx-V2iuPB)t&iCs->brSvZGsL<3W8K8wA7Ug?@;aj&AC2jc$%R`qBL| zdSvwOCdpe&d%pIK&4rQpkrkD3LrejN4lxDjC1MIN zbgOuL!KFODppd1J+?pdF&NUDdw~~%f^u#*JCbB^gHccU`=Qh4}PL3Uz9NF=4`(x0F z!4s2d^>O=SPR@_sBD`gcXa1h;e}L-8c74pSj2ky(lN<+{$Yqronrf}kB1{D$72{Sr zg21pec7W=O5Y$8JI+^Eu1%a_gQk46_CW(W;L$pl@_}KW$rQ}4Z&r>0#QMlBVns7F0E8Zllg+cxU*K5-Sf8k)>cByD zR+)FVvn&69**9`M`(WL{B4+Zf|eCMz5v#4M2e_>(&f1matzv>$xLYm+}2ysk)hGhn7C0 z(gTPkq8vJcwj0s41jbqohgBWoUbHHi+8U;|T7+t@X8;ywxom{_xz^qxr&GjB+{7?{ z?)snKaO2OeU$Eex`ugk*=bwFb>&zD)xMb4<4;6Q*3Y|V%e7a3;!|_hJy@6~o6q^?%_}agJ3LmN6ZCOp;R)DbTxD_!`^<3T^{|m{t6j{>eFWHUZf zm^jAN4w)_Frm6I$XQV5vUy8DTjRhK9CUnLm-m&`L$(?y3a^Z#NM#AhO{Xt9h{8?*e z^%*@{9vd3z(Stqc5R0b}Wx?3b;V$q0wde}vW?eScuf6D37=90||J(*bzj%*0#>V?H z=Jx0K8Tas8B2mIGC}KU1@v@<#`+~6f>6ol&u{eSF72$P?(XxpM!b9KMW(*efuT1XT z8dfLf@77nq#YUqP(nh*8r}Q=I(+>R)bpG_uk`0L$)=UkOZjMm&65nC&!Fq&!W5aTZ zcq>1=B5*_zBuv5hn#YexXy!64NHIZGAxJb)(FDv#0PQS*H3Cr^_^>gcu0V`%0IMLy zE3x$VIT~8}zWy5U&60Q~YkJu@^0NMG{lLqJ@4%HW6O9e~_IA+N2Pzw0K?h<+AR-Lf zqCJHCVQm}rU?7eIF)rlQz#;T}S| zkDDU0&~e-a63FN^N1Ke`+yL%j{4?%Uxe?v!#GC0gl^a%%-joSNhi=Hx(eq+U;+S&`Fa@@1PE$UPzM*eQ7r>_r@;&9^T|8jHMYXl7SkT z#`hU~qhNt%N5t;oAIpoW!<3=I-ZFS}+!*19z=J>_5q4xuktJ1&?ts^Gq?H}xCMWxbjzPlxD9Qk_L>0cH`(Z+GzVq^oEQf(Ocfzf3 zl6xVHWb97-J`?UiV^o0OOO>0rPUEfUG^EgwDnsl%$$mrV$^zP~Z z#$5T9V3GbNe~riJGKAiyza=jJi~b1P@E39Iu=*Fa0bA5J&+%W#E97g)nn~JNo`oy{ z9Aq2xNB$~K53phNMSkhAfCbt0{@yiFB-)gTmsV4PVs3&S0q9$Ks$mZp(2I6rax6k$S}jQBXCO;9WV$4Id%HV>U6FP06B+x-ED9c3}wu1qy@_{Yz3EU8f7CQ}8fUNcbR4E(RO5=;LRnx%r@Mm`?QTUg1HYU^S40y) zeeE|*g(uehGat~j*M|NAxqDi#LF4-sfg4U49oeo#ClF8fN zP@m|U-Bp)8eNO5wta21vH;!M$8qw^uTTBw-i#gC)&9mpp#UG zqN%=_@C`&|TOw(~H@Yy6KBy4;8WJ5DK73y6A*M_dC@d%3r!u7&X=>)ShtiWn`~@5t z5ix`gxR?cATtL`4sN*==n}>fEyEuqbxxn|McYeCmyJeI2M?b20eqHG^cSY7$U$Llk zfA=e;nvDxfi!QJJIefP_-CtWO`ImokPU(WZ@t0nzd*G%8msS7dC!Jp^Exe@q$3F^P zI=^J_>-bpD=vd5GC2r0Lr8h!5AzEl&li^1(Q#|I&Po9548x4-*aRC!KaWu+rT-3v< zLcbQ=dFN##|2d0|#&wPl-~6|cOK>fpbL0C^b3z}+ho@HhK#{0peK6wI#`<75H^)na zu|7atu~W5v(~h-2-l;!+%7*KS9c#-w^(Rhfb6us)V0^GYF}{%;YOFXEuL!#Hie*!VMmqEGUdkz?-?<3F`puEwF^~KXmeY~n!P2F|69iS2 zekIN>VohjEi$2q68Bc%4?+C)ba@`v6Ne_%^YPw4@&%OIU9;W`EtA2G`>GoHjxzNho zMlZz1*`F9MYs`pmQ4DR7sjiIXuIP9nhJQZ1lz8YimfESme%sqSS?V@@Gb+MV4oEgS zf?de21|cEuly`zIXbBA6xB^>O;lI+r(sYsj8ryptOYhWQyG_Lree*W`HL-_&EWJa2 zZ5t%B5mWgfbT-O8UBc8-Z!+zF*_u-cy!@&^T?ofd-v&S6{ieKMbjhfdVCfC!dz0YTeul6S!&fa^ zer>Z#fhirCi#LAZ?zb*#TX@lxpSzRJ*dE2Hs+EI#Q!~%Kbye1HGlgq%SI1&6 zVfr$}6FBAB@_zs;Ng#@C0oP*Zl+`&NZ90ZxAzstxfPJR+LP>*A^CLw+6f_zeVL<4h z%S4b|m+zPJy<$2T3Z~)n74y(=B9cqCm}#3`VY1Dg8y%cFrO6$0`IoIxOwpj-=9VO@ ztELg9A2!VzaHk&oYA}$V=k_jJY06c#T)42qEjnc@V-8QPH#Ie6adppR-x`cexurc| zPxjA<48EIQzPAux(B|{U+##!j$!353j9Hh@dYY}gtZnrpCX}G~)NA)!qZeHE#7gJ1 zy6(EBP>n~ncPv>G>$n^u=lJ)9o8))p98j>Ch+Uf{P=pNMft$_1P^~FPmF$uAO|~A$NM^was_1 ze0XYKq)Yu@wc~<2x-Pyrx!C6yhnnn7YgetGm&wdqziKUZChyzV&p2mFYg6v5X&1TJ zg5;d3H4E2K%KPdCYp>oq>*DJ5jg2%-K??!2P=Q5KM8j#qmxZF6W-3{tgBgkjReNi{ zJ>x(B^EX1E)vmfbT&nZCCe6kE=2EM^i}>z+4!6_Sy3fPkYxsLDe{baPNqR5hER~W; zm|>tHUK%md$oN9qW1s5i6P|ZCt2{NejmeJ69~-dakjp*cU`K~KP|LuJL~9D4&ang$ zIPWF0RtP*3G6JC=xB?kq`G`mZB99V${*39#&*?9JF1h0It1eF4ANs}f$xZigqGm#o zscsi*N(I|94V}IW+t8Yxbz4VOZLKAF#>UT%kz3jM;qrR|8!xU++Bw{-!2p_onm6Fp-Xb3Bu9Kb9%gx6GDo^8fi4y zLY6et=YUcNDC>&4q{)@63k=`vpW+|B`M=nA*mv|N$l)`4_Pm%JYcRz=JXjEaIoyt5 zH)PR3dnS=f@mc|_gDS>xzCgjF6dc`>QIlNGLa}jVi$NYG8LUPWL^4QG5R{{;wSv=w z2n*1{5wgi_5o`vNWY3V#H&5sT;T$Z&D5p4`RCsQ2h9xX!s==I`1f`xP(Kb*SxQ zN2Wpz<|LIBLexGyi#{H7W98)~s4&ZjaYmXOG*K+|4rQOE%FFX8Jh0MWV|R8T6d%|q zp`_q4nEHr*4jKDcAcy`+VHuAM@714T(hWPF)1ML_-*LkubnveLPKRD51ob6S*>2dm zfB62LHyQ_s-)M{|X2T0z)TpikG{i~H>2WC2ME4j&uuN(sT5R}f{bz_*V!J3H%!r>S zZk|Ro088`nPlB7G1+o7L}Y=BVO;jg9^4^pcHV{O%VwE=gCLp_f8W7KchluZ*2l<8b)v6HRR$)r$3K zsb$5@mt46#ms@`2B{#2NYlyP+BJ#20zZ1SGUnIRjT9bq{_B@OHo~>saemDHj?4jQi zT=si$7SVdH@VfkCnQK>Y6hN<>E6x@Nf2Tj9?~%g8-w|j1oI+2QQY`DNA63>7PL4(4JfOX|%*2>y`#BTc)D*1fwSL`O* zZ!IBiv`+scFGU0d9kr?c2sZ%Kd9)F*zKnD`XhCy@Vgrp=O-^kC?LEju;L*Y4d;v}c zHX+#r6{+!{3ez4Ti%0;Y>;ouETBsgvYv-eqLUE}$6ePk~31yXBVk_e-Djy-NtTUh! zVtJ*@;9g35O>X4W-kLJiDd!L}-1~}Xjd-KsmN25OTEba^VZ~7A@SU-Clk`-z*Y~Ir z!0}@<<*Fc`y; z50@i3geSZnq2yKRb|azH_-)K0#Q#!`hzDb3Al8`Z$a;jukBC&Flae7u9v4f1>_Qk8 zWA})I8!63k+?|e9Q*PPF)FPmPu@3OqHjIxAnh(#7<&~XaO2D*54JQMZlabJf34ts| z&ICDp?d6wQ3u}4#W&I#=IPor|g~7l0*$nK_ZTQW4o?S%ts6E3=LTRJnWZYd7Ckce$ z_R*ifPw^ksfA!K!L}DTcU%%XtdX!%Pf31_as22Df4|YL{5-1Mt@#8LV?bVH7cSwsM z*%0N$)S`&^gH+Dr%jE1agQ%)dRo7S zi|v9jWROy9wfOsBx;-@9$iwK-WC`&gMy##_vMLX&hgVgDR|hrM%pR=;ZOihsX{`m0 zMa_w@I#Of6vi)c#5)d_lx?HjrN_Ez+txl8@Ao+L*1WkzEb7!BSv|qtK`AvPCk9?C7zt zm-Kg>4ptvvr|Z9yR&ck(*YPc~hZlnW7l1!nQSGRwl0}4M3q-U=b0kx%v&Ci}Q{9}T zytwX+QF^F3hhDWIf*4|yTq1eoGv(pIrb%lt2Vgk(LZbjEW-A$TrU)6H=7xoJe(xt{ zx^GzNHGBQ%`0>8-2KUS@iodSbYmF2xd1Tp5f1NtjTg#qsPMJH!(RnF5ClG#y&0BJ_ zKjy0q_!^n-mL>YPoERrJ}@HYGXmgax&nlYmbhyp{dNo3 zAK-5MLkdvfPfHKAKlD)hp{0M`zyHr8+ke`}zJo)5+P9CNez@)M(m(Cr|EHyg+mNnI zYc!2HmifJCX8 zEEhm2LMf3Z=Vf8WR`=14{{x)g!Qk0xTV#6j7}4-7bu#hkr#i1wTB38ASx_d?BdDvT|Cv($dQ}e z_jca*Vml8TZl4b6LP>J%==^@CQs<|PAwjEaM3)nNYO|tN_i27$8O6}_(>S`E2Z}+y z{*>i$*Z|2-n(N#@@_4--J>_)@TxP%Z*5f)H(khK7Zm7zc#*d#G@PI^A%v zq#&91Tb%WBGpAjcXqTd>W5Ac1GzGL{Y2vERE)hb|WRL>13z<;nu2Nkh4JQi1-yy@} zc_nF~L^q4e)BmEUx@ z9X1dQS|A+fpfF7{2^sIuSxqijEWL;coF^3XG}oqJPEE_G0bmML&#c%SAiJx1D#(+= z0T1b=RL_ramu7OZc!9ZSE+kzdt_uRB4#}Y-{_k`W>_M?8=@j5EGh|s1h|+Y*4(O#x z6%3gaOPq4ZHt?p4RaK8R1@vc@?pl1kJL%dSJagsq!5X9G*(`Nxoo=%NP5r5Uzu6ak z+``rnX)alH`KHzSFIG8O)#X9Qn)|#}qcmbAg3^9Sgw$V0e0!|c0?{m(l6X+P?1NfvW;@SFFc>kFd6%d41Ub*|j8>e9|YV-*{2u+h0(4w($QcifKyoLxB9QCXMrgQiF=7vW{eSGiiVM!6{ z6T45pTwHy_Z}yzKM}LPL*zi^RnEjO(S&Fs1RPmubg*JJx>P@LwW|)EqxS=*-A|uoW zH7qEULGuHVq1sbH1r=-+66DBICqIV5v(%}oBvt$n3C@Ox4=uWW{GCheK57z>ecmA6 zV532g>94=|3h8wdY1Ch#k%E>OsnACB9a(CX=sSgsStne=WTlzlu2yZR7X&g9OYl~W z&D=?v1aH#WUfn*>e1{UcW zIL39L@k5E=2dYPLk|vT@1qSxyfqaY#{Epa%@+g0K5Y6*>;R~oBZ&=!Z(U)b^&t#bT z5Vv{_5jzAbVq_o2gz}T6i-8?d23#(a4?cnE3s+xv`yF?G4kA~z1J$f*NOev-}lMFTj~RP~}vfT;+LWIQ6D!#^cJg zIgN6r<`iMgxQ~k_e?FMSn?D%nkn%ZB((CywpfHYi_WaFSXKrB5V70Y+Rj|J=Z0(R* z+Re;#(I+Ae3CYz_<(jM5X2d!?S&s}rN*1j(wIQF+VfL7t>dek2m&+&1N!et#R0qu- zYt$RE*_#tHoeo>H*XgiiR=9m$cWZ6G)jh)<=$9nqEOjwSs+H`D!)s}IL!eMxu(76d}Ac2|qP#^&`&Hb*EOh*{F6D#;`_CW1~$a(c~n25MQ-Zb!({aOIWG zMvL94$knTvXqKJl()t8TQxM^&xC4<Z*{)9zOH75B7y#I+k=={;-X_P1_+_N=*?;io+w;OJ1Vh4qkqPjg=tRY)al z4mBoFSE9SD=DBqYCu(Pz41G)|=$BJaX#jvE=05yCJqNX}KAw}nYg!h2xb@aU)*IEj zB%csw{AAPZ<1z|>qsA$mhP+whjk;59!wN<88~6Mmck>5hhTgYMwh3GlKp^s{NrvE! zV^k8)*fR39DlS!Ipd$I%u&V`4pgL2OMn;PhiVq+a7J0A77D~74kCx=cKoqGW5EX#I z-ep22d?&WPkzyb01V2c-29718EjeO;7-w7xG4#60)2r z`z=AIs;LU0n5A`B&|Fw?)hHTeKq;h!8dx0+Q!?Gcq@o5WH$9+$ma;mnnT%tCGNv^n zkCPA$5RU(G!^^rLR&H} z*b8yumBjTpQrJ;xBW0NS{bjY^!~G`n%lq>4XIbI(*TJhqKP-iWPElO}yNj3A z(E1^Lwf5=IfATOLp0l}qa>j@{icp}nMQ|!4lWUZHE$!3$X|u@)!ch~7mO(*+&aP@U zR-tRG%1@AE_lUl3=;e3jM3}MM-F0X9Z5^j2^cyX6*!6y2s4nI9G!Fl!dqMsT zo5|hTn5y=(v$|(&>a7W#yTxib^VqOuj%b=SMe$s)Y|hF}XEe>z1$OYCm-Y?Rd%9X$ z+vr!%%dAzzctXF%GK+m8=m|BZ=@$oQCi({&8w2!v`5sw$=)8?*{_VJ6na+;S+JE-i zPc_E#)%Y>`6CsOxKKR zaZnY^tD5-2PsSIAqbN@SWP!6cjaArB%XlyZ(-xJQV7bCS&q=%drQ7d0@4|a-doi(g z*1VV2E1uS?<_^xAwKnnOjQ)Y(*&9||=^U8VzrJtb)Gb%#=1)Ig@_h28+irX5lO1PV zI&bd3d@>Z8dfVL7=FYqHjE=fBr}YQVxZgR1(`PA2!pKtW9@A&)jwemls zPF4=+jvo!d7&Bh<9-)k=fRAyunE43^6@;KdJpq_Zl~8Cb5r#RqWA>S653;(!!5vn| z#Rv2o|L0t9M>s!tU~q@UdGP^u2lg|Oa3VjrWAN;A2lPJ>Q-8e0y+*%}U?- z-*dg~Q}TmMJ{#Y%^KY$Jx^m&fC9OCzIH><|fZ8kZJZh>PNEKAV6bH{etq?r0su6Yv zM27McAdWCH*!LP$Uw8!#E^0Eo{7W5z6N_dOoIRuv16SbX+(xWo)LDpoE1CJF=@&fw zuD}j#NZ>M5a`F+9gY=0{o7OHg`^1jHrJ4B9wq=FXoE6hsrAMs2 z3kMpeFV8m>A1Zu)byLk=kJ93=x5zUV{Q1eD6---lzMCy$W*3U04&~3fbCzZ4GTGNQ z^Wwqzi>map%i?RBzOnz)Pdb(?Rn|6b5+mWZ>VVk-K*DRCHr(pHV_+U0fq=0r2p347 zLrnE7VTVAN7wiV8C=u>WM2UGHe;|mDKM=&{s?Zc}qCQ@OzA;;@=G70YBXAg7IR0g! zdKyTZN01chB1Fk*IFt5?QwC>|&~+=%Iij(at{m;SylNY0+kz!cYbWDUP_#BIa-<36 zh+d#2mnz7or{WTTiy=`c1T%GIsm!(@mzsRQ7gsSuAfF0rDwoYdw%5-$) zYp1O_r)j8oZTF)3aG`xpy=i z!Wf~#8(bv7Y(T?paY2HMR!0TqfmJwave|uJPXL+= zGUae1Z<#7>01QUQ%zdg=!I}W0my}vO3!_Q_PK5zAY;iw*C zohlD;OcH$sS%AAhasq&EIP`_6wq9=2aqGh&9$sNZCZkDtHF(7`g?{ zCQGZr-NefnGhMX`&@q&#^MjIqcu)iZhNtcW+Jx4_SB*$+FR!odrScx=lnZMk z`rsh!YM+mf4h2Q?CoZ86U}EZn!daO2!G|h7W@5TuDnLpQ{zS#t!_CMq&lG)zATyMnU8-xDl+#rz&r|`(V-H@X?Y4CZ)2I zys9li;xI@-NMHVd6wQH&wGX5>vRFn4jv2+>r~ES)7!fB(IHHyr<-52QTOm4mlEz;D z-`eXyd)>Uf5HJuvcD_#7z0_WN@MGGGif7~6JlbAr6R1ipKEk&Q9vN#YHJj)QNeD(+ z4Bt4#!nTa%?gCRFV+>{h$5x4Z$ruBAh`4yDC=(-2;9D7q531ykQ9|RR@4fpKN;f6X zJd#h1%tgZ89(&t3@%CwS)Hr9@lt49X0 z7DMjr$G6be&fa^J+Cn+8UwL;zBTHe^m3NJd+3_vaokx!n*$ltm2<`si_VNT@ zqrGVQ$G10BN9nwyEt=5Y0_w2x*1q>B5qx}W3+Tv_|J%0y!?cY{)Yg%4p4e7)gg4e8 zJa}a07!!bBml!;WTGflJlh6~AEpQ3AcHa4E@}@Ev7|o=zzC-d&a9+NW4xL08ie&h`Aa~I z5b*~+T_@y##U@O>-h40O`Wm2X z2^RBf))4D>$YiqFY%Zq*Ri|7wYe@ek`+_K1Y&N%DenJ0Wkw>)n^o9O_!|JXQFGlJ- zLt!_k+iCNdf2sd`jgR<|&t*=xYRqL+lLLctHO5Lg*_3L87!SmCKrB*dhcUIGPtk8@t`e8gva8;$9z=*K^)S_Vk-9~LQM9dJt2mhw#fJydT zbxkB1Yb31~`auGO4g$D&&T0er%#YS89Bms-iBDT#HxTMZeL&Pin&K6cJZqpbo0i@% zl2QHemW2i6#v{G*es<)3{Yir*&RcNf=SCRxhNW*mW@Bsa*PZw4k6=!X&&R0~&fqy- z=m%I6!EjiSNPRaoEYX_Ly3#z?1@6e_kzMI>19nEwP)r<{)$<6!N5rmj zVwUAdjt-o*yhPjy`7V{p@S&^rTy@o+$@wm$#o=`?oxWe4|G3Nhvzl@;WOgS z8vc++*v&}dvqE3sPp9(|fE?s20i0L}45L|P6JZxC6zt=2$kh(dv1&xszDS{sR4tQ= z%ew9QyHbp*5)+%CLKX4th#Vccf9s_CGcwvg_U6c@!9Sj#K6-aJe^^?d#Zc{TCI^>3L)$eK#};^5lU8(CAQC6Ma{B-xcb+k*q$x?=V9rbiGSl^#y(I zZt;$BH~*ggQ*qTp`rHSGr)Dd$SfpdxIA&Xom>`4lK;Ga$q`PC%207V-{MJFbbp<0B zB|9oTq@|<}fi|J>4cKsC!)EbY($V`5+|Pb8)&}X{&wF(Pf(^xg`cItEt4`LA5h_e> z2O?uZg^y_pB7gugJH|C->w)uLmFRANW2Em@_&_Wi*l>WojrM)+UGZBV{)vwVJx>tN zAx)TO<>a;|>~A7UmLxRu4QvLNSxduFx|#T-l;op*^#VJu8p*t;in;O~6BB zgF{MEDxDjlWkp*MH4@13G(-xxE*Ik2>7=bUq^RHFz)^5~DdOKfJR9-Mu!IY{rMLVM zE(DK#9i3{NS>gX zAp(nzkWt`eT%!WW?&VENB9|}3s5EY+Vfs7Q-K>9#S~lm#>)3`H_2l94Eqq;n_qtoq zKn*9?--v*XCoAy>!1+xs(2}0pmjFdaYGW9UL3-3As#wyPl@*%!;Bny22k>d785cf@ zbhYOz1S&lFD9o#Q8jc*kK%$I3rWQSt%9-ULU@es>@j)Ovv6^c{V2vNLV|g4$ zXL=wf^|IoHCNp$|&YN{7?;a!$6zOR_q5{Bq<-UsgOM?B`Z!MU8y zj`jliV55DYnh1*_*N9Ul=MGS0333MFpb}N#`*69e8WjX#fgk0u!zl{xN5w!d|3UJB zB4SehI`l!Z0gcMow~?np3)TXg5E1%O4|@+Onhwc)6+xC z7FJ=ELh(_N9+Z^lW==8H^Uv41Iqd*an* zlYTYr$}6HiQMbY6R`@AVrtgcT|ra4gKTFlLn zVAm!Jb~VSyD#GKBNO|K=J3_)qLx)5&Zzfsk+;K{)AZYEqU=+2r&`sR@%Q=BQbUEh*&PMN|?wt!2zE?C3FDLAZeVcSO!AG?bVgX{2D zv5~70fgOXL+=2M}A}T8LBD2t22{Y%ZK3+e;K$(nD_{dB3fMltLYW$C=)MGVP5L1^+ zQoZI;8$KQi;DI)Afd4&7)cYmxFSOGGaQR|#T?}1jZ2>{2hDDF@Kmum^Vt$MiD&uOy zph4Z^^YnwbvSRY@DxG&;sW3eED|dVac8o{x$dAa6peKSCP;ldiOmCF1YZ%8FBWg zx5IUpOIEgQJhpR-(&c~AXI361(s8?l^8u}InM!>nh-LVJDQ@qyj5bK?m=kKR7Q^$& z)Fx$LsyREriAJFbdAO7MB|J|DwV*2bQKZv@k>L_!Ggxmdgy1!}rVzf?A*1Yr>}CN3 zB#Ob*ip?uhsD8pOb3xpExZfWM`+w*U?_m8q_=dT*u=Vwu&wBh5g_&(OTlRoI=VFB%wwdS<0=0LouDekb3&R@zi zs2TOYQ||Y;%Ds42M?6jCY~jloeJP;;J-y?&^o^S!BSxyu<9R?d?EDX|{tD&*cmJqt zCHu*ECb}P9eynULRZD0xP&&Slas7bi(8xpZ#!B4eFmWgVA)tUs5KTZCLi_`91$>8d z9v;F#pOoi7pTo0hJWcd0Dc%Osn4|pJz4I$rjiEP_-Ge}sQLKji@j#9c;;Si?KkX01 z5=|{!wgM-`er+t(L{X}U*dJAE4ZDq8ZAd;&AU_$3Rv=-5s3ol12LV@5w~8-NzUA=j zttzja#2KDyQGsqmNbIvCbcOE3J7sI^HG~+6;xJ=;;NcJ(4GkQ603k*(Zz;9_cc9geb$EMrfZuz#kq7AcODK)>DIO4|cL z{v4!JwB4it20Uqt(WVodsz17$4)3N?f0O0`)f`I$128a4%mWyX@CzlfRH8A-AN5l~ z1R(ZC+fMV;i1?@6tT<}Ud&mt$_yL~VP?<% z+}oGh29Ig;wr!~shk*M*R&86eX4@(%nKgNiCwRW=Xx}P5LEh_VPbzIi_S)zik0YFd z^rw+I-jHhg2rim1$LTSKm=h=Ii@`(S`FjiGJpj=C5i^|dZ`6_rDyl;ri^DVhcO9nF+`LLxhAJT@1m+zLeY z0h>b<2zo@Y$|ypIb#oMcOfCn5)R7)849424EK9m(yLIYAoY6@u{RUf?;(p=x9tP@vctQN~Bnjo_K^ z5r()@gjJp!RHq1!tDzN~l%m3^N%I9VSd2gDpU2-n{;>R_d>U4gm~a)3a03SJ^{7=8 zsRBnLWqE^CkY$FMMTK;YdS&op6Ziwh*JQ+c7Xu-x*RMrLRrSI^(Hw9*Xl`^+;14?8 zC)karE>|h2*$^;m@ZQ5eXCb}=Mw;U9Bdx$F(L>(=X@eDb=EwzlUk z|NO7T!PRUk`iSv=Z~6ae?P`Ofy3X)@*98F)Q4tXo*AGDD!+rOA0f{J5gTzwXM6lK% zB7zDS!4DdnrY5n}8f(?0CK^qnX%nj!t+B*9Hcf2DwvOo}*0lNPbexRikBsd&X{Y04 zpwGGYS;fSD{K)Q}ecyBLInQ~|-RIuD_uO;dv)26Q9KCTQW$A`@o*9#zva0VXlVYx1 zZnw?!`Ddd?2HpDEm(7w+#(&i~I2kxGJkzWXgRU9djznBB+k?mknBfebfE5X{Uv@3& zy3-6CappF{*s;H_HS@W~jYmIYiTTfP*0QN~x8nZ70>KC4LKk!5#g9%|@tYenS%TZL zz8ig4;uf3l+66*~-Fxw$gAr%xqs`0|JU+pso4nyrFy<%EZUct4 znC^TGRmWb9?}|=$w^T(6Of5yBs+L4w$-{M-yOwkwbfqL#wYbg%Ye%J~SG8pKT`VjV zUv^7X#&}QDj75*d*FAKw(>=`XYB6mvq5Q@E8`~ZnR{9TXJnqKvdNVl@^LicGU);Yh z?gPxiF<#{DdmCsd7njlhxcyz+_jcR|Hj*h4dmWHoYl=Y|5HP#ZiMzI$lK43(1$WC* ziK2gIIEc78&gVMPY(rU7-X75G?!hQM8w;MI9Zb_tHyQzX`g@&lN8K?y#v#v2<~8|Q z#>#Zc8jrGeJ#Jv^gKo;1G{kM)$bsczcE#}TCS#cBCAwu(5ISr%-ZcAPft)a4+W?II zy+}9ZV`;k?UpF8vwk?L=jcrDc1#UO3}Nd`0|~!PSF%2473qo#;)hPu!i9lvI(_opgQ314DKUxtd&-+%t6S(Dg$Prxd5u zr)*7mf7qW=t5dsEFAq-{o;!T^h_n&)Bi0Cz(~5n=(&jUe5e5D=o{LH9u=h)~T$&W_>(1W$dD{hsItX=NtEW zc53$4?2pD*j(>jqYvZqY;yu$mm7X@w4$qAVD<_$T2?zOy>yp?$ur$nYSPU)Q*ntEwk+q94JoAXcP-z=yo*i(46@M=+0 z(axfq(~G?s-cy>ZkLX*z1YfVe-oGP|8F(S+4mJhPhSEceLnp&Y;rj5A@F$U)$jN9% zv^M&5^ipv~@si>##g|J8N;*saQaZD=x%B-R6*FEcOD&sQcBbt5J>Gkso#~ocKl5by z#PaU)zt7q{>tD0GXaBRJw4%OZzkT+457(5oj~MVo5a6gm;NSqisd){vPV*c$()gsn z6_>d2*w9*un4=4xl5e8!Lci@H>VwR+H+4692K%VTSsNupJ>Ck*G3p6cx_n4I5&BK) zL#)ZJRO-pl1Jp-Cucdz8N_WL<_^su2?cA_oL(z)WU2B?KmbJHa6fJ9S#i-48%-Qb3 zl|c*E^=!5}ah32gg3t0|#H=4$1GaiFbAPGT200J;*F!h?SD`1+1Me}b@ix~MF@z2~ zw%qE#>Q!rzdpVAVBFt8;#tH;AIE&wlTEA$`hi@GZVoOoF384k}D^O+u@~?mg`_*hqO74pFS){^GVg0`rcs^C`0lOU?u&~|U2Lo-Yv0LF-c-zuuGv-f|u^6tOX-BUMM z=3RvSy&Avr8vOn(w7LVS#{O12$LEn}AzIvk_L_ZSSmx}L`|S8_e)+JEJlIPSJOeNc zEXKYFAjRQh07s(z!pdFtBU2|f;QKusr!FxbXop%U7$*`Z@o;{XAc>MBLj==};nL6a z?GBd_*55FxH4UAr>3BexA!8&{vSch~`hOUa69KQZ4t% ze2lxUkuS*t`LcXP?uWykg;FbZvPixvi{)#wL>@FAdZa;?p-X?cG|37$rfiXwvPxD< ztF%eGtdWOgt#nAItdsS!K{iU4d|e)vP4W$SM7}AH%C}^*Jcj?2CuEC!Te{^tvQ@q- z+vG{vF5g3U)b}w^c$e&!r{rn*f$WiIn=9Fe1POnxdoavaldekLd772JvZTzchIIW51CGZ^)7R(>h3$*<&fc|*?0ujMyb z+zv~>%J1a&asge!7v)X)16Cq zNZSZVyK+doa!9*!NV{@K8)uGJ?Z!ab_>ja=;;7viq!Ukxr^Hj@De-*7^AXQSJRk9V z#Pbo)M?4?#e8lq+&rdu*@%+T|6VFdPKk@v;^ApccJU{UQ#0wBFK)e9)0>ldtFF?Ei z@dCsP5HCo)An}643lc9#ydd#{#0wHHNW38NLc|LZCq$eOaYDoi5hp~P5OG4p2@@ww zyTZf^6E94>F!92~3llF)yfE=1#ETFwLc9p^BE*XjFG9Qs@gl^F5HCu+DDk4iixMwN zyeRRa#EUw3O5Q7ZujIXYopMV4EBUYFzmoq-{ww*ftO8zVPujIdy|4RNV`LE=^ zlK)EnEBUYFzmoq-{ww*ftO8zVPujIdy|4RNV`Hv+t&3R&ulK)EnEBUYFzmoq- z{ww*ftO8zVPujIXw_e$O?d9UO>y#F|MkoQX7D|xTvy^{Az-Ya>pA%_o2{ww*f ztO8zVPujIdy|4RNV`LE=^lK)EnV@(LhUh-eben*C^B33F^`zzF+C&yytvzO0{|1%B6xsj) literal 0 HcmV?d00001 diff --git a/charbuilder/fonts/glyphicons-halflings-regular.woff b/charbuilder/fonts/glyphicons-halflings-regular.woff new file mode 100644 index 0000000000000000000000000000000000000000..2cc3e4852a5a42e6aadd6284e067b66e14a57bc7 GIT binary patch literal 16448 zcmbXJW03CL7d?tTjor45-QI26wzb=~ZQHhO@3w8*w(ZmJ@BZ(tbF0p$la(=N#>kvm zE2(5vQkCfPhySAC*&%gOhXNAMqjXaM8ZdR9h1n(j|bAOHa3xsaUpVQb^?bFN$mKV0Ewcy3Du z@-8k$`ak32WBbVi`wx;7^0Pnwe^+&aJAe9T8!-8dp8P-m^j_k+W}s`RtGffD4+(~# ztFH^%r@=P?d_)fbz?K5R0s#N*H#RfO?CBZn>6_?x^z-v0gc4w+(WBE}13CaHLhywQ z!#%^j8s6#2z4_*~82qM%VW?EZaP{qr6q7)~zyRXUfu8*DIFkvyQi}2zgVP1nasq{A zzK$~<^8~1Leh9gA7?OYdWb(rhHBCeLF_~b@=XwJtb#c@X=&{tLR~#2+TS{-c`vBYE zGBWX|sg2q1)>^5WQl6tV-S^gSSDaqgl)f0g5bP3XzB_opq(U*a%n-{&Nsp#<PXeb*#gCojQ<~*y?%~jIH!wY%g9nHSRoaSF?Kj+nhFb0uC&n_VOmpd_OBYox zmnx5#Y6>`tg|imfwPr|~9o*VGw6l}bCod<5GtgOopG#Z3FYU1yX;{uJt(#*r8r_e7 zFtr;Gdot=wqBrPOr&Auqx9S#4&q}4+IV@$;lS%g;OwuPXe}-tkmpsZwyFbf2RoE|~ z^I*n!=-?L4caqmD0 ze6gB6sXkw{<`|Cx?yb^4okCyXCb!Pswu?l=&V6!>eVjh=XD+I%?*-Gd7M;9>8h)~6 z&0J!HkB*tz&l&C|b)oTW*SdHifwpF*1$>(yA`o_PKmUNb%3cQp@DV=5e(dQG!VdB# z4zOo2dD*d^}VrwZDE>cjbvV3uXQpX;>NPr?6LUB>JyOhwrqV5Mj1Q8A=HxZxa- zQwXEXE4&D0kFPJik^cKOC{0^_Gd~wNu89<_dGZ;!WUzzZ3ld}@(h^<$4X6-4pZP0> z4cT8q?NQVurwRI1@u5c=cK!0A)|eeN43pohgBKnf%Zphd-bWZGHIQE~`m`*h=F^&l ziYiYp2Bli;gaHnZjhfJboUR`tiB7foe6NfemF%KO8OT@`0*rjk^<*{<(SKi84B6$c zSAeZ)XeDt@7mIt)7s!bPz7`HP9ftqc{+RVQxN1rHewmj8Yp3IVyy5+hfQzfO*PnR6 zhtk{-Yu&KlSEH<_;xUIck%#8F?#Q96cq(tN&Y&yCP>~SwZF+9EW+Z}7E5H4?%I{Wg z(N$R$e70H+BskvgkMrx=s0NkTo4j@vUJI?-vt>?b>ZKxs;_5=f0G)6f@U^u0(`_>iKBH|X`>9ka9q#!rMTZ#DaG+DNj4Hb@5WUDRx;OQyC`$YMi^IjCMmr8 zI(s_$k$_>i*!Zw?b0n%}L?TE;8iYNv&D5Okc@@2k64bhgEg9atc=7JTCCwE4`m2d) zotf55o`s|4kAD`L4d20r!>w61;4e~qalSSgRUGOBHl z9RTUz=#A|RA)-_XJ;fPvhjE(w=K~z`rx{{e9EixI()Jy>7>q7pDk!X2)o;7@b}3Yu z9i|Jv^->~KNaK}*?iz`k`wWk?k2H%PP(=B6#}1W+=RSZgxN>tnUk$!WK4gXlQ5YlR zTsK(s$>9-qC_*h|B?@VYC<>v5_KI>C2z_VFA`o{64(?4{0alZ{Nw|H`!{CqynYP_3XpLG_k ziP$}NfO!Bc1h;p(xMku(+}e9AFC+)*b7-cf-zFY{y5q^zfrbBu7o09H&lgsnQ0~~g zy2GlijEBH%4KeBzhNc5k{iK+Y1-<2Q>UF|@>0Y(&Q0+KPt-?=>*O;tSLw&e#b>>(F zM@%`Dp)}XMSMJ?EoMgkl7E2Dlkm_n=3YT5*wm_QDoZ>7lvtsY4O)?QU&&U>WL1boz zQpm^5oPSA<)4GyW3E#Ps%#pgS9&NNgd{L&{3U4mAPIsPKsgeU0qP%W$`ZjtthBo>w z{j$ZZ`}y)?bf|%(x(~j-JG@sY%R;$v#5BH_v+zHz7j`4+RX_0>ExySHVGK_8?ls$< zCG8GiJ4!l$_CUvA=~B4lvLPO5zU!YI$VaRmBu-~t`|-fjE8m|b--_hjHI@%Obfn<5 zqFvMMzZAUzVr-;8sF5B#27-ldl$|mdx)l)mQQFu2FIOtOc7Gu;oB3aT zkoEXW@GtHDhHTLayMa&3)3q|?*fC_}cttu?Q9^2h4(mFdWi>)r&@Pv28u{R72XTH0 zZRuM=#0U~(p`Qab%BV&JME9I}R{we>pw1JgB;y5-iwrmRLHP%hMOR#-7%AknieOMN zo?28Tc1wE+o31Am+Nv4Dye*YinTqC2UW;J%&TbQ$KFih z&(4l%v^}kxB%IPw1bwe_&i`(w`EDZ;rR4y4yR?*>qOb6Ki?AP+?18T2(HMlK=(_{9 zdm{~sd*AEH(5!TkVTELf1xG!^WBK_T~kY*#Ba=bK-yDs2kr{xCsRh;tzmzhb6>9 z!z+!FI)u7k9fl1aR<{6Rb(#qU59Ak=h_2T0ar}&kf$rP4^hRW*)_l%I!1KROf`P)) z2MGiZQI*|?s^T!TAY`p_e+dw98bH9&ELHjiE7;c;&=hB;DbKUs*7chHcwS>>?5k2X zp7QG43(FDIEQzG>$ws8!ZtSL+a~6-GO3XhBmGXD*rd@xN*P6&K%~IvQsKK~mQb@B& znOIXfL%=A0T}>ki50;ffb)L6t)Hpo7O2uKpP*QnuNkvcZ7+jf1M9EJKck{Er0rd+S z=^O6^6DG2}`u2S{E__E%YL(>)Yet6OO*dmT3ItOyJl?OsHTW3*HpI6^v($s$sAGQW&Iq+~bF@Em2$N)h_?PSD zFNSos=ZjgM*=UQLi`D+ET-=unMuvArE5e=BJ$R=i1hS?y}#89}ucRG*1PD=%dmAiyfM#)nR(>UJ0wzQnF2;OY3FpZoVXs+cy2w5;?GQ$<2e zu|#iFD=ow}--1<8ZyobjRWkurqBk9Rt{?GAKrI;Q9zBLzZJaQ;ho{E4;I!6;pT$iX zS#$C8bIak_Kk3dF92Spdm6>ggwrk&Z%+#hbn9KM1UQBdba`4JOzLqFGQ$(Mc6`_Sa z>2U(>7)j=}3e*Pz?%(KIyA1H%1{)%%Nf*%@0bM+D+(`kq2KwZ*I4VfHF!=@9FDvf( z`D5Cx&Iap(E)z~MuBMM|Ns<5%P%f*;vidnD<8)(8dNv&jv|>5$nb&i>+#`geKYw6} zs3PT6u=@HGWyd^;J@9Q$(ot!|lp4;Qrkl549^Q|)eBMOVeorn*`w#^4TIQ!@;j7&} z9jKr9SzUF3jZ=DpFN7>#&2XI5qjeoeB~fm-glu&dEb0p1Vc|JcV|rPadNR7eIg+YT zLWliky9=Z8uLXGp{|#G$P#Gg@h1E>)KAdDmO{b&8e2ke8G}t7k_78@NFc#F0JXn|K zBvx!abv-#UJu8Tw>T4$Mnk!cA>%@Qq*QbZ};0q`@1DY5aSuFp7Bp-&rG7uC;x6rA7 z-&=2G!#I_&T8pGOhQO5XUKHg8{w~_v^~rQ=q+?je+e{P>8?c)n&tiGj12TFTV;$st z=imv0loSAktP4ipl*=6htfl+=WF}G)C<@j{hH6KSSnUA^irkKXuN>mhbMO<&)L9qz ztxRgH)b)$4gWy-G7G{hdY%H>OqmH8Kiy4|O$&Qj{IOnqbUcP|=?pi__3Uy1aLIaXT z;d4MJh&5FK?Qa(sU1p@pZKR<{N-QlW{S#Orx5zh4 zlU(^I9ua#zo)9`cmCW5Kvt)91pz~0b@&G?Uw2oD%2yV27VTW}>Eenh@0=U_{(9%HS z*C(a5G=1JvO&8Gjti7os4ro{Vz)^K%IlS?fIYb%(zC8>f85Ll-9YkHMM6S$>y!cYT z1!SeBmg^~lOVX+>Lz83WdPQ++h8if4oWH1slf@6-32CtPG{~*G_I6H&G&0VYX-=$# zq7{EUG?nMAbXe7^NV!fPq7}KKeYt2&Fi7xVgvFQ%z4Z~Q27(JT@Cadr_?d|J;tJeEN9xPppq8Bu@=l-p?5xgbM{uJIeJS-PkEfhDz|l3rh3e{N z6Cl11KlvT7)QQ+Xl`qK>!Ae6u1K$q+%+?(XC?gGoN4>bRfpG6Fh@Q{H2N^RdDSz> z9#GX){2iX!;5fyiR~cPQ9@+BDz*xjn<1~BopQ?g3p6ZM_OE~H2fF1hvX;z=qfH<`i z_cPC*N)R{+*jZy%z|hj71bRpZ44Wm3Hy?9bl;fDtL3zH{a`}+!);WGv8VBmF(Ag<5 zvs#%3Mf|+(y)9->pV$x9Ce!7TyyjVegn{&u;Sw~l<2as_WBAt>PSk88Hc28D;TW4s zN>HnoZ$=YxHg+OkcX|B&kQ=@aCMH^UV@sD1ZauA(hjO!9ebL?KskYqa;piGWM1P^y z1@Y3$$V5t!4}m9XMbDLXadOE(9L3v26t;yxGY;P}ZbMx+#Gh<*J5>WKi==HW>GtE- z0k&s-L-LJ4?!0cLr4X&4>&$rrPIuZCHv!tRJ0`AyV#S}yU?7L`D3Tn$iMEOF*nn=M zIDL9;bkMPXrQN-JL+W@>%o%^wD{XBlQ>A)+uI)nFTA&;MYtebFrK1q-&0p9k<5VSF z@?(|%Gdp164bk76uKRMb82gs%moxKY-syEm0U^sI38*rKAiLv8C(>6E0j2T zI4B48ksbj&V)aN9gVR@x`Flb*{v`D=w&v8`MavBqkxb>4 zc~+y2AGRQ?Uck}=nxIDfq{ zd;hm3d8#P^Q#M5dNa3yGk(4=vl=k;PViIqw%R~LT4L*_kZ&GXvChe3)^_otV+Nkxp zwzDTrd>n_#DJ5!~)aSi&x9#_%1TxNL3@+q9!#3q%)Z6q{Z&kvpb?l?tz!i;sptI0` z;AF`$Oag5*)Xjp3N;T0yVn{^qBdF6h)Ck_Ue@nNQF+6W9>e_E0mrQRrBSGbVt!`LH zuaedju6j`$BvedYKBHA2ecp)#x8ThyKcL%t9zLH^{mpC>c*G-&;?>pDU6Zr|Y0WCHAfrOseG`WZPzMHfc-H0N> zQRK|s>|TkRlvYl_B)9L{Z4^4UG~h9l=gDh#iMZu-lkUBzpq3oxA;FJohjMo;j41a3 z22P0kqTrNq(`H}pKIwGX*)WfYX5tw$?mhDxE^3s-%sce9W=+wsS7-imPiGXkgDsM6 zowj>a_V}8QTB;`$Cr&tw#D@sFvE*wgI#!HW@wE`#gc6z(W0-fGSMu^44^NHXUmRo} zjD*Umr|s!tcFJP7>E7ch*6h#Me$J)$ULRJ>%&@s^%fD<}tyI4m=q(~k2Yj_PL@fOF z-`+Ipi3#=$i7;V#TQ|nmYadI+(l%B@20A_0h7lYrR>tmoXD6#*RMKK+TbdvI&Ek5E{W>TYiXL>cS-q5P9fP{aqMdq{g1fQ4~^4 zB<@ZMjpvP~FuYacPKg{Q#;1f<_zn4dgEE#2)(9QXIn~_#_hpayOcnnri%k!k&iK@o zdA4n#?9<(2(yYmL*41h6&YyLQs>SNJho)Ae4!c|Z%WeB2;_`&pQAN4O*{8vR4$N0D zhhEvoTE#EP8kJ#M$`|397jd)iTV#!BqUZ3uP!M?TMyhw0K{W|snIa!*7SecH%O+)y zBlwJ?4(CCz>xC!&*J+O?! z=_McM8)pWN&%c)@;2I1TcTq~;%rhf|p}0Xdve(0rcre)J-M@KB$(rDbbK2Cf84qho zMTpD#+f}g3mc3wKOn`4>|5XdTK(4L-4S9lNkMn{)-voy7QmHX9to!YvVlg8UCxLVY zCbRy9nS}dFo>PfqDk2WfN!t592XAU}6~Kvfu+A9M7_x(C79i@#lgQ}p&DhNj64FI0 zI4sc8w=JauYjuSK_t@mZnt)=kVrjm4!>34cswwp-vn0%WlVZmhF31ZR7Ptv|}&DCmE8RN2m3rG}~5+ z07c@dPb{WT!B&%LSTsSexqny^i$20G((4$QdvnGZQjq(XfnQV=5rgQdCUmabx9?zK#wco#!O>KX@_k^Je2Q$W*QEtQY*y# zP3qZ{M%>vS@*3Ru-N0RMn#E>5)5JJTgIn)vmpeMhqMH8acp{Uxy3Kv#BhBFt{omz% zZHuxMCX74Hf`Hwa?!BLx(O6;Zh{oh1 zk9?Tm2WBR8GEiCj!Ywjjg5qkgkPm)OBVoAa0Anb-81s@YwA8POu|YybRh{Z;Y(#=@ zawHH3n>7}m6HFy7o)u+jG#HquHrn`{XwYP9Kbp>0P{)$LPq58;1P&37^OF|AYi;g( zE16q5W@YMaw(_GY8gy8eh?GsirgiJ?)11BHon@2 z2k?CyXF^c}@a~onwJ2e|$bbMr`g-rOR3+#ozPd#1YrHd=nv`(%_VP<2+PIWPF9N9H zq+6r#yodRe~GJSDxd?Ysbs(A`;H~ z2cshGOmhy@h`h}Qg0l#en1aR&tgOq58Og{h_aT_b1|_!y{)7i=8)AC`425Fh09Ef; zN&2hR2k%RQ-Ib&6T}w&$)d#LE`~BN1n`xW2bBb!JP938R*}P4syXwi|1=W+q`;6tI zlglY7sem`;(Egfr5sE7uEVom^we!@iKGxnxZ#qanxh7>x2W2Z37J++aIyhFb6i6i+ z-%r|}!ZM=pgJka17$qBs#RWv}k&v)mVoP!e>9*5Rd|tQtLODMmYupBbTRto0vVNE~ zL@KHU%7Ug+km4GhdVO;$7N^1Z$9eElbk#&HRa2IB$&aL6F+ZZ~-%K8_&lArt8ZFNa zZ>>@-;66ED@^3F8hF{M-hN49}Z?RN8x47e(yE^-6Qr1~~``1k+jokRzdZJ#T ze?CJnKrp8Y165+f+?bw+@_Y?%u-$k&ci>&Vc9##X6b%V5UtVQ*F}#yDp3kS?#jw{a z&8gS$#pxj?^)F+5IVA)w(M>1t0UW|k8er6zQ)6(%j<9)3`6h+jSR~?fvI3fPVJVM+ zwCN#RBLikE)5lbgaD2zd0Gq_Nk%QjTkTEbwie6*tgDY65K~K&^CzhMnZ1OIY#TcIE z17&d65gVw?>P|QcQFP0(gEe1c%<%(p$kg7L)n0cfC3mJtR?d`sGa2(^aQ6>ISNN?a z-J^~O2SXiYVn6bO#&kDj*^5@Dq(FM5XiX4+0uyC;ECk&Q7&k8-5s%231WBA?$q0a9 zXMy6;|QB#W|+(v zO`d8rhA}$HuBy9OscnOYCeZFokYRpi@1bRp-I_&4qY0mz)dv8 z#psFjfRS)w6fSp|gt2NY0OR?&ol6BnpGjYkiYa3CnjR6X!%qwmPg)L#a&-Nb{oV2H zO_$lCeg)Jzczqn6q+{^q-BgdzhMM-Sbi>iS0zdfdq6(c8zG7_{jgca5gy~#3d7O0} z#=MarJ;x^wl?0x2m=3AZqWyJqK?Ge;x4qX#DpG8$R4pVvS1%z2%!}@Idi(P#hs=l0 zbeX2*YrM|Dr`N*!Ifv|L#sj|afrtl@aUa4)SDlXmz+EP`&5FD zH^4h6n@v8B&1dA=lz<+14Z?%#FV_l(PX(uP^O83`(#wDb`dpW)0(y8nGWxbRTN4qg zbPU*fXZ^u~Yy|M%@qq=pIZX~a)a<1{R}ixEQ{PwCmvJcSi??WZ5K>LnI@Cj9K={AN zbtd=RRU~KDiP{d~1tc=>BfLc^!n7cB9`KcuG*3h%hC>>Gc-FqGJ#D{Az`w4n z>;DvS&)uSF;os}x#=WTf%HmFzK>{QbkiW!_RO6LL>ck8dr}b%)tf7M}m$@%eVNR~$pjWIY>)K76S&6D)ErTYo$!HbpW?J(LEb1Oh$ZHwXN1VXL70mn0hQUgw2^-o1YBD=iZc88NCXQc; zG}na7)C7!ox@$qVt+U6?6dipyH+rh4^T|;1{c5 z+KB?(kr}w(*g+=mOvH}!!q=G z_xI0Tg_ykAxA`S9xAJZ$P^cB4EX&1`Ps=_2hRR4R!B zePQ~o{hbjJpb3KMMZsq1*J@(r{ltu{JFT3YkH>GUB1~8#?T>dK(ZY)hUEV?TAckZEm<8m!rW?ciPRR}Sl6Yh7Qq z@;hYn@cSF`r9^T-)LuFshVKpK(d^`c`5B{_nCxn(lLIv0F)EirmwNF7Guoeyd}Vkm zve@n34B@6edk^VE|A2|r`k( zRg-Mi;u||Z`OySCTK3@T>(UrSTgPBLBFc4pTFx2xHmpm;PO3L5{mkDGSOUGEZ$3!5 zLj6t*e#X8riT-kd@x-b6y~G?N@rX2u5QNA4ld=4cAiA!g#TjIOw^LMNR>9B~k5|tu z6}X36Ay|b*C|MGbBT5Krbc;*8Q(0;IU@;5{`tp^#?0HS14m5^2BAtv7Jr<^r1yQGu zP|-$dQdV_YmC&%Ml2j@pjzKzfk)XN2JhaOcS<=ftV9^@Nn9S(0f6rT0GqeX_^pl{X zRfjUNPfT@zW|`PwNr9da2U{AeQ|S;=R!Bq|Ku^+a?TuGF-A+MX+36CbQ(Z{d2zybS zgye5ZsWq(9HY{3t;~hhCbOvo9fcxL?@`w;9S0%{PnBWwuFQv>o!S4U=j2?e6q-vl@?G zk~X>MqMKZrw9{AkYtz>yuM4k*q2jbBOI6D#~xqViag*hj9#4yU#j=25+6~h{c5z2|Mh?PZe?Tuj&(Su5)z2AX0V3TOflX7$@yQZv$<@WkFiv(@D z#q*Q@2#_7oiKZ-KGIjCmroEgtO4+{>u$!qm+{V4gJ{&}%Je;oN$4BHJ??a?9w%Qn+ zA49Rv&qUp;b?CTvTi+K}?3$;dHhk{7-etD%(>%^w>PoIidH*fMSkYjz`n>h_E22eH zWP2%hnp{~e%kyA5zbbm8eiQY;R^eibVl@I|K36Ttm7u7d>!RA5qLM;xI$|Rk0aF2) zkQ08N{@vimdl`nE5-VHIvD{d2{e&fI;$>lRo}pCOSZNvkO>;G~q>pM-A9rCpgMP$G zWLM)e+H<~}Byt%;WYf|m{|=_vht2D&3hH^7!^#E@E6t+KD;tAYn#PR=w}VOBPmEg| zFVg;q-Ik&r)BN*&9N~=b`kPs^IpEPMVa>&Od2zB@(r!B?A2Ej(DT!k^ul2^#y-_7Z z7?2%^K~~D#ZBVWkJ>OxDi3|>V;#!jCPOm0`OW1~)ECr_^6%~w4oZvjvP)Dl~9p%1gogfOFu6PbC5kIiBpYj;{s!w655Podi3k^ zSY;L!&rb1E6)u%b+IgZ(lfz>!iiJVA5lsc&LPq;}hTQHBWee3>ZNv3Z=n~29XfgUZ z7@9a>q^mm1nTO6E=P`_GuWN{RTvOTsRy`GBffl_SeMb5?X1EsJm&1tL2X=EcYX5|B zgnsne&jRtH8Z?rnneHz$2@{_;BUU;!Ix%egsGc1LxW=C?kK!IH2K&VTG%km2N={MP zDu@Y3Rmk8EE|=^HZ+8aS`10U)bO|FJYMbA?RzVEQBlp5+_bOZFBdnZKqtyEfg7Lyl z4adqX_*%-0bpw<^A!!js3?@B)M@#atJDMOHk`m9qL}&iI^s8^z37kB^6nF#kbL}L$ zhp+R=>NZ&qczRWV#K5@2uE2C-@U7c1kfcUQ(5*<%NA9NzM&W78uQf2@albRKYyS&t*#b-9 zCxDExUpqG^6>dJ+N<1@{U39t94_ILuf_0O~AYIG;^>%!k4{xn!`(kA2|5O_x$J9}n zEmE7PW<)Uw%m4_GH>Y)d(sb2|WrJb|iOJ#9+XSU+53T9)rL0@K-*{#g>M~E$tPw(A>A*=(>X}~13FV?jQPpzRnmN~C|6*YBW zklLeHW@NO5Z)YrGuPwGO*R`)bsj5{y0u{S_4cE3JT6iVS`Sj<%N^~Zz?qHb8VzPFM zTOov74bZ1&W@=h`Fzm?fb}Csc!CweLKugfg|EA$!Gp|#fNaj8i*c{;o+uGdA&cPsH zlIW9@|A91NkcXwDplXVQX!DQ)ila%e8v5}3H)1?N3CNYLwbag@wLZ|9`)VK6V{j8Q zOd-Hf*EiA7f+HJGAVLeFm?rHg`Yc~1X>EkG9^Dv>XypCXxJYw0NMF?z;Ru_?V`rr9 zuD*C)vplMXD|@OUTP(PJES$X9Zu-u%ncLiKl35Mh7OvM6+ZV>pF5Z-j^5&oz|MGOX z=GQ#pe|gY1+g?x9)b1o8Ve@=?e{p-crf3tlx<0R?{@!#!x5dn!(bpKO*TuG#9(Adb z>mMSqiR!|`@m#6dYI2BL(0(UDHJ#<~#&J1yp~+OAD2ozOJxY`SG^+iZj04%zZ`J!W zHHkAIL;r+~$hJLV(0FbNIb}6HTpN+p)`3P2D+kuBpz$q?ozCf-V-sa{4u8VqWQ%m8 zRp7qc-EU)R%2NQl-9VK_Xl`g~qbSPDGvyx>IKg%hk!W|WysrV(81RSC$C@~NEhoAo z6#-eZi{*D9_f{)6I18^4|F8fp%16TI&tDp?FL&%rBYne-$ly1znJDh@%@~A*!?pk^ z$|;f?=ylF6FwFvS-=0y;n+I(2l+!Mxk8~J8OUemtH6*ps?Hp)#bUPns@EdOSAdcnvO?&cBxRLd z-c8puf_=_Tv!OSJ4~py(@oo&m0@>14&?UwKtrqYuz$&~t(n~zbfzg+$NuhNY9P)Bz zr)rGPm8i>=b#Fb_lKE?m*Y2L@lLZT{;;J_t@+UYN(c3jTUVFHE5W6{Scd{>ZYDAi* zt$FzH6gjxF4a*w@#CsuwwB12*hS80^S^`@%ZzpV;1o1ad_Z^1enve=#4b@=3E znJ=I+l%sH}YHV%F7)xSoCN7m^9iCC9eOjk-_nx{9)kb4cFt@wt*J=SL``S%4ACo@n za1@J9nI&*4oH8=SA_pGTclike?rlZDXP+PW;pqTs!aY2pgh%cl1IntO`9w}q&VnQcj9M@Rsh3=x6Mu?_G{(GY zby#Ytdq!xOqkSHU2#-)$$&dnIFr#tJCo9c|1RSm;4BWCwQ%Jm8qKHv%swi%1=gu42 z4ELwEFBh?KMk|r20=Qf8*D`JY7!R2ue!tCGUl5%)`x@lA@+UmkXODnW-V+N7$mT_4 z);HKUib%U=K2W77KDq?~q!bvC{;%FXungD)p|19n*txf1w9Sv9eG5s+oPXGwyv~a& zs#faFU&SgRy>F=J1m5S`_dTNj9I4t~>o|fgoRl>1|J_9|Wh_^1Z=7N5@$51j3?PiB z#f^L-Zs}MbTD@e!Y(S}rA{jAgrXa}*j0Da%$W##b9^8;KU~OBIOH^?-e6^WeNihdT ziPXHKHoG8~Z41%*(v4TfPe&n()yErElCgCfxz7kfRFt~~slt}UCyq%BS}GI?Xzz{} z4MRcUC5-LX*GhQwV>!%c{ldLUO;Qql{iqih)zZ{waPl(n+ml_sD@5wsG)8JFc*qe< z2Gy+~+JJT`VJLH?u--2+IE#*Wdy;>EY%ZkHp78V_fSxYB{#?9Qi8FJkZmW0i#TxMC zIB9xg{{(Yt)+^O|UhHl71Cy+>sPC8t$2pmYc;f+`#toUuiayt^J!hihFMz{jg0Q^M zvga}|vw#J>1hc)>MZ=BNAhNQ5zNXyRU>i`})luG<6Qxfw|5Om1ogK-1F9N>g#e2&G zu#`RXE>=j(s-U0D8}o$0{{CzX^j7c<@H&|vhUVPS$+1hO2zs{)0-3TOoRMdaCC`=F zAKR48D0?_r2reI}-2t=L6SP&!Hy8BD5=vur=)YLSHhvnm0Gfz;Wzg<-xm ze1%lC6#&fi{q`N89g}Ofx&z~#eOV8}u zf`^kf*Uv!`6t_yWNwh}K@9RcsJ}ENiRs6n;%H8K|G}N=2(kwHYi%k^Ws50a=R#h8~ zgxeJ@+?k4-PVkdP&bXyN7$(Xg$%RzqAk95;xoe0006BO)ynGqiyuYe~Co;tR62#YB z>U5WL`P<-{z;sDowb*n(;JBOFgyP_hi%r)% zIJ1qbh9DzClTf15Zvo)=>opRhCN80LG}fI6x;d&R*@=_v)y7zK04TP216M(Bpf1+QvxAP2<3 zmzy)@XiCJWn8_dtKEs{-%P&}7Moi%D3ZV~3D>y#|u`58zKe*1TG2umydw*BW(Sw?X z%go}e=M?9Fw&%eN!dL&;iMTFP_U(|N1|d5Fsmm!XqkS7b@V02=`*uz@C9fgHFky^0 z6eG;jm1aOZ#3LSL$#C**5_oqQK3@}2_#9{TvzqYs9Pv@)w7}MFTK!n_vB0(YQt$|< z^ymy2L6zGUc|E=3l%oCyF*SgCE7Qf&y#OZj=U;e!0s>iV5SP24b4wA)6slbkKPqVa z?L7vIXHveS>h38t5DB(K7mO+b>$HL{jmcsulpV9gIQ+x8|K(jy>TN9DWHsRd-ESVJQ5c}`_fCcA#g-Gmp zL9`a{aW52!x-Xv(liSJ&(t9irNI!(V-XjjUhIaKPVf1eo_X~Srh+bxvmvd1SB{2vp z%wybkv@OTW;}j214>YImKO4Mx*VExQxs$uc1oj(hCj=~pPXQce4-mYN3K~rT&4clb zV5Q3QA)*t>xFc<)$Gw1SYsK|7B|$F-FRzC1FnhN_gFTQu|AQqEncRzh0Z6B{M)+C< z?u7TwN`dnG0r#=owToakaXE%{HxfBuQy5p=EZ(YlaaVUr2=-6PP)+q>>hzs585^st zY6X>ID{0?7@ z=h44eJX;z{S1wJhYB!nt&1~C_TX)&^X*2?!zN!SN1c%|6_m5ayicG1(l*Fy;#;DzL zNcKsqTvA%YiB)@?rim}#*ZBHl+u8^>-_NuAuhV<%)0+B}?EN!mTw3Dx*D$=fr${(d ztqrI?OuuBAvJdwwJ4{1s#VOB+F3a$^pK;jc!^>uQA}tp0M?tagM(|)71f;VY>(F>& z5E?p1FmY%imeRp8ba6QUHQK$*NNA)javS{-@X&e zvtv0<#1x?N>6t|SePNQkwwJyq(K<7g@jJmdML2nT?gZO?nqU;AwC0{U8(w-dM`0*L z>xv;G(}c96S4)A_{IyijaH#&KvIJB`3D48TL;Ez}==}t%=T7tmytIby6cLutzXBlT zg%rq64!uz)`MUkLozQE9WyU#Ua)^a8;n>HbA^Aw^JVulCABWe7wT?Bmsmbw%BZu9l zbPU79H^?Pg&By<#ThlePHJnSOr_bI#q72{~2g`-%U$yB@=|A~a`97}QGD-s2vty+4 z?F!Pw8XCm3MuY0uqe?= zSwbc1gbRN{l5YYTfwFkLBUr^3bqOrHY;3XDO8DMMEd;wD9o z0A%eejz)}V2c{GY%pwWsd*cO1^>_UGe)vX~t47NI;2jX64Mv7}g@FM$!j#4Sul`SW z#=nm)7`WpG(9a%B8>tW}6R9039@&6FOZTN8uXkrKX23C2IrI@q5>*s#1UC+%g1N-D z1h%AO31q2m$!!U~l3m+Sw_b~0H?7ax{}s{iTM%x5NCr}ZRf25-dkjwlUCmZ4u4&Q2 zV|#9=YD>HC-9t2}IOGtf8q*v#9cqKe3*L?AgY^yb1@hqodI7oy3J1}Fc!1o9@PHhN zc!8)%*dlwAgpd>K7aJiLDHk$>mFLl?*(cto7^e?279nmX79uv4q)u=zd4NouMx1OEGTx(5t}jn}~>T|FSoYs}qzy6e$!tlqAX&xu>F%JdA>+;zr4f z^e7*Nj9Ks;rV*SG_#xFH#h6FpcIilIY8i2Xp!d`Cg#4)@x5w9&t&5KU(>mL;#=D)k_n!<{DfwCzCKT@`SI(eT5`YzvG~WPcZM|H&2*@KD4d z>ZZ&d%IB$Z4elssli^YR@DKb_?x&>sq=6BfclO8%R(xFRQh)rr5*PyK-r^5}4GT(l z(-Y?(M64o)+Qlq4z`myGQhFU9)CHLk2ixKqNeHfUWv*$V*`7&Ty0JGoEhhl9&h-d* zXUnhVqeXXu3;AMkfGcaZn+#+$P#2ewEuZhXC^A9#t1B5K2yqA)1ge(y_I3?h7njx@LRV0N zd5f!)3@xoilPpGM9cc?qi--H^K9$+G?rEJWw0(?itnKuT^gd8DgWm~inIvlQMQZ7z zQhJ!lM(oKppOa9PBNCMpe=5h!E2pq3NB>q%a#W7HS5AXjj)+)JkXnuzTTY=_j;dHr zvNS^e!j<@Aj@93+Gklxb6P7tJn%U=QOqZa@9;Kc+WqCxG!k9XomN^Jv;sAHd zkaN$L1KkoEq1H2~*;k}Fbg0>zq&c{#+25o&{J7B*wJ|Wc(O0!Gbh*)+wK2H4(cif- z{K?f5z%|g%)mOkZw9nO>z%@9})!)E1eBaR%(J?UI(O1zibWU{uyLCXlb%eWh$h~z8 z!gD~xbA-%u$jEaH-E~0Ob%fn@$k}xa?tMV!eT43P$m)Fz|CPz+we-=-$dIZ(H*%47 z`LytqPrY_o7p2jH+w4f$?2O%f{($h%u25c}K0$c|{f`>d{I8W5{Qp{` z;u^(eVpm0@qI=ha=jrR%ebO=Iv}$&Zr>s%Q9d}aan6^>PKh^cJ%LQk1&Zew28LN_i z^DAbass=T6%PSTa%uiSzQJq8D%l{8;TKoUrY-S?53a(E$-=e$b@!mgozD_vWqN@we z|Bo}QWPIVw{~yaPI6h%_kN*F<`CG030)I4)=;(s&#O!&yvAS)K8t;Pb6V|t=|GR7A z#uXi&wR6Pzf8#Lk*Bj=s9lzdfc_`b}WQGgXi46R*CHJ}6r+;}OrvwA{_SY+o zK)H-vy{l!P`+NG*`*x6^PGgHH4!dsolgU4RKj@I8Xz~F6o?quCX&=VQ$Q{w01;M0? zKe|5r<_7CD z=eO3*x!r$aX2iFh3;}xNfx0v;SwBfGG+@Z;->HhvqfF4r__4$mU>Dl_1w;-9`~5rF~@!3;r~xP-hZvOfOx)A z#>8O3N{L{naf215f>m=bzbp7_(ssu&cx)Qo-{)!)Yz3A@Z0uZaM2yJ8#OGlzm?JO5gbrj~@)NB4@?>KE(K-$w}{};@dKY#K3+Vi64S<@!Z{(I{7l=!p9 z&kjG^P~0f46i13(w!hEDJga;*Eb z`!n|++@H8VaKG<9>VDh(y89J#=;Z$ei=GnD5TesW#|Wf)^D+9NKN4J3H5PF_t=V+Z zdeo8*h9+8&Zfc?>>1|E4B7MAx)^uy$L>szyXre7W|81fjy+RZ1>Gd}@@${~PCOXo) z$#HZd3)V3@lNGG%(3PyIbvyJTOJAWcN@Uh!FqUkx^&BuAvc)G}0~SKI`8ZZXw$*xP zum-ZdtPciTAUn$XWb6vrS=JX~f5?M%9S(=QsdYP?K%Odn0S0-Ad<-tBtS3W06I^FK z8}d2eR_n!(uK~APZ-#tl@SycxkRJ@5wmypdWV{MFtYBUY#g-Vv?5AEBj1 z`$T^tRKca*sn7gt%s@XUD-t>bij-4q-ilku9^;QJ3Mpc`HJ_EX4TGGQ-Og)`c~qm51<|gp7D@ zp#>Grssv^#A)&M8>ulnDM_5t#Al`#jaFpZ<#YJ@>!a$w@kEZ1<@PGs#L~kxOSz7jj zEhb?;W)eS}0IQQuk4~JT30>4rFJ3!b+77}>$_>v#2FFEnN^%(ls*o80pv0Q>#t#%H z@`Yy-FXQ9ULKh{Up&oA_A4B!(x^9&>i`+T|eD!&QOLVd(_avv-bFX~4^>o{%mzzrg_i~SBnr%DeE|i+^}|8?kaV(Z32{`vA^l!sp15>Z72z52FgXf z^8ZITvJ9eXBT1~iQjW|Q`Fac^ak$^N-vI^*geh5|*CdMz;n16gV_zk|Z7q8tFfCvU zJK^Pptnn0Rc~egGIAK}uv99VZm2WLPezQQ5K<`f zg{8Ll|GioPYfNheMj-7-S87=w4N0WxHP`1V6Y)0M&SkYzVrwp>yfsEF7wj&T0!}dB z)R~gGfP9pOR;GY_e0~K^^oJ-3AT+m~?Al!{>>5gNe17?OWz)$)sMH*xuQiB>FT2{i zQ>6U_8}Ay~r4li;jzG+$&?S12{)+<*k9 z<^SX#xY|jvlvTxt(m~C7{y{3g>7TX#o2q$xQO|fc<%8rE@A3=UW(o?gVg?gDV!0q6O!{MlX$6-Bu_m&0ms66 znWS&zr{O_4O&{2uCLQvA?xC5vGZ}KV1v6)#oTewgIMSnBur0PtM0&{R5t#UEy3I9) z`LVP?3f;o}sz*7g5qdTxJl^gk3>;8%SOPH@B)rmFOJ)m6?PlYa$y=RX%;}KId{m9R#2=LNwosF@OTivgMqxpRGe}5=LtAn?VVl6VWCFLD z7l#^^H8jY~42hR)OoVF#YDW(md!g(&pJ;yMj|UBAQa}UH?ED@%ci=*(q~Opn>kE2Q z_4Kgf|0kEA6ary41A;)^Ku(*nirvP!Y>{FZYBLXLP6QL~vRL+uMlZ?jWukMV*(dsn zL~~KA@jU)(UeoOz^4Gkw{fJsYQ%|UA7i79qO5=DOPBcWlv%pK!A+)*F`3WJ}t9FU3 zXhC4xMV7Z%5RjDs0=&vC4WdvD?Zi5tg4@xg8-GLUI>N$N&3aS4bHrp%3_1u9wqL)i z)XQLsI&{Hd&bQE!3m&D0vd!4D`l1$rt_{3NS?~lj#|$GN5RmvP(j3hzJOk=+0B*2v z)Bw133RMUM%wu_+$vbzOy?yk#kvR?xGsg-ipX4wKyXqd zROKp5))>tNy$HByaEHK%$mqd>-{Yoj`oSBK;w>+eZ&TVcj^DyXjo{DDbZ>vS2cCWB z(6&~GZ}kUdN(*2-nI!hvbnVy@z2E#F394OZD&Jb04}`Tgaj?MoY?1`{ejE2iud51% zQ~J0sijw(hqr_Ckbj@pm$FAVASKY(D4BS0GYPkSMqSDONRaFH+O2+jL{hIltJSJT~e)TNDr(}=Xt7|UhcU9eoXl&QZRR<9WomW%&m)FT~j zTgGd3-j}Uk%CRD;$@X)NNV9+RJbifYu>yr{FkO;p>_&njI> zyBHh_72bW;8}oGeY0gpHOxiV597j7mY<#?WMmkf5x~Kfk*re(&tG_mX<3&2cON*2u%V29tsXUv{#-ijs2>EuNH-x3) zPBpi+V6gI=wn}u164_j8xi-y(B?Au2o;UO=r6&)i5S3Mx*)*{_;u}~i4dh$`VgUS- zMG6t*?DXDYX0D2Oj31MI!HF>|aG8rjrOPnxHu4wZl;!=NGjjDoBpXf?ntrwt^dqxm zs(lE@*QB3NH)!`rH)5kks-D89g@UX&@DU9jvrsY)aI=9b4nPy3bfdX_U;#?zsan{G>DKob2LnhCJv8o}duQK)qP{7iaaf2=K`a-VNcfC582d4a z>sBJA*%S|NEazDxXcGPW_uZ&d7xG`~JB!U>U(}acUSn=FqOA~(pn^!aMXRnqiL0;? zebEZYouRv}-0r;Dq&z9>s#Rt1HL`0p4bB)A&sMyn|rE_9nh z?NO*RrjET8D4s(-`nS{MrdYtv*kyCnJKbsftG2D#ia@;42!8xd?a3P(&Y?vCf9na< zQ&Ni*1Qel&Xq{Z?=%f0SRqQt5m|Myg+8T=GDc)@^};=tM>9IDr7hdvE9-M@@<0pqv45xZTeNecbL- zWFQt4t`9>j8~X%lz}%We>Kzh_=`XO}!;4!OWH?=p*DOs#Nt({k^IvtBEL~Qafn)I^ zm*k{y7_bIs9YE}0B6%r`EIUH8US+MGY!KQA1fi-jCx9*}oz2k1nBsXp;4K<_&SN}}w<)!EylI_)v7}3&c)V;Cfuj*eJ2yc8LK=vugqTL><#65r6%#2e| zdYzZ)9Uq7)A$ol&ynM!|RDHc_7?FlWqjW>8TIHc`jExt)f5W|;D%GC#$u!%B*S%Z0 zsj&;bIU2jrt_7%$=!h4Q29n*A^^AI8R|stsW%O@?i+pN0YOU`z;TVuPy!N#~F8Z29 zzZh1`FU(q31wa>kmw{$q=MY>XBprL<1)Py~5TW4mgY%rg$S=4C^0qr+*A^T)Q)Q-U zGgRb9%MdE-&i#X3xW=I`%xDzAG95!RG9)s?v_5+qx`7NdkQ)If5}BoEp~h}XoeK>kweAMxJ8tehagx~;Nr_WP?jXa zJ&j7%Ef3w*XWf?V*nR)|IOMrX;$*$e23m?QN` zk>sC^GE=h6?*Cr~596s_QE@>Nnr?{EU+_^G=LZr#V&0fEXQ3IWtrM{=t^qJ62Sp=e zrrc>bzX^6yFV!^v7;>J9>j;`qHDQ4uc92eVe6nO@c>H=ouLQot``E~KLNqMqJ7(G+?GWO9Ol+q$w z!^kMv!n{vF?RqLnxVk{a_Ar;^sw0@=+~6!4&;SCh^utT=I zo&$CwvhNOjQpenw2`5*a6Gos6cs~*TD`8H9P4=#jOU_`%L!W;$57NjN%4 z39(61ZC#s7^tv`_4j}wMRT9rgDo*XtZwN-L;Qc$6v8kKkhmRrxSDkUAzGPgJ?}~_t zkwoGS4=6lsD`=RL|8L3O9L()N)lmEn-M15fRC{dhZ}7eYV%O-R^gsAp{q4 z!C1}_T8gy^v@SZ5R&Li5JMJy+K8iZw3LOGA0pN1~y@w7RRl#F()ii6Y5mr~Mdy@Kz z@FT4cm^I&#Fu_9IX(HAFP{XLbRALqm&)>m_we>a`hfv?eE|t z?YdDp2yAhj-~vuw^wzVDuj%w?exOcOT(ls(F*ceCe(C5HlN{lcQ;}|mRPqFDqLEzw zR7ldY+M6xe$$qLwekmk{Z&5cME$gpC?-8)f0m$rqaS|mj9ATNJvvyCgs(f2{r;2E!oy$k5{jik#(;S>do<#m0wVcU<}>)VtYmF9O0%(C>GDzPgh6X z9OkQLMR~y7=|MtaU!LDPPY7O)L{X#SC+M|v^X2CZ?$GS>U_|aC(VA(mIvCNk+biD| zSpj>gd(v>_Cbq>~-x^Y3o|?eHmuC?E&z>;Ij`%{$Pm$hI}bl0Kd`9KD~AchY+goL1?igDxf$qxL9< z4sW@sD)nwWr`T>e2B8MQN|p*DVTT8)3(%AZ&D|@Zh6`cJFT4G^y6`(UdPLY-&bJYJ z*L06f2~BX9qX}u)nrpmHPG#La#tiZ23<>`R@u8k;ueM6 znuSTY7>XEc+I-(VvL?Y>)adHo(cZ;1I7QP^q%hu#M{BEd8&mG_!EWR7ZV_&EGO;d(hGGJzX|tqyYEg2-m0zLT}a{COi$9!?9yK zGN7&yP$a|0gL`dPUt=4d^}?zrLN?HfKP0_gdRvb}1D73Hx!tXq>7{DWPV;^X{-)cm zFa^H5oBDL3uLkaFDWgFF@HL6Bt+_^g~*o*t`Hgy3M?nHhWvTp^|AQDc9_H< zg>IaSMzd7c(Sey;1SespO=8YUUArZaCc~}}tZZX80w%)fNpMExki-qB+;8xVX@dr; z#L52S6*aM-_$P9xFuIui;dN#qZ_MYy^C^hrY;YAMg;K`!ZpKKFc z9feHsool)`tFSS}Su|cL0%F;h!lpR+ym|P>kE-O`3QnHbJ%gJ$dQ_HPTT~>6WNX41 zoDEUpX-g&Hh&GP3koF4##?q*MX1K`@=W6(Gxm1=2Tb{hn8{sJyhQBoq}S>bZT zisRz-xDBYoYxt6--g2M1yh{#QWFCISux}4==r|7+fYdS$%DZ zXVQu{yPO<)Hn=TK`E@;l!09aY{!TMbT)H-l!(l{0j=SEj@JwW0a_h-2F0MZNpyucb zPPb+4&j?a!6ZnPTB>$t`(XSf-}`&+#rI#`GB> zl=$3HORwccTnA2%>$Nmz)u7j%_ywoGri1UXVNRxSf(<@vDLKKxFo;5pTI$R~a|-sQ zd5Rfwj+$k1t0{J`qOL^q>vZUHc7a^`cKKVa{66z?wMuQAfdZBaVVv@-wamPmes$d! z>gv^xx<0jXOz;7HIQS z4RBIFD?7{o^IQ=sNQ-k!ao*+V*|-^I2=UF?{d>bE9avsWbAs{sRE-y`7r zxVAKA9amvo4T}ZAHSF-{y1GqUHlDp4DO9I3mz5h8n|}P-9nKD|$r9AS3gbF1AX=2B zyaK3TbKYqv%~JHKQH8v+%zQ8UVEGDZY|mb>Oe3JD_Z{+Pq%HB+J1s*y6JOlk`6~H) zKt)YMZ*RkbU!GPHzJltmW-=6zqO=5;S)jz{ zFSx?ryqSMxgx|Nhv3z#kFBTuTBHsViaOHs5e&vXZ@l@mVI37<+^KvTE51!pB4Tggq zz!NlRY2ZLno0&6bA|KHPYOMY;;LZG&_lzuLy{@i$&B(}_*~Zk2 z>bkQ7u&Ww%CFh{aqkT{HCbPbRX&EvPRp=}WKmyHc>S_-qbwAr0<20vEoJ(!?-ucjE zKQ+nSlRL^VnOX0h+WcjGb6WI(8;7bsMaHXDb6ynPoOXMlf9nLKre;w*#E_whR#5!! z!^%_+X3eJVKc$fMZP;+xP$~e(CIP1R&{2m+iTQhDoC8Yl@kLM=Wily_cu>7C1wjVU z-^~I0P06ZSNVaN~A`#cSBH2L&tk6R%dU1(u1XdAx;g+5S^Hn9-L$v@p7CCF&PqV{Z?R$}4EJi36+u2JP7l(@fYfP!=e#76LGy^f>~vs0%s*x@X8`|5 zGd6JOHsQ=feES4Vo8%1P_7F5qjiIm#oRT0kO1(?Z_Dk6oX&j=Xd8Klk(;gk3S(ZFnc^8Gc=d;8O-R9tlGyp=2I@1teAZpGWUi;}`n zbJOS_Z2L16nVtDnPpMn{+wR9&yU9~C<-ncppPee`>@1k7hTl5Fn_3_KzQ)u{iJPp3 z)df?Xo%9ta%(dp@DhKuQj4D8=_!*ra#Ib&OXKrsYvAG%H7Kq|43WbayvsbeeimSa= z8~{7ya9ZUAIgLLPeuNmSB&#-`Je0Lja)M$}I41KHb7dQq$wgwX+EElNxBgyyLbA2* z=c1VJR%EPJEw(7!UE?4w@94{pI3E%(acEYd8*Wmr^R7|IM2RZ-RVXSkXy-8$!(iB* zQA`qh2Ze!EY6}Zs7vRz&nr|L60NlIgnO3L*Yz2k2Ivfen?drnVzzu3)1V&-t5S~S? zw#=Sdh>K@2vA25su*@>npw&7A%|Uh9T1jR$mV*H@)pU0&2#Se`7iJlOr$mp79`DKM z5vr*XLrg7w6lc4&S{So1KGKBqcuJ!E|HVFB?vTOjQHi)g+FwJqX@Y3q(qa#6T@3{q zhc@2T-W}XD9x4u+LCdce$*}x!Sc#+rH-sCz6j}0EE`Tk*irUq)y^za`}^1gFnF)C!yf_l_}I<6qfbT$Gc&Eyr?!QwJR~RE4!gKVmqjbI+I^*^ z&hz^7r-dgm@Mbfc#{JTH&^6sJCZt-NTpChB^fzQ}?etydyf~+)!d%V$0faN(f`rJb zm_YaJZ@>Fg>Ay2&bzTx3w^u-lsulc{mX4-nH*A(32O&b^EWmSuk{#HJk}_ULC}SB(L7`YAs>opp9o5UcnB^kVB*rmW6{s0&~_>J!_#+cEWib@v-Ms`?!&=3fDot`oH9v&$f<52>{n2l* z1FRzJ#yQbTHO}}wt0!y8Eh-0*|Um3vjX-nWH>`JN5tWB_gnW%; zUJ0V?_a#+!=>ahhrbGvmvObe8=v1uI8#gNHJ#>RwxL>E^pT05Br8+$@a9aDC1~$@* zicSQCbQcr=DCHM*?G7Hsovk|{$3oIwvymi#YoXeVfWj{Gd#XmnDgzQPRUKNAAI44y z{1WG&rhIR4ipmvBmq$BZ*5tmPIZmhhWgq|TcuR{6lA)+vhj(cH`0;+B^72{&a7ff* zkrIo|pd-Yxm+VVptC@QNCDk0=Re%Sz%ta7y{5Dn9(EapBS0r zLbDKeZepar5%cAcb<^;m>1{QhMzRmRem=+0I3ERot-)gb`i|sII^A#^Gz+x>TW5A& z3PQcpM$lDy`zb%1yf!e8&_>D02RN950KzW>GN6n@2so&Wu09x@PB=&IkIf|zZ1W}P zAKf*&Mo5@@G=w&290aG1@3=IMCB^|G4L7*xn;r3v&HBrD4D)Zg+)f~Ls$7*P-^i#B z4X7ac=0&58j^@2EBZCs}YPe3rqgLAA1L3Y}o?}$%u~)7Rk=LLFbAdSy@-Uw6lv?0K z&P@@M`o2Rll3GoYjotf@WNNjHbe|R?IKVn*?Rzf9v9QoFMq)ODF~>L}26@z`KA82t z43e!^z&WGqAk$Ww8j6bc3$I|;5^BHwt`?e)zf|&+l#!8uJV_Cwy-n1yS0^Q{W*a8B zTzTYL>tt&I&9vzGQUrO?YIm6C1r>eyh|qw~-&;7s7u1achP$K3VnXd8sV8J7ZTxTh z5+^*J5%_#X)XL2@>h(Gmv$@)fZ@ikR$v(2Rax89xscFEi!3_;ORI0dBxw)S{r50qf zg&_a*>2Xe{s@)7OX9O!C?^6fD8tc3bQTq9}fxhbx2@QeaO9Ej+2m!u~+u%Q6?Tgz{ zjYS}bleKcVhW~1$?t*AO^p!=Xkkgwx6OTik*R3~yg^L`wUU9Dq#$Z*iW%?s6pO_f8 zJ8w#u#Eaw7=8n{zJ}C>w{enA6XYHfUf7h)!Qaev)?V=yW{b@-z`hAz;I7^|DoFChP z1aYQnkGauh*ps6x*_S77@z1wwGmF8ky9fMbM$dr*`vsot4uvqWn)0vTRwJqH#&D%g zL3(0dP>%Oj&vm5Re%>*4x|h1J2X*mK5BH1?Nx_#7( zepgF`+n)rHXj!RiipusEq!X81;QQBXlTvLDj=Qub(ha&D=BDx3@-V*d!D9PeXUY?l zwZ0<4=iY!sUj4G>zTS+eYX7knN-8Oynl=NdwHS*nSz_5}*5LQ@=?Yr?uj$`C1m2OR zK`f5SD2|;=BhU#AmaTKe9QaSHQ_DUj1*cUPa*JICFt1<&S3P3zsrs^yUE;tx=x^cmW!Jq!+hohv_B> zPDMT0D&08dC4x@cTD$o1$x%So1Ir(G3_AVQMvQ13un~sP(cEWi$2%5q93E7t{3VJf%K? zuwSyDke~7KuB2?*#DV8YzJw z&}SCDexnUPD!%4|y~7}VzvJ4ch)WT4%sw@ItwoNt(C*RP)h?&~^g##vnhR0!HvIYx z0td2yz9=>t3JNySl*TszmfH6`Ir;ft@RdWs3}!J88UE|gj_GMQ6$ZYphUL2~4OY7} zB*33_bjkRf_@l;Y!7MIdb~bVe;-m78Pz|pdy=O*3kjak63UnLt!{^!!Ljg0rJD3a~ z1Q;y5Z^MF<=Hr}rdoz>yRczx+p3RxxgJE2GX&Si)14B@2t21j4hnnP#U?T3g#+{W+Zb z5s^@>->~-}4|_*!5pIzMCEp|3+i1XKcfUxW`8|ezAh>y{WiRcjSG*asw6;Ef(k#>V ztguN?EGkV_mGFdq!n#W)<7E}1#EZN8O$O|}qdoE|7K?F4zo1jL-v}E8v?9qz(d$&2 zMwyK&xlC9rXo_2xw7Qe0caC?o?Pc*-QAOE!+UvRuKjG+;dk|jQhDDBe?`XT7Y5lte zqSu0t5`;>Wv%|nhj|ZiE^IqA_lZu7OWh!2Y(627zb=r7Ends}wVk7Q5o09a@ojhH7 zU0m&h*8+j4e|OqWyJ&B`V`y=>MVO;K9=hk^6EsmVAGkLT{oUtR{JqSRY{Qi{kKw1k z6s;0SMPJOLp!som|A`*q3t0wIj-=bG8a#MC)MHcMSQU98Juv$?$CvYX)(n`P^!`5| zv3q@@|G@6wMqh;d;m4qvdibx2Yjml}vG9mDv&!0ne02M#D`Bo}xIB0VWh8>>WtNZQ z$&ISlJX;*ORQIO;k62qA{^6P%3!Z=Y1EbmY02{w^yB$`;%!{kur&XTGDiO2cjA)lr zsY^XZWy^DSAaz;kZ_VG?uWnJR7qdN18$~)>(kOoybY0~QYu9||K#|$Mby{3GduV~N zk9H7$7=RSo+?CUYF502`b76ytBy}sFak&|HIwRvB=0D|S`c#QCJPq zP)uOWI)#(n&{6|C4A^G~%B~BY21aOMoz9RuuM`Ip%oBz+NoAlb7?#`E^}7xXo!4S? zFg8I~G%!@nXi8&aJSGFcZAxQf;0m}942=i#p-&teLvE{AKm7Sl2f}Io?!IqbC|J;h z`=5LFOnU5?^w~SV@YwNZx$k_(kLNxZDE z3cf08^-rIT_>A$}B%IJBPcN^)4;90BQtiEi!gT#+EqyAUZ|}*b_}R>SGloq&6?opL zuT_+lwQMgg6!Cso$BwUA;k-1NcrzyE>(_X$B0HocjY~=Pk~Q08+N}(|%HjO_i+*=o z%G6C6A30Ch<0UlG;Zdj@ed!rfUY_i9mYwK8(aYuzcUzlTJ1yPz|Bb-9b33A9zRhGl>Ny-Q#JAq-+qtI@B@&w z$;PJbyiW=!py@g2hAi0)U1v=;avka`gd@8LC4=BEbNqL&K^UAQ5%r95#x%^qRB%KLaqMnG|6xKAm}sx!Qwo}J=2C;NROi$mfADui4)y(3wVA3k~{j^_5%H)C6K zlYAm1eY**HZOj($)xfKIQFtIVw$4&yvz9>(Crs>Gh{ zya6-FG7Dgi92#K)64=9Csj5?Zqe~_9TwSI!2quAwa1w-*uC5!}xY`?tltb0Hq740< zsq2QelPveZ4chr$=~U3!+c&>xyfvA1`)owOqj=i4wjY=A1577Gwg&Ko7;?il9r|_* z8P&IDV_g2D{in5OLFxsO!kx3AhO$5aKeoM|!q|VokqMlYM@HtsRuMtBY%I35#5$+G zpp|JOeoj^U=95HLemB04Yqv{a8X<^K9G2`&ShM_6&Bi1n?o?@MXsDj9Z*A3>#XK%J zRc*&SlFl>l)9DyRQ{*%Z+^e1XpH?0@vhpXrnPPU*d%vOhKkimm-u3c%Q^v3RKp9kx@A2dS?QfS=iigGr7m><)YkV=%LA5h@Uj@9=~ABPMJ z1UE;F&;Ttg5Kc^Qy!1SuvbNEqdgu3*l`=>s5_}dUv$B%BJbMiWrrMm7OXOdi=GOmh zZBvXXK7VqO&zojI2Om9};zCB5i|<210I{iwiGznGCx=FT89=Ef)5!lB1cZ6lbzgDn07*he}G&w7m!;|E(L-?+cz@0<9ZI~LqYQE7>HnPA436}oeN2Y(VfG6 zxNZuMK3Crm^Z_AFeHc~CVRrSl0W^?+Gbteu1g8NGYa3(8f*P{(ZT>%!jtSl6WbYVv zmE(37t0C8vJ6O-5+o*lL9XRcFbd~GSBGbGh3~R!67g&l)7n!kJlWd)~TUyXus#!&G6sR%(l(h1$xyrR5j_jM1zj#giA&@(Xl26@n<9>folx!92bQ z24h570+<)4!$!IQ(5yOU|4_E6aN@4v0+{Kx~Z z;q7fp%0cHziuI%!kB~w}g9@V+1wDz0wFlzX2UOvOy|&;e;t!lAR8tV2KQHgtfk8Uf zw;rs!(4JPODERk4ckd5I2Vq|0rd@@Mwd8MID%0^fITjYIQom^q;qhP8@|eJx{?5xX zc1@Fj*kDknlk{c-rnCloQ3hGh7OU+@efO3>fkRMcM>J?AeVP& zlfzX%cdp=N+4S#E*%^=BQ+N`A7C}|k%$|QUn0yI6S3$MS-NjO!4hm55uyju)Q6e!} z*OVO@A#-mfC9Pha6ng((Xl^V7{d+&u+yx)_B1{~t7d5e8L^i4J>;x<7@5;+l7-Gge zf#9diXJ$&v^rbN5V(ee%q0xBMEgS6%qZm7hNUP%G;^J44I!BmI@M*+FWz0!+s;+iQ zU4CuI+27bvNK8v>?7PZnVxB=heJ&_ymE0nN^W#-rqB%+JXkYGDuRw>JM_LdtLkiq* z6%%3&^BX$jnM@2bjiGc-DymKly)wVkA-pq;jSWL#7_*moZZ4I|-N}o8SK?sIv)p|c zu~9-B%tMc=!)YMFp*SiC0>kfnH8+X5>;+FFVN{~a9YVdIg1uGkZ~kegFy{^PU(4{( z`CbY`XmVA3esai686Yw8djCEyF7`bfB^F1)nwv+AqYLZ&Zy=eFhYT2uMd@{sP_qS4 zbJ&>PxajjZt?&c<1^!T|pLHfX=E^FJ>-l_XCZzvRV%x}@u(FtF(mS+Umw$e+IA74e>gCdTqi;6&=euAIpxd=Y3I5xWR zBhGoT+T`V1@91OlQ}2YO*~P4ukd*TBBdt?Plt)_ou6Y@Db`ss+Q~A-48s>?eaJYA2 zRGOa8^~Em}EFTmKIVVbMb|ob)hJJ7ITg>yHAn2i|{2ZJU!cwt9YNDT0=*WO7Bq#Xj zg@FjEaKoolrF8%c;49|`IT&25?O$dq8kp3#la9&6aH z6G|{>^C(>yP7#Dr$aeFyS0Ai_$ILhL43#*mgEl(c*4?Ae;tRL&S7Vc}Szl>B`mBuI zB9Y%xp%CZwlH!3V(`6W4-ZuETssvI&B~_O;CbULfl)X1V%(H7VSPf`_Ka9ak@8A=z z1l|B1QKT}NLI`WVTRd;2En5u{0CRqy9PTi$ja^inu){LJ&E&6W%JJPw#&PaTxpt?k zpC~gjN*22Q8tpGHR|tg~ye#9a8N<%odhZJnk7Oh=(PKfhYfzLAxdE36r<6a?A;rO&ELp_Y?8Pdw(PT^Fxn!eG_|LEbSYoBrsBA|6Fgr zt5LntyusI{Q2fdy=>ditS;}^B;I2MD4=(>7fWt0Jp~y=?VvfvzHvQhj6dyIef46J$ zl4Xu7U9v_NJV?uBBC0!kcTS0UcrV7+@~is?Fi+jrr@l3XwD|uG zr26jUWiv>Ju48Y^#qn7r9mwIH-Pv6Y|V|V-GZ&+&gQ?S?-`&ts{@5GXPqbmyZjUACC&oVXfNwUX0}ba(v978 zp8z!v9~8Zx8qB@7>oFPDm^iR@+yw`79YF)w^OHB_N;&&x7c3l^3!)IY#)}x)@D(iNaOm9 zC=^*!{`7={3*S=%iU=KsPXh=DDZcc``Ss>057i{pdW8M@4q+Ba@Tt%OytH!4>rbIbQw^-pR zGGYNPzw@n=PV@)b7yVbFr;glF*Qq3>F9oBN5PUXt!?2mdGcpv^o1?Thp`jP10G2Yi z(c93td3F3SW!Le5DUwdub!aDKoVLU6g!O?Ret21l$qOC;kdd@L#M&baVu&JZGt&<6 z!VCkvgRaav6QDW2x}tUy4~Y5(B+#Ej-8vM?DM-1?J_*&PntI3E96M!`WL#<&Z5n2u zo`P!~vBT$YOT~gU9#PB)%JZ zcd_u=m^LYzC!pH#W`yA1!(fA;D~b zG#73@l)NNd;n#XrKXZEfab;@kQRnOFU2Th-1m<4mJzlj9b3pv-GF$elX7ib9!uILM_$ke zHIGB*&=5=;ynQA{y7H93%i^d)T}y@(p>8vVhJ4L)M{0Q*@D^+SPp`EW+G6E%+`Z;u zS3goV@Dic7vc5`?!pCN44Ts@*{)zwy)9?B||AM{zKlN4T}qQRL2 zgv+{K8bv7w)#xge16;kI1fU87!W4pX)N&|cq8&i^1r`W|Hg4366r(?-ecEJ9u&Eaw zrhyikXQB>C9d>cpPGiu=VU3Z-u4|0V_iap!_J3o+K_R5EXk@sfu~zHwwYkpncVh!R zqNe7Cmf_|Wmeq4#(mIO&(wCK@b4(x0?W1Qtk(`$?+$uCJCGZm_%k?l32vuShgDFMa ztc`{$8DhB9)&?~(m&EUc=LzI1=qo#zjy#2{hLT_*aj<618qQ7mD#k2ZFGou&69;=2 z1j7=Su8k}{L*h&mfs7jg^PN&9C1Z@U!p6gXk&-7xM~{X`nqH#aGO`;Xy_zbz^rYacIq0AH%4!Oh93TzJ820%ur)8OyeS@K?sF1V(iFO z37Nnqj1z#1{|v7=_CX`lQA|$<1gtuNMHGNJYp1D_k;WQk-b+T6VmUK(x=bWviOZ~T z|4e%SpuaWLWD?qN2%`S*`P;BQBw(B__wTD6epvGdJ+>DBq2oVlf&F*lz+#avb4)3P1c^Mf#olQheVvZ|Z5 z>xXfgmv!5Z^SYn+_x}K5B%G^sRwiez&z9|f!E!#oJlT2kCOV0000$L_|bHBqAarB4TD{W@grX1CUr72@caw0faEd7-K|4L_|cawbojjHdpd6 zI6~Iv5J?-Q4*&oF000000FV;^004t70Z6Qk1Xl{X9oJ{sRC2(cs?- literal 0 HcmV?d00001 diff --git a/charbuilder/js/arrayextensions.js b/charbuilder/js/arrayextensions.js new file mode 100644 index 0000000..746c99e --- /dev/null +++ b/charbuilder/js/arrayextensions.js @@ -0,0 +1,342 @@ +Array.prototype.joinConjunct || Object.defineProperty(Array.prototype, "joinConjunct", { + enumerable: false, + writable: true, + value: function(joiner, lastJoiner, nonOxford) { + if (this.length === 0) + return ""; + if (this.length === 1) + return this[0]; + if (this.length === 2) + return this.join(lastJoiner); + else { + let outStr = ""; + for (let i = 0; i < this.length; ++i) { + outStr += this[i]; + if (i < this.length - 2) + outStr += joiner; + else if (i === this.length - 2) + outStr += `${(!nonOxford && this.length > 2 ? joiner.trim() : "")}${lastJoiner}`; + } + return outStr; + } + }, +}); +Array.prototype.last || Object.defineProperty(Array.prototype, "last", { + enumerable: false, + writable: true, + value: function(arg) { + if (arg !== undefined) + this[this.length - 1] = arg; + else + return this[this.length - 1]; + }, +}); + +Array.prototype.filterIndex || Object.defineProperty(Array.prototype, "filterIndex", { + enumerable: false, + writable: true, + value: function(fnCheck) { + const out = []; + this.forEach((it,i)=>{ + if (fnCheck(it)) + out.push(i); + } + ); + return out; + }, +}); + +Array.prototype.equals || Object.defineProperty(Array.prototype, "equals", { + enumerable: false, + writable: true, + value: function(array2) { + const array1 = this; + if (!array1 && !array2) + return true; + else if ((!array1 && array2) || (array1 && !array2)) + return false; + + let temp = []; + if ((!array1[0]) || (!array2[0])) + return false; + if (array1.length !== array2.length) + return false; + let key; + for (let i = 0; i < array1.length; i++) { + key = `${(typeof array1[i])}~${array1[i]}`; + if (temp[key]) + temp[key]++; + else + temp[key] = 1; + } + for (let i = 0; i < array2.length; i++) { + key = `${(typeof array2[i])}~${array2[i]}`; + if (temp[key]) { + if (temp[key] === 0) + return false; + else + temp[key]--; + } else + return false; + } + return true; + }, +}); + +Array.prototype.segregate || Object.defineProperty(Array.prototype, "segregate", { + enumerable: false, + writable: true, + value: function(fnIsValid) { + return this.reduce(([pass,fail],elem)=>fnIsValid(elem) ? [[...pass, elem], fail] : [pass, [...fail, elem]], [[], []]); + }, +}); + +Array.prototype.partition || Object.defineProperty(Array.prototype, "partition", { + enumerable: false, + writable: true, + value: Array.prototype.segregate, +}); + +Array.prototype.getNext || Object.defineProperty(Array.prototype, "getNext", { + enumerable: false, + writable: true, + value: function(curVal) { + let ix = this.indexOf(curVal); + if (!~ix) + throw new Error("Value was not in array!"); + if (++ix >= this.length) + ix = 0; + return this[ix]; + }, +}); + +Array.prototype.shuffle || Object.defineProperty(Array.prototype, "shuffle", { + enumerable: false, + writable: true, + value: function() { + const len = this.length; + const ixLast = len - 1; + for (let i = 0; i < len; ++i) { + const j = i + Math.floor(Math.random() * (ixLast - i + 1)); + [this[i],this[j]] = [this[j], this[i]]; + } + return this; + }, +}); + +Array.prototype.mergeMap || Object.defineProperty(Array.prototype, "mergeMap", { + enumerable: false, + writable: true, + value: function(fnMap) { + return this.map((...args)=>fnMap(...args)).filter(it=>it != null).reduce((a,b)=>Object.assign(a, b), {}); + }, +}); + +Array.prototype.first || Object.defineProperty(Array.prototype, "first", { + enumerable: false, + writable: true, + value: function(fnMapFind) { + for (let i = 0, len = this.length; i < len; ++i) { + const result = fnMapFind(this[i], i, this); + if (result) + return result; + } + }, +}); + +Array.prototype.pMap || Object.defineProperty(Array.prototype, "pMap", { + enumerable: false, + writable: true, + value: async function(fnMap) { + return Promise.all(this.map((it,i)=>fnMap(it, i, this))); + }, +}); + +Array.prototype.pSerialAwaitMap || Object.defineProperty(Array.prototype, "pSerialAwaitMap", { + enumerable: false, + writable: true, + value: async function(fnMap) { + const out = []; + for (let i = 0, len = this.length; i < len; ++i) + out.push(await fnMap(this[i], i, this)); + return out; + }, +}); + +Array.prototype.pSerialAwaitFilter || Object.defineProperty(Array.prototype, "pSerialAwaitFilter", { + enumerable: false, + writable: true, + value: async function(fnFilter) { + const out = []; + for (let i = 0, len = this.length; i < len; ++i) { + if (await fnFilter(this[i], i, this)) + out.push(this[i]); + } + return out; + }, +}); + +Array.prototype.pSerialAwaitFind || Object.defineProperty(Array.prototype, "pSerialAwaitFind", { + enumerable: false, + writable: true, + value: async function(fnFind) { + for (let i = 0, len = this.length; i < len; ++i) + if (await fnFind(this[i], i, this)) + return this[i]; + }, +}); + +Array.prototype.pSerialAwaitSome || Object.defineProperty(Array.prototype, "pSerialAwaitSome", { + enumerable: false, + writable: true, + value: async function(fnSome) { + for (let i = 0, len = this.length; i < len; ++i) + if (await fnSome(this[i], i, this)) + return true; + return false; + }, +}); + +Array.prototype.pSerialAwaitFirst || Object.defineProperty(Array.prototype, "pSerialAwaitFirst", { + enumerable: false, + writable: true, + value: async function(fnMapFind) { + for (let i = 0, len = this.length; i < len; ++i) { + const result = await fnMapFind(this[i], i, this); + if (result) + return result; + } + }, +}); + +Array.prototype.pSerialAwaitReduce || Object.defineProperty(Array.prototype, "pSerialAwaitReduce", { + enumerable: false, + writable: true, + value: async function(fnReduce, initialValue) { + let accumulator = initialValue === undefined ? this[0] : initialValue; + for (let i = (initialValue === undefined ? 1 : 0), len = this.length; i < len; ++i) { + accumulator = await fnReduce(accumulator, this[i], i, this); + } + return accumulator; + }, +}); + +Array.prototype.unique || Object.defineProperty(Array.prototype, "unique", { + enumerable: false, + writable: true, + value: function(fnGetProp) { + const seen = new Set(); + return this.filter((...args)=>{ + const val = fnGetProp ? fnGetProp(...args) : args[0]; + if (seen.has(val)) + return false; + seen.add(val); + return true; + } + ); + }, +}); + +Array.prototype.zip || Object.defineProperty(Array.prototype, "zip", { + enumerable: false, + writable: true, + value: function(otherArray) { + const out = []; + const len = Math.max(this.length, otherArray.length); + for (let i = 0; i < len; ++i) { + out.push([this[i], otherArray[i]]); + } + return out; + }, +}); + +Array.prototype.nextWrap || Object.defineProperty(Array.prototype, "nextWrap", { + enumerable: false, + writable: true, + value: function(item) { + const ix = this.indexOf(item); + if (~ix) { + if (ix + 1 < this.length) + return this[ix + 1]; + else + return this[0]; + } else + return this.last(); + }, +}); + +Array.prototype.prevWrap || Object.defineProperty(Array.prototype, "prevWrap", { + enumerable: false, + writable: true, + value: function(item) { + const ix = this.indexOf(item); + if (~ix) { + if (ix - 1 >= 0) + return this[ix - 1]; + else + return this.last(); + } else + return this[0]; + }, +}); + +Array.prototype.findLast || Object.defineProperty(Array.prototype, "findLast", { + enumerable: false, + writable: true, + value: function(fn) { + for (let i = this.length - 1; i >= 0; --i) + if (fn(this[i])) + return this[i]; + }, +}); + +Array.prototype.findLastIndex || Object.defineProperty(Array.prototype, "findLastIndex", { + enumerable: false, + writable: true, + value: function(fn) { + for (let i = this.length - 1; i >= 0; --i) + if (fn(this[i])) + return i; + return -1; + }, +}); + +Array.prototype.sum || Object.defineProperty(Array.prototype, "sum", { + enumerable: false, + writable: true, + value: function() { + let tmp = 0; + const len = this.length; + for (let i = 0; i < len; ++i) + tmp += this[i]; + return tmp; + }, +}); + +Array.prototype.mean || Object.defineProperty(Array.prototype, "mean", { + enumerable: false, + writable: true, + value: function() { + return this.sum() / this.length; + }, +}); + +Array.prototype.meanAbsoluteDeviation || Object.defineProperty(Array.prototype, "meanAbsoluteDeviation", { + enumerable: false, + writable: true, + value: function() { + const mean = this.mean(); + return (this.map(num=>Math.abs(num - mean)) || []).mean(); + }, +}); + +Map.prototype.getOrSet || Object.defineProperty(Map.prototype, "getOrSet", { + enumerable: false, + writable: true, + value: function(k, orV) { + if (this.has(k)) + return this.get(k); + this.set(k, orV); + return orV; + }, +}); \ No newline at end of file diff --git a/charbuilder/js/charexport.js b/charbuilder/js/charexport.js new file mode 100644 index 0000000..7a8daff --- /dev/null +++ b/charbuilder/js/charexport.js @@ -0,0 +1,994 @@ +class CharacterExportFvtt{ + + /* CONCEPT ===================== + + */ + + /** + * @param {CharacterBuilder} builder + */ + static async exportCharacter(builder){ + //lets not be verbose here + + //Ids of brew sources that are fully loaded into memory + const brewSourceIds = CharacterExportFvtt.getBrewSourceIds(); + + const _meta = {}; + const _char = {race:null, classes:null}; + + //probably needs to be cleaned of duplicates later + let metaDataStack = []; + + //#region CLASS + const classData = await CharacterExportFvtt.getClassData(builder.compClass); + let classArray = null; + for(let i = 0; i < classData.length; ++i){ + const data = classData[i]; + if(!data.cls){continue;} + const hash = UrlUtil.URL_TO_HASH_BUILDER.class(data.cls); + if(!classArray){classArray = [];} + + //Create a block with some more easily accessible properties (thank me later when you are loading this) + const block = { + name: data.cls.name, + source: data.cls.source, + hash: hash, + srd: data.cls.srd, + level: data.targetLevel, + isPrimary: data.isPrimary + }; + //Add some data to block that may or may not always be in our given data (depends on class) + //TODO: Language, etc here? + if(data.skillProficiencies){block.skillProficiencies = data.skillProficiencies;} + if(data.toolProficiencies){block.toolProficiencies = data.toolProficiencies;} + if(data.featureOptSel){block.featureOptSel = data.featureOptSel;} + + //Create some meta information (so we can track where this class came from) + metaDataStack.push({uid: data.cls.name+"|"+data.cls.source, + name: data.cls.name, + source: data.cls.source, + type: "class", + _data: CharacterExportFvtt.getSourceMetaData(data.cls, brewSourceIds)}); + + //Check if high enough level for subclass here? + if(data.sc){ + const schash = UrlUtil.URL_TO_HASH_BUILDER.subclass(data.sc); + //Add the subclass to block, if present + block.subclass = { + name: data.sc.name, + source: data.sc.source, //maybe we should include some info here (and in class, race, etc) if this is brewed content + hash: schash + } + //Create some meta information (so we can track where this subclass came from) + //Subclasses generally dont have the 'srd' property, by the way + metaDataStack.push({uid: data.sc.name+"|"+data.sc.source, + name:data.sc.name, + source:data.sc.source, + type:"subclass", + _data:CharacterExportFvtt.getSourceMetaData(data.sc, brewSourceIds)}); + } + classArray.push(block); + } + _char.classes = classArray; + //#endregion + + //#region RACE + const raceData = this.getRaceData(builder.compRace); + if(!!raceData){ + let race = raceData.meta.race; + const hash = UrlUtil.URL_TO_HASH_BUILDER.race(race); + //Set the .race entry in the _char output + _char.race = {stateInfo: raceData.stateInfo, race: {name:race.name, source:race.source, hash: hash, srd:race.srd, + isSubRace: race._isSubRace, raceName:race.raceName, raceSource:race.raceSource, isBaseSrd:race._baseSrd, + versionBaseName:race._versionBase_name, versionBaseSource:race._versionBase_source }}; + //Remember some metadata about what source this race is from + metaDataStack.push({uid: race.name+"|"+race.source, + name:race.name, + source:race.source, + type:"race", + _data: CharacterExportFvtt.getSourceMetaData(race, brewSourceIds)}); + } + //#endregion + + //#region BACKGROUND + const background = await CharacterExportFvtt.getBackground(builder.compBackground); + if(!!background){ + //Add background to character output + _char.background = background; + //Create some meta information (so we know where the background came from) + metaDataStack.push({uid: CharacterExportFvtt.entityToUID(background), + name:background.name, + source:background.source, + type:"background", + _data:CharacterExportFvtt.getSourceMetaData(background, brewSourceIds)}); + } + //#endregion + + //#region ABILITY SCORES (our choices, maybe not the total) + const abilities = await CharacterExportFvtt.getAbilityScoreData(builder.compAbility); + //Add abilities to character output + _char.abilities = abilities; + //Don't think any meta information is needed here + //#endregion + + //#region EQUIPMENT (including gold, and bought items) + const equipment = await CharacterExportFvtt.getEquipmentData(builder.compEquipment); + //Add equipment to character output + _char.equipment = equipment; + for(let it of equipment.boughtItems){ + let uid = it.uid; //plate_armor|phb + //TODO: get the full source of the item (assuming its not a PHB item or something) + } + //#endregion + + //#region SPELLS + const spellSources = await CharacterExportFvtt.getAllSpellSources(builder.compSpell); + //Loop through each source for spells + for(let srcIx = 0; srcIx < spellSources.length; ++srcIx){ + let spellSource = spellSources[srcIx]; + //Loop through each spell level in the source + for(let lvlix = 0; lvlix < spellSource.spellsByLvl.length; ++lvlix){ + let lvl = spellSource.spellsByLvl[lvlix]; + //Loop through each spell in the spell level + for(let spix = 0; spix < lvl.length; ++spix){ + let sp = lvl[spix]; + //Create some meta information (so we know where the spell came from) + metaDataStack.push({uid: sp.name+"|"+sp.source, + _data:CharacterExportFvtt.getSourceMetaData(sp.spell, brewSourceIds)}); + //delete .spell info + delete sp.spell; + lvl[spix] = sp; + } + spellSource.spellsByLvl[lvlix] = lvl; + } + spellSources[srcIx] = spellSource; + } + _char.spellsBySource = spellSources; + //#endregion + + //Feats + + //optional feature stuff? + + //Character description + _char.about = { + name: builder.compDescription.__state["description_name"], + alignment: builder.compDescription.__state["description_alignment"], + height: builder.compDescription.__state["description_height"], + weight: builder.compDescription.__state["description_weight"], + hair: builder.compDescription.__state["description_hair"], + skin: builder.compDescription.__state["description_skin"], + eyes: builder.compDescription.__state["description_eyes"], + faith: builder.compDescription.__state["description_faith"], + text: builder.compDescription.__state["description_text"], + }; + + //Build meta + + //Optionally, instead of tracking each and every item, race, subclass, etc that we incorporated on our character, + //we can just check which sources were enabled at the time of the exporting of the character + //This does of course include bloat, but it's more accurate than what we have going right now + _meta.sourceIds = SourceManager.cachedSourceIds.map(id => SourceManager.minifySourceId(id)); + _meta.uploadedFileMetas = SourceManager.cachedUploadedFileMetas; + _meta.customUrls = SourceManager.cachedUploadedCustomUrls; + + const output = {character: _char, _meta:_meta}; + + console.log("Export Character", output); + let importStr = this.test_printExportJsonAsString(output); + + localStorage.setItem("lastCharacter", importStr); + + const currentUid = CharacterBuilder.currentUid; + CookieManager.saveCharacterInfo(output, currentUid); + + } + /** + * @param {CharacterBuilder} builder + */ + static async exportCharacterFvtt(builder){ + + const deletePropIfNull = (obj, prop) => { + if(obj[prop] == null){delete obj[prop];} + } + const grabFormsFromComponents = async (components, toObj, toObjProp) => { + let forms = []; + if(components == null){toObj[toObjProp] = forms; return;} + if(!(components.constructor === Array)){components = [components];} + for(let comp of components){ + if(!comp){continue;} + const form = await comp.pGetFormData(); + forms.push(form); + } + toObj[toObjProp] = forms; + } + let output = {}; + + //#region ABILITY SCORES + let outAbi = {}; + const totals = builder.compAbility.getTotals(); + outAbi.totals = totals; + output.abilities = outAbi; + //#endregion + //#region CLASSES + let classes = []; + const primaryClassIndex = builder.compClass._state['class_ixPrimaryClass'] || 0; + for (let ix = 0; ix < builder.compClass.state['class_ixMax'] + 1; ++ix) { + let out = {}; + const { propIxClass: propIxClass, propIxSubclass: propIxSubclass, propCurLevel:propCurLevel, propTargetLevel: propTargetLevel } = ActorCharactermancerBaseComponent.class_getProps(ix); + const cls = builder.compClass.getClass_({ propIxClass: propIxClass }); + if (!cls) { continue; } + //out.classUid = cls.name + "|" + cls.source; + //We will send the entire class entity, instead of a UID + out.classEntity = cls; + out.targetLevel = builder.compClass.state[propTargetLevel]; + out.isPrimary = ix == primaryClassIndex; + const sc = builder.compClass.getSubclass_({ cls: cls, propIxSubclass: propIxSubclass }); + if(sc){ + //out.subclassUid = sc.name + "|" + sc.source; + //We will send the entire subclass entity, instead of a UID + out.subclassEntity = sc; + } + //HITPOINTS + const {mode, customFormula} = (await builder.compClass.compsClassHpIncreaseMode[ix].pGetFormData()).data; + out.hpMode = mode; + out.hpCustomFormula = customFormula; + //FEATURE OPTIONS SELECT + let featureOptionsSelectForms = []; + for(let fosComp of builder.compClass.compsClassFeatureOptionsSelect[ix]){ + if(!fosComp){continue;} + const form = await fosComp.pGetFormData(); + featureOptionsSelectForms.push(form); + } + out.featureOptSel = featureOptionsSelectForms; + //CLASS SKILL PROFICIENCIES + let skillProfForms = []; + for(let comp of builder.compClass.compsClassSkillProficiencies){ + if(!comp){continue;} + const form = await comp.pGetFormData(); + skillProfForms.push(form); + } + out.skillProfs = skillProfForms; + //CLASS TOOL PROFICIENCIES + let toolProfForms = []; + for(let comp of builder.compClass.compsClassToolProficiencies){ + if(!comp){continue;} + const form = await comp.pGetFormData(); + toolProfForms.push(form); + } + out.toolProfs = toolProfForms; + //SPELL SLOT LEVEL SELECT + const slotLevelSelectComp = builder.compSpell.compsSpellSpellSlotLevelSelect[ix]; + out.spellSlotLevelSelection = slotLevelSelectComp?.isAnyChoice()? slotLevelSelectComp.getFlagsChoicesState() : null; + classes.push(out); + } + if(classes.length > 0){output.classes = classes;} + //#endregion + //#region RACE + let outRace = {}; + const curRace = builder.compRace.getRace_(); + if(curRace){ + //We need to include the entire race entity into the exported json + outRace.raceEntity = curRace; + if(builder.compRace.compRaceSize){ + outRace.size = [(await builder.compRace.compRaceSize.pGetFormData().data) || Parser.SZ_VARIES]; + } + //RACE SKILL PROFICIENCIES + await grabFormsFromComponents([...(builder.compRace.compRaceSkillProficiencies || []), + ...(builder.compRace.compRaceSkillToolLanguageProficiencies || [])], outRace, "skillProfs"); + //RACE TOOL PROFICIENCIES + await grabFormsFromComponents([...(builder.compRace.compRaceToolProficiencies || []), + ...(builder.compRace.compRaceSkillToolLanguageProficiencies || [])], outRace, "toolProfs"); + //RACE LANGUAGE PROFICIENCIES + await grabFormsFromComponents(builder.compClass.compRaceLanguageProficiencies, outRace, "langProfs"); + //RACE EXPERTISE + await grabFormsFromComponents(builder.compClass.compRaceExpertise, outRace, "expertises"); + //RACE ARMOR PROFICIENCIES + await grabFormsFromComponents(builder.compClass.compRaceArmorProficiencies, outRace, "armorProfs"); + //RACE WEAPON PROFICIENCIES + await grabFormsFromComponents(builder.compClass.compRaceWeaponProficiencies, outRace, "weaponProfs"); + //RACE DAMAGE IMMUNITIES + await grabFormsFromComponents(builder.compClass.compRaceDamageImmunity, outRace, "dmgImmunities"); + //RACE DAMAGE RESISTANCE + await grabFormsFromComponents(builder.compClass.compRaceDamageResistance, outRace, "dmgResistances"); + //RACE DAMAGE VULNERABILITY + await grabFormsFromComponents(builder.compClass.compRaceDamageVulnerability, outRace, "dmgVulnerabilities"); + //RACE CONDITION IMMUNITIES + await grabFormsFromComponents(builder.compClass.compRaceConditionImmunity, outRace, "conImmunities"); + output.race = outRace; + } + //#endregion + //#region BACKGROUND + let outBk = {}; + const bkInfo = builder.compBackground.getFeatureCustomizedBackground_({isAllowStub:false}); + if(bkInfo){ + //outBk.backgroundUid = CharacterExportFvtt.entityToUID(bkInfo); + //We will include the entire background entity instead + bkInfo.backgroundEntity = builder.compBackground.getBackground_(); + await grabFormsFromComponents(builder.compBackground.compBackgroundFeatures, outBk, "features"); + //BACKGROUND SKILL PROFICIENCIES + await grabFormsFromComponents(builder.compBackground.compBackgroundSkillProficiencies, outBk, "skillProfs"); + //BACKGROUND TOOL PROFICIENCIES + await grabFormsFromComponents(builder.compBackground.isCustomizeLanguagesTools? + builder.compBackground.compBackgroundLanguageToolProficiencies : builder.compBackground.compBackgroundToolProficiencies, outBk, "toolProfs"); + //BACKGROUND LANGUAGE PROFICIENCIES + await grabFormsFromComponents(builder.compBackground.isCustomizeLanguagesTools ? + builder.compBackground.compBackgroundLanguageToolProficiencies : builder.compBackground.compBackgroundLanguageProficiencies, outBk, "langProfs"); + //BACKGROUND EXPERTISE + await grabFormsFromComponents(builder.compBackground.compBackgroundExpertise, outBk, "expertises"); + //BACKGROUND ARMOR PROFICIENCIES + await grabFormsFromComponents(builder.compBackground.compBackgroundArmorProficiencies, outBk, "armorProfs"); + //BACKGROUND WEAPON PROFICIENCIES + await grabFormsFromComponents(builder.compBackground.compBackgroundWeaponProficiencies, outBk, "weaponProfs"); + //BACKGROUND DAMAGE IMMUNITIES + await grabFormsFromComponents(builder.compBackground.compBackgroundDamageImmunity, outBk, "dmgImmunities"); + //BACKGROUND DAMAGE RESISTANCE + await grabFormsFromComponents(builder.compBackground.compBackgroundDamageResistance, outBk, "dmgResistances"); + //BACKGROUND DAMAGE VULNERABILITY + await grabFormsFromComponents(builder.compBackground.compBackgroundDamageVulnerability, outBk, "dmgVulnerabilities"); + //BACKGROUND CONDITION IMMUNITIES + await grabFormsFromComponents(builder.compBackground.compBackgroundConditionImmunity, outBk, "conImmunities"); + if(builder.compBackground.compBackgroundCharacteristics){ + await grabFormsFromComponents(builder.compBackground.compBackgroundCharacteristics, outBk, "characteristics"); + } + output.background = outBk; + } + //#endregion + //#region FEATS + let outFeats = {}; + const additionalFeatFormData = await builder.compFeat.feat_pGetAdditionalFeatFormData(); + if(additionalFeatFormData.isAnyData){ + outFeats.additionalFeats = additionalFeatFormData; + const asi = builder.compAbility.compStatgen.getFormDataAsi(); + outFeats.asi = asi; + } + output.feats = outFeats; + //#endregion + //#region SPELLS + const simplifySpellForm = (formData) => { + for(let i = 0; i < formData.data.spells.length; ++i){ + let name = formData.data.spells[i].spell.name; + let source = formData.data.spells[i].spell.source; + formData.data.spells[i].spellEntity = formData.data.spells[i].spell; + delete formData.data.spells[i].spell; + //formData.data.spells[i].spellId = `${name}|${source}`; + deletePropIfNull(formData.data.spells[i], "usesMax"); + deletePropIfNull(formData.data.spells[i], "usesPer"); + deletePropIfNull(formData.data.spells[i], "usesValue"); + deletePropIfNull(formData.data.spells[i], "existingItemId"); + } + return formData; + } + const filterValues = builder.compSpell.filterValuesSpellsCache || builder.compSpell.filterBoxSpells.getValues(); + let spellsBySource = []; + for (let ix = 0; ix < builder.compClass.state['class_ixMax'] + 1; ++ix) { + let out = {}; + const { propIxClass: propIxClass, propIxSubclass: propIxSubclass } = ActorCharactermancerBaseComponent.class_getProps(ix); + const cls = builder.compClass.getClass_({ propIxClass: propIxClass }); + if (!cls) { continue; } + const sc = builder.compClass.getSubclass_({ cls: cls, propIxSubclass: propIxSubclass }); + if (builder.compSpell.compsSpellSpells[ix]) { + let formData = await builder.compSpell.compsSpellSpells[ix].pGetFormData(filterValues); + out.spells = simplifySpellForm(formData); + } + if (builder.compSpell.compsSpellAdditionalSpellClass[ix]) { + const formData = await builder.compSpell.compsSpellAdditionalSpellClass[ix].pGetFormData(); + out.additionalSpells = formData; + } + if (!sc) { spellsBySource.push(out); continue; } + if (builder.compSpell.compsSpellAdditionalSpellSubclass[ix]) { + const formData = await builder.compSpell.compsSpellAdditionalSpellSubclass[ix].pGetFormData(); + out.additionalSpellsSubclass = formData; + } + spellsBySource.push(out); + } + output.spells = {spellsBySource:spellsBySource}; + //#endregion + //#region EQUIPMENT + let outEqu = {}; + outEqu.currency = await builder.compEquipment.compEquipmentCurrency.pGetFormData(); + outEqu.starting = await builder.compEquipment.compEquipmentStartingDefault.pGetFormData(); + outEqu.shop = await builder.compEquipment.compEquipmentShopGold.pGetFormData(); + output.equipment = outEqu; + //#endregion + console.log(output); + const str = JSON.stringify(output); + console.log(str); + navigator.clipboard.writeText(str); //Write to browser clipboard + return str; + } + + + //#region Pull Info From Builder + /** + * @param {ActorCharactermancerClass} compClass + * @returns {{cls: any, isPrimary: boolean, propIxClass:string, propIxSubclass:string, targetLevel:number, sc:any}[]} + */ + static async getClassData(compClass) { + const primaryClassIndex = compClass._state.class_ixPrimaryClass; + //If we have 2 classes, this will be 1 + const highestClassIndex = compClass._state.class_ixMax; + let deletedClassesCount = 0; + + const classList = []; + for(let i = 0; i <= highestClassIndex; ++i){ + const isPrimary = i == primaryClassIndex; + //Get a string property that will help us grab actual class data + let { propIxClass: propIxClass, propIxSubclass: propIxSubclass, propCurLevel:propCurLevel, propTargetLevel: propTargetLevel } = + ActorCharactermancerBaseComponent.class_getProps(i); + const isDeleted = ActorCharactermancerBaseComponent.class_isDeleted(i); + if(isDeleted){deletedClassesCount++; continue;} + + //Grab actual class data + const cls = compClass.getClass_({propIxClass: propIxClass}); + if(!cls){continue;} + const targetLevel = compClass._state[propTargetLevel]; + if(deletedClassesCount>0){ + //If there were deleted classes before us, we need to shift the indices + //Create a lower index + const newProps = ActorCharactermancerBaseComponent.class_getProps(i-deletedClassesCount); + //Use the new index instead + propIxClass = newProps.propIxClass; + propIxSubclass = newProps.propIxSubclass; + } + + const block = { + cls: cls, + isPrimary: isPrimary, + propIxClass: propIxClass, + propIxSubclass:propIxSubclass, + targetLevel:targetLevel + } + let skillProficienciesForm = this.getClassSkills(compClass, i); + if(skillProficienciesForm != null){ + block.skillProficiencies = skillProficienciesForm; + } + let toolProficienciesForm = this.getClassTools(compClass, i); + if(toolProficienciesForm != null){ + block.toolProficiencies = toolProficienciesForm; + } + //Get choices from the featureOptionsSelect components + let featureOptSel = await this.getClassFeatureChoices(compClass, i); + if(featureOptSel != null){ block.featureOptSel = featureOptSel; } + //Now we want to ask compClass if there is a subclass selected for this index + const sc = compClass.getSubclass_({cls:cls, propIxSubclass:propIxSubclass}); + if(sc != null) { block.sc = sc; } + classList.push(block); + } + return classList; + } + /** + * @param {ActorCharactermancerRace} compRace + * @returns {{stateInfo:{subcomps:any, _compRaceSize:any}}} + */ + static getRaceData(compRace){ + let out = null; + const grabForm = (subCompName) => { + if(compRace[subCompName]){ + const form = compRace[subCompName].pGetFormData(); + out.formInfo.subcomps[subCompName] = form; + } + } + const grabState = (subCompName) => { + if(compRace[subCompName]){ + out.stateInfo.subcomps[subCompName] = compRace[subCompName].__state; + } + } + const race = compRace.getRace_(); + if(!!race){ + out = {meta:{race:race}, stateInfo:{subcomps:{}}}; + + //Get states from subcomponents + grabState("_compRaceSkillProficiencies"); + grabState("_compRaceArmorProficiencies"); + grabState("_compRaceConditionImmunity"); + grabState("_compRaceDamageImmunity"); + grabState("_compRaceDamageResistance"); + grabState("_compRaceDamageVulnerability"); + grabState("_compRaceExpertise"); + grabState("_compRaceLanguageProficiencies"); + grabState("_compRaceSkillToolLanguageProficiencies"); + grabState("_compRaceToolProficiencies"); + grabState("_compRaceWeaponProficiencies"); + + if(compRace.compRaceSize){out.stateInfo._compRaceSize = compRace.compRaceSize.__state;} + + } + else{ + console.error("Race is null"); + } + return out; + } + /** + * @param {ActorCharactermancerClass} compClass + * @param {number} ix + * @returns {any} returns a form + */ + static getClassSkills(compClass, ix){ + if(compClass.compsClassSkillProficiencies.length<=ix){return null;} + const comp = compClass.compsClassSkillProficiencies[ix]; + if(comp==null){return null;} + const form = comp._getFormData(); + return form; + } + /** + * @param {ActorCharactermancerClass} compClass + * @param {number} ix + * @returns {any} returns a form + */ + static getClassTools(compClass, ix){ + if(compClass.compsClassToolProficiencies.length<=ix){return null;} + const comp = compClass.compsClassToolProficiencies[ix]; + if(comp==null){return null;} + const form = comp._getFormData(); + return form; + } + /** + * @param {ActorCharactermancerClass} compClass + * @param {number} classIx + * @returns {{compIx:number, state:any, subCompDatas:{parentCompIx:number, subCompProp:string, state:any}[]}} + */ + static async getClassFeatureChoices(compClass, classIx){ + if(compClass.compsClassFeatureOptionsSelect.length<=classIx){return null;} + const compArray = compClass.compsClassFeatureOptionsSelect[classIx]; + let compDatas = []; + for(let k = 0; k < compArray.length; ++k){ + let comp = compArray[k]; + if(comp==null){return null;} + //let hashes = comp._optionsSet.map(set => {return set.hash}); + + //Most of the time, comp will just have subcomponents that deal with specialized questions (pick language, skill, etc) + let subCompDatas = []; + const subCompsNames = comp.allSubComponentNames; //Get all the names of the possible kinds of subcomponents + for(let j = 0; j < subCompsNames.length; ++j){ + let prop = subCompsNames[j]; + let subCompArray = comp[prop]; + if(!subCompArray){continue;} + for(let i = 0; i < subCompArray.length; ++i){ + let subComp = subCompArray[i]; + //apparently the array can have null entries. huh. + if(!subComp){continue;} + //Now that we have found a subcomponent, copy the __state and include pointers on how to find it + //No need to include class ix here, that is already handled by the code that wraps this one + subCompDatas.push({parentCompIx: k, subCompProp:prop, subCompIx: i, state: subComp.__state}); + } + } + + //In some cases (like Fighting Style for lvl 1 fighters), comp renders itself, instead of having subcomponents + //In this case, we need to get the __state from comp itself + //Alternatively, we will also grab __state even if it's not rendered, and we just found subcomponents + let isSelfRendered = !!comp._lastMeta; //lazy but for now working way to dictate if this component is rendered + if(isSelfRendered || subCompDatas.length > 0){ + compDatas.push({compIx: k, state: comp.__state, subCompDatas:subCompDatas}); + } + //The important thing here is, we dont want to include *all* components in compArray, as some are never rendered and we just get issues trying to read/write their states + } + return compDatas; + } + /** + * @param {ActorCharactermancerAbility} compAbility + * @returns {any} + */ + static async getAbilityScoreData(compAbility){ + const statgen = compAbility._compStatgen; + const s = statgen.__state; + const chosenModeIx = statgen._meta.ixActiveTab___default; + //const chosenMode = (["none", "rolled", "std_array", "pointbuy", "manual"])[chosenModeIx]; + let out = {}; + if(chosenModeIx == 1){ //ROLL + out = {rolled_str_abilSelectedRollIx:s["rolled_str_abilSelectedRollIx"], + rolled_dex_abilSelectedRollIx:s["rolled_dex_abilSelectedRollIx"], + rolled_con_abilSelectedRollIx:s["rolled_con_abilSelectedRollIx"], + rolled_wis_abilSelectedRollIx:s["rolled_wis_abilSelectedRollIx"], + rolled_int_abilSelectedRollIx:s["rolled_int_abilSelectedRollIx"], + rolled_cha_abilSelectedRollIx:s["rolled_cha_abilSelectedRollIx"], + rolled_formula:s["rolled_formula"], + pb_isCustom:s["pb_isCustom"], + rolled_rolls:s["rolled_rolls"]}; + } + else if(chosenModeIx == 2){ //STANDARD ARRAY + out = {array_str_abilSelectedScoreIx:s["array_str_abilSelectedScoreIx"], + array_dex_abilSelectedScoreIx:s["array_dex_abilSelectedScoreIx"], + array_con_abilSelectedScoreIx:s["array_con_abilSelectedScoreIx"], + array_int_abilSelectedScoreIx:s["array_int_abilSelectedScoreIx"], + array_wis_abilSelectedScoreIx:s["array_wis_abilSelectedScoreIx"], + array_cha_abilSelectedScoreIx:s["array_cha_abilSelectedScoreIx"]}; + } + else if(chosenModeIx == 3){ //POINT BUY + out = {pb_str:s["pb_str"], pb_dex:s["pb_dex"], pb_con:s["pb_con"], + pb_wis:s["pb_wis"], pb_int:s["pb_int"], pb_cha:s["pb_cha"], + pb_budget:s["pb_budget"], pb_isCustom:s["pb_isCustom"], pb_points:s["pb_points"]}; + } + else if(chosenModeIx == 4){ //MANUAL + out = {manual_str_abilValue:s["manual_str_abilValue"], + manual_dex_abilValue:s["manual_dex_abilValue"], + manual_con_abilValue:s["manual_con_abilValue"], + manual_wis_abilValue:s["manual_wis_abilValue"], + manual_int_abilValue:s["manual_int_abilValue"], + manual_cha_abilValue:s["manual_cha_abilValue"]}; + } + + const specifiedSave = true; //Keep true for now, otherwise for some reason ASI feats dont get properly saved/loaded + + if(specifiedSave){ + //ASI data + let asiData = {}; + //We can get feats from ASIs, Races, and... what else? Backgrounds? + //TODO: Add background support here + for(let prop of Object.keys(s)){ + if(prop.startsWith("common_asi_") || prop.startsWith("common_additionalFeats_")){ + asiData[prop] = s[prop]; + } + //Some races provide ASI choices, include them here while we are at it + else if(prop.startsWith("common_raceChoice")){ + out[prop] = s[prop]; + } + //Keep track of number of custom feats + //TODO: only include custom feats with index lower than this number + else if (prop.startsWith("common_cntFeatsCustom")){ + out[prop] = s[prop]; + } + } + + return {state:out, stateAsi:asiData, mode:chosenModeIx}; + } + else{ + //We can just grab every property starting with common_ and include that + for(let prop of Object.keys(s)){ + if(prop.startsWith("common_")){ + out[prop] = s[prop]; + } + } + return {state:out, mode:chosenModeIx}; + } + + } + /** + * @param {ActorCharactermancerEquipment} compEquipment + */ + static async getEquipmentData(compEquipment){ + //Get gold + const compGold = compEquipment._compEquipmentCurrency; + + const compDefault = compEquipment._compEquipmentStartingDefault; + //Get the _state from compDefault, it contains info about which items were selected + const stateDefault = compDefault.__state; + //We also need info on if we rolled for gold or not. We can get this from compGold + const cpRolled = compGold.__state.cpRolled; + //Then we need info on which items were purchased in the item shop + const compShop = compEquipment._compEquipmentShopGold; + const boughtItems = compShop.__state.itemPurchases.map(it => { + return {isIgnoreCost: it.data.isIgnoreCost, quantity: it.data.quantity, value: it.data.value, uid: it.data.uid};}); + + const out = {stateDefault:stateDefault, cpRolled:cpRolled, boughtItems:boughtItems}; + + return out; + } + /** + * @param {ActorCharactermancerSpell} compSpell + * @returns {{className:string, classSource:string, spellsByLvl:Charactermancer_Spell_SpellMeta[][]}[]} + */ + static getAllSpellSources(compSpell){ + const filterValues = compSpell.filterValuesSpellsCache || compSpell.filterBoxSpells.getValues(); + let spellsBySource = []; + let forms = []; + for(let j = 0; j < compSpell.compsSpellSpells.length; ++j){ + //Assume this component handles spells for a certain class + let comp = compSpell.compsSpellSpells[j]; + if(!comp){continue;} + let className = comp._className; + let classSource = comp._classSource; + forms.push(compSpell.compsSpellSpells[j].pGetFormData(filterValues)); + let spellsByLvl = compSpell.compsSpellSpells[j]._test_getKnownSpells().map(arr => arr.map( + spell => {return {name: spell.spell.name, source:spell.spell.source, + isLearned:spell.isLearned, isPrepared:spell.isPrepared, spell:spell.spell};} + )); + spellsBySource.push({className: className, classSource:classSource, ix:j, spellsByLvl: spellsByLvl}); + } + return spellsBySource; + } + /** + * Get background save data + * @param {ActorCharactermancerBackground} compBackground + * @returns {any} + */ + static getBackground(compBackground){ + let bk = compBackground.getBackground_(); + if(!bk){return null;} + const bkSource = bk.source; + const bkName = bk.name; + const isCompletelyCustom = bk.name == "Custom Background"; + + let out = { name: bkName, source: bkSource}; + //Skill proficiencies + out.isCustomizeSkills = !!compBackground.__state.background_isCustomizeSkills; + out.stateSkillProficiencies = compBackground.compBackgroundSkillProficiencies?.__state; + //Languages & tools + out.isCustomizeLanguagesTools = !!compBackground.__state.background_isCustomizeLanguagesTools; + out.stateLanguageToolProficiencies = compBackground.compBackgroundLanguageToolProficiencies?.__state; + out.stateToolProficiencies = compBackground.compBackgroundToolProficiencies?.__state; + out.stateLanguageProficiencies = compBackground.compBackgroundLanguageProficiencies?.__state; + //Others + out.stateArmorProficiencies = compBackground.compBackgroundArmorProficiencies?.__state; + out.stateWeaponProficiencies = compBackground.compBackgroundWeaponProficiencies?.__state; + out.stateImmunity = compBackground.compBackgroundDamageImmunity?.__state; + out.stateResistance = compBackground.compBackgroundDamageResistance?.__state; + out.stateVulnerability = compBackground.compBackgroundDamageVulnerability?.__state; + out.stateExpertises = compBackground.compBackgroundExpertise?.__state; + out.stateConditionImmunities = compBackground.compBackgroundConditionImmunity?.__state; + //Characteristics + out.stateCharacteristics = compBackground.compBackgroundCharacteristics.__state; + //features + out.stateFeatures = compBackground.compBackgroundFeatures.__state; + + //Delete all properties that are null + out = Object.fromEntries(Object.entries(out).filter(([_, v]) => v != null)); + + return out; + } + //#endregion + + + /** + * @param {{name:string, source:string}} entity a subclass, race, class etc + * @param {string} type "subclass", "class", "race", etc + * @returns {{filename:string, url:string, isLocal:string, checksum:string}[]} + */ + static async _test_MatchEntityToSource(entity, type){ + const returnOnFirstMatch = false; + //Let's get all enabled sources first (one of them might Be 'Upload File') + const enabledSources = CharacterExportFvtt.getBrewSourceIds(); + const brew = await BrewUtil2.pGetBrew(); + let matchedSources = []; + for(let source of brew){ + //Even an uploaded file can appear here + //source.head.filename + //source.head.url (might be that uploaded files have this as null) + + const content = source.body[type]; //type array. subclass, class, race, etc + if(!content){continue;} + let matchedEntity = false; //This loop will close once a match is made + for(let ci = 0; !matchedEntity && ci < content.length; ++ci){ + //Let's do a simple match: just 'name' and 'source' + matchedEntity = (content[ci].name == entity.name && content[ci].source == entity.source); + } + if(matchedEntity){ + if(returnOnFirstMatch){return [source.head];} + matchedSources.push(source.head); + } + } + return matchedSources; + } + /** + * @param {{filename:string}} source + * @returns {boolean} + */ + static isUploadedFileSource(source){ + //First, get all the brew sourceIds from somewhere + let srcIds = CharacterExportFvtt.getLoadedSources(); + //Next, filter the sources until we get one called 'Upload File'. + //Alternatively, just check if source.isFile + srcIds = srcIds.filter(srcId => srcId.isFile); + if(!srcIds || srcIds.length < 1){return false;} + //Now that we have confirmed the user did include an 'Upload File' source, we need to check if any uploaded files match + //We need the uploadedFileMetas for this + const fileMetas = SourceManager._testUploadedFileMetas; + return fileMetas.filter(meta => meta.name == source.filename).length > 0; + } + + //#region Get Source Metadata + /** + * Gets source metadata for an object (race/class/subclass/spell/etc) + * @param {{name:string, source:string}} item + * @param {{name:string, url:string, abbreviations:string[], isDefault:boolean, isFile:boolean}[]} brewSourceIds + * @returns {{isOfficialContent:boolean, brewSource:{name:string, url:string, abbreviations:string[]}}} + */ + static getSourceMetaData(item, brewSourceIds){ + //This is not a trustable way to confirm if something is from official sources or not. Yet. + if(item.srd || this.isFromOfficialSource(item)){ + return {isOfficialContent:true}; + } + else { + const match = this.matchToBrewSourceID(item, brewSourceIds); + if(!match){throw new Error(`Failed to get brew source for ${item.name}|${item.source}`);} + return {isOfficialContent:false, brewSource:match}; + } + } + /** + * @returns {{name:string, url:string, isDefault:boolean, isFile:boolean, isWorldSelectable:boolean, cacheKey:string, _brewUtil: any + * filterTypes:string[], _pPostLoad:Function, _isExistingPrereleaseBrew:boolean, _isAutoDetectPrereleaseBrew:boolean, + * abbreviations:string[] _isAutoDetectPrereleaseBrew:boolean, _isExistingPrereleaseBrew:boolean}[]} + */ + static getLoadedSources(){ + return SourceManager.cachedSourceIds; + } + static getBrewSourceIds(){ + //Only return non-default sources for now + return this.getLoadedSources().filter(src => !src.isDefault); + } + /** + * @param {{source:string, name:string}} item + * @param {{name:string, url:string, abbreviations:string[]}} sourceId + * * @param {boolean} pendanticMode (default is true) in pedantic mode, we don't just check that abbreviations match, we do a deeper match + * @returns {boolean} + */ + static doesMatchToBrewSource(item, sourceId, pendanticMode=false){ + + //NEW STUFF + //check if custom upload or default source, we do not handle those + if(sourceId.isFile){return false;} + if(sourceId.isDefault){return false;} + + const itemAbbreviation = item.source.toLowerCase(); + const srcUsedAbbreviations = sourceId.abbreviations.map(a => a.toLowerCase()); + //Match abbreviations. If brewer made a typo on the abbreviation somewhere, this will fail + if(!srcUsedAbbreviations.includes(itemAbbreviation)){return false;} + //Now that we know the abbreivations matched, we can return true, or if we want to be pedantic, + //we can check to see that source names also match + //(WARNING:) keep in mind, the user may have enabled more than 1 source that uses the same abbrevation! + + const src = BrewUtil2.sourceJsonToSource(item.source); + const srcs = BrewUtil2.getSources(); + + //let brewMetas = BrewUtil2._getBrewMetas(); + //first, + + if(!pendanticMode){return true;} + + //Since we can't rely on the itemAbbreviation alone to find us the correct source, we need to be more rough + //We probably need to use the sourceId to load a source, then check if it mentions this item + //const src = BrewUtil2.sourceJsonToSource(item.source); + + //Lets get the full name of the source. We expect the name to be split like this: "AUTHOR; BREW_NAME" + let sourceFullName = ""; + if(!sourceId.name.includes(";")){sourceFullName = sourceId.name;} //Let's have a fallback in case for some reason the name doesnt have a section for the author + else { sourceFullName = sourceId.name.substring(sourceId.name.indexOf(";") + 1).trim(); } //This is the expected outcome + + //Now we need the source full name on the end of the item in question (like a subclass, feat, or spell) + //Parser should have this information + let itemSourceFull = Parser.sourceJsonToFull(item.source); //Expect a full name of the brew here + //Remove all non-latin and non-numeral characters (sometimes people misplace ' and ยด, among other things) + const removeNonAlphanumericCharacters = (inputString) => { + return inputString.replace(/[^a-zA-Z0-9]/gi, ''); + } + + + //If the brewer made a typo between any two (of the numerous places) where you define the source's full name, this will fail and return null + + sourceFullName = removeNonAlphanumericCharacters(sourceFullName).toLowerCase(); + itemSourceFull = removeNonAlphanumericCharacters(itemSourceFull).toLowerCase(); + + return sourceFullName == itemSourceFull; + } + /** + * @param {{name:string, source:string}} item + * @param {{full:string, url:string, abbreviation:string}} fileSourceId + * @returns {boolean} + */ + static doesMatchToUploadedFileSource(item, fileSourceId){ + + //test + let itemSourceFull = Parser.sourceJsonToFull(item.source); //Ok so we cant rely on this to get the correct source name from an abbreviation + + if(item.source == fileSourceId.abbreviation){return true;} + //Fallback - not sure if this will ever work, but perhaps the sourceId has 'abbreviations' (a string array) + if(!!fileSourceId.abbreviations && fileSourceId.abbreviations.includes(item.source)){return true;} + //There might be a way to ask BrewUtil2 for the source by just using this abbreviation + return false; + } + /** + * Matches an item to a brew source ID + * @param {{name:string, source:string}} item a class, subclass, race, item, feat, background, etc + * @param {{name:string, abbreviations:string[]}[]} brewSourceIds + * @returns {{name:string, abbreviations:string[]}} + */ + static matchToBrewSourceID(item, brewSourceIds){ + + let matchedIds = brewSourceIds.filter(srcId => this.doesMatchToBrewSource(item, srcId)); + + //DEBUG - try to only match to uploaded files + const uploadedFileSources = brewSourceIds.filter(srcId => srcId.isFile); + if(uploadedFileSources.length > 0){ + const fileMetas = SourceManager._testUploadedFileMetas; + for(let file of fileMetas){ + if(!file.contents?._meta){continue;} + for(let src of file.contents._meta.sources){ + let isMatch = this.doesMatchToUploadedFileSource(item, src); + if(isMatch){matchedIds.push(src);} + } + } + } + + + if(matchedIds.length < 1){return null;} + if(matchedIds.length > 1){ console.error("Matched item to multiple brew sources", item, matchedIds); } //something went wrong + return matchedIds[0]; + } + + /** + * Checks if an object(race/class/subclass/etc) is from an official source + * @param {{__diagnostic:any}} item + * @returns {boolean} + */ + static isFromOfficialSource(item){ + return [Parser.SOURCES_VANILLA, + ...Parser.SOURCES_CORE_SUPPLEMENTS, + ...Parser.SOURCES_PARTNERED_WOTC, + ...Parser.SOURCES_NON_STANDARD_WOTC, + ...Parser.SOURCES_NON_FR, + ...Parser.SOURCES_CORE_SUPPLEMENTS, + ...Parser.SOURCES_COMEDY, + ...Parser.SOURCES_ADVENTURES, + //...Parser.SOURCES_AVAILABLE_DOCS_BOOK, + //...Parser.SOURCES_AVAILABLE_DOCS_ADVENTURE, + ].includes(item.source); + + + //we can check check if parser knows of the name + //actually, SOURCE_JSON_TO_FULL probably gets filled by properties from homebrew. + //So we cant rely on it to validate official sources + //return !!Parser.SOURCE_JSON_TO_FULL[item.source]; + return false; + } + static matchToOfficialSource(item){ + return Parser.sourceJsonToFull(item.source); + } + //#endregion + + static test_getSourceFromSubclass(){ + const item = { + className: "Sorcerer", + classSource: "PHB", + name: "Blood Magic", + shortName: "Blood Magic", + source: "FFBloodSorc", + __diagnostic: {filename: "Foxfire94; Blood Magic Sorcerous Origin.json" }, + __prop: "subclass" + }; + + //First of all, try to figure out if this is a brewed subclass + //one easy way (maybe?) of doing this is to check the __diagnostic property (only brewed (and maybe prerelease?) stuff has this) + const isBrewedContent = CharacterExportFvtt.isFromOfficialSource(item); + + if(!isBrewedContent){ + const sourceNameFull = this.matchToOfficialSource(item); + const meta = { + isOfficial: true, + source: item.source, + sourceFull: sourceNameFull + }; + return meta; + } + + + const brewSources = CharacterExportFvtt.getBrewSourceIds(); + + const matchedBrewSources = brewSources.filter(src => this.doesMatchToBrewSource(item, src)); + if(matchedBrewSources.length<1){} + } + + static test_printExportJsonAsString(exportJson){ + const str = JSON.stringify(exportJson); + return str; + } + /** + * Creates an UID for the entity. Example: "Ranger|PHB" + * @param {{name:string, source:string}} entity + * @returns {string} + */ + static entityToUID(entity){ + return entity.name + "|" + entity.source; + } + + /** + * Save file schema + * _meta: + * character: + * -classes (array) + * --[0] + * ---name (string) + * ---source (string) + * ---skillsTools + * --- + */ +} \ No newline at end of file diff --git a/charbuilder/js/charselect.js b/charbuilder/js/charselect.js new file mode 100644 index 0000000..bec911d --- /dev/null +++ b/charbuilder/js/charselect.js @@ -0,0 +1,139 @@ +class CharacterSelectScreen { + constructor(){ + + } + + myContent; + + render(){ + + ActorCharactermancerBaseComponent.class_clearDeleted(); + + const _root = $("#window-root"); + + const content = $$`

    `; + + const btnNew = $$``; + btnNew.click(() => { + this.createNewCharacter(); + }); + + const header = $$` +
    +

    My Characters

    + ${btnNew} +
    `; + + header.appendTo(content); + + const list = $$`
      +
    `; + + const hierarchy = $$` +
    + ${list} +
    + `; + hierarchy.appendTo(content); + + content.appendTo(_root); + this.myContent = content; + + const infos = CookieManager.getAllCharacterInfos(); + for(let ix = 0; ix < infos.length; ++ix){ + const result = infos[ix]; + if(!result){continue;} + const card = this.createCharacterElement(this, result.uid); + card.appendTo(list); + } + } + + createCharacterElement(parent, charUid){ + const defaultIconUrl = `charbuilder/img/default_img.jpg`; + const url = `url(${defaultIconUrl})`; + + const btnView = $$``; + btnView.click(() => { + parent.openCharacter(charUid, true); + }); + const btnEdit = $$``; + btnEdit.click(() => { + parent.openCharacter(charUid, false); + }); + const btnDelete = $$``; + btnDelete.click(() => { + parent.deleteCharacter(charUid); + this.close(); + this.render(); + }); + + //Get the character data + const charData = CookieManager.getCharacterInfo(charUid).result.character; + let classString = ""; + let totalLevels = 0; + for(let ix = 0; ix < charData?.classes?.length || 0; ++ix){ + const d = charData.classes[ix]; + const mode = "classOnly"; + totalLevels += d.level; + if(mode == "classOnly"){ + if(ix>0){classString+="/";} + classString += `${d.name}`; + } + else { //classSubclass + if(ix>0){classString+=" ";} + classString += `${d.name}${d.subclass? `/${d.subclass.name}` : ""}`; + } + } + if(classString == ""){classString = "No class";} + let nameString = charData.about?.name || "Unnamed Character"; + let raceString = charData.race?.race?.name || ""; + let infoString = ""; + const addToInfoString = (str) => {if(str.length>0){infoString += (infoString.length>0?" | ":"") + str;}} + addToInfoString(`Level ${totalLevels}`); + addToInfoString(raceString); + addToInfoString(classString); + + let div = $$` +
  • +
    +
    +
    +
    +
    +
    +
    +
    +
    +

    ${nameString}

    +
    ${infoString}
    +
    +
    +
    + +
    +
  • `; + return div; + } + + close(){ + this.myContent.remove(); + } + createNewCharacter(){ + this.close(); + SourceManager.defaultStart({actor:null}); + } + deleteCharacter(uid){ + CookieManager.deleteCharacter(uid); + } + openCharacter(uid, viewMode=false){ + console.log("Open character", uid); + this.close(); + SourceManager.defaultStart({actor:null, cookieUid:uid, page:"sheet"}); + } +} \ No newline at end of file diff --git a/charbuilder/js/helperfunctions.js b/charbuilder/js/helperfunctions.js new file mode 100644 index 0000000..366a195 --- /dev/null +++ b/charbuilder/js/helperfunctions.js @@ -0,0 +1,25 @@ +class HelperFunctions{ + + /** + * Will set the modal to only have the sources provided enabled (all others will be set to 0) + * @param {ModalFilter} modalFilter + * @param {String[]} sourcesToUse example: ["PHB", "XGE", "TCE"] + */ + static setModalFilterSourcesStrings(modalFilter, sourcesToUse){ + let sources = {}; + for(let src of sourcesToUse){ + sources[src] = 1; + } + modalFilter.pageFilter.filterBox.setFromValues({Source: sources}); + } + + static getClassFeaturesFromClassInData(myData, cls){ + return myData.classFeature.filter(f => !!f && f.className == cls.name && f.classSource == cls.source); + } + + static async loadJSONFile(localURL){ + //const localURL = "data/class/index.json"; + const result = await DataUtil.loadRawJSON(localURL); + return result; + } +} \ No newline at end of file diff --git a/charbuilder/js/main.js b/charbuilder/js/main.js new file mode 100644 index 0000000..f5be025 --- /dev/null +++ b/charbuilder/js/main.js @@ -0,0 +1,818 @@ +document.addEventListener('DOMContentLoaded', function () { + JqueryUtil.initEnhancements(); +}); +window.addEventListener('load', function () { + + //Run init, ready, and then load the program + handleInit().then(() => handleReady().then(() => + { + //Try to get a state cookie (telling us which page the user last visited, and which character) + const cookie = CookieManager.getState(); + const charExists = (cookie && cookie.uid) && !!CookieManager.getCharacterInfo(cookie.uid)?.result; + //Fallback: go to character select screen + if(!charExists || cookie.page == "select"){ + const charSelect = new CharacterSelectScreen(); + charSelect.render(); + //Write a new cookie saying we are at char select screen + CookieManager.setState(CharacterBuilder.createStateCookie("select", null)); + } + else { + //Tell sourcemanager to load sources to display the character who's uid was in the cookie, then switch to the right page + SourceManager.defaultStart({cookieUid: cookie.uid, page:cookie.page}); + } + })); +}); +async function handleInit(){ + //UtilGameSettings.prePreInit(); + //Vetools.doMonkeyPatchPreConfig(); + Config.prePreInit(); //Important + Vetools.doMonkeyPatchPostConfig(); //Makes roll buttons work + + //We need this to be true, since BrewUtil freaks out otherwise and tries to grab json from a url that is 404 + Object.defineProperty(globalThis, "IS_DEPLOYED", { + get() { return true; }, + set(val) {}, + }); + console.log("Init complete"); +} +async function handleReady(){ + await Config.pInit(); //Important + //Prepare indexes of homebrew content + await Vetools.pDoPreload(); + SideDataInterfaces.init(); //Important + //Hide rollbox ui + Renderer.dice._$minRoll.hideVe(); + Renderer.dice._$wrpRoll.hideVe(); + console.log("Ready complete"); +} + +class SourceManager { + static _BREW_DIRS = ["class", 'subclass', "race", "subrace", "background", + "item", 'baseitem', "magicvariant", "spell", "feat", "optionalfeature"]; + static _DATA_PROPS_EXPECTED = ['class', "subclass", 'classFeature', "subclassFeature", + "race", "background", "item", "spell", "feat", 'optionalfeature']; + static cacheKey = "sourceIds"; + static _curWindow; + + /** + * Fetches sourceIds from a saved character (or default ones as fallback), + * and creates a character builder window that can play around with those sources + * @param {string} cookieUid The uid which is attached to the character + * @param {string} page The page the character builder will jump to upon launch + */ + static async defaultStart({ cookieUid, page }) { + + //Try to load source ids from localstorage by referring to a character saved in localstorage under 'cookieUid' + const attemptedLoad = await this._loadSourceIdsFromSave(cookieUid); + let {sourceIds, uploadedFileMetas, customUrls} = attemptedLoad || {sourceIds: null, uploadedFileMetas:null, customUrls:null}; + //If that failed, just load default source ids + if(!sourceIds){sourceIds = await this._getDefaultSourceIds(); uploadedFileMetas = []; customUrls = []; } + + //Cache which sources we chose, and let them process the source ids into ready data entries (classes, races, etc) + const data = await SourceManager._loadSources({sourceIds: sourceIds, uploadedFileMetas: uploadedFileMetas, customUrls: customUrls}); + + //Create the character builder UI, and try to navigate to the correct page, showing the correct character + const window = new CharacterBuilder(data, cookieUid, page); + this._curWindow = window; + } + + /** + * Load sources and return parsed entities + * @param {{sourceIds:any[], uploadedFileMetas:any, customUrls:any}} sourceInfo + * @returns {{class:{}[], background:{}[], classFeature:{}[], race:{}[], monster:{}[], item:{}[] + * , spell:{}[], subclassFeature:{}[], feat:{}[], optionalFeature:{}[], foundryClass:{}[]}} + */ + static async _loadSources(sourceInfo){ + //Process and post-process the data + //Get entities such as classes, races, backgrounds using the source ids + const content = await SourceManager._getOutputEntities(sourceInfo.sourceIds, sourceInfo.uploadedFileMetas, sourceInfo.customUrls, true); + //Then perform some post processing + const postProcessedData = SourceManager._postProcessAllSelectedData(content); + const mergedData = postProcessedData; + //Make sure that the data always has an array for classes, races, feats, etc, even if none were provided by the sources + SourceManager._DATA_PROPS_EXPECTED.forEach(propExpected => mergedData[propExpected] = mergedData[propExpected] || []); + SourceManager._setUsedSourceIds(sourceInfo); + + return mergedData; + } + + /** + * Perform some post-processing on entities extracted from sources + * @param {{class:{}[], background:{}[], classFeature:{}[], race:{}[], monster:{}[], item:{}[] + * , spell:{}[], subclass:{}[], subclassFeature:{}[], feat:{}[], optionalFeature:{}[], foundryClass:{}[]}} data + * @returns {{class:{}[], background:{}[], classFeature:{}[], race:{}[], monster:{}[], item:{}[] + * , spell:{}[], subclassFeature:{}[], feat:{}[], optionalFeature:{}[], foundryClass:{}[]}} + */ + static _postProcessAllSelectedData(data) { + + data = ImportListClass.Utils.getDedupedData({allContentMerged: data}); + + data = ImportListClass.Utils.getBlocklistFilteredData({dedupedAllContentMerged: data}); + + delete data.subclass; + Charactermancer_Feature_Util.addFauxOptionalFeatureEntries(data, data.optionalfeature); + + Charactermancer_Class_Util.addFauxOptionalFeatureFeatures(data.class, data.optionalfeature); + return data; + } + + /** + * Get objects containing information about sources, such as urls, abbreviations and names. Doesn't include any game content itself + * @returns {{name:string, isDefault:boolean, cacheKey:string}[]} + */ + static async _pGetSources() { + + const isStreamerMode = true;//Config.get('ui', 'isStreamerMode'); + + return [new UtilDataSource.DataSourceSpecial(isStreamerMode ? "SRD" : "5etools", SourceManager._pLoadVetoolsSource.bind(this), { + cacheKey: '5etools-charactermancer', + filterTypes: [UtilDataSource.SOURCE_TYP_OFFICIAL_ALL], + isDefault: true, + pPostLoad: SourceManager._pPostLoad.bind(this, {}) + }), ...UtilDataSource.getSourcesCustomUrl({ + pPostLoad: SourceManager._pPostLoad.bind(this, { + isBrewOrPrerelease: true + }) + }), ...UtilDataSource.getSourcesUploadFile({ + pPostLoad: SourceManager._pPostLoad.bind(this, { + isBrewOrPrerelease: true + }) + }), ...(await UtilDataSource.pGetSourcesPrerelease(ActorCharactermancerSourceSelector._BREW_DIRS, { + pPostLoad: SourceManager._pPostLoad.bind(this, { isPrerelease: true}) + })), ...(await UtilDataSource.pGetSourcesBrew(ActorCharactermancerSourceSelector._BREW_DIRS, { + pPostLoad: SourceManager._pPostLoad.bind(this, { isBrew: true}) + }))].filter(dataSource => !UtilWorldDataSourceSelector.isFiltered(dataSource)); + } + + /** + * Extracts entities such as classes, subclasses, races and backgrounds out of an array of sources + * @param {{name:string, isDefault:boolean, cacheKey:string}[]} sourceIds + * @param {any} uploadedFileMetas + * @param {any} customUrls + * @returns {{class:{}[], background:{}[], classFeature:{}[], race:{}[], monster:{}[], item:{}[] + * , spell:{}[], subclass:{}[], subclassFeature:{}[], feat:{}[], optionalFeature:{}[], foundryClass:{}[]}} + */ + static async _getOutputEntities(sourceIds, uploadedFileMetas, customUrls, getDeduped=false) { + + //Should contain all spells, classes, etc from every source we provide + const allContentMeta = await UtilDataSource.pGetAllContent({ + sources: sourceIds, + uploadedFileMetas: uploadedFileMetas, + customUrls: customUrls,/* + isBackground, + + page: this._page, + + isDedupable: this._isDedupable, + fnGetDedupedData: this._fnGetDedupedData, + + fnGetBlocklistFilteredData: this._fnGetBlocklistFilteredData, + + isAutoSelectAll, */ + }); + + const out = getDeduped? allContentMeta.dedupedAllContentMerged : allContentMeta; + + //TEMPFIX + /* Renderer.spell.populatePrereleaseLookup(await PrereleaseUtil.pGetBrewProcessed(), {isForce: true}); +Renderer.spell.populateBrewLookup(await BrewUtil2.pGetBrewProcessed(), {isForce: true}); + +(out.spell || []).forEach(sp => { Renderer.spell.uninitBrewSources(sp); Renderer.spell.initBrewSources(sp); }); */ + + return out; + } + + /** + * Callback function to handle parsing JSON for "core" content hosted on 5eTools + * @returns {any} + */ + static async _pLoadVetoolsSource() { + const combinedSource = {}; + const [classResult, raceResult, backgroundResult, itemResults, spellResults, featResults, optionalFeatureResults] + = await Promise.all([Vetools.pGetClasses(), Vetools.pGetRaces(), DataUtil.loadJSON(Vetools.DATA_URL_BACKGROUNDS), + Vetools.pGetItems(), Vetools.pGetAllSpells(), DataUtil.loadJSON(Vetools.DATA_URL_FEATS), DataUtil.loadJSON(Vetools.DATA_URL_OPTIONALFEATURES)]); + Object.assign(combinedSource, classResult); + combinedSource.race = raceResult.race; + combinedSource.background = backgroundResult.background; + combinedSource.item = itemResults.item; + combinedSource.spell = spellResults.spell; + combinedSource.feat = featResults.feat; + combinedSource.optionalfeature = optionalFeatureResults.optionalfeature; + return combinedSource; + } + /** + * Called when a source has been loaded + * @param {any} data + * @param {{isBrewOrPrerelease:boolean}} opts + * @returns {any} data + */ + static async _pPostLoad(opts, data) { + let isBrew = false; let isPrerelease = false; + const isBrewOrPrerelease = opts.isBrewOrPrerelease || false; + if (isBrewOrPrerelease) { + const { isPrerelease: _isPre, isBrew: _isBrew } = + UtilDataSource.getSourceType(data, { isErrorOnMultiple: true }); + isPrerelease = _isPre; + isBrew = _isBrew; + } + + //Load the actual content + data = await UtilDataSource.pPostLoadGeneric({ isBrew: isBrew, isPrerelease: isPrerelease }, data); + + + if (data.class || data.subclass) { + //TEMPFIX + /* const { DataConverterClassSubclassFeature: convSubclFeature } = await Promise.resolve().then(function () { + return DataConverterClassSubclassFeature; + }); + const isIgnoredLookup = await convSubclFeature.pGetClassSubclassFeatureIgnoredLookup({ data: data }); + */ + + const isIgnoredLookup = await DataConverterClassSubclassFeature.pGetClassSubclassFeatureIgnoredLookup({ data: data }); + const postLoadedData = await PageFilterClassesFoundry.pPostLoad({ + class: data.class, + subclass: data.subclass, + classFeature: data.classFeature, + subclassFeature: data.subclassFeature + }, { + actor: null, + isIgnoredLookup: isIgnoredLookup + }); + Object.assign(data, postLoadedData); + if (data.class) { + data.class.forEach(cls => PageFilterClasses.mutateForFilters(cls)); + } + } + if (data.feat) { data.feat = MiscUtil.copy(data.feat); } + if (data.optionalfeature) { + data.optionalfeature = MiscUtil.copy(data.optionalfeature); + } + return data; + } + /** + * Apply new source IDs, and fetch entities from them. Completely reloads the entire character builder. + * @param {{sourceIds:any[], uploadedFileMetas:any, customUrls:any}} sourceInfo + */ + static async changeSources(sourceInfo){ + //Cache which sources we chose, and let them process the source ids into ready data entries (classes, races, etc) + const data = await SourceManager._loadSources(sourceInfo); + //Get a cookie explaining the existing character shown, and what page + let state = CookieManager.getState(); + if(!state){ + //This really shouldn't happen, but it is good form to have a fallback anyway + state = {uid: CharacterBuilder.uid, page:"class"}; + } + //Tear down the existing window + this._curWindow.teardown(); + //Create a new window + const window = new CharacterBuilder(data, state.uid, state.page); + this._curWindow = window; + } + /** + * @param {{sourceIds:any[], uploadedFileMetas:any[], customUrls:any[]}} opts + */ + static async _setUsedSourceIds(opts){ + SourceManager.cachedSourceIds = opts.sourceIds; + SourceManager.cachedUploadedFileMetas = opts.uploadedFileMetas; + SourceManager.cachedCustomUrls = opts.customUrls; + } + /** + * @param {string} cookieUid + * @returns {{name:string, isDefault:boolean, cacheKey:string}[]} + */ + static async _loadSourceIdsFromSave(cookieUid){ + let ids = []; + let metas = []; + let customUrls = []; + try { + //Load a character from localstorage using the cookie ID, that character contains info about the sources + const info = CookieManager.getCharacterInfo(cookieUid); + if(!info?.result?._meta?.sourceIds?.length){return null;} + ids = info?.result._meta?.sourceIds; + metas = info?.result._meta?.uploadedFileMetas || []; + metas = info?.result._meta?.customUrls || []; + } + catch(e){ + console.error("Failed to parse saved source ids!"); + throw e; + } + //Assume something is wrong if no source id is in the array + if(ids.length < 1){return null;} + //Get all sources. These contain more info than is in the minified version + const allSources = await this._pGetSources(); + + //Match the full sources to the minified sources we pulled from localstorage + //Then return the full sources that were matched + const matchedSourceIds = allSources.filter(src => { + let match = false; //loop will stop when match is made + for(let i = 0; !match && i < ids.length; ++i){ + match = ids[i].name == src.name; //Simple name match for now + } + return match; + }); + + return {sourceIds: matchedSourceIds, uploadedFileMetas: metas, customUrls: customUrls} + } + static async _getDefaultSourceIds(){ + const isStreamerMode = true; + //Create a source obj that contains all the official sources (PHB, XGE, TCE, etc) + //This object will have the 'isDefault' property set to true + const officialSources = new UtilDataSource.DataSourceSpecial( + isStreamerMode? "SRD" : "5etools", this._pLoadVetoolsSource.bind(this), + { + cacheKey: '5etools-charactermancer', + filterTypes: [UtilDataSource.SOURCE_TYP_OFFICIAL_ALL], + isDefault: true, + pPostLoad: this._pPostLoad.bind(this, { }) + }); + + /* const allBrews = await Vetools.pGetBrewSources(...SourceManager._BREW_DIRS); + + const chosenBrewSourceUrl = new UtilDataSource.DataSourceUrl(chosenBrew.name, chosenBrew.url,{ + pPostLoad: this._pPostLoad.bind(this, { isBrew: true, actor: actor }), + filterTypes: [UtilDataSource.SOURCE_TYP_BREW], + abbreviations: chosenBrew.abbreviations, + brewUtil: BrewUtil2, + }); */ + + return [officialSources]; + } + static minifySourceId(sourceId){ + let out = {name:sourceId.name}; + if(!!sourceId.isDefault){out.isDefault = sourceId.isDefault;} + //if(!!s._isAutoDetectPrereleaseBrew){out._isAutoDetectPrereleaseBrew = s._isAutoDetectPrereleaseBrew;} + //if(!!s._isExistingPrereleaseBrew){out._isExistingPrereleaseBrew = s._isExistingPrereleaseBrew;} + //if(!!sourceId.cacheKey){out.cacheKey = sourceId.cacheKey;} + return out; + } +} +class SETTINGS{ + static FILTERS = true; + static PARENTLESS_MODE = true; + static DO_RENDER_DICE = false; + static USE_EXISTING = false; + static LOCK_EXISTING_CHOICES = false; + /**This boolean toggles loading from a cookie save file */ + static USE_EXISTING_WEB = true; + static LOCALPATH_REDIRECT = true; + static USE_FVTT = false; + /**Should changing class/race that transfer the already set choices (for like proficiencies?), so if both the new and the old race could pick Perception as a proficiency, and the old one did, make sure the new one also has that choice set */ + static TRANSFER_CHOICES = false; + static DICE_TOMESSAGE = false; + //By default, this is off. This means that loading in a high level character, they dont get to pick any spells that they would have gained at earlier levels + //Turning this to true fixes that, and lets us pick spells from lower levels + static GET_CASTERPROG_UP_TO_CURLEVEL = true; + static GET_FEATOPTSEL_UP_TO_CURLEVEL = true; + static SPECIFIED_ABILITY_SAVE = false; +} +class CharacterBuilder { + tabButtonParent; + tabClass; + tabRace; + tabAbilities; + tabBackground; + tabSpells; + tabEquipment; + tabShop; + tabFeats; + tabSheet; + compClass; + compRace; + compAbility; + compBackground; + compEquipment; + compSpell; + compFeat; + compSheet; + tabs; + _featureSourceTracker; + static useHeaderTitleAndReturnButton = false; + static enableFVTTExport = false; + + /** + * @param {{class:{}[], background:{}[], classFeature:{}[], race:{}[], monster:{}[], item:{}[] + * , spell:{}[], subclassFeature:{}[], feat:{}[], optionalFeature:{}[], foundryClass:{}[]}} data + * @param {string} existingUid + * @param {string} page + * @returns {CharacterBuilder} + */ + constructor(data, existingUid, page){ + + ActorCharactermancerBaseComponent.class_clearDeleted(); + CharacterBuilder.currentUid = null; //Reset the publicly readable uid + this.parent = this; + this._data = data; + + const _root = $("#window-root"); + + //Create header + this._createHeader(_root); + + this._createTabs(_root); //Create the small tab buttons + this._createPanels(_root); //Create the panels that hold components + + //Try to load a character from cookies using a cookie uid + const charInfo = existingUid? CookieManager.getCharacterInfo(existingUid).result : null; + if(!!charInfo){ //If that succeded, load the character stored in the cookie + this.actor = charInfo.character; + CharacterBuilder.currentUid = existingUid; //And cache the uid we used, available publicly to read + } + else { //If that failed, just create a fresh uid for the blank character we are about to show + CharacterBuilder.currentUid = CookieManager.createUid(); + } + + //Create a feature source tracker (this one gets used alot by the components) + this._featureSourceTracker = new Charactermancer_FeatureSourceTracker(); + + //Create components + this.compClass = new ActorCharactermancerClass(this); + this.compRace = new ActorCharactermancerRace(this); + this.compAbility = new ActorCharactermancerAbility(this); + this.compBackground = new ActorCharactermancerBackground(this); + this.compEquipment = new ActorCharactermancerEquipment(this); + this.compSpell = new ActorCharactermancerSpell(this); + this.compFeat = new ActorCharactermancerFeat(this); + this.compDescription = new ActorCharactermancerDescription(this); + this.compSheet = new ActorCharactermancerSheet(this); + + //Call this to let the components load some content before we start using them + this._pLoad() + .then(() => this._renderComponents({charInfo})) //Then render the components + .then(() => this.e_switchTab(page)); //Then switch to the tab we want to start off with + } + + _createTabs($wrp){ + const tabHolder = $$`
    `.appendTo($wrp); + const createTabBtn = (label) => { + return $$``.appendTo(tabHolder); + } + const createRightSideBtn = (label) => { + return $$``.appendTo(tabHolder); + } + + //Create the tabs + createTabBtn("Class").click(()=>{ this.e_switchTab("class"); }).addClass("active"); //Set class button as active + createTabBtn("Race").click(()=>{ this.e_switchTab("race"); }); + createTabBtn("Abilities").click(()=>{ this.e_switchTab("abilities"); }); + createTabBtn("Background").click(()=>{ this.e_switchTab("background"); }); + createTabBtn("Starting Equipment").click(()=>{ this.e_switchTab("startingEquipment"); }); + createTabBtn("Equipment Shop").click(()=>{ this.e_switchTab("shop"); }); + createTabBtn("Spells").click(()=>{ this.e_switchTab("spells"); }); + createTabBtn("Feats").click(()=>{ this.e_switchTab("feats"); }); + createTabBtn("Description").click(()=>{ this.e_switchTab("description"); }); + createTabBtn("Sheet").click(()=>{ this.e_switchTab("sheet"); }); + + if(!CharacterBuilder.useHeaderTitleAndReturnButton){ + createRightSideBtn("Return To Select").click(() => { + this._returnToCharSelect(); + }); + } + + if(CharacterBuilder.enableFVTTExport){ + createRightSideBtn("Export to FVTT").click(async ()=>{ + const json = await CharacterExportFvtt.exportCharacterFvtt(this); + }); + } + createRightSideBtn("Configure Sources").click(async()=>{ + await this.e_changeSourcesDialog(); + }); + createRightSideBtn("Save").click(()=>{ + CharacterExportFvtt.exportCharacter(this); + }); + + + this.tabButtonParent = tabHolder; + } + _createPanels($wrp){ + const newPanel = () => {return new CharacterBuilderPanel($(`
    `).appendTo($wrp)); } + + this.tabClass = newPanel(); + this.tabRace = newPanel(); + this.tabAbilities = newPanel(); + this.tabBackground = newPanel(); + this.tabEquipment = newPanel(); + this.tabShop = newPanel(); + this.tabSpells = newPanel(); + this.tabFeats = newPanel(); + this.tabDescription = newPanel(); + this.tabSheet = newPanel(); + } + async _pLoad(){ + if(!SETTINGS.FILTERS){return;} + await this.compRace.pLoad(); + await this.compBackground.pLoad(); + //This sets state based on what is in the savefile (if USE_EXISTING_WEB) is true + //Only handles class, subclass, level and isPrimary + await this.compClass.pLoad(); + await this.compSpell.pLoad(); + await this.compFeat.pLoad(); + } + _renderComponents(opts){ + //this.compClass.render(); //Goes on for quite long, and will trigger hooks for many ms after + const doLoad = SETTINGS.USE_EXISTING_WEB && !!this.actor; + + this.compClass.render().then(() => { + + if(doLoad){ this.compClass.setStateFromSaveFile(this.actor); } + this.compRace.render(); + if(doLoad){this.compRace.setStateFromSaveFile(this.actor);} + this.compAbility.render(); + + + this.compBackground.render(); + + + this.compEquipment.pRenderStarting().then(() => this.compEquipment.pRenderShop()) + .then(() => {if(doLoad){this.compEquipment.setStateFromSaveFile(this.actor)}}); + this.compSpell.pRender().then(() => {if(doLoad){this.compSpell.setStateFromSaveFile(this.actor);}}); + this.compFeat.render(); + + if(doLoad){this.compDescription.setStateFromSaveFile(this.actor);} + this.compDescription.render(); + + if(doLoad){this.compAbility.setStateFromSaveFile(this.actor);} + if(doLoad){this.compBackground.setStateFromSaveFile(this.actor);} + }).then(()=> { + this.compSheet.render({charInfo: opts?.charInfo}); + }); + } + + _createHeader($wrp){ + + if(!CharacterBuilder.useHeaderTitleAndReturnButton){return;} + const btnBackToSelect = $$``; + btnBackToSelect.click(() => { + this._returnToCharSelect(); + }); + + const header = $$` +
    +

    5e Character Builder

    + ${btnBackToSelect} +
    `; + + header.appendTo($wrp); + } + + //#region Events + e_switchTab(tabName){ + let newActivePanel = null; + let tabIx = 0; + this.tabButtonParent.children().each(function() {$(this).removeClass("active");}); + this._setActive(this.tabClass.$wrpTab, false); + this._setActive(this.tabRace.$wrpTab, false); + this._setActive(this.tabAbilities.$wrpTab, false); + this._setActive(this.tabBackground.$wrpTab, false); + this._setActive(this.tabEquipment.$wrpTab, false); + this._setActive(this.tabShop.$wrpTab, false); + this._setActive(this.tabSpells.$wrpTab, false); + this._setActive(this.tabFeats.$wrpTab, false); + this._setActive(this.tabDescription.$wrpTab, false); + this._setActive(this.tabSheet.$wrpTab, false); + + switch(tabName){ + case "race": newActivePanel = this.tabRace; tabIx = 1; break; + case "abilities": newActivePanel = this.tabAbilities; tabIx = 2; break; + case "background": newActivePanel = this.tabBackground; tabIx = 3; break; + case "startingEquipment": newActivePanel = this.tabEquipment; tabIx = 4; break; + case "shop": newActivePanel = this.tabShop; tabIx = 5; break; + case "spells": newActivePanel = this.tabSpells; tabIx = 6; break; + case "feats": newActivePanel = this.tabFeats; tabIx = 7; break; + case "description": newActivePanel = this.tabDescription; tabIx = 8; break; + case "sheet": newActivePanel = this.tabSheet; tabIx = 9; break; + default: newActivePanel = this.tabClass; tabIx = 0; break; + } + const pressedBtn = this.tabButtonParent.children().eq(tabIx); + pressedBtn.addClass("active"); + this._setActive(newActivePanel.$wrpTab, true); + + //Write to localstorage that we are on this tab now, so if browser refreshes, we go back to it + const cookie = CharacterBuilder.createStateCookie(tabName, CharacterBuilder.currentUid); + CookieManager.setState(cookie); + } + async e_changeSourcesDialog(){ + //Get all available sources + const allSources = await SourceManager._pGetSources(); + //Get the names of the sources we already have set as enabled + const preEnabledSources = SourceManager.cachedSourceIds; + //Open up the source selector window and wait for a reply + const sourceSelector = new ActorCharactermancerSourceSelector({ + title: "Select Sources", + filterNamespace: 'ActorCharactermancerSourceSelector_filter', + savedSelectionKey: "ActorCharactermancerSourceSelector_savedSelection", + sourcesToDisplay: allSources, + preEnabledSources: preEnabledSources + }); + const result = await sourceSelector.pWaitForUserInput(); + //If user just tried to simply exit the dialog without confirming any choices, an empty array should be returned + //Since the dialog won't let the user confirm without choosing at least one source, this is a good way to tell if user aborted + if(!result || result.length < 1){return;} + + //Then tell SourceManager that we have these new sourceIds, and let them take it from here + SourceManager.changeSources(result); + } + //#endregion + //#region Getters + /** + * @returns {{class:{}[], background:{}[], classFeature:{}[], race:{}[], monster:{}[], item:{}[] + * , spell:{}[], subclassFeature:{}[], feat:{}[], optionalFeature:{}[], foundryClass:{}[]}} + */ + get data(){ + return this._data; + } + get featureSourceTracker_() { + return this._featureSourceTracker; + } + //#endregion + + _returnToCharSelect(){ + this.teardown(); + const charSelect = new CharacterSelectScreen(); + charSelect.render(); + + const cookie = CharacterBuilder.createStateCookie("select", null); + CookieManager.setState(cookie); + } + + _setActive($tab, active){ + const hi = "ve-hidden"; + if(!$tab){return;} + if(active && $tab.hasClass(hi)){$tab.removeClass(hi);} + else if(!active && !$tab.hasClass(hi)){$tab.addClass(hi);} + } + teardown(){ + CharacterBuilder.currentUid = null; + $(`#window-root`).empty(); + } + /** + * Create a cookie to remember which page and character the browser is viewing + * @param {string} tabName + * @param {string} charUid + * @returns {{uid:string, page:string}} + */ + static createStateCookie(tabName, charUid){ + return { + page: tabName, + uid: charUid + }; + } +} +/**A wrapper for a div that contains components. Only used by CharacterBuilder */ +class CharacterBuilderPanel { + $wrpTab; + constructor(parentDiv){ + this.$wrpTab = parentDiv; + } + +} +class CookieManager { + /** + * @returns {number} + */ + static getNumCharacters(){ + const registry = this.getCharacterRegistry(); + return registry?.uids?.length || 0; + } + /** + * @returns {{uid:string, page:string}} + */ + static getState(){ + const foundState = localStorage.getItem("lastState"); //may be null + if(!foundState){return null;} + return JSON.parse(foundState); + } + /** + * Saves the state where the user is (which page and which character), so refreshing the browser will put us back on the same page + * @param {{uid:string, page:string}} state + */ + static setState(state){ + localStorage.setItem("lastState", JSON.stringify(state)); + } + /** + * @param {string} uid + * @returns {boolean} + */ + static getCharacterExists(uid){ + const character = this.getCharacterInfo(uid); + return !!character; + } + /** + * @param {string} uid + * @returns {{result:{_meta:any, character:any}, uid:string}} + */ + static getCharacterInfo(uid){ + const str = localStorage.getItem(`"char_"${uid}`); + if(!str){ + console.error("Failed to load character with uid ", uid); + return null; + } + const character = JSON.parse(str); + return {result: character, uid:uid}; + } + /** + * @returns {{result:{_meta:any, character:any}, uid:string}[]} + */ + static getAllCharacterInfos(){ + const registry = this.getCharacterRegistry(); + if(!registry || !registry.uids){return [];} + let output = []; + for(let i = 0; i < registry.uids.length; ++i){ + output.push(this.getCharacterInfo(registry.uids[i])); + } + return output; + } + /** + * @param {any} character + * @param {string} uid + */ + static saveCharacterInfo(character, uid){ + this.setCharacterToRegistry(uid); + const str = JSON.stringify(character); + localStorage.setItem(`"char_"${uid}`, str); + } + /** + * @param {any} character + */ + static saveNewCharacter(character){ + const uid = this.createUid(); + this.saveCharacterInfo(character, uid); + } + /** + * @param {string} uid + */ + static setCharacterToRegistry(uid){ + const existingRegistry = this.getCharacterRegistry(); + if(!existingRegistry){ + const newRegistry = {uids:[uid]}; + this.setCharacterRegistry(newRegistry); + return; + } + + if(!existingRegistry.uids.includes(uid)){ + existingRegistry.uids.push(uid); + } + else{ + existingRegistry.uids[existingRegistry.uids.indexOf(uid)] = uid; + } + + + this.setCharacterRegistry(existingRegistry); + } + /** + * @param {string} uid + * @returns {boolean} + */ + static getCharacterExistsInRegistry(uid){ + const existingRegistry = this.getCharacterRegistry(); + if(!existingRegistry){ + return false; + } + + return existingRegistry.includes(uid); + } + /** + * @param {{uids:string[]}} registry + */ + static setCharacterRegistry(registry){ + const str = JSON.stringify(registry); + localStorage.setItem("character_registry", str); + } + /** + * @returns {{uids:string[]}} + */ + static getCharacterRegistry(){ + const str = localStorage.getItem("character_registry"); + if(!str){return null;} + return JSON.parse(str); + } + /** + * @returns {string} + */ + static createUid(){ + const generateUid = () => { + return "id" + Math.random().toString(16).slice(2); + } + const registry = this.getCharacterRegistry(); + if(!registry || !registry.uids.length){ + return generateUid(); + } + const MAX_ATTEMPTS = 256; + for(let i = 0; i < MAX_ATTEMPTS; ++i){ + const id = generateUid(); + if(!registry.uids.includes(id)){return id;} + } + throw new Error("Failed to generate a unique ID for character!"); + } + /** + * @param {string} uid + */ + static deleteCharacter(uid){ + let existingRegistry = this.getCharacterRegistry(); + if(existingRegistry){ + const ix = existingRegistry.uids.indexOf(uid); + existingRegistry.uids.splice(ix, 1); + } + this.setCharacterRegistry(existingRegistry); + localStorage.removeItem(`"char_"${uid}`); + } +} \ No newline at end of file diff --git a/charbuilder/js/plutonium/brewutil.js b/charbuilder/js/plutonium/brewutil.js new file mode 100644 index 0000000..b02ddbf --- /dev/null +++ b/charbuilder/js/plutonium/brewutil.js @@ -0,0 +1,1836 @@ +class _BrewUtil2Base { + _STORAGE_KEY_LEGACY; + _STORAGE_KEY_LEGACY_META; + + _STORAGE_KEY; + _STORAGE_KEY_META; + + _STORAGE_KEY_CUSTOM_URL; + _STORAGE_KEY_MIGRATION_VERSION; + + _VERSION; + + _PATH_LOCAL_DIR; + _PATH_LOCAL_INDEX; + + IS_EDITABLE; + PAGE_MANAGE; + URL_REPO_DEFAULT; + DISPLAY_NAME; + DISPLAY_NAME_PLURAL; + DEFAULT_AUTHOR; + STYLE_BTN; + + _LOCK = new VeLock({ + name: this.constructor.name + }); + + _cache_iteration = 0; + _cache_brewsProc = null; + _cache_metas = null; + _cache_brews = null; + _cache_brewsLocal = null; + + _isDirty = false; + + _brewsTemp = []; + _addLazy_brewsTemp = []; + + _storage = StorageUtil; + + _pActiveInit = null; + + pInit() { + this._pActiveInit ||= (async()=>{ + await this._pGetBrew_pGetLocalBrew(); + + this._pInit_doBindDragDrop(); + this._pInit_pDoLoadFonts().then(null); + } + )(); + return this._pActiveInit; + } + + _pInit_doBindDragDrop() { + throw new Error("Unimplemented!"); + } + + async _pInit_pDoLoadFonts() { + const fontFaces = Object.entries((this._getBrewMetas() || []).map(({_meta})=>_meta?.fonts || {}).mergeMap(it=>it), ).map(([family,fontUrl])=>new FontFace(family,`url("${fontUrl}")`)); + + const results = await Promise.allSettled(fontFaces.map(async fontFace=>{ + await fontFace.load(); + return document.fonts.add(fontFace); + } + ), ); + + const errors = results.filter(({status})=>status === "rejected").map(({reason},i)=>({ + message: `Font "${fontFaces[i].family}" failed to load!`, + reason + })); + if (errors.length) { + errors.forEach(({message})=>JqueryUtil.doToast({ + type: "danger", + content: message + })); + setTimeout(()=>{ + throw new Error(errors.map(({message, reason})=>[message, reason].join("\n")).join("\n\n")); + } + ); + } + + return document.fonts.ready; + } + + async pGetCustomUrl() { + return this._storage.pGet(this._STORAGE_KEY_CUSTOM_URL); + } + + async pSetCustomUrl(val) { + return !val ? this._storage.pRemove(this._STORAGE_KEY_CUSTOM_URL) : this._storage.pSet(this._STORAGE_KEY_CUSTOM_URL, val); + } + + isReloadRequired() { + return this._isDirty; + } + + _getBrewMetas() { + return [...(this._storage.syncGet(this._STORAGE_KEY_META) || []), ...(this._cache_brewsLocal || []).map(brew=>this._getBrewDocReduced(brew)), ]; + } + + _setBrewMetas(val) { + this._cache_metas = null; + return this._storage.syncSet(this._STORAGE_KEY_META, val); + } + + async pGetBrewProcessed() { + if (this._cache_brewsProc) + return this._cache_brewsProc; + try { + const lockToken = await this._LOCK.pLock(); + await this._pGetBrewProcessed_({ + lockToken + }); + } catch (e) { + setTimeout(()=>{ + throw e; + } + ); + } finally { + this._LOCK.unlock(); + } + return this._cache_brewsProc; + } + + async _pGetBrewProcessed_({lockToken}) { + const cpyBrews = MiscUtil.copyFast([...await this.pGetBrew({ + lockToken + }), ...this._brewsTemp, ]); + if (!cpyBrews.length) + return this._cache_brewsProc = {}; + + await this._pGetBrewProcessed_pDoBlocklistExtension({ + cpyBrews + }); + + const cpyBrewsLoaded = await cpyBrews.pSerialAwaitMap(async({head, body})=>{ + const cpyBrew = await DataUtil.pDoMetaMerge(head.url || head.docIdLocal, body, { + isSkipMetaMergeCache: true + }); + this._pGetBrewProcessed_mutDiagnostics({ + head, + cpyBrew + }); + return cpyBrew; + } + ); + + this._cache_brewsProc = this._pGetBrewProcessed_getMergedOutput({ + cpyBrewsLoaded + }); + return this._cache_brewsProc; + } + + async _pGetBrewProcessed_pDoBlocklistExtension({cpyBrews}) { + for (const {body} of cpyBrews) { + if (!body?.blocklist?.length || !(body.blocklist instanceof Array)) + continue; + await ExcludeUtil.pExtendList(body.blocklist); + } + } + + _pGetBrewProcessed_mutDiagnostics({head, cpyBrew}) { + if (!head.filename) + return; + + for (const arr of Object.values(cpyBrew)) { + if (!(arr instanceof Array)) + continue; + for (const ent of arr) { + if (!("__prop"in ent)) + break; + ent.__diagnostic = { + filename: head.filename + }; + } + } + } + + _pGetBrewProcessed_getMergedOutput({cpyBrewsLoaded}) { + return BrewDoc.mergeObjects(undefined, ...cpyBrewsLoaded); + } + + getBrewProcessedFromCache(prop) { + return this._cache_brewsProc?.[prop] || []; + } + + async pGetBrew({lockToken}={}) { + if (this._cache_brews) + return this._cache_brews; + + try { + lockToken = await this._LOCK.pLock({ + token: lockToken + }); + + const out = [...(await this._pGetBrewRaw({ + lockToken + })), ...(await this._pGetBrew_pGetLocalBrew({ + lockToken + })), ]; + + return this._cache_brews = out.filter(brew=>brew?.body?._meta?.sources?.length); + } finally { + this._LOCK.unlock(); + } + } + + async pGetBrewBySource(source, {lockToken}={}) { + const brews = await this.pGetBrew({ + lockToken + }); + return brews.find(brew=>brew?.body?._meta?.sources?.some(src=>src?.json === source)); + } + + async _pGetBrew_pGetLocalBrew({lockToken}={}) { + if (this._cache_brewsLocal) + return this._cache_brewsLocal; + if (IS_VTT || IS_DEPLOYED || typeof window === "undefined") + return this._cache_brewsLocal = []; + + try { + await this._LOCK.pLock({ + token: lockToken + }); + return (await this._pGetBrew_pGetLocalBrew_()); + } finally { + this._LOCK.unlock(); + } + } + + async _pGetBrew_pGetLocalBrew_() { + const indexLocal = await DataUtil.loadJSON(`${Renderer.get().baseUrl}${this._PATH_LOCAL_INDEX}`); + if (!indexLocal?.toImport?.length) + return this._cache_brewsLocal = []; + + const brewDocs = (await indexLocal.toImport.pMap(async name=>{ + name = `${name}`.trim(); + const url = /^https?:\/\//.test(name) ? name : `${Renderer.get().baseUrl}${this._PATH_LOCAL_DIR}/${name}`; + const filename = UrlUtil.getFilename(url); + try { + const json = await DataUtil.loadRawJSON(url); + return this._getBrewDoc({ + json, + url, + filename, + isLocal: true + }); + } catch (e) { + JqueryUtil.doToast({ + type: "danger", + content: `Failed to load local homebrew from URL "${url}"! ${VeCt.STR_SEE_CONSOLE}` + }); + setTimeout(()=>{ + throw e; + } + ); + return null; + } + } + )).filter(Boolean); + + return this._cache_brewsLocal = brewDocs; + } + + async _pGetBrewRaw({lockToken}={}) { + try { + await this._LOCK.pLock({ + token: lockToken + }); + return (await this._pGetBrewRaw_()); + } finally { + this._LOCK.unlock(); + } + } + + async _pGetBrewRaw_() { + const brewRaw = (await this._storage.pGet(this._STORAGE_KEY)) || []; + + if (brewRaw.length) + return brewRaw; + + const {version, existingMeta, existingBrew} = await this._pGetMigrationInfo(); + + if (version === this._VERSION) + return brewRaw; + + if (!existingMeta || !existingBrew) { + await this._storage.pSet(this._STORAGE_KEY_MIGRATION_VERSION, this._VERSION); + return brewRaw; + } + + const brewEditable = this._getNewEditableBrewDoc(); + + const cpyBrewEditableDoc = BrewDoc.fromObject(brewEditable, { + isCopy: true + }).mutMerge({ + json: { + _meta: existingMeta || {}, + ...existingBrew, + }, + }); + + await this._pSetBrew_({ + val: [cpyBrewEditableDoc], + isInitialMigration: true + }); + + await this._storage.pSet(this._STORAGE_KEY_MIGRATION_VERSION, this._VERSION); + + JqueryUtil.doToast(`Migrated ${this.DISPLAY_NAME} from version ${version} to version ${this._VERSION}!`); + + return this._storage.pGet(this._STORAGE_KEY); + } + + _getNewEditableBrewDoc() { + const json = { + _meta: { + sources: [] + } + }; + return this._getBrewDoc({ + json, + isEditable: true + }); + } + + async _pGetMigrationInfo() { + if (!this._STORAGE_KEY_LEGACY && !this._STORAGE_KEY_LEGACY_META) + return { + version: this._VERSION, + existingBrew: null, + existingMeta: null + }; + + const version = await this._storage.pGet(this._STORAGE_KEY_MIGRATION_VERSION); + + if (version === this._VERSION) + return { + version + }; + + const existingBrew = await this._storage.pGet(this._STORAGE_KEY_LEGACY); + const existingMeta = await this._storage.syncGet(this._STORAGE_KEY_LEGACY_META); + + return { + version: version ?? 1, + existingBrew, + existingMeta, + }; + } + + getCacheIteration() { + return this._cache_iteration; + } + + async pSetBrew(val, {lockToken}={}) { + try { + await this._LOCK.pLock({ + token: lockToken + }); + await this._pSetBrew_({ + val + }); + } finally { + this._LOCK.unlock(); + } + } + + async _pSetBrew_({val, isInitialMigration}) { + this._mutBrewsForSet(val); + + if (!isInitialMigration) { + if (this._cache_brewsProc) + this._cache_iteration++; + this._cache_brews = null; + this._cache_brewsProc = null; + } + await this._storage.pSet(this._STORAGE_KEY, val); + + if (!isInitialMigration) + this._isDirty = true; + } + + _mutBrewsForSet(val) { + if (!(val instanceof Array)) + throw new Error(`${this.DISPLAY_NAME.uppercaseFirst()} array must be an array!`); + + this._setBrewMetas(val.map(brew=>this._getBrewDocReduced(brew))); + } + + _getBrewId(brew) { + if (brew.head.url) + return brew.head.url; + if (brew.body._meta?.sources?.length) + return brew.body._meta.sources.map(src=>(src.json || "").toLowerCase()).sort(SortUtil.ascSortLower).join(" :: "); + return null; + } + + _getNextBrews(brews, brewsToAdd) { + const idsToAdd = new Set(brewsToAdd.map(brews=>this._getBrewId(brews)).filter(Boolean)); + brews = brews.filter(brew=>{ + const id = this._getBrewId(brew); + if (id == null) + return true; + return !idsToAdd.has(id); + } + ); + return [...brews, ...brewsToAdd]; + } + + async _pGetBrewDependencies({brewDocs, brewsRaw=null, brewsRawLocal=null, lockToken}) { + try { + lockToken = await this._LOCK.pLock({ + token: lockToken + }); + return (await this._pGetBrewDependencies_({ + brewDocs, + brewsRaw, + brewsRawLocal, + lockToken + })); + } finally { + this._LOCK.unlock(); + } + } + + async _pGetBrewDependencies_({brewDocs, brewsRaw=null, brewsRawLocal=null, lockToken}) { + const urlRoot = await this.pGetCustomUrl(); + const brewIndex = await this._pGetSourceIndex(urlRoot); + + const toLoadSources = []; + const loadedSources = new Set(); + const out = []; + + brewsRaw = brewsRaw || await this._pGetBrewRaw({ + lockToken + }); + brewsRawLocal = brewsRawLocal || await this._pGetBrew_pGetLocalBrew({ + lockToken + }); + + const trackLoaded = brew=>(brew.body._meta?.sources || []).filter(src=>src.json).forEach(src=>loadedSources.add(src.json)); + brewsRaw.forEach(brew=>trackLoaded(brew)); + brewsRawLocal.forEach(brew=>trackLoaded(brew)); + + brewDocs.forEach(brewDoc=>toLoadSources.push(...this._getBrewDependencySources({ + brewDoc, + brewIndex + }))); + + while (toLoadSources.length) { + const src = toLoadSources.pop(); + if (loadedSources.has(src)) + continue; + loadedSources.add(src); + + const url = this.getFileUrl(brewIndex[src], urlRoot); + const brewDocDep = await this._pGetBrewDocFromUrl({ + url + }); + out.push(brewDocDep); + trackLoaded(brewDocDep); + + toLoadSources.push(...this._getBrewDependencySources({ + brewDoc: brewDocDep, + brewIndex + })); + } + + return out; + } + + async pGetSourceUrl(source) { + const urlRoot = await this.pGetCustomUrl(); + const brewIndex = await this._pGetSourceIndex(urlRoot); + + if (brewIndex[source]) + return this.getFileUrl(brewIndex[source], urlRoot); + + const sourceLower = source.toLowerCase(); + if (brewIndex[sourceLower]) + return this.getFileUrl(brewIndex[sourceLower], urlRoot); + + const sourceOriginal = Object.keys(brewIndex).find(k=>k.toLowerCase() === sourceLower); + if (!brewIndex[sourceOriginal]) + return null; + return this.getFileUrl(brewIndex[sourceOriginal], urlRoot); + } + + async _pGetSourceIndex(urlRoot) { + throw new Error("Unimplemented!"); + } + getFileUrl(path, urlRoot) { + throw new Error("Unimplemented!"); + } + pLoadTimestamps(urlRoot) { + throw new Error("Unimplemented!"); + } + pLoadPropIndex(urlRoot) { + throw new Error("Unimplemented!"); + } + pLoadMetaIndex(urlRoot) { + throw new Error("Unimplemented!"); + } + + _PROPS_DEPS = ["dependencies", "includes"]; + _PROPS_DEPS_DEEP = ["otherSources"]; + + _getBrewDependencySources({brewDoc, brewIndex}) { + const out = new Set(); + + this._PROPS_DEPS.forEach(prop=>{ + const obj = brewDoc.body._meta?.[prop]; + if (!obj || !Object.keys(obj).length) + return; + Object.values(obj).flat().filter(src=>brewIndex[src]).forEach(src=>out.add(src)); + } + ); + + this._PROPS_DEPS_DEEP.forEach(prop=>{ + const obj = brewDoc.body._meta?.[prop]; + if (!obj || !Object.keys(obj).length) + return; + return Object.values(obj).map(objSub=>Object.keys(objSub)).flat().filter(src=>brewIndex[src]).forEach(src=>out.add(src)); + } + ); + + return out; + } + + async pAddBrewFromUrl(url, {lockToken, isLazy}={}) { + try { + return (await this._pAddBrewFromUrl({ + url, + lockToken, + isLazy + })); + } catch (e) { + JqueryUtil.doToast({ + type: "danger", + content: `Failed to load ${this.DISPLAY_NAME} from URL "${url}"! ${VeCt.STR_SEE_CONSOLE}` + }); + setTimeout(()=>{ + throw e; + } + ); + } + return []; + } + + async _pGetBrewDocFromUrl({url}) { + const json = await DataUtil.loadRawJSON(url); + return this._getBrewDoc({ + json, + url, + filename: UrlUtil.getFilename(url) + }); + } + + async _pAddBrewFromUrl({url, lockToken, isLazy}) { + const brewDoc = await this._pGetBrewDocFromUrl({ + url + }); + + if (isLazy) { + try { + await this._LOCK.pLock({ + token: lockToken + }); + this._addLazy_brewsTemp.push(brewDoc); + } finally { + this._LOCK.unlock(); + } + + return [brewDoc]; + } + + const brewDocs = [brewDoc]; + try { + lockToken = await this._LOCK.pLock({ + token: lockToken + }); + const brews = MiscUtil.copyFast(await this._pGetBrewRaw({ + lockToken + })); + + const brewDocsDependencies = await this._pGetBrewDependencies({ + brewDocs, + brewsRaw: brews, + lockToken + }); + brewDocs.push(...brewDocsDependencies); + + const brewsNxt = this._getNextBrews(brews, brewDocs); + await this.pSetBrew(brewsNxt, { + lockToken + }); + } finally { + this._LOCK.unlock(); + } + + return brewDocs; + } + + async pAddBrewsFromFiles(files) { + try { + const lockToken = await this._LOCK.pLock(); + return (await this._pAddBrewsFromFiles({ + files, + lockToken + })); + } catch (e) { + JqueryUtil.doToast({ + type: "danger", + content: `Failed to load ${this.DISPLAY_NAME} from file(s)! ${VeCt.STR_SEE_CONSOLE}` + }); + setTimeout(()=>{ + throw e; + } + ); + } finally { + this._LOCK.unlock(); + } + return []; + } + + async _pAddBrewsFromFiles({files, lockToken}) { + const brewDocs = files.map(file=>this._getBrewDoc({ + json: file.json, + filename: file.name + })); + + const brews = MiscUtil.copyFast(await this._pGetBrewRaw({ + lockToken + })); + + const brewDocsDependencies = await this._pGetBrewDependencies({ + brewDocs, + brewsRaw: brews, + lockToken + }); + brewDocs.push(...brewDocsDependencies); + + const brewsNxt = this._getNextBrews(brews, brewDocs); + await this.pSetBrew(brewsNxt, { + lockToken + }); + + return brewDocs; + } + + async pAddBrewsLazyFinalize({lockToken}={}) { + try { + lockToken = await this._LOCK.pLock({ + token: lockToken + }); + return (await this._pAddBrewsLazyFinalize_({ + lockToken + })); + } finally { + this._LOCK.unlock(); + } + } + + async _pAddBrewsLazyFinalize_({lockToken}) { + const brewsRaw = await this._pGetBrewRaw({ + lockToken + }); + const brewDeps = await this._pGetBrewDependencies({ + brewDocs: this._addLazy_brewsTemp, + brewsRaw, + lockToken + }); + const out = MiscUtil.copyFast(brewDeps); + const brewsNxt = this._getNextBrews(MiscUtil.copyFast(brewsRaw), [...this._addLazy_brewsTemp, ...brewDeps]); + await this.pSetBrew(brewsNxt, { + lockToken + }); + this._addLazy_brewsTemp = []; + return out; + } + + async pPullAllBrews({brews}={}) { + try { + const lockToken = await this._LOCK.pLock(); + return (await this._pPullAllBrews_({ + lockToken, + brews + })); + } finally { + this._LOCK.unlock(); + } + } + + async _pPullAllBrews_({lockToken, brews}) { + let cntPulls = 0; + + brews = brews || MiscUtil.copyFast(await this._pGetBrewRaw({ + lockToken + })); + const brewsNxt = await brews.pMap(async brew=>{ + if (!this.isPullable(brew)) + return brew; + + const json = await DataUtil.loadRawJSON(brew.head.url, { + isBustCache: true + }); + + const localLastModified = brew.body._meta?.dateLastModified ?? 0; + const sourceLastModified = json._meta?.dateLastModified ?? 0; + + if (sourceLastModified <= localLastModified) + return brew; + + cntPulls++; + return BrewDoc.fromObject(brew).mutUpdate({ + json + }).toObject(); + } + ); + + if (!cntPulls) + return cntPulls; + + await this.pSetBrew(brewsNxt, { + lockToken + }); + return cntPulls; + } + + isPullable(brew) { + return !brew.head.isEditable && !!brew.head.url; + } + + async pPullBrew(brew) { + try { + const lockToken = await this._LOCK.pLock(); + return (await this._pPullBrew_({ + brew, + lockToken + })); + } finally { + this._LOCK.unlock(); + } + } + + async _pPullBrew_({brew, lockToken}) { + const brews = await this._pGetBrewRaw({ + lockToken + }); + if (!brews?.length) + return; + + let isPull = false; + const brewsNxt = await brews.pMap(async it=>{ + if (it.head.docIdLocal !== brew.head.docIdLocal || !this.isPullable(it)) + return it; + + const json = await DataUtil.loadRawJSON(it.head.url, { + isBustCache: true + }); + + const localLastModified = it.body._meta?.dateLastModified ?? 0; + const sourceLastModified = json._meta?.dateLastModified ?? 0; + + if (sourceLastModified <= localLastModified) + return it; + + isPull = true; + return BrewDoc.fromObject(it).mutUpdate({ + json + }).toObject(); + } + ); + + if (!isPull) + return isPull; + + await this.pSetBrew(brewsNxt, { + lockToken + }); + return isPull; + } + + async pAddBrewFromLoaderTag(ele) { + const $ele = $(ele); + if (!$ele.hasClass("rd__wrp-loadbrew--ready")) + return; + let jsonPath = ele.dataset.rdLoaderPath; + const name = ele.dataset.rdLoaderName; + const cached = $ele.html(); + const cachedTitle = $ele.title(); + $ele.title(""); + $ele.removeClass("rd__wrp-loadbrew--ready").html(`${name.qq()}`); + + jsonPath = jsonPath.unescapeQuotes(); + if (!UrlUtil.isFullUrl(jsonPath)) { + const brewUrl = await this.pGetCustomUrl(); + jsonPath = this.getFileUrl(jsonPath, brewUrl); + } + + await this.pAddBrewFromUrl(jsonPath); + $ele.html(`${name.qq()}`); + setTimeout(()=>$ele.html(cached).addClass("rd__wrp-loadbrew--ready").title(cachedTitle), 500); + } + + _getBrewDoc({json, url=null, filename=null, isLocal=false, isEditable=false}) { + return BrewDoc.fromValues({ + head: { + json, + url, + filename, + isLocal, + isEditable, + }, + body: json, + }).toObject(); + } + + _getBrewDocReduced(brewDoc) { + return { + docIdLocal: brewDoc.head.docIdLocal, + _meta: brewDoc.body._meta + }; + } + + async pDeleteBrews(brews) { + try { + const lockToken = await this._LOCK.pLock(); + await this._pDeleteBrews_({ + brews, + lockToken + }); + } finally { + this._LOCK.unlock(); + } + } + + async _pDeleteBrews_({brews, lockToken}) { + const brewsStored = await this._pGetBrewRaw({ + lockToken + }); + if (!brewsStored?.length) + return; + + const idsToDelete = new Set(brews.map(brew=>brew.head.docIdLocal)); + + const nxtBrews = brewsStored.filter(brew=>!idsToDelete.has(brew.head.docIdLocal)); + await this.pSetBrew(nxtBrews, { + lockToken + }); + } + + async pUpdateBrew(brew) { + try { + const lockToken = await this._LOCK.pLock(); + await this._pUpdateBrew_({ + brew, + lockToken + }); + } finally { + this._LOCK.unlock(); + } + } + + async _pUpdateBrew_({brew, lockToken}) { + const brews = await this._pGetBrewRaw({ + lockToken + }); + if (!brews?.length) + return; + + const nxtBrews = brews.map(it=>it.head.docIdLocal !== brew.head.docIdLocal ? it : brew); + await this.pSetBrew(nxtBrews, { + lockToken + }); + } + + pGetEditableBrewDoc(brew) { + throw new Error("Unimplemented"); + } + pGetOrCreateEditableBrewDoc() { + throw new Error("Unimplemented"); + } + pSetEditableBrewDoc() { + throw new Error("Unimplemented"); + } + pGetEditableBrewEntity(prop, uniqueId, {isDuplicate=false}={}) { + throw new Error("Unimplemented"); + } + pPersistEditableBrewEntity(prop, ent) { + throw new Error("Unimplemented"); + } + pRemoveEditableBrewEntity(prop, uniqueId) { + throw new Error("Unimplemented"); + } + pAddSource(sourceObj) { + throw new Error("Unimplemented"); + } + pEditSource(sourceObj) { + throw new Error("Unimplemented"); + } + pIsEditableSourceJson(sourceJson) { + throw new Error("Unimplemented"); + } + pMoveOrCopyToEditableBySourceJson(sourceJson) { + throw new Error("Unimplemented"); + } + pMoveToEditable({brews}) { + throw new Error("Unimplemented"); + } + pCopyToEditable({brews}) { + throw new Error("Unimplemented"); + } + + _PAGE_TO_PROPS__SPELLS = [...UrlUtil.PAGE_TO_PROPS[UrlUtil.PG_SPELLS], "spellFluff"]; + _PAGE_TO_PROPS__BESTIARY = ["monster", "legendaryGroup", "monsterFluff"]; + + _PAGE_TO_PROPS = { + [UrlUtil.PG_SPELLS]: this._PAGE_TO_PROPS__SPELLS, + [UrlUtil.PG_CLASSES]: ["class", "subclass", "classFeature", "subclassFeature"], + [UrlUtil.PG_BESTIARY]: this._PAGE_TO_PROPS__BESTIARY, + [UrlUtil.PG_BACKGROUNDS]: ["background"], + [UrlUtil.PG_FEATS]: ["feat"], + [UrlUtil.PG_OPT_FEATURES]: ["optionalfeature"], + [UrlUtil.PG_RACES]: [...UrlUtil.PAGE_TO_PROPS[UrlUtil.PG_RACES], "raceFluff"], + [UrlUtil.PG_OBJECTS]: ["object"], + [UrlUtil.PG_TRAPS_HAZARDS]: ["trap", "hazard"], + [UrlUtil.PG_DEITIES]: ["deity"], + [UrlUtil.PG_ITEMS]: [...UrlUtil.PAGE_TO_PROPS[UrlUtil.PG_ITEMS], "itemFluff"], + [UrlUtil.PG_REWARDS]: ["reward"], + [UrlUtil.PG_PSIONICS]: ["psionic"], + [UrlUtil.PG_VARIANTRULES]: ["variantrule"], + [UrlUtil.PG_CONDITIONS_DISEASES]: ["condition", "disease", "status"], + [UrlUtil.PG_ADVENTURES]: ["adventure", "adventureData"], + [UrlUtil.PG_BOOKS]: ["book", "bookData"], + [UrlUtil.PG_TABLES]: ["table", "tableGroup"], + [UrlUtil.PG_MAKE_BREW]: [...this._PAGE_TO_PROPS__SPELLS, ...this._PAGE_TO_PROPS__BESTIARY, "makebrewCreatureTrait", ], + [UrlUtil.PG_MANAGE_BREW]: ["*"], + [UrlUtil.PG_MANAGE_PRERELEASE]: ["*"], + [UrlUtil.PG_DEMO_RENDER]: ["*"], + [UrlUtil.PG_VEHICLES]: ["vehicle", "vehicleUpgrade"], + [UrlUtil.PG_ACTIONS]: ["action"], + [UrlUtil.PG_CULTS_BOONS]: ["cult", "boon"], + [UrlUtil.PG_LANGUAGES]: ["language", "languageScript"], + [UrlUtil.PG_CHAR_CREATION_OPTIONS]: ["charoption"], + [UrlUtil.PG_RECIPES]: ["recipe"], + [UrlUtil.PG_CLASS_SUBCLASS_FEATURES]: ["classFeature", "subclassFeature"], + [UrlUtil.PG_DECKS]: ["card", "deck"], + }; + + getPageProps({page, isStrict=false, fallback=null}={}) { + page = this._getBrewPage(page); + + const out = this._PAGE_TO_PROPS[page]; + if (out) + return out; + if (fallback) + return fallback; + + if (isStrict) + throw new Error(`No ${this.DISPLAY_NAME} properties defined for category ${page}`); + + return null; + } + + getPropPages() { + return Object.entries(this._PAGE_TO_PROPS).map(([page,props])=>[page, props.filter(it=>it !== "*")]).filter(([,props])=>props.length).map(([page])=>page); + } + + _getBrewPage(page) { + return page || (IS_VTT ? this.PAGE_MANAGE : UrlUtil.getCurrentPage()); + } + + getDirProp(dir) { + switch (dir) { + case "creature": + return "monster"; + case "makebrew": + return "makebrewCreatureTrait"; + } + return dir; + } + + getPropDisplayName(prop) { + switch (prop) { + case "adventure": + return "Adventure Contents/Info"; + case "book": + return "Book Contents/Info"; + } + return Parser.getPropDisplayName(prop); + } + + _doCacheMetas() { + if (this._cache_metas) + return; + + this._cache_metas = {}; + + (this._getBrewMetas() || []).forEach(({_meta})=>{ + Object.entries(_meta || {}).forEach(([prop,val])=>{ + if (!val) + return; + if (typeof val !== "object") + return; + + if (val instanceof Array) { + (this._cache_metas[prop] = this._cache_metas[prop] || []).push(...MiscUtil.copyFast(val)); + return; + } + + this._cache_metas[prop] = this._cache_metas[prop] || {}; + Object.assign(this._cache_metas[prop], MiscUtil.copyFast(val)); + } + ); + } + ); + + this._cache_metas["_sources"] = (this._getBrewMetas() || []).mergeMap(({_meta})=>{ + return (_meta?.sources || []).mergeMap(src=>({ + [(src.json || "").toLowerCase()]: MiscUtil.copyFast(src) + })); + } + ); + } + + hasSourceJson(source) { + if (!source) + return false; + source = source.toLowerCase(); + return !!this.getMetaLookup("_sources")[source]; + } + + sourceJsonToFull(source) { + if (!source) + return ""; + source = source.toLowerCase(); + return this.getMetaLookup("_sources")[source]?.full || source; + } + + sourceJsonToAbv(source) { + if (!source) + return ""; + source = source.toLowerCase(); + return this.getMetaLookup("_sources")[source]?.abbreviation || source; + } + + sourceJsonToDate(source) { + if (!source) + return ""; + source = source.toLowerCase(); + return this.getMetaLookup("_sources")[source]?.dateReleased || "1970-01-01"; + } + + sourceJsonToSource(source) { + if (!source) + return null; + source = source.toLowerCase(); + return this.getMetaLookup("_sources")[source]; + } + + sourceJsonToStyle(source) { + const stylePart = this.sourceJsonToStylePart(source); + if (!stylePart) + return stylePart; + return `style="${stylePart}"`; + } + + sourceToStyle(source) { + const stylePart = this.sourceToStylePart(source); + if (!stylePart) + return stylePart; + return `style="${stylePart}"`; + } + + sourceJsonToStylePart(source) { + if (!source) + return ""; + const color = this.sourceJsonToColor(source); + if (color) + return this._getColorStylePart(color); + return ""; + } + + sourceToStylePart(source) { + if (!source) + return ""; + const color = this.sourceToColor(source); + if (color) + return this._getColorStylePart(color); + return ""; + } + + _getColorStylePart(color) { + return `color: #${color} !important; border-color: #${color} !important; text-decoration-color: #${color} !important;`; + } + + sourceJsonToColor(source) { + if (!source) + return ""; + source = source.toLowerCase(); + if (!this.getMetaLookup("_sources")[source]?.color) + return ""; + return BrewUtilShared.getValidColor(this.getMetaLookup("_sources")[source].color); + } + + sourceToColor(source) { + if (!source?.color) + return ""; + return BrewUtilShared.getValidColor(source.color); + } + + getSources() { + this._doCacheMetas(); + return Object.values(this._cache_metas["_sources"]); + } + + getMetaLookup(type) { + if (!type) + return null; + this._doCacheMetas(); + return this._cache_metas[type]; + } + + getMergedData(data, homebrew) { + const out = {}; + Object.entries(data).forEach(([prop,val])=>{ + if (!homebrew[prop]) { + out[prop] = [...val]; + return; + } + + if (!(homebrew[prop]instanceof Array)) + throw new Error(`${this.DISPLAY_NAME.uppercaseFirst()} was not array!`); + if (!(val instanceof Array)) + throw new Error(`Data was not array!`); + out[prop] = [...val, ...homebrew[prop]]; + } + ); + + return out; + } + + async pGetSearchIndex({id=0}={}) { + const indexer = new Omnidexer(id); + + const brew = await this.pGetBrewProcessed(); + + await [...Omnidexer.TO_INDEX__FROM_INDEX_JSON, ...Omnidexer.TO_INDEX].pSerialAwaitMap(async arbiter=>{ + if (arbiter.isSkipBrew) + return; + if (!brew[arbiter.brewProp || arbiter.listProp]?.length) + return; + + if (arbiter.pFnPreProcBrew) { + const toProc = await arbiter.pFnPreProcBrew.bind(arbiter)(brew); + await indexer.pAddToIndex(arbiter, toProc); + return; + } + + await indexer.pAddToIndex(arbiter, brew); + } + ); + + return Omnidexer.decompressIndex(indexer.getIndex()); + } + + async pGetAdditionalSearchIndices(highestId, addiProp) { + const indexer = new Omnidexer(highestId + 1); + + const brew = await this.pGetBrewProcessed(); + + await [...Omnidexer.TO_INDEX__FROM_INDEX_JSON, ...Omnidexer.TO_INDEX].filter(it=>it.additionalIndexes && (brew[it.listProp] || []).length).pMap(it=>{ + Object.entries(it.additionalIndexes).filter(([prop])=>prop === addiProp).pMap(async([,pGetIndex])=>{ + const toIndex = await pGetIndex(indexer, { + [it.listProp]: brew[it.listProp] + }); + toIndex.forEach(add=>indexer.pushToIndex(add)); + } + ); + } + ); + + return Omnidexer.decompressIndex(indexer.getIndex()); + } + + async pGetAlternateSearchIndices(highestId, altProp) { + const indexer = new Omnidexer(highestId + 1); + + const brew = await this.pGetBrewProcessed(); + + await [...Omnidexer.TO_INDEX__FROM_INDEX_JSON, ...Omnidexer.TO_INDEX].filter(ti=>ti.alternateIndexes && (brew[ti.listProp] || []).length).pSerialAwaitMap(async arbiter=>{ + await Object.keys(arbiter.alternateIndexes).filter(prop=>prop === altProp).pSerialAwaitMap(async prop=>{ + await indexer.pAddToIndex(arbiter, brew, { + alt: arbiter.alternateIndexes[prop] + }); + } + ); + } + ); + + return Omnidexer.decompressIndex(indexer.getIndex()); + } + + async pGetUrlExportableSources() { + const brews = await this._pGetBrewRaw(); + const brewsExportable = brews.filter(brew=>!brew.head.isEditable && !brew.head.isLocal); + return brewsExportable.flatMap(brew=>brew.body._meta.sources.map(src=>src.json)).unique(); + } +} + +class _BrewUtil2 extends _BrewUtil2Base { + _STORAGE_KEY_LEGACY = "HOMEBREW_STORAGE"; + _STORAGE_KEY_LEGACY_META = "HOMEBREW_META_STORAGE"; + + _STORAGE_KEY = "HOMEBREW_2_STORAGE"; + _STORAGE_KEY_META = "HOMEBREW_2_STORAGE_METAS"; + + _STORAGE_KEY_CUSTOM_URL = "HOMEBREW_CUSTOM_REPO_URL"; + _STORAGE_KEY_MIGRATION_VERSION = "HOMEBREW_2_STORAGE_MIGRATION"; + + _VERSION = 2; + + _PATH_LOCAL_DIR = "homebrew"; + _PATH_LOCAL_INDEX = VeCt.JSON_BREW_INDEX; + + IS_EDITABLE = true; + PAGE_MANAGE = UrlUtil.PG_MANAGE_BREW; + URL_REPO_DEFAULT = VeCt.URL_BREW; + DISPLAY_NAME = "homebrew"; + DISPLAY_NAME_PLURAL = "homebrews"; + DEFAULT_AUTHOR = ""; + STYLE_BTN = "btn-info"; + + _pInit_doBindDragDrop() { + document.body.addEventListener("drop", async evt=>{ + if (EventUtil.isInInput(evt)) + return; + + evt.stopPropagation(); + evt.preventDefault(); + + const files = evt.dataTransfer?.files; + if (!files?.length) + return; + + const pFiles = [...files].map((file,i)=>{ + if (!/\.json$/i.test(file.name)) + return null; + + return new Promise(resolve=>{ + const reader = new FileReader(); + reader.onload = ()=>{ + let json; + try { + json = JSON.parse(reader.result); + } catch (ignored) { + return resolve(null); + } + + resolve({ + name: file.name, + json + }); + } + ; + + reader.readAsText(files[i]); + } + ); + } + ); + + const fileMetas = (await Promise.allSettled(pFiles)).filter(({status})=>status === "fulfilled").map(({value})=>value).filter(Boolean); + + await this.pAddBrewsFromFiles(fileMetas); + + if (this.isReloadRequired()) + location.reload(); + } + ); + + document.body.addEventListener("dragover", evt=>{ + if (EventUtil.isInInput(evt)) + return; + + evt.stopPropagation(); + evt.preventDefault(); + } + ); + } + + async _pGetSourceIndex(urlRoot) { + return DataUtil.brew.pLoadSourceIndex(urlRoot); + } + + getFileUrl(path, urlRoot) { + return DataUtil.brew.getFileUrl(path, urlRoot); + } + + pLoadTimestamps(brewIndex, src, urlRoot) { + return DataUtil.brew.pLoadTimestamps(urlRoot); + } + + pLoadPropIndex(brewIndex, src, urlRoot) { + return DataUtil.brew.pLoadPropIndex(urlRoot); + } + + pLoadMetaIndex(brewIndex, src, urlRoot) { + return DataUtil.brew.pLoadMetaIndex(urlRoot); + } + + async pGetEditableBrewDoc() { + return this._findEditableBrewDoc({ + brewRaw: await this._pGetBrewRaw() + }); + } + + _findEditableBrewDoc({brewRaw}) { + return brewRaw.find(it=>it.head.isEditable); + } + + async pGetOrCreateEditableBrewDoc() { + const existing = await this.pGetEditableBrewDoc(); + if (existing) + return existing; + + const brew = this._getNewEditableBrewDoc(); + const brews = [...MiscUtil.copyFast(await this._pGetBrewRaw()), brew]; + await this.pSetBrew(brews); + + return brew; + } + + async pSetEditableBrewDoc(brew) { + if (!brew?.head?.docIdLocal || !brew?.body) + throw new Error(`Invalid editable brew document!`); + await this.pUpdateBrew(brew); + } + + async pGetEditableBrewEntity(prop, uniqueId, {isDuplicate=false}={}) { + if (!uniqueId) + throw new Error(`A "uniqueId" must be provided!`); + + const brew = await this.pGetOrCreateEditableBrewDoc(); + + const out = (brew.body?.[prop] || []).find(it=>it.uniqueId === uniqueId); + if (!out || !isDuplicate) + return out; + + if (isDuplicate) + out.uniqueId = CryptUtil.uid(); + + return out; + } + + async pPersistEditableBrewEntity(prop, ent) { + if (!ent.uniqueId) + throw new Error(`Entity did not have a "uniqueId"!`); + + const brew = await this.pGetOrCreateEditableBrewDoc(); + + const ixExisting = (brew.body?.[prop] || []).findIndex(it=>it.uniqueId === ent.uniqueId); + if (!~ixExisting) { + const nxt = MiscUtil.copyFast(brew); + MiscUtil.getOrSet(nxt.body, prop, []).push(ent); + + await this.pUpdateBrew(nxt); + + return; + } + + const nxt = MiscUtil.copyFast(brew); + nxt.body[prop][ixExisting] = ent; + + await this.pUpdateBrew(nxt); + } + + async pRemoveEditableBrewEntity(prop, uniqueId) { + if (!uniqueId) + throw new Error(`A "uniqueId" must be provided!`); + + const brew = await this.pGetOrCreateEditableBrewDoc(); + + if (!brew.body?.[prop]?.length) + return; + + const nxt = MiscUtil.copyFast(brew); + nxt.body[prop] = nxt.body[prop].filter(it=>it.uniqueId !== uniqueId); + + if (nxt.body[prop].length === brew.body[prop]) + return; + await this.pUpdateBrew(nxt); + } + + async pAddSource(sourceObj) { + const existing = await this.pGetEditableBrewDoc(); + + if (existing) { + const nxt = MiscUtil.copyFast(existing); + const sources = MiscUtil.getOrSet(nxt.body, "_meta", "sources", []); + sources.push(sourceObj); + + await this.pUpdateBrew(nxt); + + return; + } + + const json = { + _meta: { + sources: [sourceObj] + } + }; + const brew = this._getBrewDoc({ + json, + isEditable: true + }); + const brews = [...MiscUtil.copyFast(await this._pGetBrewRaw()), brew]; + await this.pSetBrew(brews); + } + + async pEditSource(sourceObj) { + const existing = await this.pGetEditableBrewDoc(); + if (!existing) + throw new Error(`Editable brew document does not exist!`); + + const nxt = MiscUtil.copyFast(existing); + const sources = MiscUtil.get(nxt.body, "_meta", "sources"); + if (!sources) + throw new Error(`Source "${sourceObj.json}" does not exist in editable brew document!`); + + const existingSourceObj = sources.find(it=>it.json === sourceObj.json); + if (!existingSourceObj) + throw new Error(`Source "${sourceObj.json}" does not exist in editable brew document!`); + Object.assign(existingSourceObj, sourceObj); + + await this.pUpdateBrew(nxt); + } + + async pIsEditableSourceJson(sourceJson) { + const brew = await this.pGetEditableBrewDoc(); + if (!brew) + return false; + + const sources = MiscUtil.get(brew.body, "_meta", "sources") || []; + return sources.some(it=>it.json === sourceJson); + } + + async pMoveOrCopyToEditableBySourceJson(sourceJson) { + if (await this.pIsEditableSourceJson(sourceJson)) + return; + + const brews = (await this._pGetBrewRaw()).filter(brew=>(brew.body._meta?.sources || []).some(src=>src.json === sourceJson)); + const brewsLocal = (await this._pGetBrew_pGetLocalBrew()).filter(brew=>(brew.body._meta?.sources || []).some(src=>src.json === sourceJson)); + + let brew = brews.find(brew=>BrewDoc.isOperationPermitted_moveToEditable({ + brew + })); + if (!brew) + brew = brewsLocal.find(brew=>BrewDoc.isOperationPermitted_moveToEditable({ + brew, + isAllowLocal: true + })); + + if (!brew) + return; + + if (brew.head.isLocal) + return this.pCopyToEditable({ + brews: [brew] + }); + + return this.pMoveToEditable({ + brews: [brew] + }); + } + + async pMoveToEditable({brews}) { + const out = await this.pCopyToEditable({ + brews + }); + await this.pDeleteBrews(brews); + return out; + } + + async pCopyToEditable({brews}) { + const brewEditable = await this.pGetOrCreateEditableBrewDoc(); + + const cpyBrewEditableDoc = BrewDoc.fromObject(brewEditable, { + isCopy: true + }); + brews.forEach((brew,i)=>cpyBrewEditableDoc.mutMerge({ + json: brew.body, + isLazy: i !== brews.length - 1 + })); + + await this.pSetEditableBrewDoc(cpyBrewEditableDoc.toObject()); + + return cpyBrewEditableDoc; + } +} + +class _PrereleaseUtil extends _BrewUtil2Base { + _STORAGE_KEY_LEGACY = null; + _STORAGE_KEY_LEGACY_META = null; + + _STORAGE_KEY = "PRERELEASE_STORAGE"; + _STORAGE_KEY_META = "PRERELEASE_META_STORAGE"; + + _STORAGE_KEY_CUSTOM_URL = "PRERELEASE_CUSTOM_REPO_URL"; + _STORAGE_KEY_MIGRATION_VERSION = "PRERELEASE_STORAGE_MIGRATION"; + + _PATH_LOCAL_DIR = "prerelease"; + _PATH_LOCAL_INDEX = VeCt.JSON_PRERELEASE_INDEX; + + _VERSION = 1; + + IS_EDITABLE = false; + PAGE_MANAGE = UrlUtil.PG_MANAGE_PRERELEASE; + URL_REPO_DEFAULT = VeCt.URL_PRERELEASE; + DISPLAY_NAME = "prerelease content"; + DISPLAY_NAME_PLURAL = "prereleases"; + DEFAULT_AUTHOR = "Wizards of the Coast"; + STYLE_BTN = "btn-primary"; + + _pInit_doBindDragDrop() {} + + async _pGetSourceIndex(urlRoot) { + return DataUtil.prerelease.pLoadSourceIndex(urlRoot); + } + + getFileUrl(path, urlRoot) { + return DataUtil.prerelease.getFileUrl(path, urlRoot); + } + + pLoadTimestamps(brewIndex, src, urlRoot) { + return DataUtil.prerelease.pLoadTimestamps(urlRoot); + } + + pLoadPropIndex(brewIndex, src, urlRoot) { + return DataUtil.prerelease.pLoadPropIndex(urlRoot); + } + + pLoadMetaIndex(brewIndex, src, urlRoot) { + return DataUtil.prerelease.pLoadMetaIndex(urlRoot); + } + + pGetEditableBrewDoc(brew) { + return super.pGetEditableBrewDoc(brew); + } + pGetOrCreateEditableBrewDoc() { + return super.pGetOrCreateEditableBrewDoc(); + } + pSetEditableBrewDoc() { + return super.pSetEditableBrewDoc(); + } + pGetEditableBrewEntity(prop, uniqueId, {isDuplicate=false}={}) { + return super.pGetEditableBrewEntity(prop, uniqueId, { + isDuplicate + }); + } + pPersistEditableBrewEntity(prop, ent) { + return super.pPersistEditableBrewEntity(prop, ent); + } + pRemoveEditableBrewEntity(prop, uniqueId) { + return super.pRemoveEditableBrewEntity(prop, uniqueId); + } + pAddSource(sourceObj) { + return super.pAddSource(sourceObj); + } + pEditSource(sourceObj) { + return super.pEditSource(sourceObj); + } + pIsEditableSourceJson(sourceJson) { + return super.pIsEditableSourceJson(sourceJson); + } + pMoveOrCopyToEditableBySourceJson(sourceJson) { + return super.pMoveOrCopyToEditableBySourceJson(sourceJson); + } + pMoveToEditable({brews}) { + return super.pMoveToEditable({ + brews + }); + } + pCopyToEditable({brews}) { + return super.pCopyToEditable({ + brews + }); + } + +} + +globalThis.BrewUtil2 = new _BrewUtil2(); +globalThis.PrereleaseUtil = new _PrereleaseUtil(); + +class BrewDoc { + constructor(opts) { + opts = opts || {}; + this.head = opts.head; + this.body = opts.body; + } + + toObject() { + return MiscUtil.copyFast({ + ...this + }); + } + + static fromValues({head, body}) { + return new this({ + head: BrewDocHead.fromValues(head), + body, + }); + } + + static fromObject(obj, opts={}) { + const {isCopy=false} = opts; + return new this({ + head: BrewDocHead.fromObject(obj.head, opts), + body: isCopy ? MiscUtil.copyFast(obj.body) : obj.body, + }); + } + + mutUpdate({json}) { + this.body = json; + this.head.mutUpdate({ + json, + body: this.body + }); + return this; + } + + static isOperationPermitted_moveToEditable({brew, isAllowLocal=false}={}) { + return !brew.head.isEditable && (isAllowLocal || !brew.head.isLocal); + } + + mutMerge({json, isLazy=false}) { + this.body = this.constructor.mergeObjects({ + isCopy: !isLazy, + isMutMakeCompatible: false + }, this.body, json); + this.head.mutMerge({ + json, + body: this.body, + isLazy + }); + return this; + } + + static mergeObjects({isCopy=true, isMutMakeCompatible=true}={}, ...jsons) { + const out = {}; + + jsons.forEach(json=>{ + json = isCopy ? MiscUtil.copyFast(json) : json; + + if (isMutMakeCompatible) + this._mergeObjects_mutMakeCompatible(json); + + Object.entries(json).forEach(([prop,val])=>{ + switch (prop) { + case "_meta": + return this._mergeObjects_key__meta({ + out, + prop, + val + }); + default: + return this._mergeObjects_default({ + out, + prop, + val + }); + } + } + ); + } + ); + + return out; + } + + static _META_KEYS_MERGEABLE_OBJECTS = ["skills", "senses", "spellSchools", "spellDistanceUnits", "optionalFeatureTypes", "psionicTypes", "currencyConversions", ]; + + static _META_KEYS_MERGEABLE_SPECIAL = { + "dateAdded": (a,b)=>a != null && b != null ? Math.min(a, b) : a ?? b, + "dateLastModified": (a,b)=>a != null && b != null ? Math.max(a, b) : a ?? b, + + "dependencies": (a,b)=>this._metaMerge_dependenciesIncludes(a, b), + "includes": (a,b)=>this._metaMerge_dependenciesIncludes(a, b), + "internalCopies": (a,b)=>[...(a || []), ...(b || [])].unique(), + + "otherSources": (a,b)=>this._metaMerge_otherSources(a, b), + + "status": (a,b)=>this._metaMerge_status(a, b), + }; + + static _metaMerge_dependenciesIncludes(a, b) { + if (a != null && b != null) { + Object.entries(b).forEach(([prop,arr])=>a[prop] = [...(a[prop] || []), ...arr].unique()); + return a; + } + + return a ?? b; + } + + static _metaMerge_otherSources(a, b) { + if (a != null && b != null) { + Object.entries(b).forEach(([prop,obj])=>a[prop] = Object.assign(a[prop] || {}, obj)); + return a; + } + + return a ?? b; + } + + static _META_MERGE__STATUS_PRECEDENCE = ["invalid", "deprecated", "wip", "ready", ]; + + static _metaMerge_status(a, b) { + return [a || "ready", b || "ready"].sort((a,b)=>this._META_MERGE__STATUS_PRECEDENCE.indexOf(a) - this._META_MERGE__STATUS_PRECEDENCE.indexOf(b))[0]; + } + + static _mergeObjects_key__meta({out, val}) { + out._meta = out._meta || {}; + + out._meta.sources = [...(out._meta.sources || []), ...(val.sources || [])]; + + Object.entries(val).forEach(([metaProp,metaVal])=>{ + if (this._META_KEYS_MERGEABLE_SPECIAL[metaProp]) { + out._meta[metaProp] = this._META_KEYS_MERGEABLE_SPECIAL[metaProp](out._meta[metaProp], metaVal); + return; + } + if (!this._META_KEYS_MERGEABLE_OBJECTS.includes(metaProp)) + return; + Object.assign(out._meta[metaProp] = out._meta[metaProp] || {}, metaVal); + } + ); + } + + static _mergeObjects_default({out, prop, val}) { + if (!(val instanceof Array)) + return out[prop] === undefined ? out[prop] = val : null; + + out[prop] = [...out[prop] || [], ...val]; + } + + static _mergeObjects_mutMakeCompatible(json) { + if (json.variant) { + json.magicvariant = json.variant; + delete json.variant; + } + + if (json.subrace) { + json.subrace.forEach(sr=>{ + if (!sr.race) + return; + sr.raceName = sr.race.name; + sr.raceSource = sr.race.source || sr.source || Parser.SRC_PHB; + } + ); + } + + if (json.monster) { + json.monster.forEach(mon=>{ + if (typeof mon.size === "string") + mon.size = [mon.size]; + + if (mon.summonedBySpell && !mon.summonedBySpellLevel) + mon.summonedBySpellLevel = 1; + } + ); + } + + if (json.object) { + json.object.forEach(obj=>{ + if (typeof obj.size === "string") + obj.size = [obj.size]; + } + ); + } + } +} + +class BrewDocHead { + constructor(opts) { + opts = opts || {}; + + this.docIdLocal = opts.docIdLocal; + this.timeAdded = opts.timeAdded; + this.checksum = opts.checksum; + this.url = opts.url; + this.filename = opts.filename; + this.isLocal = opts.isLocal; + this.isEditable = opts.isEditable; + } + + toObject() { + return MiscUtil.copyFast({ + ...this + }); + } + + static fromValues({json, url=null, filename=null, isLocal=false, isEditable=false, }, ) { + return new this({ + docIdLocal: CryptUtil.uid(), + timeAdded: Date.now(), + checksum: CryptUtil.md5(JSON.stringify(json)), + url: url, + filename: filename, + isLocal: isLocal, + isEditable: isEditable, + }); + } + + static fromObject(obj, {isCopy=false}={}) { + return new this(isCopy ? MiscUtil.copyFast(obj) : obj); + } + + mutUpdate({json}) { + this.checksum = CryptUtil.md5(JSON.stringify(json)); + return this; + } + + mutMerge({json, body, isLazy}) { + if (!isLazy) + this.checksum = CryptUtil.md5(JSON.stringify(body ?? json)); + return this; + } +} + +class BrewUtilShared { + static getValidColor(color, {isExtended=false}={}) { + if (isExtended) + return color.replace(/[^-a-zA-Z\d]/g, ""); + return color.replace(/[^a-fA-F\d]/g, "").slice(0, 8); + } +} \ No newline at end of file diff --git a/charbuilder/js/plutonium/charactermancer.js b/charbuilder/js/plutonium/charactermancer.js new file mode 100644 index 0000000..2efc888 --- /dev/null +++ b/charbuilder/js/plutonium/charactermancer.js @@ -0,0 +1,20228 @@ +//#region Base Components +class ActorCharactermancerBaseComponent extends BaseComponent { + get state() {return this._state; } + addHookBase(prop, hook) { + this._addHookBase(prop, hook); + } + proxyAssignSimple(hookProp, toObj, isOverwrite) { + return this._proxyAssignSimple(hookProp, toObj, isOverwrite); + } + /**Simply creates a property out of an index, to use for asking _state for information */ + static class_getProps(ix) { + return { + 'propPrefixClass': 'class_' + ix + '_', + 'propIxClass': "class_" + ix + "_ixClass", + 'propPrefixSubclass': "class_" + ix + "_subclass_", + 'propIxSubclass': "class_" + ix + "_subclass_ixSubclass", + 'propCntAsi': "class_" + ix + "_cntAsi", + 'propCurLevel': "class_" + ix + "_curLevel", + 'propTargetLevel': "class_" + ix + "_targetLevel" + }; + } + //This is static! Make sure to clear this once we switch viewed character + static deletedClassIndices = []; + + /** + * Returns true if a class with this index has been marked as deleted. + * @param {number} ix + * @returns {boolean} + */ + static class_isDeleted(ix){ + return this.deletedClassIndices.includes(ix); + } + static class_clearDeleted(){ + this.deletedClassIndices = [] + } + _shared_renderEntity_stgOtherProficiencies({ + $stg: element, + ent: entity, + propComp: propComp, + propProficiencies: propProf, + title: title, + CompClass: CompClass, + propPathActorExistingProficiencies: propPathActorExistingProficiencies, + fnGetExistingFvtt: fnGetExistingFvtt, + fnGetMappedProficiencies: fnGetMappedProficiencies + }) { + element.empty(); + this._parent.featureSourceTracker_.unregister(this[propComp]); + if (entity && entity[propProf]) { + element.showVe().append("
    " + title + "
    "); + const existingFvtt = fnGetExistingFvtt ? fnGetExistingFvtt(this._actor) : { + [propProf]: MiscUtil.get(this._actor, '_source', ...propPathActorExistingProficiencies) + }; + this[propComp] = new CompClass({ + 'featureSourceTracker': this._parent.featureSourceTracker_, + 'existing': CompClass.getExisting(existingFvtt), + 'existingFvtt': existingFvtt, + 'available': fnGetMappedProficiencies ? fnGetMappedProficiencies(entity[propProf], propProf) : entity[propProf] + }); + this[propComp].render(element); + } + else { element.hideVe(); this[propComp] = null; } + } + _shared_renderEntity_stgDiDrDvCi({ + $stg: $stg, + ent: ent, + propComp: propComp, + CompClass: compClass, + title: title, + propRaceData: propRaceData, + propTraits: propTraits + }) { + $stg.empty(); + if (ent && ent[propRaceData]) { + $stg.showVe().append("
    " + title + "
    "); + const actorRaceData = {[propRaceData]: MiscUtil.get(this._actor, "_source", "system", "traits", propTraits)}; + this[propComp] = new compClass({ + existing: compClass.getExisting(actorRaceData), + existingFvtt: actorRaceData, + available: ent[propRaceData] + }); + this[propComp].render($stg); + } + else { + $stg.hideVe(); + this[propComp] = null; + } + } + + /** + * @param {string} type + * @param {{name:string, source:string}} item + * @returns {any} + */ + matchItemToData(type, item){ + const handleStrangeResults = (matches) => { + if(matches.length > 1){console.error("Found more than 1 instance of ", (item.name + "_"+item.source).toLowerCase(), "in data ", type);} + else if(matches.length < 1){console.error("Could not find item", (item.name + "_"+item.source).toLowerCase(), "in data ", type);} + } + switch(type){ + case "background": { + const matches = this._data[type].filter((it) => it.name == item.name && it.source == item.source); + handleStrangeResults(matches); + return matches[0] || null; + } + default: { + console.error("Not implemented"); + return null; + } + } + } +} +//#endregion + +//#region Charactermancer Class +class ActorCharactermancerClass extends ActorCharactermancerBaseComponent { + _data; + _tabClass; + _actor; + + /** + * @param {{parent:CharacterBuilder}} parentInfo + * @returns {any} + */ + constructor(parentInfo) { + parentInfo = parentInfo || {}; + super(); + this._actor = parentInfo.actor; + this._data = parentInfo.data; //data is an object containing information about all classes, subclasses, feats, etc + this._parent = parentInfo.parent; + this._tabClass = parentInfo.tabClass; + //TEMPFIX + this._modalFilterClasses = new ModalFilterClasses({//ModalFilterClassesFvtt({ + 'namespace': "ActorCharactermancer.classes", 'allData': this._data.class + }); + this._metaHksClassStgSubclass = []; + this._compsClassStartingProficiencies = []; + this._compsClassHpIncreaseMode = []; + this._compsClassHpInfo = []; + this._compsClassLevelSelect = []; + this._compsClassFeatureOptionsSelect = []; + this._compsClassSkillProficiencies = []; + this._compsClassToolProficiencies = []; + this._metaHksClassStgSkills = []; + this._metaHksClassStgTools = []; + this._metaHksClassStgStartingProficiencies = []; + this._$wrpsClassTable = []; + this._existingClassMetas = []; + + } + async render() { + let wrptab = this._tabClass?.$wrpTab; + if (!wrptab) { return; } + let classChoiceElement = $(`
    `); + let sidebarElement = $(`
    `); + for (let i = 0; i < this._state.class_ixMax + 1; ++i) { + await this._class_renderClass(classChoiceElement, sidebarElement, i); + } + //Fire a pulse change whenever primary class is switched + this._addHookBase("class_ixPrimaryClass", () => this._state.class_pulseChange = !this._state.class_pulseChange); + + //ADD CLASS BUTTON + const addClassBtn = $(``) + .click(() => { + this._class_renderClass(classChoiceElement, sidebarElement, ++this._state.class_ixMax); + }); + + + let o = $$`
    +
    + ${classChoiceElement} +
    ${addClassBtn}
    +
    +
    + ${sidebarElement} +
    `.appendTo(wrptab); + } + async _class_renderClass(parentDiv_left, parentDiv_right, ix) { + //Main properties for asking our _state for information on this class + const { + propPrefixClass: propPrefixClass, + propIxClass: propIxClass, + propPrefixSubclass: propPrefixSubclass, + propIxSubclass: propIxSubclass, + propCntAsi: propCntAsi, + propCurLevel: propCurLevel, + propTargetLevel: propTargetLevel + } = ActorCharactermancerBaseComponent.class_getProps(ix); + const filter_evnt_valchange_class = FilterBox.EVNT_VALCHANGE + ".class_" + ix + "_classLevels"; + const filter_evnt_valchange_subclass = FilterBox.EVNT_VALCHANGE + ".class_" + ix + "_subclass"; + + const { + lockChangeClass: lockChangeClass, + lockChangeSubclass: lockChangeSubclass, + lockRenderFeatureOptionsSelects: lockRenderFeatureOptionsSelects + } = this.constructor._class_getLocks(ix); + + this._addHookBase(propIxClass, () => this._state.class_pulseChange = !this._state.class_pulseChange); + //TEMPFIX, add a hook for when subclass is changed. This is so sheets can sense when we change subclass + this._addHookBase(propIxSubclass, () => this._state.class_pulseChange = !this._state.class_pulseChange); + + //Create a searchable select field for choosing a class + const { + $wrp: wrapper, //Wrapper DOM for the dropdown menu DOM object + $iptDisplay: inputDisplay, //a function that returns the visible name of a class that you provide the index for + $iptSearch: inputSearch, + fnUpdateHidden: fnUpdateHidden + } = ComponentUiUtil.$getSelSearchable(this, propIxClass, { + values: this._data.class.map((key, val) => val), //Think this is just the ix's of the classes + isAllowNull: true, + fnDisplay: clsIx => { + //Using a simple index, ask _data for the class + const cls = this.getClass_({'ix': clsIx }); + if (!cls) { + console.warn(...LGT, "Could not find class with index " + clsIx + " (" + this._data.class.length + " classes were available)"); + return '(Unknown)'; + } + //Then return what should be the displayed name + return cls.name + " " + (cls.source !== Parser.SRC_PHB ? '[' + Parser.sourceJsonToAbv(cls.source) + ']' : ''); + }, + fnGetAdditionalStyleClasses: classIx => { + if (classIx == null) { return null; } + const cls = this.getClass_({ix: classIx}); + if (!cls) { return; } + return cls._versionBase_isVersion ? ['italic'] : null; + }, + asMeta: true, + isDisabled: this._class_isClassSelectionDisabled({ix: ix }) + }); + + inputDisplay.addClass("bl-0"); + inputSearch.addClass("bl-0"); + + const updateHiddenClasses = () => { + const filterValues = this._modalFilterClasses.pageFilter.filterBox.getValues(); + const classes = this._data.class.map(cls => !this._modalFilterClasses.pageFilter.toDisplay(filterValues, cls)); + fnUpdateHidden(classes, false); + }; + + /** + * Apply the filter on which subclasses can be picked from the dropdown menu + */ + const applySubclassFilter = () => { + const cls = this.getClass_({propIxClass: propIxClass}); + if (!cls || !this._metaHksClassStgSubclass[ix]) { return; } + const filteredValues = this._modalFilterClasses.pageFilter.filterBox.getValues(); + const displayableSubclasses = cls.subclasses.map(val => !this._modalFilterClasses.pageFilter.toDisplay(filteredValues, val)); + this._metaHksClassStgSubclass[ix].fnUpdateHidden(displayableSubclasses, false); + }; + + if(SETTINGS.FILTERS){ + this._modalFilterClasses.pageFilter.filterBox.on(FilterBox.EVNT_VALCHANGE, () => updateHiddenClasses()); + updateHiddenClasses(); + } + //Filter button and what happens when we click it + const filterBtn = $("") + .click(async () => { + const cls = this.getClass_({'propIxClass': propIxClass }); + const subcls = this.getSubclass_({'cls': cls, 'propIxSubclass': propIxSubclass}); + const classSelectDisabled = this._class_isClassSelectionDisabled({ 'ix': ix }); + const subclassSelectDisabled = this._class_isSubclassSelectionDisabled({ 'ix': ix }); + const userSelection = await this._modalFilterClasses.pGetUserSelection({ + 'selectedClass': cls, + 'selectedSubclass': subcls, + 'isClassDisabled': classSelectDisabled, + 'isSubclassDisabled': subclassSelectDisabled + }); + if (classSelectDisabled && subclassSelectDisabled) { + return; + } + if (userSelection == null || !userSelection.class) { + return; + } + const class_index = this._data.class.findIndex(ix => ix.name === userSelection.class.name && ix.source === userSelection['class'].source); + if (!~class_index) { throw new Error("Could not find selected class: " + JSON.stringify(userSelection["class"])); } + this._state[propIxClass] = class_index; + await this._pGate(lockChangeClass); + + if (userSelection.subclass != null) { + const cls = this.getClass_({ + 'propIxClass': propIxClass + }); + const subcls_index = cls.subclasses.findIndex(ix => ix.name === userSelection.subclass.name && ix.source === userSelection.subclass.source); + if (!~subcls_index) { throw new Error("Could not find selected subclass: " + JSON.stringify(userSelection.subclass)); } + this._state[propIxSubclass] = subcls_index; + } + else { this._state[propIxSubclass] = null; } + }); + + //#region Render Class + const renderClassComponents = async doClearState => { + if(SETTINGS.FILTERS){this._modalFilterClasses.pageFilter.filterBox.off(filter_evnt_valchange_subclass);} + //FIXME SET STATE! + if (doClearState) { + const toObj = Object.keys(this.__state).filter(propName => + propName.startsWith(propPrefixClass) && propName !== propIxClass).mergeMap(p => ({ + [p]: null + })); + this._proxyAssignSimple("state", toObj); + } + //First time this function is called, we will probably not get anything out of getClass since we haven't set anything to _state yet + const cls = this.getClass_({'propIxClass': propIxClass}); + const subcls = this.getSubclass_({ 'cls': cls, 'propIxSubclass': propIxSubclass }); + + //Render the dropdown for choosing a subclass + this._class_renderClass_stgSelectSubclass({ + '$stgSelectSubclass': holder_selectSubclass, + 'cls': cls, + 'ix': ix, + 'propIxSubclass': propIxSubclass, + 'idFilterBoxChangeSubclass': filter_evnt_valchange_subclass, + 'doApplyFilterToSelSubclass': applySubclassFilter + }); + this._class_renderClass_stgHpMode({ + '$stgHpMode': holder_hpMode, + 'ix': ix, + 'cls': cls + }); + this._class_renderClass_stgHpInfo({ + '$stgHpInfo': holder_hpInfo, + 'ix': ix, + 'cls': cls + }); + //Element that shows which proficiencies we always start with (usually weapons and armor) + this._class_renderClass_stgStartingProficiencies({ + $stgStartingProficiencies: holder_startingProf, + ix: ix, + cls: cls + }); + + //Now create the level select UI + await this._class_renderClass_pStgLevelSelect({ + '$stgLevelSelect': holder_levelSelect, + '$stgFeatureOptions': holder_featureOptions, + 'ix': ix, + 'cls': cls, + 'sc': subcls, + 'propIxSubclass': propIxSubclass, + 'propCurLevel': propCurLevel, + 'propTargetLevel': propTargetLevel, + 'propCntAsi': propCntAsi, + 'lockRenderFeatureOptionsSelects': lockRenderFeatureOptionsSelects, + 'idFilterBoxChangeClassLevels': filter_evnt_valchange_class + }); + this._state.class_totalLevels = this.class_getTotalLevels(); + + //Create the element that lets us choose skill proficiencies + this._class_renderClass_stgSkills({ '$stgSkills': holder_skills, 'ix': ix, 'propIxClass': propIxClass }); + + //Create the element that lets us choose tool proficiencies + this._class_renderClass_stgTools({ '$stgTools': holder_tools, 'ix': ix, 'propIxClass': propIxClass }) + + //Create the element that handles drawing info about our class + await this._class_renderClass_pDispClass({ + 'ix': ix, + '$dispClass': holder_dispClass, + 'cls': cls, + 'sc': subcls + }); + //Also clear the element that displays info about our subclass + disp_subclass.empty(); + }; + + const renderClassComponents_safe = async doClearState => { + try { + await this._pLock(lockChangeClass); + await renderClassComponents(doClearState); + } + finally { this._unlock(lockChangeClass); } + }; + + //Add a hook so that when propIxClass changes, we try to render the class components again + this._addHookBase(propIxClass, renderClassComponents_safe); + //#endregion + + //#region Render Subclass + const renderSubclassComponents = async () => { + if(SETTINGS.FILTERS){this._modalFilterClasses.pageFilter.filterBox.off(filter_evnt_valchange_subclass);} + const toObj = Object.keys(this.__state).filter(prop => prop.startsWith(propPrefixSubclass) && prop !== propIxSubclass).mergeMap(key => ({ + [key]: null + })); + this._proxyAssignSimple("state", toObj); + const cls = this.getClass_({ propIxClass: propIxClass }); + const subcls = this.getSubclass_({ cls: cls, propIxSubclass: propIxSubclass }); + const filteredFeatures = this._class_getFilteredFeatures(cls, subcls); + if (this._compsClassLevelSelect[ix]) { this._compsClassLevelSelect[ix].setFeatures(filteredFeatures); } + + //Re-render feature options select, since we changed subclass + await this._class_pRenderFeatureOptionsSelects({ + ix: ix, + propCntAsi: propCntAsi, + filteredFeatures: filteredFeatures, + $stgFeatureOptions: holder_featureOptions, + lockRenderFeatureOptionsSelects: lockRenderFeatureOptionsSelects + }); + if(SETTINGS.FILTERS){this._modalFilterClasses.pageFilter.filterBox.on(filter_evnt_valchange_subclass, () => applySubclassFilter());} + applySubclassFilter(); + //Render the text for the subclass on the right-side panel + await this._class_renderClass_pDispSubclass({ + ix: ix, + $dispSubclass: disp_subclass, + cls: cls, + sc: subcls + }); + }; + const renderSubclass_safe = async () => { + try { + await this._pLock(lockChangeClass); //Use the same lock as for change class, otherwise they can run on top of eachother and cause chaos + await renderSubclassComponents(); + } + finally { + await this._unlock(lockChangeClass); + } + }; + this._addHookBase(propIxSubclass, renderSubclass_safe); + //#endregion + + //Create parent objects to hold subcomponents, hide the later ones + const header = $("
    Select a Class
    "); + const holder_selectSubclass = $(`
    `).hideVe(); + const holder_hpMode = $(`
    `).hideVe(); + const holder_hpInfo = $(`
    `).hideVe(); + const holder_startingProf = $(`
    `).hideVe(); + const holder_levelSelect = $(`
    `).hideVe(); + const holder_featureOptions = $(`
    `).hideVe(); + const holder_skills = $(`
    `).hideVe(); + const holder_tools = $(`
    `).hideVe(); + + let primaryBtn = null; + let removeClassBtn = null; + if (!this._existingClassMetas.length || !SETTINGS.LOCK_EXISTING_CHOICES) { + primaryBtn = $("").click(() => this._state.class_ixPrimaryClass = ix); + const primaryBtnHook = () => { + primaryBtn.text(this._state.class_ixPrimaryClass === ix ? "Primary Class" : "Make Primary") + .title(this._state.class_ixPrimaryClass === ix ? "This is your primary class, i.e. the one you chose at level 1 for the purposes of proficiencies/etc." + : "Make this your primary class, i.e. the one you chose at level 1 for the purposes of proficiencies/etc.") + .prop("disabled", this._state.class_ixPrimaryClass === ix); + }; + this._addHookBase("class_ixPrimaryClass", primaryBtnHook); + primaryBtnHook(); + + removeClassBtn = $("").click(() => {console.log("Remove class " + ix); + + this.wipeClassState(ix); + //Honestly, we might just have to re-render all the class components + //If we delete class of index 1, that means class of inded 2 should become 1, and that breaks so many hooks + //A better approach could just be to mark index 1 as unused. When the character is serialized, THEN we rearrange indices + classChoicePanelsWrapper.remove(); + }); + const removeClassBtnHook = () => { + removeClassBtn.text("Remove Class") + .title(this._state.class_ixPrimaryClass === ix ? "Cannot remove your primary class." + : "Remove this class and all features and abilities gained from it.") + .prop("disabled", this._state.class_ixPrimaryClass === ix); + }; + this._addHookBase("class_ixPrimaryClass", removeClassBtnHook); + removeClassBtnHook(); + } + + + + const minimizerToggle = $("
    [โ€’]
    ").click(() => { + const isMinimized = minimizerToggle.text() === '[+]'; + minimizerToggle.text(isMinimized ? "[โ€’]" : "[+]"); + if (isMinimized) { + header.text("Select a Class"); + } else { + const cls = this.getClass_({'propIxClass': propIxClass}); + const subcls = this.getSubclass_({ + 'cls': cls, + 'propIxSubclass': propIxSubclass + }); + if (cls) { header.text('' + cls.name + (subcls ? " (" + subcls.name + ')' : ''));} + else { header.text("Select a Class"); } + } + classChoicePanels.toggleVe(); + }); + + const holder_dispClass = $(`
    `); + const disp_subclass = $(`
    `); + + const classChoicePanels = $$`
    +
    +
    + ${filterBtn} +
    +
    + ${wrapper} + ${holder_selectSubclass} +
    +
    + ${holder_hpMode} + ${holder_hpInfo} + ${holder_startingProf} + ${holder_skills} + ${holder_tools} + ${holder_levelSelect} + ${holder_featureOptions} +
    `; + + const classChoicePanelsWrapper = $$`
    + ${ix>0? `
    `:''} +
    + ${header} +
    + ${removeClassBtn} + ${primaryBtn} + ${minimizerToggle} +
    +
    + + ${classChoicePanels} +
    `; + classChoicePanelsWrapper.appendTo(parentDiv_left); + + //Sidebar display (class text info) + const sidebarContent = $$`
    + ${ix>0?'':''} + + ${holder_dispClass} + ${disp_subclass} +
    `.appendTo(parentDiv_right); + + const onHookPulse = () => { + //If class is marked as deleted, then hide the ui + if(ActorCharactermancerBaseComponent.class_isDeleted(ix)){ + sidebarContent.hideVe(); + } + } + this._parent.compClass.addHookBase("class_pulseChange", onHookPulse); + + //TEMPFIX + //renderClassComponents_safe().then(() => renderSubclass_safe()).then(() => this._test_tryLoadFOS(ix)); + await renderClassComponents_safe(); + await renderSubclass_safe(); + } + + get modalFilterClasses() { + return this._modalFilterClasses; + } + get compsClassStartingProficiencies() { + return this._compsClassStartingProficiencies; + } + get compsClassHpIncreaseMode() { + return this._compsClassHpIncreaseMode; + } + get compsClassLevelSelect() { + return this._compsClassLevelSelect; + } + /** + * @returns {Charactermancer_FeatureOptionsSelect[][]} + */ + get compsClassFeatureOptionsSelect() { + return this._compsClassFeatureOptionsSelect; + } + + /** + * @returns {Charactermancer_OtherProficiencySelect[]} + */ + get compsClassSkillProficiencies() { + return this._compsClassSkillProficiencies; + } + get compsClassToolProficiencies() { + return this._compsClassToolProficiencies; + } + get existingClassMetas() { + return this._existingClassMetas; + } + /**Load some information prior to first rendering. Just to do with loading from the modal and loading existing data from actor */ + async pLoad() { + await this._modalFilterClasses.pPreloadHidden(); + if(this._actor){await this._doHandleExistingClassItems(this._actor.classes);} + } + async setStateFromSaveFile(actor){ + //Some of this loading logic has been moved to pLoad, which runs right before render + for (let ix = 0; ix < this._state.class_ixMax + 1; ++ix) { + await this.loadFeatureOptionsSelectFromState(ix); //needs to be async so FOS components have time to create their subcpomponents + } + } + /** + * Description + * @param {{name:string, source:string, hash:string, isPrimary:boolean, subclass:{name:string, source:string, hash:string}}[]} classes + * @returns {any} + */ + async _doHandleExistingClassItems(classes){ + + if(!classes){return;} + //Collect metas + this._existingClassMetas = classes.map(cls => { + const _clsIx = this._test_getExistingClassIndex(cls); + let _scIx = this.test_getExistingSubclassIndex(_clsIx, cls.subclass); + const isPrimaryClass = cls.isPrimary || false; + + const failMatchCls = ~_clsIx ? null : "Could not find class \"" + cls.name + "\" (\"" + + UtilDocumentSource.getDocumentSourceDisplayString(cls) + "\") in loaded data. " + Charactermancer_Util.STR_WARN_SOURCE_SELECTION; + if (failMatchCls) { + //ui.notifications.warn(failMatchCls); + console.warn(...LGT, failMatchCls, "Strict source matching is: " + Config.get("import", "isStrictMatching") + '.'); + } + const failMatchSc = ~_scIx ? null : "Could not find subclass \"" + cls.subclass.name + "\" in loaded data. " + Charactermancer_Util.STR_WARN_SOURCE_SELECTION; + if (failMatchSc) { + //ui.notifications.warn(failMatchSc); + console.warn(...LGT, failMatchSc, "Strict source matching is: " + Config.get("import", "isStrictMatching") + '.'); + } + + //Should be (class level - 1) or 0, whichever is higher + const classLevel = Math.max((cls.level||0) - 1, 0); //Keep in mind that our save file schema currently stores levels with base 1, whereas in this program we could 0 as lvl 1 + //Create one ExistingClassMeta per class + return new ActorCharactermancerClass.ExistingClassMeta({ + item: cls, //Class data object + ixClass: _clsIx, //index of this class + isUnknownClass: !~_clsIx, + ixSubclass: _scIx, //index of the subclass + isUnknownSubclass: _scIx == null && !~_scIx, + level: classLevel, //level + isPrimary: isPrimaryClass, //is this the primary class? + //TEMPFIX 'spellSlotLevelSelection': cls?.flags?.[SharedConsts.MODULE_ID]?.['spellSlotLevelSelection'] + }); + }); + + if (!this._existingClassMetas.length) { return; } + + //Write to state + this._state.class_ixMax = this._existingClassMetas.length - 1; + for (let i = 0; i < this._existingClassMetas.length; ++i) { + const meta = this._existingClassMetas[i]; + const { propIxClass: propIxClass, propIxSubclass: propIxSubclass } = ActorCharactermancerBaseComponent.class_getProps(i); + await this._pDoProxySetBase(propIxClass, meta.ixClass); + await this._pDoProxySetBase(propIxSubclass, meta.ixSubclass); + if (meta.isPrimary) { this._state.class_ixPrimaryClass = i; } + } + //Set first class as primary, if none is marked already + if (!this._existingClassMetas.some(meta => meta.isPrimary)) { this._state.class_ixPrimaryClass = 0; } + + //Prepare some data for FeatureOptionsSelect components to load into their state (only once) after first render + if(SETTINGS.USE_EXISTING_WEB){ + this.loadedSaveData_FeatureOptSel = []; + for(let classIndex = 0; classIndex < classes.length; ++classIndex){ + const cls = classes[classIndex]; + if(!cls.featureOptSel){ this.loadedSaveData_FeatureOptSel.push(null); continue;} + this.loadedSaveData_FeatureOptSel.push(cls.featureOptSel); + } + } + } + async pLoadLate(){ + + } + _test_getExistingClassIndex(cls){ + if (cls.source && cls.hash) { + const ix = this._data.class.findIndex(ourDataClass => cls.source === ourDataClass.source + && cls.hash === UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_CLASSES](ourDataClass)); + if (~ix) { return ix; } + } + const _classNameLower = cls.name.toLowerCase().trim(); //(IntegrationBabele.getOriginalName(cls) || '').toLowerCase().trim(); + + //FALLBACK 1 + /* const clsIndex = this._data.class.findIndex(ourDataClass => { + return _classNameLower === ourDataClass.name.toLowerCase().trim() && + (!Config.get("import", "isStrictMatching") || + (UtilDocumentSource.getDocumentSource(cls).source || '').toLowerCase() === Parser.sourceJsonToAbv(ourDataClass.source).toLowerCase()); + }); + if (~clsIndex) { return clsIndex; } */ + + return this._data.class.findIndex(c => c.name.toLowerCase().trim() === cls.name.toLowerCase().trim()); + + return false; + + //FALLBACK 2 + /* const filteredName = /^(.*?)\(.*\)$/.exec(_classNameLower); + if (!filteredName) { return -1; } + return this._data.class.findIndex(_cls => { + return filteredName[1].trim() === _cls.name.toLowerCase().trim() + && (!Config.get("import", "isStrictMatching") || + (UtilDocumentSource.getDocumentSource(cls).source || '').toLowerCase() === Parser.sourceJsonToAbv(_cls.source).toLowerCase()); + }); */ + } + test_getExistingSubclassIndex(classIx, subclass) { + if (!subclass || !~classIx) { return null; } + const ourDataClass = this._data.class[classIx]; //Grab our class from data + if (subclass.source && subclass.hash) { + const subclassIx = ourDataClass.subclasses.findIndex(ourDataSubclass => subclass.source === ourDataSubclass.source + && subclass.hash === UrlUtil.URL_TO_HASH_BUILDER.subclass(ourDataSubclass)); + if (~subclassIx) { return subclassIx; } + } + //FALLBACK + /* return ourDataClass.subclasses.findIndex(sc => + (IntegrationBabele.getOriginalName(subclass) || '').toLowerCase().trim() + === sc.name.toLowerCase().trim() && (!Config.get("import", 'isStrictMatching') + || (UtilDocumentSource.getDocumentSource(subclass).source || '').toLowerCase() + === Parser.sourceJsonToAbv(sc.source).toLowerCase())); */ + + return ourDataClass.subclasses.findIndex(sc => sc.name.toLowerCase().trim() === subclass.name.toLowerCase().trim()); + } + //#endregion + getExistingClassTotalLevels_() { + if(!this.existingClassMetas?.length){return 0;} + return this._existingClassMetas.filter(Boolean).map(cls => cls.level).sum(); + } + _getExistingClassCount() { + return this._existingClassMetas.filter(Boolean).length; + } + /** + * @param {number} ix used to get a class directly from cached memory (this._data[ix]). + * @param {string} propIxClass Used to get the class that propIxClass points to + * @returns {any} Returns a full class object, or a stub of one if all else fails + */ + getClass_({ix: ix, propIxClass: propIxClass}) { + if (ix == null && propIxClass == null) { throw new Error("At least one argument must be provided!"); } + //If a propIxClass was provived, try to get the class from this._state + if (propIxClass != null) { + if (this._state[propIxClass] == null) { return null; } + if (!~this._state[propIxClass]) { return DataConverterClass.getClassStub(); } + return this._data.class[this._state[propIxClass]]; + } + //Otherwise, try to get it from this._data, if we have an ix + if (ix != null && ~ix) { return this._data.class[ix];} + return DataConverterClass.getClassStub(); + } + /** + * @param {any} cls A class object + * @param {string} propIxSubclass + * @param {number} ix optional, only used if propIxSubclass is not provided + * @returns {any} Returns a full subclass object, or a stub of one if all else fails + */ + getSubclass_({ cls: cls, propIxSubclass: propIxSubclass, ix: ix }) + { + if (ix == null && propIxSubclass == null) { + throw new Error("At least one argument must be provided!"); + } + if (!cls) { return null; } //We need to have the class object to continue + if (propIxSubclass != null) { + if (this._state[propIxSubclass] == null) { return null; } + //If state doesnt have an entry for this prop, just return a stub + if (!~this._state[propIxSubclass]) { + return DataConverterClass.getSubclassStub({ 'cls': cls }); + } + //If the class doesnt seem to have any subclasses to choose from, just return a stub + if (!cls.subclasses?.length) { + return DataConverterClass.getSubclassStub({ 'cls': cls }); + } + //Now we use state to find the subclass from the class object + return cls.subclasses[this._state[propIxSubclass]]; + } + if (ix != null && ~ix) { return cls.subclasses[ix]; } + return DataConverterClass.getSubclassStub({ 'cls': cls }); + } + _class_isClassSelectionDisabled({ ix: ix }) { + //TEMPFIX + return SETTINGS.LOCK_EXISTING_CHOICES && !!this._existingClassMetas[ix]; + } + _class_isSubclassSelectionDisabled({ ix: ix }) { + //TEMPFIX + return SETTINGS.LOCK_EXISTING_CHOICES && this._existingClassMetas[ix] + && (this._existingClassMetas[ix].ixSubclass != null || this._existingClassMetas[ix].isUnknownClass); + } + + static _class_getLocks(ix) { + return { + 'lockChangeClass': 'class_' + ix + '_pHkChangeClass', + 'lockChangeSubclass': "class_" + ix + '_pHkChangeSubclass', + 'lockRenderFeatureOptionsSelects': 'class_' + ix + "_renderFeatureOptionsSelects" + }; + } + + _class_renderClass_stgSelectSubclass({ + $stgSelectSubclass: stgSelectSubclass, + cls: cls, + ix: ix, + propIxSubclass: propIxSubclass, + idFilterBoxChangeSubclass: idFilterBoxChangeSubclass, + doApplyFilterToSelSubclass: doApplyFilterToSelSubclass + }) { + stgSelectSubclass.empty(); + if (this._metaHksClassStgSubclass[ix]) { this._metaHksClassStgSubclass[ix].unhook(); } + //We will be looking att cls.subclasses to see which subclasses we have to choose from + if (cls && cls.subclasses && cls.subclasses.length) { + const uiSearchElement = ComponentUiUtil.$getSelSearchable(this, propIxSubclass, { + 'values': cls.subclasses.map((a, b) => b), + 'isAllowNull': true, + 'fnDisplay': ix => { + const subcls = this.getSubclass_({'cls': cls, 'ix': ix }); + if (!subcls) { + console.warn(...LGT, "Could not find subclass with index " + ix + " (" + cls.subclasses.length + " subclasses were available for class " + cls.name + ')'); + return '(Unknown)'; + } + return subcls.name + " " + (subcls.source !== Parser.SRC_PHB ? '[' + Parser.sourceJsonToAbv(subcls.source) + ']' : ''); + }, + 'fnGetAdditionalStyleClasses': ix => { + if (ix == null) { return null; } + const subcls = this.getSubclass_({'cls': cls, 'ix': ix }); + if (!subcls) { return; } + return subcls._versionBase_isVersion ? ['italic'] : null; + }, + 'asMeta': true, + 'isDisabled': this._class_isSubclassSelectionDisabled({'ix': ix}), + 'displayNullAs': "Select a Subclass" + }); + uiSearchElement.$iptDisplay.addClass('bl-0'); + uiSearchElement.$iptSearch.addClass("bl-0"); + this._metaHksClassStgSubclass[ix] = uiSearchElement; + this._modalFilterClasses.pageFilter.filterBox.on(idFilterBoxChangeSubclass, () => doApplyFilterToSelSubclass()); + doApplyFilterToSelSubclass(); + const wrp = $$`
    ${uiSearchElement.$wrp}
    `; + stgSelectSubclass.showVe().append(wrp); + } + else { + if(cls){ + console.error("No subclasses found for class " + cls.name + ". Is the class.subclasses property not set?", cls); + } + + stgSelectSubclass.hideVe(); + this._metaHksClassStgSubclass[ix] = null; + } + } + + //#region Health + /**Create the display for the choice of HP gain mode for our class (average, roll, etc) */ + _class_renderClass_stgHpMode({ + $stgHpMode: parentElement, + ix: ix, + cls: cls + }) { + parentElement.empty(); + if (cls && Charactermancer_Class_HpIncreaseModeSelect.isHpAvailable(cls)) { + parentElement.showVe().append("
    Hit Points Increase Mode
    "); + this._compsClassHpIncreaseMode[ix] = new Charactermancer_Class_HpIncreaseModeSelect(); + this._compsClassHpIncreaseMode[ix].render(parentElement); + } + else { + parentElement.hideVe(); + this._compsClassHpIncreaseMode[ix] = null; + } + } + /**Create the display for how our health will look as we level up */ + _class_renderClass_stgHpInfo({ + $stgHpInfo: parentElement, + ix: ix, + cls: cls + }) { + parentElement.empty(); + if (cls && Charactermancer_Class_HpIncreaseModeSelect.isHpAvailable(cls)) { + parentElement.showVe().append("
    Hit Points
    "); + this._compsClassHpInfo[ix] = new Charactermancer_Class_HpInfo({ + 'className': cls.name, + 'hitDice': cls.hd + }); + this._compsClassHpInfo[ix].render(parentElement); + } + else { + parentElement.hideVe(); + this._compsClassHpInfo[ix] = null; + } + } + //#endregion + + //#region Level Select + /**Render a LevelSelect element */ + async _class_renderClass_pStgLevelSelect({ + $stgLevelSelect: ele_levelSelect, + $stgFeatureOptions: ele_featureOptions, + ix: ix, + cls: cls, + sc: sc, + propIxSubclass: propIxSubclass, + propCurLevel: propCurLevel, + propTargetLevel: propTargetLevel, + propCntAsi: propCntAsi, + lockRenderFeatureOptionsSelects: lockRenderFeatureOptionsSelects, + idFilterBoxChangeClassLevels: idFilterBoxChangeClassLevels + }) { + ele_levelSelect.empty(); + if (cls) { + ele_levelSelect.showVe().append("
    Select Levels
    "); + const filteredFeatures = this._class_getFilteredFeatures(cls, sc); + + //Load existingClassMeta, or create a new one if needed + //TEMPFIX + const existingClassMeta = (SETTINGS.USE_EXISTING || SETTINGS.USE_EXISTING_WEB)? this._class_getExistingClassMeta(ix) : null; + //Any level <= this will be forcefully locked in, and we cannot choose them as options Default is 0 + const maxPrevLevel = existingClassMeta?.level || 0; + //Are we going to forcefully select a level? + //Default is true + //TODO: improve this, depending on if we have existingClassMeta and if SETTINGS.LOCK_EXISTING_CHOICES is true + const isForceSelect = true; //!existingClassMeta || (this.getExistingClassTotalLevels_() === 0 && SETTINGS.LOCK_EXISTING_CHOICES); + //Create a level select UI component + this._compsClassLevelSelect[ix] = new Charactermancer_Class_LevelSelect({ + features: filteredFeatures, + isRadio: true, + isForceSelect: isForceSelect, + maxPreviousLevel: maxPrevLevel, + isSubclass: true + }); + //Render it + this._compsClassLevelSelect[ix].render(ele_levelSelect); + + //Create a hook for re-rendering it again if needed + const e_onChangeLevelSelected = async () => { + const subclass = this.getSubclass_({cls: cls, propIxSubclass: propIxSubclass}); + const _features = this._class_getFilteredFeatures(cls, subclass); + //_features should have loadeds, an each in loadeds should have entity + //some of these entity should have an entryData, but this is only for specific choice class features + + //Re-render the Feature Options Selects, since we changed level + await this._class_pRenderFeatureOptionsSelects({ + ix: ix, + propCntAsi: propCntAsi, + filteredFeatures: _features, + $stgFeatureOptions: ele_featureOptions, + lockRenderFeatureOptionsSelects: lockRenderFeatureOptionsSelects + }); + this._state[propCurLevel] = this._compsClassLevelSelect[ix].getCurLevel(); + this._state[propTargetLevel] = this._compsClassLevelSelect[ix].getTargetLevel(); + this._state.class_totalLevels = this.class_getTotalLevels(); + }; + + this._compsClassLevelSelect[ix].onchange(e_onChangeLevelSelected); + await e_onChangeLevelSelected(); + + + + if(SETTINGS.FILTERS){ //TEMPFIX + this._modalFilterClasses.pageFilter.filterBox.on(idFilterBoxChangeClassLevels, () => { + if (!this._compsClassLevelSelect[ix]) { return; } + const subclass = this.getSubclass_({cls: cls, propIxSubclass: propIxSubclass}); + const filteredFeatures = this._class_getFilteredFeatures(cls, subclass); + if (this._compsClassLevelSelect[ix]) { + this._compsClassLevelSelect[ix].setFeatures(filteredFeatures); + } + this._class_pRenderFeatureOptionsSelects({ + ix: ix, + propCntAsi: propCntAsi, + filteredFeatures: filteredFeatures, + $stgFeatureOptions: ele_featureOptions, + lockRenderFeatureOptionsSelects: lockRenderFeatureOptionsSelects + }); + + }); + } + } + else { + ele_levelSelect.hideVe(); + this._compsClassLevelSelect[ix] = null; + ele_featureOptions.empty().hideVe(); + this._class_unregisterFeatureSourceTrackingFeatureComps(ix); + this._state[propCntAsi] = null; + if(SETTINGS.FILTERS){this._modalFilterClasses.pageFilter.filterBox.off(idFilterBoxChangeClassLevels);} + } + } + //#endregion + + //#region Skill and Tool proficiencies + /**Create an element to display skill proficiency choices that our class gives us*/ + _class_renderClass_stgSkills({ $stgSkills: parentElement, ix: ix, propIxClass: propIxClass }) { + this._class_renderClass_stgSkillsTools({ + '$stg': parentElement, + 'ix': ix, + 'propIxClass': propIxClass, + 'propMetaHks': "_metaHksClassStgSkills", + 'propCompsClass': '_compsClassSkillProficiencies', + 'propSystem': 'skills', + 'fnGetProfs': ({ cls: cls, isPrimaryClass: isPrimaryClass }) => { + if (!cls) { return null; } + return isPrimaryClass ? cls.startingProficiencies?.skills : cls.multiclassing?.proficienciesGained?.skills; + }, + 'headerText': "Skill Proficiencies", + 'fnGetMapped': Charactermancer_OtherProficiencySelect.getMappedSkillProficiencies.bind(Charactermancer_OtherProficiencySelect) + }); + } + /**Create an element to display tool proficiency choices that our class gives us*/ + _class_renderClass_stgTools({ $stgTools: parentElement, ix: ix, propIxClass: propIxClass }) { + this._class_renderClass_stgSkillsTools({ + '$stg': parentElement, + 'ix': ix, + 'propIxClass': propIxClass, + 'propMetaHks': "_metaHksClassStgTools", + 'propCompsClass': '_compsClassToolProficiencies', + 'propSystem': "tools", + 'fnGetProfs': ({ cls: cls, isPrimaryClass: isPrimaryClass }) => { + if (!cls) { return null; } + return isPrimaryClass ? Charactermancer_Class_Util.getToolProficiencyData(cls.startingProficiencies) : Charactermancer_Class_Util.getToolProficiencyData(cls.multiclassing?.['proficienciesGained']); + }, + 'headerText': "Tool Proficiencies", + 'fnGetMapped': Charactermancer_OtherProficiencySelect.getMappedToolProficiencies.bind(Charactermancer_OtherProficiencySelect) + }); + } + + /** + * Create an element to display skill or tool proficiency choices that our class gives us. + * Use _class_renderClass_stgSkills or _class_renderClass_stgTools if you specifically know which one you want to use + * */ + _class_renderClass_stgSkillsTools({ + $stg: parentElement, + ix: ix, + propIxClass: propIxClass, + propMetaHks: propMetaHks, + propCompsClass: propCompsClass, + propSystem: propSystem, + fnGetProfs: fnGetProfs, + headerText: headerText, + fnGetMapped: fnGetMapped + }) { + + if(SETTINGS.USE_EXISTING){ + const existingMeta = this._class_getExistingClassMeta(ix); + if (existingMeta?.profSkillsTools) { return; } + } + + if (this[propMetaHks][ix]) { this[propMetaHks][ix].unhook(); } + + const doRenderSkillsTools = () => { + parentElement.empty(); + const cls = this.getClass_({ propIxClass: propIxClass }); + const isPrimaryClass = this._state.class_ixPrimaryClass === ix; + this._parent.featureSourceTracker_.unregister(this[propCompsClass][ix]); + const proficiencies = fnGetProfs({ cls: cls, isPrimaryClass: isPrimaryClass }); + + if (cls && proficiencies) { + parentElement.showVe().append("
    " + headerText + "
    "); + //TEMPFIX + let existing = {}; + let existingFvtt = null; + if (SETTINGS.USE_EXISTING && this._actor){ + existingFvtt = { skillProficiencies: MiscUtil.get(this._actor, "_source", "system", propSystem) }; + existing = Charactermancer_OtherProficiencySelect.getExisting(existingFvtt); + } + else if(SETTINGS.USE_EXISTING_WEB && this._actor){ + //Filling in 'existing' will only mark a choice as (you already have this proficiency from another source) + //existing = this._actor.classes[ix].skillProficiencies.data; + } + //Create the component + this[propCompsClass][ix] = new Charactermancer_OtherProficiencySelect({ + featureSourceTracker: this._parent.featureSourceTracker_, + existing: existing, + existingFvtt: existingFvtt, + available: fnGetMapped(proficiencies) + }); + + this[propCompsClass][ix].render(parentElement); + + //LOAD FROM SAVE FILE + //Set state to component AFTER first render, this way all other components have hooks set up and can react to the changes we are about to make + if(SETTINGS.USE_EXISTING_WEB && ix < this._actor?.classes?.length){ + //So we can set the state of the proficiency select component here + const comp = this[propCompsClass][ix]; + //Make sure this is the same class + if(SETTINGS.TRANSFER_CHOICES || (this._actor.classes[ix].name == cls.name && this._actor.classes[ix].source == cls.source)){ + const chooseOptions = proficiencies[0]; //Proficiencies is an array, usually only with one entry + //This should be a secure way to get the options we have to pick from, regardless if it is choose from, or choose any + + if(comp._available[0].choose){ + const numToPick = comp._available[0].choose[0].count; + const namesToPickFrom = comp._available[0].choose[0].from.map(n=>n.name.toLowerCase()); + const staticNames = comp._available[0].static? comp._available[0].static.map(n=>n.name.toLowerCase()) : []; + + let mode = "skills"; + if(propCompsClass == "_compsClassToolProficiencies"){mode = "tools"}; + + let chosenProficiencies = {} + if(mode == "skills"){chosenProficiencies = this._actor.classes[ix].skillProficiencies?.data?.skillProficiencies;} + else if (mode == "tools"){chosenProficiencies = this._actor.classes[ix].toolProficiencies?.data?.toolProficiencies;} + + //Its possible that one class outputs null on either tools or skills because they dont provide an option + const chosenNames = !!chosenProficiencies? Object.keys(chosenProficiencies) : []; + for(let i = 0; i < chosenNames.length; ++i){ //Not going to double-check that we aren't adding more proficiencies than numToPick + //Make sure we go from a {name:string} to just an array of strings, then match index + //push to lowercase just for simplicity + const profName = chosenNames[i].toLowerCase(); + let profIx = namesToPickFrom.indexOf(profName); + //Make sure a match is made + if(profIx<0){ + if(staticNames.includes(profName)){ + continue; //This was just tool part of the static choices, no need for alarm + } + console.warn("Failed to match ", profName, "to any option in", + namesToPickFrom); + } + else{ + const prop = `otherProfSelect_${ix}__isActive_${profIx}`; + comp._state[prop] = true; + } + } + } + + } + } + + } + else { parentElement.hideVe(); this[propCompsClass][ix] = null; } + }; + + this._addHookBase("class_ixPrimaryClass", doRenderSkillsTools); + this[propMetaHks][ix] = { + 'unhook': () => this._removeHookBase("class_ixPrimaryClass", doRenderSkillsTools) + }; + + doRenderSkillsTools(); + } + + /**Create the element displaying starting proficiencies for our class */ + _class_renderClass_stgStartingProficiencies({ + $stgStartingProficiencies: element, ix: ix, cls: cls}) { + + const existingClassMeta = SETTINGS.USE_EXISTING? this._class_getExistingClassMeta(ix) : null; + if (existingClassMeta) {return;} + if (this._metaHksClassStgStartingProficiencies[ix]) { + this._metaHksClassStgStartingProficiencies[ix].unhook(); + } + element.empty(); + + //Our parent should be an ActorCharactermancer + this._parent.featureSourceTracker_.unregister(this._compsClassStartingProficiencies[ix]); + + if (cls && (cls.startingProficiencies || cls.multiclassing?.proficienciesGained)) { + element.showVe().append("
    Proficiencies
    "); + this._compsClassStartingProficiencies[ix] = Charactermancer_Class_StartingProficiencies.get({ + 'featureSourceTracker': this._parent.featureSourceTracker_, + 'primaryProficiencies': cls.startingProficiencies, + 'multiclassProficiencies': cls.multiclassing?.proficienciesGained, + 'savingThrowsProficiencies': cls.proficiency, + 'existingProficienciesFvttArmor': MiscUtil.get(this._actor, "_source", "system", "traits", "armorProf"), + 'existingProficienciesFvttWeapons': MiscUtil.get(this._actor, '_source', "system", "traits", "weaponProf"), + 'existingProficienciesFvttSavingThrows': Charactermancer_Class_StartingProficiencies.getExistingProficienciesFvttSavingThrows(this._actor) + }); + this._compsClassStartingProficiencies[ix].render(element); + } + else { + element.hideVe(); + this._compsClassStartingProficiencies[ix] = null; + } + + const onPrimaryClassChanged = () => { + if (this._compsClassStartingProficiencies[ix]) { + this._compsClassStartingProficiencies[ix].mode = + this._state.class_ixPrimaryClass === ix ? Charactermancer_Class_ProficiencyImportModeSelect.MODE_PRIMARY + : Charactermancer_Class_ProficiencyImportModeSelect.MODE_MULTICLASS; + } + }; + + this._addHookBase("class_ixPrimaryClass", onPrimaryClassChanged); + this._metaHksClassStgStartingProficiencies[ix] = { + 'unhook': () => this._removeHookBase("class_ixPrimaryClass", onPrimaryClassChanged) + }; + + onPrimaryClassChanged(); + } + /** + * Each class component we're handling should have an ExistingClassMeta. This function returns the one that matches classIx. If none exists, try to create a new one. + * @param {number} classIx + * @returns {ActorCharactermancerClass.ExistingClassMeta} + */ + _class_getExistingClassMeta(classIx) { + if (this._existingClassMetas[classIx]) {return this._existingClassMetas[classIx];} + console.warn("Creating new ExistingClassMeta. Not tested!"); + if(!this._actor){return null;} + + const {propIxClass: propIxClass } = ActorCharactermancerBaseComponent.class_getProps(classIx); + + const cls = this.getClass_({'propIxClass': propIxClass }); + + const classItems = Charactermancer_Class_Util.getExistingClassItems(this._actor, cls); + const firstItem = classItems.length ? classItems[0] : null; + if (!firstItem) { return null; } + return {'item': firstItem, 'level': Number(firstItem.system.levels || 0)}; + } + //#endregion + + //#region Text Roller Display + async _class_renderClass_pDispClass({ ix: ix, $dispClass: parentElement, cls: cls, sc: sc }) { + if (this._$wrpsClassTable[ix]) {this._$wrpsClassTable[ix].detach(); } + else {this._$wrpsClassTable[ix] = $("
    ");} + parentElement.empty(); + + if (cls) { + //A problem is that the class.classFeatures array doesn't contain the text content that we want to display. It only has the name + //So what we need to do is get that text information from somewhere + + const classInfo = cls._isStub ? cls : await DataLoader.pCacheAndGet("class", cls.source, + UrlUtil.URL_TO_HASH_BUILDER["class"](cls)); //The hash will look something like 'barbarian_phb' (depending on class name and source) + let entries = MiscUtil.copy(classInfo.classFeatures || []).flat(); + if(SETTINGS.FILTERS){entries = Charactermancer_Class_Util.getFilteredEntries_bySource(entries, + this._modalFilterClasses.pageFilter, this._modalFilterClasses.pageFilter.filterBox.getValues());} + else { + //if we dont use the filters (the intended method), the 'entries' array is hidden deep within loadeds + //we can still use that entries array, but for simplicities sake we can just get the raw classfeatures again + entries = ContentGetter.getFeaturesFromClass(cls); + } + + const toRender = { 'type': "section", 'entries': entries}; + this._class_renderEntriesSection(parentElement, cls.name, toRender, { '$wrpTable': this._$wrpsClassTable[ix] }); + //Render the class table + await this._class_renderClass_pClassTable({ 'ix': ix, 'cls': cls, 'sc': sc }); + } + } + async _class_renderClass_pDispSubclass({ix: ix, $dispSubclass: parentElement, cls: cls, sc: sc }) { + parentElement.empty(); + if (sc) { + parentElement.append("
    "); + const subclassInfo = sc._isStub ? sc : await DataLoader.pCacheAndGet("subclass", sc.source, UrlUtil.URL_TO_HASH_BUILDER.subclass(sc)); + let scFeatures = MiscUtil.copy(subclassInfo.subclassFeatures).flat(); + scFeatures = Charactermancer_Class_Util.getFilteredEntries_bySource(scFeatures, this._modalFilterClasses.pageFilter, this._modalFilterClasses.pageFilter.filterBox.getValues()); + const obj = { type: "section", entries: scFeatures }; + if (obj.entries[0] && obj.entries[0].name) { delete obj.entries[0].name; } + this._class_renderEntriesSection(parentElement, sc.name, obj); + } + await this._class_renderClass_pClassTable({ + 'ix': ix, + 'cls': cls, + 'sc': sc + }); + } + async _class_renderClass_pClassTable({ ix: ix, cls: cls, sc: sc }) { + const classInfo = cls ? cls._isStub ? cls : await DataLoader.pCacheAndGet('class', cls.source, UrlUtil.URL_TO_HASH_BUILDER["class"](cls)) : null; + const subclassInfo = sc ? sc._isStub ? sc : await DataLoader.pCacheAndGet("subclass", sc.source, UrlUtil.URL_TO_HASH_BUILDER.subclass(sc)) : null; + const features = classInfo?.["classFeatures"] || subclassInfo?.["subclassFeatures"] ? this._modalFilterClasses.pageFilter.filterBox.getValues() : null; + if (classInfo?.["classFeatures"]) { + classInfo.classFeatures = classInfo.classFeatures.map(f => { + f = MiscUtil.copy(f); + f = Charactermancer_Class_Util.getFilteredEntries_bySource(f, this._modalFilterClasses.pageFilter, features); + return f; + }); + } + if (subclassInfo?.["subclassFeatures"]) { + subclassInfo.subclassFeatures = subclassInfo.subclassFeatures.map(f => { + f = MiscUtil.copy(f); + f = Charactermancer_Class_Util.getFilteredEntries_bySource(f, this._modalFilterClasses.pageFilter, features); + return f; + }); + } + const table = DataConverterClass.getRenderedClassTableFromDereferenced(classInfo, subclassInfo); + this._$wrpsClassTable[ix].html('').fastSetHtml(table); + } + _class_renderEntriesSection(parentElement, cls, toRender, { $wrpTable = null } = {}) { + const minimizeButton = $("
    [โ€’]
    ").click(() => { + minimizeButton.text(minimizeButton.text() === '[+]' ? '[โ€’]' : "[+]"); + if ($wrpTable) { $wrpTable.toggleVe(); } + displayedElement.toggleVe(); + }); + const displayedElement = Renderer.hover.$getHoverContent_generic(toRender); + $$`
    +
    +
    ${(cls || '').qq()}
    + ${minimizeButton} +
    + ${$wrpTable} + ${displayedElement} +
    `.appendTo(parentElement); + } + //#endregion + + /**Get the features of our current class and subclass */ + _class_getFilteredFeatures(cls, sc) { + if (!cls) {return [];} + cls = MiscUtil.copy(cls); + cls.subclasses = [sc].filter(Boolean); + + //TEMPFIX + if(!SETTINGS.FILTERS) { + return Charactermancer_Class_Util.getAllFeatures(cls); + } + return Charactermancer_Util.getFilteredFeatures(Charactermancer_Class_Util.getAllFeatures(cls), + this._modalFilterClasses.pageFilter, this._modalFilterClasses.pageFilter.filterBox.getValues()); + } + + //#region Feature Options Selects + /** + * HOW FEATURE OPTIONS SELECTS WORKS + * ------------------------------------- + * Whenever our class or subclass levels up, it might need to create a FeatureOptionsSelect component + * These components may also be deleted when we are leveled down + * A FeatureOptionsSelect component has subcomponents (like expertise choice, language choice, etc) that are created during rendering + * Whenever a FeatureOptionsSelect is to be rendered, it deletes its old version of itself, but copies over the list of subcomponents from the old one + * It then creates new subcomponents, but copies over the state from the old ones + */ + + /** + * Safely creates and then renders FeatureOptionsSelect components + * @param {any} options + */ + async _class_pRenderFeatureOptionsSelects(options) { + const { lockRenderFeatureOptionsSelects: lockRenderFeatureOptionsSelects } = options; + try { + await this._pLock(lockRenderFeatureOptionsSelects); + return this._class_pRenderFeatureOptionsSelects_(options); + } + finally { this._unlock(lockRenderFeatureOptionsSelects); } + } + /** + * Creates and then renders FeatureOptionsSelect components, deleting the previous ones but copying over their states + */ + async _class_pRenderFeatureOptionsSelects_({ix: ix, propCntAsi: propCntAsi, filteredFeatures: filteredFeatures, $stgFeatureOptions: stgFeatureOptions + }) { + + //Get the previous components we had + const previousComponents = this._compsClassFeatureOptionsSelect[ix] || []; + previousComponents.forEach(e => this._parent.featureSourceTracker_.unregister(e)); //Unregister each of them from the feature source tracker + stgFeatureOptions.empty(); + const existingFeatureChecker = this._existingClassMetas[ix] ? new Charactermancer_Class_Util.ExistingFeatureChecker(this._actor) : null; + const importableFeatures = Charactermancer_Util.getImportableFeatures(filteredFeatures); + const features = MiscUtil.copy(importableFeatures); + if(SETTINGS.FILTERS){ //TEMPFIX + Charactermancer_Util.doApplyFilterToFeatureEntries_bySource(features, + this._modalFilterClasses.pageFilter, this._modalFilterClasses.pageFilter.filterBox.getValues()); + } + + //by this point, 'features' should be an array of classFeatures with property 'loadeds' + const groupedByOptionsSet = Charactermancer_Util.getFeaturesGroupedByOptionsSet(features); + //groupedByOptionsSet should be an array of objects like this: {optionsSets: [...], topLevelFeature: {...}} + const {lvlMin: lvlMin, lvlMax: lvlMax } = await this._class_pGetMinMaxLevel(ix); + //Unregister and delete previousComponents + this._class_unregisterFeatureSourceTrackingFeatureComps(ix); + + + + let asiCount = 0; + for (const grp of groupedByOptionsSet) { + const { topLevelFeature: topLevelFeature, optionsSets: optionsSets} = grp; + //Only render features of the right level (using a setting to always render features of lower level than we are) + if ((topLevelFeature.level < lvlMin && !SETTINGS.GET_FEATOPTSEL_UP_TO_CURLEVEL) || topLevelFeature.level > lvlMax) {continue; } + const featureName = topLevelFeature.name.toLowerCase(); + if (featureName === "ability score improvement") { asiCount++; continue; } + for (const set of optionsSets) { + //Create the new FeatureOptionsSelect for this optionset + const component = new Charactermancer_FeatureOptionsSelect({ + featureSourceTracker: this._parent.featureSourceTracker_, + existingFeatureChecker: existingFeatureChecker, + //TEMPFIX 'actor': this._actor, + optionsSet: set, + level: topLevelFeature.level, + modalFilterSpells: this._parent.compSpell.modalFilterSpells + }); + //Add a featureoptionselect component for the class 'ix' + this._compsClassFeatureOptionsSelect[ix].push(component); + //Copy the state from a previous component that matches our optionsset + //Doesnt copy over states from subcomponents, instead just adds those subcomponents to the memory of component, preserving the state + component.findAndCopyStateFrom(previousComponents); + } + } + + //Remember the amount of ASI's + this._state[propCntAsi] = asiCount; + + //Do a first render on these freshly created components + await this._class_pRenderFeatureComps(ix, {'$stgFeatureOptions': stgFeatureOptions}); + } + + /** + * Render the FeatureOptionsSelects and create their subcomponents + * @param {any} ix + * @param {any} {$stgFeatureOptions:stgFeatureOptions} + * @returns {any} + */ + async _class_pRenderFeatureComps(ix, { $stgFeatureOptions: stgFeatureOptions }) { + for (let i = 0; i < this.compsClassFeatureOptionsSelect[ix].length; ++i) { + let component = this.compsClassFeatureOptionsSelect[ix][i]; + + //component._optionsSet[0].entity.entryData exists + if ((await component.pIsNoChoice()) && !(await component.pIsAvailable())) {continue;} + + if (!(await component.pIsNoChoice()) || (await component.pIsForceDisplay())) { + stgFeatureOptions.showVe().append('' + (component.modalTitle ? + "
    " + component.modalTitle + "
    " : '')); + } + await component.render(stgFeatureOptions); + } + } + async loadFeatureOptionsSelectFromState(classIx){ + //Try to load saved cached save data + if(SETTINGS.USE_EXISTING_WEB && this.loadedSaveData_FeatureOptSel && classIx < this.loadedSaveData_FeatureOptSel.length){ + if(this.loadedSaveData_FeatureOptSel[classIx] != null){ + await this._loadFeatureOptionsSelectFromState(classIx, this.loadedSaveData_FeatureOptSel[classIx]); + } + //Then wipe the cached save data so we cant accidentally set it again + this.loadedSaveData_FeatureOptSel[classIx] = null; + } + } + /** + * Description + * @param {any} classIx + * @param {{compIx:number, state:any, subCompDatas:{parentCompIx:number, subCompProp:string, subCompIx:number, state:any}[]}[]} saveData + * @returns {any} + */ + async _loadFeatureOptionsSelectFromState(classIx, saveData){ + for(let fosData of saveData){ //Go through the feature opt sel data + + const myComp = this.compsClassFeatureOptionsSelect[classIx][fosData.compIx]; + if(myComp==null){ + console.error(`Failed to grab the ix ${fosData.compIx} FOS component from class ${classIx}`, fosData); + continue; + } + + //First give state to ourselves + const parentState = fosData.state; + for(let propName of Object.keys(parentState)){ + let propValue = parentState[propName]; + if(propName == "ixsChosen"){continue;} //We do not want to fire the ixsChosen hook yet + myComp._state[propName] = propValue; + } + const waitForHook = async () => { + myComp.__state["ixsChosen"] = fosData.state["ixsChosen"]; //Set the state without firing the hook + //Then manually call the render hook instead + await myComp._render_pHkIxsChosen({$stgSubChoiceData: myComp.$stgSubChoiceData}); + }; + waitForHook().then(()=>{ + console.log("Applying state to FOS subcomponents"); + + //Then give state to subcomponents + //A problem here might be that our parents havent created the subcomponents yet + for(let subData of fosData.subCompDatas){ + //Grab the subcomponent that we want to paste the state onto + const subComp = this._getFeatureOptionsSelectComponent(classIx, subData.parentCompIx, subData.subCompProp, subData.subCompIx); + if(subComp==null){ + console.error("Failed to load data to FOS subcomponent", myComp, subData); + } + //Paste the state onto the subcomponent + const subState = subData.state; + for(let propName of Object.keys(subState)){ + let propValue = subState[propName]; + subComp._state[propName] = propValue; + } + } + }); + + + + + } + } + _getFeatureOptionsSelectComponent(classIx, parentCompIx, subCompName, subCompIx){ + if(!this._compsClassFeatureOptionsSelect[classIx]?.length){ + console.error("FeatureOptionsSelect components have not been created for class " + classIx + " yet!"); + } + try{ + return this._compsClassFeatureOptionsSelect[classIx][parentCompIx][subCompName][subCompIx]; + } + catch(e){ + console.error(classIx, parentCompIx, subCompName, subCompIx, this._compsClassFeatureOptionsSelect.length); + console.error(this._compsClassFeatureOptionsSelect); + throw e; + } + + } + + matchFeatureOptionSelectSaveDataToComponent(componentData, optionSet){ + const hashes = optionSet.map(set => {return set.hash}); //Use this to pull choices from parentData + //if(!componentData.hashes){continue;} //This shouldnt happen + for(let hash of hashes){ + if(componentData.hashes.includes(hash)){return f;} + } + return null; + } + //#endregion + + /** + * Unregisters FeatureOptionsSelect components specific to the class from the featureSourceTracker and wipes them from memory + * @param {number} ix ix of the class we are talking about + */ + _class_unregisterFeatureSourceTrackingFeatureComps(ix) { + (this._compsClassFeatureOptionsSelect[ix] || []).forEach(comp => comp.unregisterFeatureSourceTracking()); + this._compsClassFeatureOptionsSelect[ix] = []; + } + async _class_pGetMinMaxLevel(ix) { + let lvlMin = 0; + let lvlMax = 0; + if (this._compsClassLevelSelect[ix]) { + const data = await this._compsClassLevelSelect[ix].pGetFormData().data; + lvlMin = Math.min(...data) + 1; + lvlMax = Math.max(...data) + 1; + } + return { lvlMin: lvlMin, lvlMax: lvlMax }; + } + + + class_getPrimaryClass() { + if (!~this._state.class_ixPrimaryClass) { return null; } + const { propIxClass: propIxClass } = ActorCharactermancerBaseComponent.class_getProps(this._state.class_ixPrimaryClass); + return this._data.class[this._state[propIxClass]]; + } + class_getTotalLevels() { + return this._compsClassLevelSelect.filter(Boolean).map(comp => comp.getTargetLevel() + || comp.getCurLevel()).reduce((a, b) => a + b, 0); + } + class_getMinMaxSpellLevel() { + const spellLevelLows = []; + const spellLevelHighs = []; + let isAnyCantrips = false; + for (let classIx = 0; classIx < this._state.class_ixMax + 1; ++classIx) { + const { + propIxClass: propIxClass, + propIxSubclass: propIxSubclass, + propCurLevel: propCurLevel, + propTargetLevel: propTargetLevel + } = ActorCharactermancerBaseComponent.class_getProps(classIx); + const cls = this.getClass_({ propIxClass: propIxClass }); + if (!cls) { continue; } + const subcls = this.getSubclass_({ + 'cls': cls, + 'propIxSubclass': propIxSubclass + }); + const curLevel = this._state[propCurLevel]; + const targetLevel = this._state[propTargetLevel]; + const casterProgression = DataConverter.getMaxCasterProgression(cls.casterProgression, subcls?.["casterProgression"]); + const spellLevelLow = Charactermancer_Spell_Util.getCasterProgressionMeta({ + 'casterProgression': casterProgression, + 'curLevel': curLevel, + 'targetLevel': targetLevel, + 'isBreakpointsOnly': true + })?.spellLevelLow; + + if (spellLevelLow != null) { + spellLevelLows.push(spellLevelLow); + } + const spellLevelHigh = Charactermancer_Spell_Util.getCasterProgressionMeta({ + 'casterProgression': casterProgression, + 'curLevel': curLevel, + 'targetLevel': targetLevel, + 'isBreakpointsOnly': true + })?.spellLevelHigh; + + if (spellLevelHigh != null) { + spellLevelHighs.push(spellLevelHigh); + } + isAnyCantrips = isAnyCantrips || Charactermancer_Spell_Util.getMaxLearnedCantrips({ + 'cls': cls, + 'sc': subcls, + 'targetLevel': targetLevel + }) != null; + } + return { + 'min': spellLevelLows.length ? Math.min(...spellLevelLows) : null, + 'max': spellLevelHighs.length ? Math.max(...spellLevelHighs) : null, + 'isAnyCantrips': isAnyCantrips + }; + } + + wipeClassState(ix){ + const wipe = (comp) => { + if(comp){ + try { + + //We need to loop through each value in state and set it to null + //This should fire some hooks + const propNames = Object.keys(comp._state); + for(let prop of propNames){ + comp._setStateValue(prop, null, {isForceTriggerHooks: true}); + } + //Then we can go in and completely reset the _state, wiping hooks + comp._setState(comp._getDefaultState()); + + //Needs to be called last + this._parent.featureSourceTracker_.unregister(comp); + } + catch (e){ + console.error("Failed to wipe component ", comp, comp.constructor.name, "belonging to ix ", ix); + throw e; + } + } + } + //Wipe states in each subcomponent belonging to the ix of my class + wipe(this.compsClassSkillProficiencies[ix]); + wipe(this.compsClassToolProficiencies[ix]); + wipe(this.compsClassLevelSelect[ix]); + wipe(this.compsClassHpIncreaseMode[ix]); + wipe(this.compsClassStartingProficiencies[ix]); + for(let comp of this.compsClassFeatureOptionsSelect[ix]){ + if(comp) { wipe(comp); } + } + //We need to reduce class_ixMax, OR mark this class index as unused somehow + //Simplest is probably to just mark the class as unused. It will be ignored until the next time we save + ActorCharactermancerBaseComponent.deletedClassIndices.push(ix); + this.state.class_pulseChange = !this.state.class_pulseChange; + + //List all classes + for(let i = 0; i <= this._state.class_ixMax; ++i){ + if(ActorCharactermancerBaseComponent.class_isDeleted(i)){continue;} + const clsInfo = ActorCharactermancerBaseComponent.class_getProps(i); + const cls = this.getClass_({propIxClass:clsInfo.propIxClass}); + console.log(cls); + } + + + //This should be enough wiping for now + } + + /**Defines the starting default values of our _state proxy */ + _getDefaultState() { + return { + 'class_ixPrimaryClass': 0, + 'class_ixMax': 0, + 'class_totalLevels': 0, + 'class_pulseChange': false + }; + } +} +class Charactermancer_Class_HpIncreaseModeSelect extends BaseComponent { + static async pGetUserInput() { + if (this.isNoChoice()) { + const comp = new this(); + return comp.pGetFormData(); + } + + return UtilApplications.pGetImportCompApplicationFormData({ + comp: new this(), + width: 480, + height: 150, + }); + } + + static isHpAvailable(cls) { + return cls.hd && cls.hd.number && !isNaN(cls.hd.number) && cls.hd.faces && !isNaN(cls.hd.faces); + } + + static isNoChoice() { + if (game.user.isGM) + return false; + + if (Config.get("importClass", "hpIncreaseMode") === ConfigConsts.C_IMPORT_CLASS_HP_INCREASE_MODE__ROLL_CUSTOM && Config.get("importClass", "hpIncreaseModeCustomRollFormula") == null) + return false; + + return Config.get("importClass", "hpIncreaseMode") != null; + } + + /** + * @returns {{isFormComplete:boolean, data{mode:number, customFormula:string}}} + */ + pGetFormData() { + return { + isFormComplete: true, + data: { + mode: this._state.mode, + customFormula: this._state.customFormula, + }, + }; + } + + get modalTitle() { + return `Select Hit Points Increase Mode`; + } + + render($wrp) { + const $sel = ComponentUiUtil.$getSelEnum(this, "mode", { + values: [ConfigConsts.C_IMPORT_CLASS_HP_INCREASE_MODE__TAKE_AVERAGE, ConfigConsts.C_IMPORT_CLASS_HP_INCREASE_MODE__MIN, ConfigConsts.C_IMPORT_CLASS_HP_INCREASE_MODE__MAX, ConfigConsts.C_IMPORT_CLASS_HP_INCREASE_MODE__ROLL, ConfigConsts.C_IMPORT_CLASS_HP_INCREASE_MODE__ROLL_CUSTOM, ConfigConsts.C_IMPORT_CLASS_HP_INCREASE_MODE__DO_NOT_INCREASE, ], + fnDisplay: mode=>ConfigConsts.C_IMPORT_CLASS_HP_INCREASE_MODE___NAMES[mode], + }, ); + + /* if (!game.user.isGM && Config.get("importClass", "hpIncreaseMode") != null) + $sel.disable(); */ + + const $iptCustom = ComponentUiUtil.$getIptStr(this, "customFormula").addClass("code"); + + /* if (!game.user.isGM && Config.get("importClass", "hpIncreaseModeCustomRollFormula") != null) + $iptCustom.disable(); */ + + const $stgCustom = $$`
    +
    Custom Formula:
    + ${$iptCustom} +
    `; + + const hkMode = ()=>{ + $stgCustom.toggleVe(this._state.mode === ConfigConsts.C_IMPORT_CLASS_HP_INCREASE_MODE__ROLL_CUSTOM); + }; + this._addHookBase("mode", hkMode); + hkMode(); + + $$`
    + ${$sel} + ${$stgCustom} +
    `.appendTo($wrp); + } + + _getDefaultState() { + return { + mode: Config.get("importClass", "hpIncreaseMode") ?? ConfigConsts.C_IMPORT_CLASS_HP_INCREASE_MODE__TAKE_AVERAGE, + customFormula: Config.get("importClass", "hpIncreaseModeCustomRollFormula") ?? "(2 * @hd.number)d(@hd.faces / 2)", + }; + } +} +class Charactermancer_Class_HpInfo extends BaseComponent { + constructor({className, hitDice}) { + super(); + this._className = className; + this._hitDice = hitDice; + } + + render($wrp) { + const hdEntry = Renderer.class.getHitDiceEntry(this._hitDice); + + const we = $$`
    +
    Hit Dice:
    ${SETTINGS.DO_RENDER_DICE? Vetools.withUnpatchedDiceRendering(()=> + Renderer.getEntryDice(hdEntry, "Hit die")) : hdEntry.toRoll}
    +
    Hit Points:
    ${Renderer.class.getHitPointsAtFirstLevel(this._hitDice)}
    +
    Hit Points at Higher Levels:
    + ${Vetools.withUnpatchedDiceRendering(()=>Renderer.class.getHitPointsAtHigherLevels(this._className, this._hitDice, hdEntry))}
    +
    `; + we.appendTo($wrp); + } + + //Functions in case some external party wants to get some info + get hitDice(){return this._hitDice;} //How many faces, not how many dice + get hitPointsAtFirstLevel() { return Renderer.class.getHitPointsAtFirstLevel(this._hitDice); } + +} +class Charactermancer_AdditionalSpellsUtil { + static getFlatData(additionalSpells) { + additionalSpells = MiscUtil.copy(additionalSpells); + + return additionalSpells.map(additionalSpellBlock=>{ + const outMeta = {}; + const outSpells = {}; + + const keyPath = []; + + Object.entries(additionalSpellBlock).forEach(([additionType,additionMeta])=>{ + keyPath.push(additionType); + + switch (additionType) { + case "name": + case "ability": + outMeta[additionType] = additionMeta; + break; + + case "resourceName": + break; + + case "innate": + case "known": + case "prepared": + case "expanded": + { + this._getFlatData_doProcessAdditionMeta({ + additionType, + additionMeta, + outSpells, + keyPath, + resourceName: additionalSpellBlock.resourceName + }); + break; + } + + default: + throw new Error(`Unhandled spell addition type "${additionType}"`); + } + + keyPath.pop(); + } + ); + + return { + meta: outMeta, + spells: outSpells + }; + } + ); + } + + static _getFlatData_doProcessAdditionMeta(opts) { + const {additionMeta, keyPath} = opts; + + Object.entries(additionMeta).forEach(([levelOrCasterLevel,levelMeta])=>{ + keyPath.push(levelOrCasterLevel); + + if (levelMeta instanceof Array) { + levelMeta.forEach((spellItem,ix)=>this._getFlatData_doProcessSpellItem({ + ...opts, + levelOrCasterLevel, + spellItem, + ix + })); + } else { + Object.entries(levelMeta).forEach(([rechargeType,levelMetaInner])=>{ + this._getFlatData_doProcessSpellRechargeBlock({ + ...opts, + levelOrCasterLevel, + rechargeType, + levelMetaInner + }); + } + ); + } + + keyPath.pop(); + } + ); + } + + static _getFlatData_doProcessSpellItem(opts) { + const {additionType, additionMeta, outSpells, keyPath, spellItem, ix, rechargeType, uses, usesPer, levelOrCasterLevel, consumeType, consumeAmount, consumeTarget, vetConsumes} = opts; + + keyPath.push(ix); + + const outSpell = { + isExpanded: additionType === "expanded", + isAlwaysPrepared: additionType === "prepared", + isAlwaysKnown: additionType === "known", + isPrepared: additionType === "prepared" || additionType === "innate" || additionType === "known", + preparationMode: (additionType === "prepared" || additionType === "known") ? "always" : "innate", + consumeType, + consumeAmount, + consumeTarget, + vetConsumes, + }; + + if (levelOrCasterLevel !== "_") { + const mCasterLevel = /^s(\d+)$/.exec(levelOrCasterLevel); + if (mCasterLevel) + outSpell.requiredCasterLevel = Number(mCasterLevel[1]); + else if (!isNaN(levelOrCasterLevel)) + outSpell.requiredLevel = Number(levelOrCasterLevel); + } + + if (rechargeType) { + switch (rechargeType) { + case "rest": + case "daily": + break; + case "will": + case "ritual": + case "resource": + { + outSpell.preparationMode = "atwill"; + outSpell.isPrepared = rechargeType !== "ritual"; + break; + } + + case "_": + break; + + default: + throw new Error(`Unhandled recharge type "${rechargeType}"`); + } + } + + if (uses) + outSpell.uses = uses; + if (usesPer) + outSpell.usesPer = usesPer; + + if (typeof spellItem === "string") { + const key = keyPath.join("__"); + + outSpells[key] = new Charactermancer_AdditionalSpellsUtil.FlatSpell({ + type: "spell", + + key, + ...outSpell, + uid: spellItem, + }); + } else { + if (spellItem.all != null) { + const key = keyPath.join("__"); + + outSpells[key] = new Charactermancer_AdditionalSpellsUtil.FlatSpell({ + type: "all", + + key, + ...outSpell, + filterExpression: spellItem.all, + }); + } else if (spellItem.choose != null) { + if (typeof spellItem.choose === "string") { + const count = spellItem.count || 1; + + for (let i = 0; i < count; ++i) { + keyPath.push(i); + + const key = keyPath.join("__"); + + outSpells[key] = new Charactermancer_AdditionalSpellsUtil.FlatSpell({ + type: "choose", + + key, + ...outSpell, + filterExpression: spellItem.choose, + }); + + keyPath.pop(); + } + } else if (spellItem.choose.from) { + const count = spellItem.choose.count || 1; + + const groupId = CryptUtil.uid(); + [...spellItem.choose.from].sort((a,b)=>SortUtil.ascSortLower(a, b)).forEach((uid,i)=>{ + keyPath.push(i); + + const key = keyPath.join("__"); + + outSpells[key] = new Charactermancer_AdditionalSpellsUtil.FlatSpell({ + type: "chooseFrom", + + key, + ...outSpell, + uid: uid, + chooseFromGroup: groupId, + chooseFromCount: count, + }); + + keyPath.pop(); + } + ); + } else { + throw new Error(`Unhandled additional spell format: "${JSON.stringify(spellItem)}"`); + } + } else + throw new Error(`Unhandled additional spell format: "${JSON.stringify(spellItem)}"`); + } + + keyPath.pop(); + } + + static _getFlatData_doProcessSpellRechargeBlock(opts) { + const {additionType, additionMeta, outSpells, keyPath, resourceName, levelOrCasterLevel, rechargeType, levelMetaInner} = opts; + + keyPath.push(rechargeType); + + switch (rechargeType) { + case "rest": + case "daily": + { + const usesPer = rechargeType === "rest" ? "sr" : "lr"; + + Object.entries(levelMetaInner).forEach(([numTimesCast,spellList])=>{ + keyPath.push(numTimesCast); + + numTimesCast = numTimesCast.replace(/^(\d+)e$/, "$1"); + const uses = Number(numTimesCast); + + spellList.forEach((spellItem,ix)=>this._getFlatData_doProcessSpellItem({ + ...opts, + spellItem, + ix, + uses, + usesPer + })); + + keyPath.pop(); + } + ); + + break; + } + + case "resource": + { + Object.entries(levelMetaInner).forEach(([consumeAmount,spellList])=>{ + keyPath.push(consumeAmount); + + spellList.forEach((spellItem,ix)=>this._getFlatData_doProcessSpellItem({ + ...opts, + spellItem, + ix, + vetConsumes: { + name: resourceName, + amount: Number(consumeAmount) + } + })); + + keyPath.pop(); + } + ); + + break; + } + + case "will": + case "ritual": + case "_": + { + levelMetaInner.forEach((spellItem,ix)=>this._getFlatData_doProcessSpellItem({ + ...opts, + spellItem, + ix + })); + break; + } + + default: + throw new Error(`Unhandled spell recharge type "${rechargeType}"`); + } + + keyPath.pop(); + } +} +Charactermancer_AdditionalSpellsUtil.FlatSpell = class { + #opts = null; + constructor(opts) { + this.#opts = opts; + + this.type = opts.type; + this.key = opts.key; + this.isExpanded = opts.isExpanded; + this.isPrepared = opts.isPrepared; + this.isAlwaysKnown = opts.isAlwaysKnown; + this.isAlwaysPrepared = opts.isAlwaysPrepared; + this.preparationMode = opts.preparationMode; + this.requiredCasterLevel = opts.requiredCasterLevel; + this.requiredLevel = opts.requiredLevel; + + this.uses = opts.uses; + this.usesPer = opts.usesPer; + + this.consumeType = opts.consumeType; + this.consumeAmount = opts.consumeAmount; + this.consumeTarget = opts.consumeTarget; + + this.vetConsumes = opts.vetConsumes; + + this.isCantrip = false; + + this.uid = null; + this.castAtLevel = null; + if (opts.uid) { + const {uid, isCantrip, castAtLevel} = Charactermancer_AdditionalSpellsUtil.FlatSpell._getExpandedUid(opts.uid); + this.uid = uid; + this.isCantrip = isCantrip; + this.castAtLevel = castAtLevel; + } + + this.filterExpression = opts.filterExpression; + if (opts.filterExpression && opts.filterExpression.split("|").filter(Boolean).some(it=>/^level=0$/i.test(it.trim()))) { + this.isCantrip = true; + } + + this.chooseFromGroup = opts.chooseFromGroup; + this.chooseFromCount = opts.chooseFromCount; + + if (this.isCantrip && !this.isExpanded) + this.isAlwaysKnown = true; + } + + static _getExpandedUid(uidRaw) { + const [uidPart,castAtLevelPart] = uidRaw.split("#").map(it=>it.trim()).filter(Boolean); + + let[name,source] = Renderer.splitTagByPipe(uidPart.toLowerCase()); + source = source || Parser.SRC_PHB.toLowerCase(); + const uid = `${name}|${source}`; + + const isCantrip = castAtLevelPart && castAtLevelPart.toLowerCase() === "c"; + const castAtLevel = isCantrip ? null : (castAtLevelPart && !isNaN(castAtLevelPart)) ? Number(castAtLevelPart) : null; + + return { + uid, + isCantrip, + castAtLevel + }; + } + + getCopy(optsNxt=null) { + return new this.constructor({ + ...this.#opts, + ...optsNxt || {}, + }); + } + + toObject() { + return MiscUtil.copy(this); + } +}; + +class Charactermancer_AdditionalSpellsSelect extends BaseComponent { + static async pGetUserInput(opts) { + opts = opts || {}; + const {additionalSpells} = opts; + + if (!additionalSpells || !additionalSpells.length) + return { + isFormComplete: true, + data: [] + }; + + const comp = this.getComp(opts); + + if (comp.isNoChoice({ + curLevel: opts.curLevel, + targetLevel: opts.targetLevel, + isStandalone: opts.isStandalone + })) + return comp.pGetFormData(); + + return UtilApplications.pGetImportCompApplicationFormData({ + comp, + width: 640, + height: 220, + }); + } + + static _MODAL_FILTER_SPELLS_DEFAULT = null; + + static async pGetInitModalFilterSpells() { + if (!this._MODAL_FILTER_SPELLS_DEFAULT) { + this._MODAL_FILTER_SPELLS_DEFAULT = new ModalFilterSpellsFvtt({ + namespace: "Charactermancer_AdditionalSpellsSelect.spells", + isRadio: true + }); + await this._MODAL_FILTER_SPELLS_DEFAULT.pPreloadHidden(); + } + return this._MODAL_FILTER_SPELLS_DEFAULT; + } + + static getComp(opts) { + opts = opts || {}; + + const comp = new this({ + ...opts + }); + comp.curLevel = opts.curLevel; + comp.targetLevel = opts.targetLevel; + comp.spellLevelLow = opts.spellLevelLow; + comp.spellLevelHigh = opts.spellLevelHigh; + comp.isAnyCantrips = !!opts.isAnyCantrips; + + return comp; + } + + static async pApplyFormDataToActor(actor, formData, opts) { + opts = opts || {}; + + if (!formData || !formData?.data?.length) + return []; + + const ability = ((opts.abilityAbv === "inherit" ? opts.parentAbilityAbv : opts.abilityAbv) || (formData.abilityAbv === "inherit" ? opts.parentAbilityAbv : formData.abilityAbv)) ?? undefined; + + const {ImportListSpell} = await Promise.resolve().then(function() { + return ImportListSpell$1; + }); + const importListSpell = new ImportListSpell({ + actor + }); + + const out = []; + + for (const spellMeta of formData.data) { + if (spellMeta.isExpanded) + continue; + + let[name,source] = spellMeta.uid.split("|"); + if (!source) + source = Parser.SRC_PHB; + const hash = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_SPELLS]({ + name, + source + }); + + const spell = await DataLoader.pCacheAndGet(UrlUtil.PG_SPELLS, source, hash); + if (!spell) { + const message = `Could not find spell "${hash}" when applying additional spells!`; + ui.notifications.warn(message); + console.warn(...LGT, message); + continue; + } + + const importSummary = await importListSpell.pImportEntry(spell, { + taskRunner: opts.taskRunner, + opts_pGetSpellItem: { + ability, + usesMax: spellMeta.uses, + usesValue: spellMeta.uses, + usesPer: spellMeta.usesPer, + consumeType: spellMeta.consumeType, + consumeAmount: spellMeta.consumeAmount, + consumeTarget: spellMeta.consumeTarget, + vetConsumes: spellMeta.vetConsumes, + isPrepared: spellMeta.isPrepared, + preparationMode: spellMeta.preparationMode, + castAtLevel: spellMeta.castAtLevel, + + isIgnoreExisting: true, + }, + }, ); + out.push(importSummary); + } + + return out; + } + + static isNoChoice(additionalSpells, {additionalSpellsFlat=null, curLevel=null, targetLevel=null, isStandalone=false}={}) { + if (additionalSpells.length !== 1) + return false; + additionalSpellsFlat = additionalSpellsFlat || Charactermancer_AdditionalSpellsUtil.getFlatData(additionalSpells); + + const minLevel = curLevel ?? Number.MIN_SAFE_INTEGER; + const maxLevel = targetLevel ?? Number.MAX_SAFE_INTEGER; + + const spellsInRange = additionalSpellsFlat.some(it=>Object.values(it.spells).some(it=>(!isStandalone || !it.isExpanded) && (it.requiredLevel == null || (it.requiredLevel >= minLevel && it.requiredLevel <= maxLevel))), ); + + if (!spellsInRange) + return true; + + return !additionalSpellsFlat.some(it=>it.meta.ability?.choose || Object.values(it.spells).some(it=>(it.type !== "all" && it.filterExpression != null) || it.chooseFromGroup != null)); + } + + constructor(opts) { + opts = opts || {}; + super(); + + this._additionalSpells = opts.additionalSpells; + this._sourceHintText = opts.sourceHintText; + this._modalFilterSpells = opts.modalFilterSpells; + + this._additionalSpellsFlat = Charactermancer_AdditionalSpellsUtil.getFlatData(opts.additionalSpells); + + } + + get modalTitle() { + return `Choose Additional Spell Set${this._sourceHintText ? ` (${this._sourceHintText})` : ""}`; + } + + set curLevel(val) { + this._state.curLevel = val; + } + set targetLevel(val) { + this._state.targetLevel = val; + } + set spellLevelLow(val) { + this._state.spellLevelLow = val; + } + set spellLevelHigh(val) { + this._state.spellLevelHigh = val; + } + set isAnyCantrips(val) { + this._state.isAnyCantrips = !!val; + } + + addHookAlwaysPreparedSpells(hk) { + this._addHookBase("spellsAlwaysPrepared", hk); + } + addHookExpandedSpells(hk) { + this._addHookBase("spellsExpanded", hk); + } + addHookAlwaysKnownSpells(hk) { + this._addHookBase("spellsAlwaysKnown", hk); + } + + get spellsAlwaysPrepared() { + return this._state.spellsAlwaysPrepared; + } + get spellsExpanded() { + return this._state.spellsExpanded; + } + get spellsAlwaysKnown() { + return this._state.spellsAlwaysKnown; + } + + _render_addLastAlwaysPreparedSpellsHook() { + return this._render_addLastBoolSpellsHook({ + propState: "spellsAlwaysPrepared", + propIsBool: "isAlwaysPrepared" + }); + } + _render_addLastExpandedSpellsHook() { + return this._render_addLastBoolSpellsHook({ + propState: "spellsExpanded", + propIsBool: "isExpanded" + }); + } + _render_addLastAlwaysKnownSpellsHook() { + return this._render_addLastBoolSpellsHook({ + propState: "spellsAlwaysKnown", + propIsBool: "isAlwaysKnown" + }); + } + + _render_addLastBoolSpellsHook({propState, propIsBool}) { + const hk = ()=>{ + const formData = this.getFormData(); + const nxt = formData.data.filter(it=>it[propIsBool]).map(it=>it.uid.toLowerCase()); + + const setCurr = new Set(this._state[propState]); + const setNxt = new Set(nxt); + if (!CollectionUtil.setEq(setCurr, setNxt)) + this._state[propState] = nxt; + } + ; + this._addHookBase("ixSet", hk); + this._addHookBase("curLevel", hk); + this._addHookBase("targetLevel", hk); + this._addHookBase("spellLevelLow", hk); + this._addHookBase("spellLevelHigh", hk); + this._addHookBase("isAnyCantrips", hk); + this._addHookBase("pulseChoose", hk); + hk(); + } + + render($wrp) { + this._render_addLastAlwaysPreparedSpellsHook(); + this._render_addLastExpandedSpellsHook(); + this._render_addLastAlwaysKnownSpellsHook(); + + const $wrpOptionsButtons = $(`
    `); + const $wrpOptions = $(`
    `); + + for (let i = 0; i < this._additionalSpellsFlat.length; ++i) + this._render_renderOptions($wrpOptionsButtons, $wrpOptions, i); + + $$($wrp)` + ${$wrpOptionsButtons} + ${$wrpOptions} + `; + } + + _render_renderOptions($wrpOptionsButtons, $wrpOptions, ix) { + const additionalSpellsFlatBlock = this._additionalSpellsFlat[ix]; + + const $btnSelect = this._additionalSpellsFlat.length === 1 ? null : $(``).click(()=>this._state.ixSet = ix); + + const isInnatePreparedList = this._isAnyInnatePrepared(ix); + const isExpandedList = this._isAnyExpanded(ix); + + const sortedSpells = Object.values(additionalSpellsFlatBlock.spells).sort((a,b)=>SortUtil.ascSort(a.requiredLevel || 0, b.requiredLevel || 0) || SortUtil.ascSort(a.requiredCasterLevel || 0, b.requiredCasterLevel || 0)); + + const $wrpInnatePreparedHeaders = isInnatePreparedList ? $(`
    +
    Level
    +
    Spells
    +
    `) : null; + + const $wrpExpandedHeaders = isExpandedList ? $(`
    +
    Spell Level
    +
    Spells
    +
    `) : null; + + const $rowsInnatePrepared = isInnatePreparedList ? this._render_$getRows(ix, sortedSpells, { + isExpandedMatch: false + }) : null; + const $rowsExpanded = isExpandedList ? this._render_$getRows(ix, sortedSpells, { + isExpandedMatch: true + }) : null; + + const $wrpNoneAvailableInnatePrepared = isInnatePreparedList ? $(`
    No spells at this level
    `) : null; + const $wrpNoneAvailableExpanded = isExpandedList ? $(`
    No spells at this level
    `) : null; + + const hkSpellsAvailable = ()=>{ + const isInnatePreparedAvailable = !!this._getFlatInnatePreparedSpellsInRange(ix).length; + if ($wrpInnatePreparedHeaders) + $wrpInnatePreparedHeaders.toggleVe(isInnatePreparedAvailable); + if ($wrpNoneAvailableInnatePrepared) + $wrpNoneAvailableInnatePrepared.toggleVe(!isInnatePreparedAvailable); + + const isExpandedAvailable = !!this._getFlatExpandedSpellsInRange(ix).length; + if ($wrpExpandedHeaders) + $wrpExpandedHeaders.toggleVe(isExpandedAvailable); + if ($wrpNoneAvailableExpanded) + $wrpNoneAvailableExpanded.toggleVe(!isExpandedAvailable); + } + ; + this._addHookBase("spellLevelLow", hkSpellsAvailable); + this._addHookBase("spellLevelHigh", hkSpellsAvailable); + this._addHookBase("isAnyCantrips", hkSpellsAvailable); + this._addHookBase("curLevel", hkSpellsAvailable); + this._addHookBase("targetLevel", hkSpellsAvailable); + this._addHookBase("ixSet", hkSpellsAvailable); + hkSpellsAvailable(); + + const $stgInnatePrepared = isInnatePreparedList ? $$`
    +
    Innate/Prepared/Known Spells
    + ${$wrpInnatePreparedHeaders} + ${$rowsInnatePrepared} + ${$wrpNoneAvailableInnatePrepared} +
    ` : null; + + const $stgExpanded = isExpandedList ? $$`
    +
    Expanded Spell List
    + ${$wrpExpandedHeaders} + ${$rowsExpanded} + ${$wrpNoneAvailableExpanded} +
    ` : null; + + const isChooseAbility = this._isChooseAbility(ix); + + const $wrpChooseAbility = isChooseAbility ? this._render_$getSelChooseAbility(ix) : null; + + const $stgAbility = isChooseAbility ? $$`
    +
    Ability Score
    + ${$wrpChooseAbility} +
    ` : null; + + if ($btnSelect) + $wrpOptionsButtons.append($btnSelect); + + const $stg = $$`
    + ${$stgInnatePrepared} + ${$stgExpanded} + ${$stgAbility} +
    `.appendTo($wrpOptions); + + if (this._additionalSpellsFlat.length !== 1) { + const hkIsActive = ()=>{ + $btnSelect.toggleClass("active", this._state.ixSet === ix); + $stg.toggleVe(this._state.ixSet === ix); + } + ; + this._addHookBase("ixSet", hkIsActive); + hkIsActive(); + + const hkResetActive = (prop,value,prevValue)=>{ + const prevBlock = this._additionalSpellsFlat[prevValue]; + const nxtState = Object.values(prevBlock.spells).mergeMap(it=>({ + [it.key]: null + })); + this._proxyAssignSimple("state", nxtState); + } + ; + this._addHookBase("ixSet", hkResetActive); + } + } + + _getProps_chooseFrom({groupUid}) { + return { + propBase: `chooseFrom_${groupUid}`, + }; + } + + _render_$getRows(ix, spells, {isExpandedMatch}) { + if (!spells.length) + return null; + + const byLevel = {}; + spells.forEach(flat=>{ + if (flat.isExpanded !== isExpandedMatch) + return; + + const level = flat.requiredCasterLevel || flat.requiredLevel; + (byLevel[level] = byLevel[level] || []).push(flat); + } + ); + + const getFlatVars = flat=>({ + requiredLevel: flat.requiredLevel, + requiredCasterLevel: flat.requiredCasterLevel, + isRequiredCasterLevel: flat.requiredCasterLevel != null, + isRequiredLevel: flat.requiredLevel != null, + isExpanded: flat.isExpanded, + }); + + return Object.entries(byLevel).sort(([kA],[kB])=>SortUtil.ascSort(Number(kA), Number(kB))).map(([,flats])=>{ + const {requiredLevel, requiredCasterLevel, isRequiredCasterLevel, isRequiredLevel, isExpanded: isAnyExpanded, } = getFlatVars(flats[0]); + + const metasRenderedFlats = []; + + const chooseFromGroups = {}; + flats = flats.filter(flat=>{ + if (!flat.chooseFromGroup) + return true; + + chooseFromGroups[flat.chooseFromGroup] = chooseFromGroups[flat.chooseFromGroup] || { + from: [], + count: flat.chooseFromCount ?? 1, + ...getFlatVars(flat), + }; + chooseFromGroups[flat.chooseFromGroup].from.push(flat); + + return false; + } + ); + + const [flatsBasic,flatsFilter] = flats.segregate(it=>it.filterExpression == null); + flatsBasic.sort((a,b)=>SortUtil.ascSortLower(a.uid, b.uid)); + flatsFilter.sort((a,b)=>SortUtil.ascSortLower(a.filterExpression, b.filterExpression)); + + const $colSpells = $$`
    `; + + flatsBasic.forEach(flat=>{ + const $pt = $(`
    `).fastSetHtml(Renderer.get().render(`{@spell ${flat.uid.toSpellCase()}}`)).appendTo($colSpells); + const $sep = $(`
    ,
    `).appendTo($colSpells); + + metasRenderedFlats.push({ + flat, + $pt, + $sep, + ...getFlatVars(flat), + }); + } + ); + + const [flatsFilterChoose,flatsFilterAll] = flatsFilter.segregate(it=>it.type !== "all"); + + flatsFilterChoose.forEach(flat=>{ + const $dispSpell = $(`
    `); + const hkChosenSpell = ()=>{ + $dispSpell.html(this._state[flat.key] != null && this._state.ixSet === ix ? `
    ${Renderer.get().render(`{@spell ${this._state[flat.key].toLowerCase()}}`)}
    ` : `
    (select a spell)
    `, ); + } + ; + this._addHookBase(flat.key, hkChosenSpell); + if (this._additionalSpellsFlat.length !== 1) { + this._addHookBase("ixSet", hkChosenSpell); + } + hkChosenSpell(); + + const $btnFilter = $(``).click(async()=>{ + const selecteds = await this._modalFilterSpells.pGetUserSelection({ + filterExpression: flat.filterExpression + }); + if (selecteds == null || !selecteds.length) + return; + + const selected = selecteds[0]; + + this._state[flat.key] = DataUtil.proxy.getUid("spell", { + name: selected.name, + source: selected.values.sourceJson + }); + this._state.pulseChoose = !this._state.pulseChoose; + } + ); + + if (this._additionalSpellsFlat.length !== 1) { + const hkDisableBtnFilter = ()=>$btnFilter.prop("disabled", this._state.ixSet !== ix); + this._addHookBase("ixSet", hkDisableBtnFilter); + hkDisableBtnFilter(); + } + + const $pt = $$`
    ${$btnFilter}${$dispSpell}
    `.appendTo($colSpells); + const $sep = $(`
    ,
    `).appendTo($colSpells); + + metasRenderedFlats.push({ + flat, + $pt, + $sep, + ...getFlatVars(flat), + }); + } + ); + + flatsFilterAll.forEach(flat=>{ + const ptFilter = this._modalFilterSpells.getRenderedFilterExpression({ + filterExpression: flat.filterExpression + }); + const $pt = $$`
    Spells matching: ${ptFilter ? `${ptFilter} ` : ""}
    `.appendTo($colSpells); + const $sep = $(`
    ,
    `).appendTo($colSpells); + + metasRenderedFlats.push({ + flat, + $pt, + $sep, + ...getFlatVars(flat), + }); + } + ); + + Object.entries(chooseFromGroups).forEach(([groupUid,group])=>{ + const {propBase} = this._getProps_chooseFrom({ + groupUid + }); + + const meta = ComponentUiUtil.getMetaWrpMultipleChoice(this, propBase, { + values: group.from.map(it=>it.uid), + fnDisplay: v=>Renderer.get().render(`{@spell ${v}}`), + count: group.count, + }, ); + + const hkPulse = ()=>this._state.pulseChoose = !this._state.pulseChoose; + this._addHookBase(meta.propPulse, hkPulse); + + if (this._additionalSpellsFlat.length !== 1) { + const hkDisableUi = ()=>{ + meta.rowMetas.forEach(({$cb})=>$cb.prop("disabled", this._state.ixSet !== ix)); + } + ; + this._addHookBase("ixSet", hkDisableUi); + hkDisableUi(); + } + + const $ptsInline = meta.rowMetas.map(({$cb, displayValue})=>{ + return $$`
    ${displayValue}${$cb.addClass("ml-1")}
    `; + } + ); + + const $pt = $$`
    Choose ${group.count === 1 ? "" : `${group.count} `}from:${$ptsInline}
    `.appendTo($colSpells); + const $sep = $(`
    ,
    `).appendTo($colSpells); + + metasRenderedFlats.push({ + flat: null, + $pt, + $sep, + ...group, + }); + } + ); + + const $row = $$`
    +
    ${Parser.getOrdinalForm(requiredCasterLevel || requiredLevel) || `Current`}
    + ${$colSpells} +
    `; + + const doShowInitialPts = ()=>{ + metasRenderedFlats.forEach(({$pt, $sep},i)=>{ + $pt.showVe(); + $sep.toggleVe(i !== metasRenderedFlats.length - 1); + } + ); + } + ; + + const doShowExpandedPts = ()=>{ + let isExpandedVisible; + let isAllVisible; + if (isRequiredCasterLevel) { + isExpandedVisible = this._isRequiredCasterLevelInRangeUpper(requiredCasterLevel); + isAllVisible = this._isRequiredCasterLevelInRange(requiredCasterLevel); + } else if (isRequiredLevel) { + isExpandedVisible = this._isRequiredLevelInRangeUpper(requiredCasterLevel); + isAllVisible = this._isRequiredLevelInRange(requiredCasterLevel); + } else + throw new Error(`No need to use this method!`); + + if (isAllVisible || !isExpandedVisible) + return doShowInitialPts(); + + metasRenderedFlats.forEach((meta,i)=>{ + meta.$pt.toggleVe(meta.isExpanded); + meta.$sep.toggleVe(i !== metasRenderedFlats.length - 1); + } + ); + + let isFoundFirst = false; + for (let i = metasRenderedFlats.length - 1; i >= 0; --i) { + const meta = metasRenderedFlats[i]; + + meta.$sep.hideVe(); + if (!meta.isExpanded) + continue; + + if (isFoundFirst) { + meta.$sep.showVe(); + break; + } + + isFoundFirst = true; + } + } + ; + + if (isRequiredCasterLevel) { + const hkLevel = ()=>{ + const isVisible = isAnyExpanded ? this._isRequiredCasterLevelInRangeUpper(requiredCasterLevel) : this._isRequiredCasterLevelInRange(requiredCasterLevel); + $row.toggleVe(isVisible); + if (!isVisible || !isAnyExpanded) + return doShowInitialPts(); + doShowExpandedPts(); + } + ; + this._addHookBase("spellLevelLow", hkLevel); + this._addHookBase("spellLevelHigh", hkLevel); + this._addHookBase("isAnyCantrips", hkLevel); + hkLevel(); + } else if (isRequiredLevel) { + const hkLevel = ()=>{ + const isVisible = isAnyExpanded ? this._isRequiredLevelInRangeUpper(requiredLevel) : this._isRequiredLevelInRange(requiredLevel); + $row.toggleVe(isVisible); + if (!isVisible && !isAnyExpanded) + return doShowInitialPts(); + doShowExpandedPts(); + } + ; + this._addHookBase("curLevel", hkLevel); + this._addHookBase("targetLevel", hkLevel); + hkLevel(); + } else { + $row.showVe(); + doShowInitialPts(); + } + + return $row; + } + ); + } + + _render_$getSelChooseAbility(ix) { + return ComponentUiUtil.$getSelEnum(this, "ability", { + values: this._additionalSpells[ix].ability.choose, + fnDisplay: abv=>Parser.attAbvToFull(abv), + isAllowNull: true, + }, ); + } + + _isRequiredLevelInRange(requiredLevel) { + return this._isRequiredLevelInRangeLower(requiredLevel) && this._isRequiredLevelInRangeUpper(requiredLevel); + } + + _isRequiredLevelInRangeLower(requiredLevel) { + return requiredLevel > (this._state.curLevel ?? Number.MAX_SAFE_INTEGER); + } + + _isRequiredLevelInRangeUpper(requiredLevel) { + return requiredLevel <= (this._state.targetLevel ?? Number.MIN_SAFE_INTEGER); + } + + _isRequiredCasterLevelInRange(requiredCasterLevel) { + if (requiredCasterLevel === 0) + return this._state.isAnyCantrips; + + return this._isRequiredCasterLevelInRangeLower(requiredCasterLevel) && this._isRequiredCasterLevelInRangeUpper(requiredCasterLevel); + } + + _isRequiredCasterLevelInRangeLower(requiredCasterLevel) { + if (requiredCasterLevel === 0) + return this._state.isAnyCantrips; + + return requiredCasterLevel >= (this._state.spellLevelLow ?? Number.MAX_SAFE_INTEGER); + } + + _isRequiredCasterLevelInRangeUpper(requiredCasterLevel) { + if (requiredCasterLevel === 0) + return this._state.isAnyCantrips; + + return requiredCasterLevel <= (this._state.spellLevelHigh == null ? Number.MIN_SAFE_INTEGER : this._state.spellLevelHigh); + } + + _getFlatSpellsInRange(ixSet=null, {isExpandedMatch=null}={}) { + if (ixSet == null) + ixSet = this._state.ixSet; + + return Object.values((this._additionalSpellsFlat[ixSet] || { + spells: [] + }).spells).filter(flat=>{ + if (isExpandedMatch != null) { + if (flat.isExpanded !== isExpandedMatch) + return false; + } + + if (flat.isExpanded) { + if (flat.requiredCasterLevel != null) + return this._isRequiredCasterLevelInRangeUpper(flat.requiredCasterLevel); + else if (flat.requiredLevel != null) + return this._isRequiredLevelInRangeUpper(flat.requiredLevel); + return true; + } + + if (flat.requiredCasterLevel != null) + return this._isRequiredCasterLevelInRange(flat.requiredCasterLevel); + else if (flat.requiredLevel != null) + return this._isRequiredLevelInRange(flat.requiredLevel); + return true; + } + ); + } + + _getFlatInnatePreparedSpellsInRange(ixSet) { + return this._getFlatSpellsInRange(ixSet, { + isExpandedMatch: false + }); + } + _getFlatExpandedSpellsInRange(ixSet) { + return this._getFlatSpellsInRange(ixSet, { + isExpandedMatch: true + }); + } + + _isAnyInnatePrepared(ixSet) { + return this._isAnyInnatePreparedExpanded(ixSet, { + isExpandedMatch: false + }); + } + _isAnyExpanded(ixSet) { + return this._isAnyInnatePreparedExpanded(ixSet, { + isExpandedMatch: true + }); + } + + _isAnyInnatePreparedExpanded(ixSet, {isExpandedMatch}) { + if (ixSet == null) + ixSet = this._state.ixSet; + + return Object.values((this._additionalSpellsFlat[ixSet] || { + spells: [] + }).spells).some(flat=>flat.isExpanded === isExpandedMatch); + } + + _isChooseAbility(ixSet) { + if (ixSet == null) + ixSet = this._state.ixSet; + return (this._additionalSpells[ixSet]?.ability?.choose?.length ?? 0) > 1; + } + + isNoChoice({curLevel, targetLevel, isStandalone}={}) { + return this.constructor.isNoChoice(this._additionalSpells, { + additionalSpellsFlat: this._additionalSpellsFlat, + curLevel, + targetLevel, + isStandalone + }); + } + + getFormData() { + let flatSpellsInRange = this._getFlatSpellsInRange().map(it=>it.getCopy()); + + const chooseFromGroups = {}; + flatSpellsInRange.forEach(flat=>{ + if (!flat.chooseFromGroup) + return; + + chooseFromGroups[flat.chooseFromGroup] = chooseFromGroups[flat.chooseFromGroup] || { + from: [], + selectedValues: [], + isAcceptable: false, + count: flat.chooseFromCount ?? 1, + }; + chooseFromGroups[flat.chooseFromGroup].from.push(flat); + } + ); + + Object.entries(chooseFromGroups).forEach(([groupUid,groupMeta])=>{ + const {propBase} = this._getProps_chooseFrom({ + groupUid + }); + + groupMeta.isAcceptable = this._state[ComponentUiUtil.getMetaWrpMultipleChoice_getPropIsAcceptable(propBase)]; + + groupMeta.selectedValues = ComponentUiUtil.getMetaWrpMultipleChoice_getSelectedValues(this, propBase, { + values: groupMeta.from.map(it=>it.uid) + }); + } + ); + + let cntNotChosen = 0; + flatSpellsInRange = flatSpellsInRange.filter(flat=>{ + if (flat.type === "all") + return true; + if (flat.filterExpression != null) { + const choiceMade = this._state[flat.key]; + if (!choiceMade) { + cntNotChosen++; + return false; + } + + flat.filterExpression = null; + flat.uid = this._state[flat.key]; + return true; + } + + if (flat.chooseFromGroup != null) { + return chooseFromGroups[flat.chooseFromGroup].selectedValues.includes(flat.uid); + } + + return true; + } + ); + + flatSpellsInRange = flatSpellsInRange.flatMap(flat=>{ + if (flat.type !== "all") + return flat; + + if (flat.filterExpression != null) { + const filterExpression = flat.filterExpression; + flat.filterExpression = null; + return this._modalFilterSpells.getItemsMatchingFilterExpression({ + filterExpression + }).map((li,i)=>flat.getCopy({ + type: "spell", + key: `${flat.key}__${i}`, + uid: DataUtil.proxy.getUid("spell", { + name: li.name, + source: li.values.sourceJson + }), + }, )); + } + + if (flat.chooseFromGroup != null) { + if (chooseFromGroups[flat.chooseFromGroup].selectedValues.includes(flat.uid)) + return flat; + } + + return null; + } + ).filter(Boolean); + + let abilityAbv; + if (this._isChooseAbility(this._state.ixSet)) { + abilityAbv = this._state.ability; + if (abilityAbv == null) + cntNotChosen++; + } else + abilityAbv = (this._additionalSpellsFlat[this._state.ixSet] || { + meta: {} + }).meta.ability; + + return { + isFormComplete: cntNotChosen === 0 && Object.values(chooseFromGroups).every(it=>it.isAcceptable), + data: flatSpellsInRange.map(it=>it.toObject()), + abilityAbv, + }; + } + + pGetFormData() { + return this.getFormData(); + } + + _getDefaultState() { + return { + ixSet: 0, + + curLevel: null, + targetLevel: null, + spellLevelLow: null, + spellLevelHigh: null, + isAnyCantrips: false, + + spellsAlwaysPrepared: [], + spellsExpanded: [], + spellsAlwaysKnown: [], + + ability: null, + + pulseChoose: false, + }; + } +} +class Charactermancer_Class_LevelSelect extends BaseComponent { + static async pGetUserInput(opts) { + return UtilApplications.pGetImportCompApplicationFormData({ + comp: new this(opts), + isUnskippable: true, + fnGetInvalidMeta: (formData)=>{ + if (formData.data.length === 0) + return { + type: "error", + message: `Please select some levels first!` + }; + } + , + isAutoResize: true, + width: 640, + }); + } + + constructor(opts) { + super(); + + this._isSubclass = !!opts.isSubclass; + this._isRadio = !!opts.isRadio; + this._isForceSelect = !!opts.isForceSelect; + this._featureArr = this.constructor._getLevelGroupedFeatures(opts.features, this._isSubclass); + this._maxPreviousLevel = opts.maxPreviousLevel || 0; + + this._list = null; + this._listSelectClickHandler = null; + + this._fnsOnChange = []; + } + + get modalTitle() { + return `Select ${this._isSubclass ? "Subclass" : "Class"} Levels`; + } + + onchange(fn) { + this._fnsOnChange.push(fn); + } + + _doRunFnsOnchange() { + this._fnsOnChange.forEach(fn=>fn()); + } + + setFeatures(features) { + this._featureArr = this.constructor._getLevelGroupedFeatures(features, this._isSubclass); + this._list.items.forEach(it=>it.data.fnUpdateRowText()); + } + + render($wrp) { + const $cbAll = this._isRadio ? null : $(``); + const $wrpList = $(`
    `); + + this._list = new List({ $wrpList: $wrpList, fnSort: null, isUseJquery: true, }); + + this._listSelectClickHandler = new ListSelectClickHandler({ list: this._list }); + + for (let ix = 0; ix < this._featureArr.length; ++ix) { + const $cb = this._render_$getCbRow(ix); + + const $dispFeatures = $(``); + const fnUpdateRowText = ()=>$dispFeatures.text(this.constructor._getRowText(this._featureArr[ix])); + fnUpdateRowText(); + + const $li = $$``.click((evt)=>{ + this._handleSelectClick(listItem, evt); + } + ); + + const listItem = new ListItem(ix,$li,"",{},{ cbSel: $cb[0], fnUpdateRowText, },); + this._list.addItem(listItem); + } + + if (!this._isRadio) { this._listSelectClickHandler.bindSelectAllCheckbox($cbAll); } + + this._list.init(); + + $$`
    +
    + + + +
    + + ${$wrpList} +
    `.appendTo($wrp); + } + + _render_$getCbRow(ix) { + if (!this._isRadio) { return $(``); } + + const $cb = $(``); + + if(!SETTINGS.LOCK_EXISTING_CHOICES){ + //If we dont want to be hardlocked into a level after we loaded a save file, we can avoid disabling checkboxes of a lower level + if (ix === this._maxPreviousLevel && this._isForceSelect) {$cb.prop("checked", true); } + + return $cb; + } + + if (ix === this._maxPreviousLevel && this._isForceSelect) {$cb.prop("checked", true); } + else if (ix < this._maxPreviousLevel){$cb.prop("disabled", true);} + + return $cb; + } + + _handleSelectClick(listItem, evt) { + if (!this._isRadio) { return this._listSelectClickHandler.handleSelectClick(listItem, evt);} + + const isCheckedOld = listItem.data.cbSel.checked; + + const isDisabled = this._handleSelectClickRadio(this._list, listItem, evt); + if (isDisabled) { return; } + + const isCheckedNew = listItem.data.cbSel.checked; + if (isCheckedOld !== isCheckedNew) {this._doRunFnsOnchange();} + } + + /** + * @param {any} list list of elements, each one has a radio button + * @param {any} item the element with our radio button + * @param {any} evt the click event + * @returns {boolean} true if the radio button is disabled + */ + _handleSelectClickRadio(list, item, evt) { + evt.preventDefault(); + evt.stopPropagation(); + + if (item.data.cbSel.disabled) { return true; } + + list.items.forEach(it=>{ + if (it === item) { //For the radio button we clicked + //You can uncheck radio buttons if forceSelect is off + if (it.data.cbSel.checked && !this._isForceSelect) { + it.data.cbSel.checked = false; + it.ele.removeClass("list-multi-selected"); + return; + } + + //Otherwise just check button + it.data.cbSel.checked = true; + it.ele.addClass("list-multi-selected"); + } + else { //For the radio buttons we didnt click + //Make sure they are unchecked + it.data.cbSel.checked = false; + //But also mark all levels lower than the one we clicked as selected (grayed) + if (it.ix < item.ix) { it.ele.addClass("list-multi-selected");} + else { it.ele.removeClass("list-multi-selected"); } + } + }); + } + + pGetFormData() { + let out = this._list.items.filter(it=>it.data.cbSel.checked).map(it=>it.ix); + + if (this._isRadio && out.length) { + const max = out[0] + 1; + out = []; + for (let i = this._maxPreviousLevel; i < max; ++i) + out.push(i); + } + + return { + isFormComplete: !!out.length, + data: out, + }; + } + + getCurLevel() { + if (this._maxPreviousLevel) { return this._maxPreviousLevel; } + return 0; + } + + getTargetLevel() { + const ixs = this._list.items.filter(it=>it.data.cbSel.checked).map(it=>it.ix); + if (!ixs.length) { return null; } + return Math.max(...ixs) + 1; + } + + static _getRowText(lvl) { + return lvl.map(f=>f.tableDisplayName || f.name).join(", ") || "\u2014"; + } + + static _getLevelGroupedFeatures(allFeatures, isSubclass) { + allFeatures = MiscUtil.copy(allFeatures); + if (!isSubclass) + allFeatures = allFeatures.filter(it=>it.classFeature); + const allFeaturesByLevel = []; + + let level = 1; + let stack = []; + const output = ()=>{ + allFeaturesByLevel.push(stack); + stack = []; + } + ; + allFeatures.forEach(f=>{ + while (level < f.level) { + output(); + level++; + } + stack.push(f); + level = f.level; + } + ); + output(); + + while (level < Consts.CHAR_MAX_LEVEL) { + output(); + level++; + } + + return allFeaturesByLevel; + } +} +class Charactermancer_Class_ProficiencyImportModeSelect extends BaseComponent { + static async pGetUserInput() { + return UtilApplications.pGetImportCompApplicationFormData({ + comp: new this(), + isUnskippable: true, + isAutoResize: true, + }); + } + + pGetFormData() { + return { + isFormComplete: true, + data: this._state.mode, + }; + } + + get modalTitle() { + return `Select Class Proficiency Import Mode`; + } + + render($wrp) { + const $sel = ComponentUiUtil.$getSelEnum(this, "mode", { + values: [Charactermancer_Class_ProficiencyImportModeSelect.MODE_MULTICLASS, Charactermancer_Class_ProficiencyImportModeSelect.MODE_PRIMARY, Charactermancer_Class_ProficiencyImportModeSelect.MODE_NONE, ], + fnDisplay: mode=>Charactermancer_Class_ProficiencyImportModeSelect.DISPLAY_MODES[mode], + }, ); + + $$`
    + ${$sel} +
    `.appendTo($wrp); + } + + _getDefaultState() { + return { + mode: Charactermancer_Class_ProficiencyImportModeSelect.MODE_MULTICLASS, + }; + } +} +Charactermancer_Class_ProficiencyImportModeSelect.MODE_MULTICLASS = 0; +Charactermancer_Class_ProficiencyImportModeSelect.MODE_PRIMARY = 1; +Charactermancer_Class_ProficiencyImportModeSelect.MODE_NONE = 2; +Charactermancer_Class_ProficiencyImportModeSelect.DISPLAY_MODES = { + [Charactermancer_Class_ProficiencyImportModeSelect.MODE_MULTICLASS]: "Add multiclass proficiencies (this is my second+ class)", + [Charactermancer_Class_ProficiencyImportModeSelect.MODE_PRIMARY]: "Add base class proficiencies and equipment (this is my first class)", + [Charactermancer_Class_ProficiencyImportModeSelect.MODE_NONE]: "Do not add proficiencies or equipment", +}; + +class Charactermancer_Class_StartingProficiencies extends BaseComponent { + + constructor({featureSourceTracker, primaryProficiencies, multiclassProficiencies, savingThrowsProficiencies, existingProficienciesVetArmor, existingProficienciesVetWeapons, existingProficienciesVetSavingThrows, existingProficienciesFvttArmor, existingProficienciesFvttWeapons, existingProficienciesFvttSavingThrows, existingProficienciesCustomArmor, existingProficienciesCustomWeapons, }={}, ) { + super(); + this._featureSourceTracker = featureSourceTracker; + this._primaryProficiencies = Charactermancer_Class_StartingProficiencies._getCleanVetProfs(primaryProficiencies); + this._multiclassProficiencies = Charactermancer_Class_StartingProficiencies._getCleanVetProfs(multiclassProficiencies); + this._savingThrowsProficiencies = savingThrowsProficiencies; + + this._existingProficienciesVetArmor = existingProficienciesVetArmor; + this._existingProficienciesVetWeapons = existingProficienciesVetWeapons; + this._existingProficienciesVetSavingThrows = existingProficienciesVetSavingThrows; + + this._existingProficienciesCustomArmor = existingProficienciesCustomArmor; + this._existingProficienciesCustomWeapons = existingProficienciesCustomWeapons; + this._existingProficienciesFvttArmor = existingProficienciesFvttArmor ? MiscUtil.copy(existingProficienciesFvttArmor) : null; + this._existingProficienciesFvttWeapons = existingProficienciesFvttWeapons ? MiscUtil.copy(existingProficienciesFvttWeapons) : null; + this._existingProficienciesFvttSavingThrows = existingProficienciesFvttSavingThrows ? MiscUtil.copy(existingProficienciesFvttSavingThrows) : null; + } + + + /** + * @param {any} {featureSourceTracker + * @param {any} primaryProficiencies + * @param {any} multiclassProficiencies + * @param {any} savingThrowsProficiencies + * @param {any} mode + * @param {any} existingProficienciesFvttArmor + * @param {any} existingProficienciesFvttWeapons + * @param {any} existingProficienciesFvttSavingThrows + * @param {any} }={} + * @returns {Charactermancer_Class_StartingProficiencies} + */ + static get({featureSourceTracker, primaryProficiencies, multiclassProficiencies, savingThrowsProficiencies, mode, existingProficienciesFvttArmor, existingProficienciesFvttWeapons, existingProficienciesFvttSavingThrows, }={}, ) { + const {existingProficienciesVetArmor, existingProficienciesCustomArmor, + existingProficienciesVetWeapons, existingProficienciesCustomWeapons, + existingProficienciesVetSavingThrows, } = this._getExistingProficienciesVet({ + existingProficienciesFvttArmor, + existingProficienciesFvttWeapons, + existingProficienciesFvttSavingThrows, + }); + + const comp = new this({ + featureSourceTracker, + primaryProficiencies, + multiclassProficiencies, + savingThrowsProficiencies, + existingProficienciesVetArmor, + existingProficienciesVetWeapons, + existingProficienciesVetSavingThrows, + + existingProficienciesCustomArmor, + existingProficienciesCustomWeapons, + + existingProficienciesFvttArmor, + existingProficienciesFvttWeapons, + existingProficienciesFvttSavingThrows, + }); + + if (mode != null) + comp.mode = mode; + + return comp; + } + + static async pGetUserInput({featureSourceTracker, primaryProficiencies, multiclassProficiencies, savingThrowsProficiencies, mode, existingProficienciesFvttArmor, existingProficienciesFvttWeapons, existingProficienciesFvttSavingThrows, }={}, ) { + return this.get({ + featureSourceTracker, + primaryProficiencies, + multiclassProficiencies, + savingThrowsProficiencies, + mode, + existingProficienciesFvttArmor, + existingProficienciesFvttWeapons, + existingProficienciesFvttSavingThrows, + }).pGetFormData(); + } + + static applyFormDataToActorUpdate(actUpdate, formData) { + MiscUtil.getOrSet(actUpdate, "system", "traits", {}); + + this._applyFormDataToActorUpdate_applyProfList({ + actUpdate, + profList: formData?.data?.armor || [], + profsExisting: formData?.existingDataFvtt?.existingProficienciesArmor || {}, + propTrait: "armorProf", + fnGetMapped: UtilActors.getMappedArmorProficiency.bind(UtilActors), + }); + + this._applyFormDataToActorUpdate_applyProfList({ + actUpdate, + profList: formData.data?.weapons || [], + profsExisting: formData?.existingDataFvtt?.existingProficienciesWeapons || {}, + propTrait: "weaponProf", + fnGetMapped: UtilActors.getMappedWeaponProficiency.bind(UtilActors), + fnGetPreMapped: UtilActors.getItemUIdFromWeaponProficiency.bind(UtilActors), + }); + + const tgtAbils = MiscUtil.getOrSet(actUpdate, "system", "abilities", {}); + [...(formData.data?.savingThrows || []), ...(formData.existingDataFvtt?.savingThrows || [])].forEach(abv=>(tgtAbils[abv] = tgtAbils[abv] || {}).proficient = 1); + } + + static _applyFormDataToActorUpdate_addIfNotExists(arr, itm) { + if (!arr.some(it=>it.toLowerCase().trim() === itm.toLowerCase().trim())) + arr.push(itm); + } + + static _applyFormDataToActorUpdate_applyProfList({actUpdate, profList, profsExisting, propTrait, fnGetMapped, fnGetPreMapped, }, ) { + if (!profList?.length) + return; + + const tgt = MiscUtil.getOrSet(actUpdate, "system", "traits", propTrait, {}); + tgt.value = tgt.value || []; + tgt.custom = tgt.custom || ""; + + const customArr = tgt.custom.split(";").map(it=>it.trim()).filter(Boolean); + + (profsExisting.value || []).forEach(it=>this._applyFormDataToActorUpdate_addIfNotExists(tgt.value, it)); + + (profsExisting.custom || "").split(";").map(it=>it.trim()).filter(Boolean).forEach(it=>this._applyFormDataToActorUpdate_addIfNotExists(customArr, it)); + + profList.forEach(it=>{ + const clean = (fnGetPreMapped ? fnGetPreMapped(it) : null) ?? Renderer.stripTags(it).toLowerCase(); + const mapped = fnGetMapped(clean); + if (mapped) + return this._applyFormDataToActorUpdate_addIfNotExists(tgt.value, mapped); + + const [itemTag] = /{@item [^}]+}/i.exec(it) || []; + if (itemTag) { + const mappedAlt = fnGetMapped(Renderer.stripTags(itemTag)); + if (mappedAlt) + return this._applyFormDataToActorUpdate_addIfNotExists(tgt.value, mappedAlt); + } + + this._applyFormDataToActorUpdate_addIfNotExists(customArr, Renderer.stripTags(it)); + } + ); + + tgt.custom = customArr.join("; "); + } + + static getExistingProficienciesFvttSavingThrows(actor) { + return Object.entries(MiscUtil.get(actor, "_source", "system", "abilities") || {}).filter(([,abMeta])=>abMeta.proficient).map(([ab])=>ab); + } + + static _getExistingProficienciesVet({existingProficienciesFvttArmor, existingProficienciesFvttWeapons, existingProficienciesFvttSavingThrows}) { + const vetValidWeapons = new Set(); + const customWeapons = new Set(); + const vetValidArmors = new Set(); + const customArmors = new Set(); + + this._getExistingProficienciesVet_({ + existingFvtt: existingProficienciesFvttWeapons, + fnGetUnmapped: UtilActors.getUnmappedWeaponProficiency.bind(UtilActors), + fnCheckUnmappedAlt: UtilActors.getItemUIdFromWeaponProficiency.bind(UtilActors), + vetValidSet: vetValidWeapons, + customSet: customWeapons, + }); + + this._getExistingProficienciesVet_({ + existingFvtt: existingProficienciesFvttArmor, + fnGetUnmapped: UtilActors.getUnmappedArmorProficiency.bind(UtilActors), + vetValidSet: vetValidArmors, + customSet: customArmors, + }); + + return { + existingProficienciesVetWeapons: [...vetValidWeapons], + existingProficienciesCustomWeapons: [...customWeapons], + existingProficienciesVetArmor: [...vetValidArmors], + existingProficienciesCustomArmor: [...customArmors], + existingProficienciesVetSavingThrows: existingProficienciesFvttSavingThrows, + }; + } + + static _getExistingProficienciesVet_({existingFvtt, vetValidSet, customSet, fnGetUnmapped, fnCheckUnmappedAlt, }) { + (existingFvtt?.value || []).forEach(it=>{ + const unmapped = fnGetUnmapped(it); + if (unmapped) + vetValidSet.add(unmapped); + else { + if (fnCheckUnmappedAlt) { + const unmappedVet = fnCheckUnmappedAlt(it); + if (unmappedVet) + vetValidSet.add(it); + else + customSet.add(it); + } else { + customSet.add(it); + } + } + } + ); + + (existingFvtt?.custom || "").trim().split(";").map(it=>it.trim()).filter(Boolean).forEach(it=>{ + const low = it.toLowerCase(); + const unmapped = fnGetUnmapped(low); + if (unmapped) + vetValidSet.add(unmapped); + else { + if (fnCheckUnmappedAlt) { + const unmappedVet = fnCheckUnmappedAlt(low); + if (unmappedVet) + vetValidSet.add(low); + else + customSet.add(it); + } else { + customSet.add(it); + } + } + } + ); + } + + static _getCleanVetProfs(vetProfs) { + if (!vetProfs) + return {}; + + const out = {}; + + if (vetProfs.armor) + out.armor = this._getCleanVetProfs_getMappedItemTags(vetProfs.armor.map(it=>it.proficiency || it)); + if (vetProfs.weapons) + out.weapons = this._getCleanVetProfs_getMappedItemTags(vetProfs.weapons.map(it=>(it.proficiency || it).toLowerCase().trim())); + + return out; + } + + static _getCleanVetProfs_getMappedItemTags(arr) { + return arr.map(it=>it.replace(/^{@item ([^}]+)}$/g, (...m)=>{ + const [name,source] = Renderer.splitTagByPipe(m[1]); + return `${name}|${source || Parser.SRC_DMG}`.toLowerCase(); + } + )); + } + + + + set mode(mode) { + this._state.mode = mode; + } + + _getFormData() { + const isPrimary = this._state.mode === Charactermancer_Class_ProficiencyImportModeSelect.MODE_PRIMARY; + const profs = isPrimary ? this._primaryProficiencies : this._multiclassProficiencies; + + if (!profs) + return { + isFormComplete: true, + data: {}, + existingData: {} + }; + + return { + isFormComplete: true, + data: { + armor: profs.armor || [], + weapons: profs.weapons || [], + savingThrows: isPrimary ? (this._savingThrowsProficiencies || []) : [], + }, + existingDataFvtt: { + existingProficienciesArmor: this._existingProficienciesFvttArmor, + existingProficienciesWeapons: this._existingProficienciesFvttWeapons, + existingProficienciesSavingThrows: this._existingProficienciesFvttSavingThrows, + }, + }; + } + + pGetFormData() { + return this._getFormData(); + } + + render($wrp) { + const $wrpDisplay = $(`
    `).appendTo($wrp); + + const fnsCleanup = []; + + const hkMode = ()=>{ + fnsCleanup.forEach(fn=>fn()); + fnsCleanup.splice(0, fnsCleanup.length); + + $wrpDisplay.empty(); + const isPrimary = this._state.mode === Charactermancer_Class_ProficiencyImportModeSelect.MODE_PRIMARY; + + + const profs = isPrimary ? this._primaryProficiencies : this._multiclassProficiencies; + if (profs) { + this._render_profType({ + profList: profs.armor, + title: "Armor", + $wrpDisplay, + propTracker: "armorProficiencies", + propTrackerPulse: "pulseArmorProficiencies", + fnsCleanup, + existing: this._existingProficienciesVetArmor, + existingProficienciesCustom: this._existingProficienciesCustomArmor, + fnDisplay: str=>["light", "medium", "heavy"].includes(str) ? `${str} armor` : str.includes("|") ? `{@item ${str}}` : str, + }); + + this._render_profType({ + profList: profs.weapons, + title: "Weapons", + $wrpDisplay, + propTracker: "weaponProficiencies", + propTrackerPulse: "pulseWeaponProficiencies", + fnsCleanup, + existing: this._existingProficienciesVetWeapons, + existingProficienciesCustom: this._existingProficienciesCustomWeapons, + fnDisplay: str=>["simple", "martial"].includes(str) ? `${str} weapons` : str.includes("|") ? `{@item ${str}}` : str, + }); + } + + if (isPrimary && this._savingThrowsProficiencies) { + this._render_profType({ + profList: this._savingThrowsProficiencies, + title: "Saving Throws", + $wrpDisplay, + propTracker: "savingThrowProficiencies", + propTrackerPulse: "pulseSavingThrowProficiencies", + fnsCleanup, + existing: this._existingProficienciesVetSavingThrows, + fnDisplay: str=>Parser.attAbvToFull(str), + }); + } + + if (this._featureSourceTracker) {this._featureSourceTracker.setState(this, this._getStateTrackerData());} + }; + this._addHookBase("mode", hkMode); + hkMode(); + } + + _getStateTrackerData() { + const formData = this._getFormData(); + + const getNoTags = (arr)=>arr.map(it=>this.constructor._getUid(it)).filter(Boolean); + + return { + armorProficiencies: getNoTags(formData.data?.armor || []).mergeMap(it=>({ + [it]: true + })), + weaponProficiencies: getNoTags(formData.data?.weapons || []).mergeMap(it=>({ + [it]: true + })), + }; + } + + static _getUid(str) { + if (!str.startsWith("{@item")) + return str; + + let[name,source] = Renderer.splitTagByPipe((Renderer.splitFirstSpace(str.slice(1, -1))[1] || "").toLowerCase()); + source = source || Parser.SRC_DMG.toLowerCase(); + if (!name) + return null; + + return `${name}|${source}`; + } + + _render_profType({profList, title, $wrpDisplay, propTracker, propTrackerPulse, fnsCleanup, existing, existingProficienciesCustom, fnDisplay}) { + if (!profList?.length) + return; + + const profListUids = profList.map(prof=>this.constructor._getUid(prof)); + + const $ptsExisting = {}; + + const $wrps = profList.map((it,i)=>{ + const $ptExisting = $(`
    `); + const uid = profListUids[i]; + $ptsExisting[uid] = $ptExisting; + const isNotLast = i < profList.length - 1; + return $$`
    ${Renderer.get().render(fnDisplay ? fnDisplay(it) : it)}${$ptExisting}${isNotLast ? `,` : ""}
    `; + } + ); + + $$`
    +
    ${title}:
    ${$wrps} +
    `.appendTo($wrpDisplay); + + const pHkUpdatePtsExisting = async()=>{ + try { + await this._pLock("updateExisting"); + await pHkUpdatePtsExisting_(); + } + finally { + this._unlock("updateExisting"); + } + }; + + const pHkUpdatePtsExisting_ = async()=>{ + const otherStates = this._featureSourceTracker ? this._featureSourceTracker.getStatesForKey(propTracker, { + ignore: this + }) : null; + + for (const v of profListUids) { + if (!$ptsExisting[v]) + return; + + const parentGroup = await UtilDataConverter.pGetItemWeaponType(v); + + let isExisting = (existing || []).includes(v) || (parentGroup && (existing || []).includes(parentGroup)) || (existingProficienciesCustom || []).includes(v) || (parentGroup && (existingProficienciesCustom || []).includes(parentGroup)); + + isExisting = isExisting || (otherStates || []).some(otherState=>!!otherState[v] || (parentGroup && !!otherState[parentGroup])); + + $ptsExisting[v].title(isExisting ? "Proficient from Another Source" : "").toggleClass("ml-1", isExisting).html(isExisting ? `()` : ""); + } + }; + if (this._featureSourceTracker) { + this._featureSourceTracker.addHook(this, propTrackerPulse, pHkUpdatePtsExisting); + fnsCleanup.push(()=>this._featureSourceTracker.removeHook(this, propTrackerPulse, pHkUpdatePtsExisting)); + } + pHkUpdatePtsExisting(); + } + + _getDefaultState() { + return { + mode: Charactermancer_Class_ProficiencyImportModeSelect.MODE_PRIMARY, + }; + } +} + +ActorCharactermancerClass.ExistingClassMeta = class { + constructor({ + item: item, + ixClass: ixClass, + isUnknownClass: isUnknownClass, + ixSubclass: ixSubclass, + isUnknownSubclass: isUnknownSubclass, + level: level, + isPrimary: isPrimary, + spellSlotLevelSelection: spellSlotLevelSelection + }) { + this.item = item; + this.ixClass = ixClass; + this.isUnknownClass = isUnknownClass; + this.ixSubclass = ixSubclass; + this.isUnknownSubclass = isUnknownSubclass; + this.level = level; + this.isPrimary = isPrimary; + this.spellSlotLevelSelection = spellSlotLevelSelection; + if (isNaN(this.level)) { this.level = 0; } + } + }; + +//#endregion + +//#region Charactermancer Ability +/**The panel that handles adjusting ability scores */ +class ActorCharactermancerAbility extends ActorCharactermancerBaseComponent { + static _STORAGE_KEY__PB_CUSTOM = "actor_charactermancer_ability"; + constructor(parentInfo) { + parentInfo = parentInfo || {}; + super(); + this._actor = parentInfo.actor; + this._data = parentInfo.data; + this._parent = parentInfo.parent; + this._tabAbilities = parentInfo.tabAbilities; + this._compStatgen = null; + } + render() { + const parentDiv = this._tabAbilities?.$wrpTab; + if (!parentDiv) {return;} + + //This element will handle the heavy lifting, both UI wise and logic wise + this._compStatgen = new StatGenUiCharactermancer({ + 'isCharacterMode': true, + 'isFvttMode': true, + 'races': this._data.race, + 'backgrounds': this._data.background, + 'feats': this._data.feat, + 'modalFilterRaces': this._parent.compRace.modalFilterRaces, + 'modalFilterBackgrounds': this._parent.compBackground.modalFilterBackgrounds, + 'modalFilterFeats': this._parent.compFeat.modalFilterFeats, + /* 'existingScores': this._getExistingScores() */ + }); + + /* const clientThenWorld = GameStorage.getClientThenWorld(this.constructor._STORAGE_KEY__PB_CUSTOM); + if (clientThenWorld != null) {this._compStatgen.setStateFrom(clientThenWorld);} */ + + const pbRulesHook = MiscUtil.throttle(this._doSavePbRules.bind(this), 100); + this._compStatgen.addHookPointBuyCustom(pbRulesHook); + + //Render the ui for changing ability scores + this._compStatgen.render(parentDiv); + + //Create a hook for recounting ASI + const e_recountASI = () => { + let asiCount = 0; + //Go through each class open on the class component + for (let ix = 0; ix < this._parent.compClass.state.class_ixMax + 1; ++ix) { + //Get the index the component uses for the class, and get the ASI count that this class has unlocked + const { propIxClass: propIxClass, propCntAsi: propCntAsi } = + ActorCharactermancerBaseComponent.class_getProps(ix); + //Get the class + const cls = this._parent.compClass.getClass_({ propIxClass: propIxClass }); + if (!cls) { continue; } + asiCount += Number(this._parent.compClass.state[propCntAsi]) || 0; + } + this._compStatgen.common_cntAsi = asiCount; + }; + this._parent.compClass.addHookBase("class_pulseChange", e_recountASI); + this._parent.compClass.addHookBase('class_totalLevels', e_recountASI); + e_recountASI(); + + const onAsiPulse = () => this._parent.compFeat.setAdditionalFeatStateFromStatgen_(); + this._compStatgen.addHookPulseAsi(onAsiPulse); + + this._parent.compFeat.setAdditionalFeatStateFromStatgen_(); + + //If we change our race or background, call these events to change it elsewhere too + const onRaceChangedHere = () => this._parent.compRace.state.race_ixRace = this._compStatgen.ixRace; + this._compStatgen.addHookIxRace(onRaceChangedHere); + const onBackgroundChangedHere = () => this._parent.compBackground.state.background_ixBackground = this._compStatgen.ixBackground; + this._compStatgen.addHookIxBackground(onBackgroundChangedHere); + + //If state changes their race or background, call these events to change it here too + const onRaceChangedThere = () => this._compStatgen.ixRace = this._parent.compRace.state.race_ixRace; + this._parent.compRace.addHookBase("race_ixRace", onRaceChangedThere); + const onBackgroundChangedThere = () => this._compStatgen.ixBackground = this._parent.compBackground.state.background_ixBackground; + this._parent.compBackground.addHookBase("background_ixBackground", onBackgroundChangedThere); + + //Set our race and background to be what state says it is + this._compStatgen.ixRace = this._parent.compRace.state.race_ixRace; + this._compStatgen.ixBackground = this._parent.compBackground.state.background_ixBackground; + } + + get compStatgen() { return this._compStatgen; } + addHookAbilityScores(...hook) { + return this._compStatgen.addHookAbilityScores(...hook); + } + getMode(...ix) { + return this._compStatgen.getMode(...ix); + } + getTotals(...ix) { + return this._compStatgen.getTotals(...ix); + } + + _doSavePbRules() { + const pointBuySavedState = this._compStatgen.getSaveableStatePointBuyCustom(); + GameStorage.pSetWorldThenClient(this.constructor._STORAGE_KEY__PB_CUSTOM, pointBuySavedState).then(null); + } + _getExistingScores() { + if (!Charactermancer_Util.getCurrentLevel(this._actor)) { + return null; + } + return Charactermancer_Util.getBaseAbilityScores(this._actor); + } + _getDefaultState() { + return {}; + } + + /** + * Sets the state of the StatgenUI based on a save file. This should be called just after first render. + * @param {{abilities:{mode:number, state:any}} actor + */ + setStateFromSaveFile(actor){ + const data = actor.abilities; + + //Set the mode for the state + this._compStatgen._meta.ixActiveTab___default = data.mode; + //Set the UI select element to have the right value selected + this._compStatgen._selModeElement[0].value = data.mode; + + //Print values over onto state + for(let prop of Object.keys(data.state)){ + let val = data.state[prop]; + this._compStatgen._state[prop] = val; + } + + //Now do the same for props regarding ASI's and feat choices + if(data.stateAsi){ + for(let prop of Object.keys(data.stateAsi)){ + let val = data.stateAsi[prop]; + this._compStatgen._state[prop] = val; + } + } + } +} + +class StatGenUi extends BaseComponent { + static _PROPS_POINT_BUY_CUSTOM = ["pb_rules", "pb_budget", "pb_isCustom", ]; + + constructor(opts) { + super(); + opts = opts || {}; + + TabUiUtilSide.decorate(this, { + isInitMeta: true + }); + + this._races = opts.races; //Set races + this._backgrounds = opts.backgrounds; + this._feats = opts.feats; + this._tabMetasAdditional = opts.tabMetasAdditional; + this._isCharacterMode = opts.isCharacterMode; + this._isFvttMode = opts.isFvttMode; + + this._MODES = this._isFvttMode ? StatGenUi.MODES_FVTT : StatGenUi.MODES; + if (this._isFvttMode) { + let cnt = 0; + this._IX_TAB_NONE = cnt++; + this._IX_TAB_ROLLED = cnt++; + this._IX_TAB_ARRAY = cnt++; + this._IX_TAB_PB = cnt++; + this._IX_TAB_MANUAL = cnt; + } + else { + this._IX_TAB_NONE = -1; + let cnt = 0; + this._IX_TAB_ROLLED = cnt++; + this._IX_TAB_ARRAY = cnt++; + this._IX_TAB_PB = cnt++; + this._IX_TAB_MANUAL = cnt; + } + + this._modalFilterRaces = opts.modalFilterRaces || new ModalFilterRaces({ + namespace: "statgen.races", + isRadio: true, + allData: this._races + }); + this._modalFilterBackgrounds = opts.modalFilterBackgrounds || new ModalFilterBackgrounds({ + namespace: "statgen.backgrounds", + isRadio: true, + allData: this._backgrounds + }); + this._modalFilterFeats = opts.modalFilterFeats || new ModalFilterFeats({ + namespace: "statgen.feats", + isRadio: true, + allData: this._feats + }); + + this._isLevelUp = !!opts.existingScores; + this._existingScores = opts.existingScores; + + this._$rollIptFormula = null; + + this._compAsi = new StatGenUi.CompAsi({ parent: this }); + } + + get MODES() { + return this._MODES; + } + + render($parent) { + $parent.empty().addClass("statgen"); + + //If we are leveling up, some UI changes + const iptTabMetas = this._isLevelUp ? [new TabUiUtil.TabMeta({ + name: "Existing", + icon: this._isFvttMode ? `fas fa-fw fa-user` : `far fa-fw fa-user`, + hasBorder: true + }), ...this._tabMetasAdditional || [], ] : [this._isFvttMode ? new TabUiUtil.TabMeta({ + name: "Select...", + icon: this._isFvttMode ? `fas fa-fw fa-square` : `far fa-fw fa-square`, + hasBorder: true, + isNoPadding: this._isFvttMode + }) : null, new TabUiUtil.TabMeta({ + name: "Roll", + icon: this._isFvttMode ? `fas fa-fw fa-dice` : `far fa-fw fa-dice`, + hasBorder: true, + isNoPadding: this._isFvttMode + }), new TabUiUtil.TabMeta({ + name: "Standard Array", + icon: this._isFvttMode ? `fas fa-fw fa-signal` : `far fa-fw fa-signal-alt`, + hasBorder: true, + isNoPadding: this._isFvttMode + }), new TabUiUtil.TabMeta({ + name: "Point Buy", + icon: this._isFvttMode ? `fas fa-fw fa-chart-bar` : `far fa-fw fa-chart-bar`, + hasBorder: true, + isNoPadding: this._isFvttMode + }), new TabUiUtil.TabMeta({ + name: "Manual", + icon: this._isFvttMode ? `fas fa-fw fa-tools` : `far fa-fw fa-tools`, + hasBorder: true, + isNoPadding: this._isFvttMode + }), ...this._tabMetasAdditional || [], ].filter(Boolean); + + const tabMetas = this._renderTabs(iptTabMetas, {$parent: this._isFvttMode ? null : $parent}); + if (this._isFvttMode) { + if (!this._isLevelUp) { + const {propActive: propActiveTab, propProxy: propProxyTabs} = this._getTabProps(); + const $selMode = ComponentUiUtil.$getSelEnum(this, propActiveTab, { + values: iptTabMetas.map((_,ix)=>ix), + fnDisplay: ix=>iptTabMetas[ix].name, + propProxy: propProxyTabs, + }, ).addClass("max-w-200p"); + $$`
    +
    Mode
    + ${$selMode} +
    +
    `.appendTo($parent); + this._selModeElement = $selMode; //just caching this here so we can access it and change it from elsewhere if we want to + } + + tabMetas.forEach(it=>it.$wrpTab.appendTo($parent)); + } + + const $wrpAll = $(`
    `); + this._render_all($wrpAll); + + const hkTab = ()=> { tabMetas[this.ixActiveTab || 0].$wrpTab.append($wrpAll); }; + + this._addHookActiveTab(hkTab); + hkTab(); + + this._addHookBase("common_cntAsi", ()=>this._state.common_pulseAsi = !this._state.common_pulseAsi); + this._addHookBase("common_cntFeatsCustom", ()=>this._state.common_pulseAsi = !this._state.common_pulseAsi); + } + + _render_all($wrpTab) { + if (this._isLevelUp){return this._render_isLevelUp($wrpTab);} + this._render_isLevelOne($wrpTab); + } + + _render_isLevelOne($wrpTab) { + let $stgNone; + let $stgMain; + const $elesRolled = []; + const $elesArray = []; + const $elesPb = []; + const $elesManual = []; + + const $stgRolledHeader = this._render_$getStgRolledHeader(); + const hkStgRolled = ()=>$stgRolledHeader.toggleVe(this.ixActiveTab === this._IX_TAB_ROLLED); + this._addHookActiveTab(hkStgRolled); + hkStgRolled(); + + const $stgPbHeader = this._render_$getStgPbHeader(); + const $stgPbCustom = this._render_$getStgPbCustom(); + const $vrPbCustom = $(`
    `); + const $hrPbCustom = $(`
    `); + const hkStgPb = ()=>{ + $stgPbHeader.toggleVe(this.ixActiveTab === this._IX_TAB_PB); + $stgPbCustom.toggleVe(this.ixActiveTab === this._IX_TAB_PB); + $vrPbCustom.toggleVe(this.ixActiveTab === this._IX_TAB_PB); + $hrPbCustom.toggleVe(this.ixActiveTab === this._IX_TAB_PB); + }; + this._addHookActiveTab(hkStgPb); + hkStgPb(); + + const $stgArrayHeader = this._render_$getStgArrayHeader(); + const hkStgArray = ()=>$stgArrayHeader.toggleVe(this.ixActiveTab === this._IX_TAB_ARRAY); + this._addHookActiveTab(hkStgArray); + hkStgArray(); + + const $stgManualHeader = this._render_$getStgManualHeader(); + const hkStgManual = ()=>$stgManualHeader.toggleVe(this.ixActiveTab === this._IX_TAB_MANUAL); + this._addHookActiveTab(hkStgManual); + hkStgManual(); + + const hkElesMode = ()=>{ + $stgNone.toggleVe(this.ixActiveTab === this._IX_TAB_NONE); + $stgMain.toggleVe(this.ixActiveTab !== this._IX_TAB_NONE); + + $elesRolled.forEach($ele=>$ele.toggleVe(this.ixActiveTab === this._IX_TAB_ROLLED)); + $elesArray.forEach($ele=>$ele.toggleVe(this.ixActiveTab === this._IX_TAB_ARRAY)); + $elesPb.forEach($ele=>$ele.toggleVe(this.ixActiveTab === this._IX_TAB_PB)); + $elesManual.forEach($ele=>$ele.toggleVe(this.ixActiveTab === this._IX_TAB_MANUAL)); + }; + this._addHookActiveTab(hkElesMode); + + const $btnResetRolledOrArrayOrManual = $(``).click(()=>this._doReset()); + const hkRolledOrArray = ()=>$btnResetRolledOrArrayOrManual.toggleVe(this.ixActiveTab === this._IX_TAB_ROLLED || this.ixActiveTab === this._IX_TAB_ARRAY || this.ixActiveTab === this._IX_TAB_MANUAL); + this._addHookActiveTab(hkRolledOrArray); + hkRolledOrArray(); + + const $wrpsBase = Parser.ABIL_ABVS.map(ab=>{ + const {propAbilSelectedRollIx} = this.constructor._rolled_getProps(ab); + + const $selRolled = $(``).change(()=>{ + const ix = Number($selRolled.val()); + + const nxtState = { + ...Parser.ABIL_ABVS.map(ab=>this.constructor._rolled_getProps(ab).propAbilSelectedRollIx).filter(prop=>ix != null && this._state[prop] === ix).mergeMap(prop=>({ + [prop]: null + })), + [propAbilSelectedRollIx]: ~ix ? ix : null, + }; + this._proxyAssignSimple("state", nxtState); + } + ); + $(``.appendTo($inputAlignment); + for(let i = 0; i < ActorCharactermancerDescription.ALIGNMENTS.length; ++i){ + const n = ActorCharactermancerDescription.ALIGNMENTS[i]; + $$``.appendTo($inputAlignment); + } + $inputAlignment.val(getVal("description_alignment")); + $inputAlignment.change(() => { + this._state["description_alignment"] = $inputAlignment.val(); + }); + + + const $inputHair = $$``; + $inputHair.val(getVal("description_hair")); + $inputHair.change(() => { + this._state["description_hair"] = $inputHair.val(); + }); + const $inputSkin = $$``; + $inputSkin.val(getVal("description_skin")); + $inputSkin.change(() => { + this._state["description_skin"] = $inputSkin.val(); + }); + const $inputEyes = $$``; + $inputEyes.val(getVal("description_eyes")); + $inputEyes.change(() => { + this._state["description_eyes"] = $inputEyes.val(); + }); + const $inputWeight = $$``; + $inputWeight.val(getVal("description_weight")); + $inputWeight.change(() => { + this._state["description_weight"] = $inputWeight.val(); + }); + const $inputHeight = $$``; + $inputHeight.val(getVal("description_height")); + $inputHeight.change(() => { + this._state["description_height"] = $inputHeight.val(); + }); + const $inputFaith = $$``; + $inputFaith.val(getVal("description_faith")); + $inputFaith.change(() => { + this._state["description_faith"] = $inputFaith.val(); + }); + + const startHeight = "300px"; + const $inputDescription = $$``; + $inputDescription.val(getVal("description_text")); + $inputDescription.change(() => { + this._state["description_text"] = $inputDescription.val(); + }); + + const ui = $$`
    +
      +
    • + ${$inputName} +
    • +
    • + ${$inputAlignment} +
    • +
    • + ${$inputHeight} +
    • +
    • + ${$inputWeight} +
    • +
    • + ${$inputHair} +
    • +
    • + ${$inputSkin} +
    • +
    • + ${$inputEyes} +
    • +
    • + ${$inputFaith} +
    • +
        +
    `; + const ui2 = $$`
    • ${$inputDescription}
    `; + + + + $$`
    +
    + ${ui} + ${ui2} +
    +
    `.appendTo(parentDiv); + } + + /** + * Sets the state of the component and subcomponents based on a save file. This should be called just after first render. + * @param {{background:{name:string, source:string, isFullyCustom:boolean, stateSkillProficiencies:any, stateLanguageToolProficiencies:any, + * stateCharacteristics:any, isCustomizeSkills:boolean, isCustomizeLanguagesTools:boolean}} actor + */ + setStateFromSaveFile(actor){ + const data = actor.about; + if(!data){return;} + + const printToState = (input, state) => { + for(let prop of Object.keys(input)){ + let val = input[prop]; + state["description_"+prop] = val; + } + } + printToState(data, this.state); + } + async pLoad() { + + } + _getDefaultState() { + return { + 'description_name': "", + 'description_alignment': "", + 'description_height': "", + 'description_weight': "", + 'description_hair': "", + 'description_skin': "", + 'description_eyes': "", + 'description_faith': "", + 'description_text': "", + }; + } + + static ALIGNMENTS = [ + "Lawful Good", + "Lawful Neutral", + "Lawful Evil", + "Neutral Good", + "Neutral", + "Neutral Evil", + "Chaotic Good", + "Chaotic Neutral", + "Chaotic Evil" + ] +} +//#endregion + +//#region CHARACTERMANCER UTILS +class Charactermancer_Util { + static getCurrentLevel(actor) { + return actor.items.filter(it=>it.type === "class").map(it=>Number(it.system.levels || 0)).sum(); + } + + static getBaseAbilityScores(actor) { + return this._getAbilityScores(actor, true); + } + + static getCurrentAbilityScores(actor) { + return this._getAbilityScores(actor, false); + } + + static _getAbilityScores(actor, isBase) { + const actorData = isBase ? (actor.system._source || actor.system) : actor.system; + const out = { + str: Number(MiscUtil.get(actorData, "abilities", "str", "value") || 0), + dex: Number(MiscUtil.get(actorData, "abilities", "dex", "value") || 0), + con: Number(MiscUtil.get(actorData, "abilities", "con", "value") || 0), + int: Number(MiscUtil.get(actorData, "abilities", "int", "value") || 0), + wis: Number(MiscUtil.get(actorData, "abilities", "wis", "value") || 0), + cha: Number(MiscUtil.get(actorData, "abilities", "cha", "value") || 0), + }; + Object.entries(out).forEach(([abv,val])=>{ + if (isNaN(val)) + out[abv] = 0; + } + ); + return out; + } + + static getBaseHp(actor) { + return this._getHp(actor, true); + } + + static _getHp(actor, isBase) { + const actorData = isBase ? (actor.system._source || actor.system) : actor.system; + return { + value: (actorData?.attributes?.hp?.value || 0), + max: actorData?.attributes?.hp?.max, + }; + } + + static getAttackAbilityScore(itemAttack, abilityScores, mode) { + if (!itemAttack || !abilityScores) + return null; + switch (mode) { + case "melee": + { + const isFinesse = !!MiscUtil.get(itemAttack, "system", "properties", "fin"); + if (!isFinesse) + return abilityScores.str; + return abilityScores.str > abilityScores.dex ? abilityScores.str : abilityScores.dex; + } + case "ranged": + { + const isThrown = !!MiscUtil.get(itemAttack, "system", "properties", "thr"); + if (!isThrown) + return abilityScores.dex; + return abilityScores.str > abilityScores.dex ? abilityScores.str : abilityScores.dex; + } + default: + throw new Error(`Unhandled mode "${mode}"`); + } + } + + static getFilteredFeatures(allFeatures, pageFilter, filterValues) { + return allFeatures.filter(f=>{ + //Try to get the source of the feature + const source = f.source || (f.classFeature ? DataUtil.class.unpackUidClassFeature(f.classFeature).source + : f.subclassFeature ? DataUtil.class.unpackUidSubclassFeature(f.subclassFeature) : null); + + //Then filter out this feature if we don't allow the source + if (!pageFilter.sourceFilter.toDisplay(filterValues, source)){return false;} + + f.loadeds = f.loadeds.filter(meta=>{ + return Charactermancer_Class_Util.isClassEntryFilterMatch(meta.entity, pageFilter, filterValues); + }); + + return f.loadeds.length; + }); + } + + /**Filters an array of features to only those we should import. For example removes features such as those that grant you a subclass */ + static getImportableFeatures(allFeatures) { + return allFeatures.filter(f=>{ + if (f.gainSubclassFeature && !f.gainSubclassFeatureHasContent){return false;} + if(!f.name){console.error("Feature does not have property 'name' assigned!", f);} + + const lowName = f.name.toLowerCase(); + switch (lowName) { + case "proficiency versatility": return false; + default: return true; + } + }); + } + + static doApplyFilterToFeatureEntries_bySource(allFeatures, pageFilter, filterValues) { + allFeatures.forEach(f=>{ + f.loadeds.forEach(loaded=>{ + switch (loaded.type) { + case "classFeature": + case "subclassFeature": + { + if (loaded.entity.entries) + loaded.entity.entries = Charactermancer_Class_Util.getFilteredEntries_bySource(loaded.entity.entries, pageFilter, filterValues); + break; + } + } + } + ); + } + ); + + return allFeatures; + } + + /** + * Expects each feature to have a .loadeds property + * @param {any} allFeatures + * @returns {{topLevelFeature:{name:string, level:number}, optionSets:any[]}[]} + */ + static getFeaturesGroupedByOptionsSet(allFeatures) { + return allFeatures.map(topLevelFeature=>{ + + if(!topLevelFeature.loadeds){console.error("Feature does not have any loadeds!", topLevelFeature);} + const optionsSets = []; //Blank optionsSets array + let optionsStack = []; + let lastOptionsSetId = null; + //Go through each loadeds + topLevelFeature.loadeds.forEach(l=>{ + //try to get l.optionsMeta.setId; + const optionsSetId = MiscUtil.get(l, "optionsMeta", "setId") || null; + if (lastOptionsSetId !== optionsSetId) { + if (optionsStack.length) { optionsSets.push(optionsStack); } + optionsStack = [l]; + lastOptionsSetId = optionsSetId; + } + else { optionsStack.push(l); } + }); + if (optionsStack.length) { optionsSets.push(optionsStack); } + + return {topLevelFeature, optionsSets}; + }); + } + + /** + * Create a select element that can search for its options and can use a modalfilter + * @param {any} comp + * @param {string} prop + * @param {string} propVersion + * @param {any} data + * @param {ModalFilter} modalFilter + * @param {any} title + * @returns {any} + */ + static getFilterSearchMeta({comp, prop, propVersion=null, data, modalFilter, title}) { + const {$wrp: $sel, fnUpdateHidden: fnUpdateSelHidden, unhook} = + ComponentUiUtil.$getSelSearchable(comp, prop, { + values: data.map((_,i)=>i), + isAllowNull: true, + fnDisplay: ix=>{ + const it = data[ix]; + + if (!it) { + console.warn(...LGT, `Could not find ${prop} with index ${ix} (${data.length} ${prop} entries were available)`); + return "(Unknown)"; + } + + return `${it.name} ${it.source !== Parser.SRC_PHB ? `[${Parser.sourceJsonToAbv(it.source)}]` : ""}`; + } + , + fnGetAdditionalStyleClasses: ix=>{ + if (ix == null) + return null; + const it = data[ix]; + if (!it) + return; + return it._versionBase_isVersion ? ["italic"] : null; + } + , + asMeta: true, + }, ); + + const doApplyFilterToSel = ()=>{ + //try to set filterbox to only be PHB + const f = modalFilter.pageFilter.filterBox.getValues(); + let sourcesEnabled = []; for(let key of Object.keys(f.Source)){ if(f.Source[key] > 0){sourcesEnabled[key] = f.Source[key];}} + const isHiddenPer = data.map(it=>!modalFilter.pageFilter.toDisplay(f, it)); + fnUpdateSelHidden(isHiddenPer, false); + }; + + //TEMPFIX + if(SETTINGS.FILTERS){modalFilter.pageFilter.filterBox.on(FilterBox.EVNT_VALCHANGE, doApplyFilterToSel, ); + doApplyFilterToSel();} + + const $btnFilter = $(``).click(async()=>{ + const selecteds = await modalFilter.pGetUserSelection(); + if (selecteds == null || !selecteds.length) + return; + + const selected = selecteds[0]; + const ix = data.findIndex(it=>it.name === selected.name && it.source === selected.values.sourceJson); + if (!~ix) + throw new Error(`Could not find selected entity: ${JSON.stringify(selected)}`); + comp._state[prop] = ix; + } + ); + + const {$stg: $stgSelVersion=null, unhook: unhookVersion=null} = this._getFilterSearchMeta_getVersionMeta({ + comp, + prop, + propVersion, + data + }) || {}; + + return { + $sel, + $btnFilter, + $stgSelVersion, + unhook: ()=>{ + unhook(); + modalFilter.pageFilter.filterBox.off(FilterBox.EVNT_VALCHANGE, doApplyFilterToSel); + if (unhookVersion) + unhookVersion(); + } + , + }; + } + + static _getFilterSearchMeta_getVersionMeta({comp, prop, propVersion, data}) { + if (!propVersion) + return; + + const {$sel, setValues, unhook} = ComponentUiUtil.$getSelEnum(comp, propVersion, { + values: [], + isAllowNull: true, + displayNullAs: "(Base version)", + fnDisplay: it=>`${it.name}${it.source !== data[comp._state[prop]]?.source ? ` (${Parser.sourceJsonToAbv(it.source)})` : ""}`, + asMeta: true, + isSetIndexes: true, + }, ); + + const hkProp = ()=>{ + const ent = data[comp._state[prop]]; + if (ent == null) { + setValues([]); + return $stg.hideVe(); + } + + const versions = DataUtil.generic.getVersions(ent); + setValues(versions); + $stg.toggleVe(versions.length); + } + ; + comp._addHookBase(prop, hkProp); + + const $stg = $$`
    + +
    `; + + hkProp(); + + return { + $stg, + unhook: ()=>{ + unhook(); + comp._removeHookBase(prop, hkProp); + } + , + }; + } +} +Charactermancer_Util.STR_WARN_SOURCE_SELECTION = `Did you change your source selection since using the Charactermancer initially?`; + +class Charactermancer_Class_Util { + /**return all features from a class (UID string format, except for gainSubclass features, which are objects with properties)*/ + static getAllFeatures(cls) { + let allFeatures = []; + const seenSubclassFeatureHashes = new Set(); + + const gainSubclassFeatureLevels = cls.classFeatures.filter(it=>it.gainSubclassFeature).map(cf=>cf.level ?? + DataUtil.class.unpackUidClassFeature(cf.classFeature || cf).level); + + cls.classFeatures.forEach(cf=>{ + allFeatures.push(cf); + + const cfLevel = cf.level ?? DataUtil.class.unpackUidClassFeature(cf.classFeature || cf).level; + const nxtCfLevel = gainSubclassFeatureLevels.includes(cfLevel) ? gainSubclassFeatureLevels[gainSubclassFeatureLevels.indexOf(cfLevel) + 1] : null; + + cls.subclasses.forEach(sc=>{ + sc.subclassFeatures.filter(scf=>{ + const scfHash = scf.hash ?? DataUtil.class.unpackUidSubclassFeature(scf.subclassFeature || scf).hash; + const scfLevel = scf.level ?? DataUtil.class.unpackUidSubclassFeature(scf.subclassFeature || scf).level; + + if (seenSubclassFeatureHashes.has(scfHash)) + return false; + + if (scf.isGainAtNextFeatureLevel) { + if (!cf.gainSubclassFeature) + return false; + + if (cfLevel === gainSubclassFeatureLevels[0] && scfLevel <= cfLevel) + return true; + + if (scfLevel <= cfLevel && (nxtCfLevel == null || scfLevel < nxtCfLevel)) + return true; + + return false; + } + + return scfLevel === cfLevel; + } + ).forEach(scf=>{ + const scfHash = scf.hash ?? DataUtil.class.unpackUidSubclassFeature(scf.subclassFeature || scf).hash; + seenSubclassFeatureHashes.add(scfHash); + + scf.level = cfLevel; + + allFeatures.push(scf); + } + ); + } + ); + } + ); + + return MiscUtil.copy(allFeatures); + } + + static isClassEntryFilterMatch(entry, pageFilter, filterValues) { + const source = entry.source; + const options = entry.isClassFeatureVariant ? {isClassFeatureVariant: true} : null; + + if (pageFilter.filterBox) { + return pageFilter.filterBox.toDisplayByFilters(filterValues, ...[{ + filter: pageFilter.sourceFilter, value: source, }, pageFilter.optionsFilter ? { + filter: pageFilter.optionsFilter, + value: options, + } : null, ].filter(Boolean), ); + } + + return pageFilter.sourceFilter.toDisplay(filterValues, source) && (!pageFilter.optionsFilter + || pageFilter.optionsFilter.toDisplay(filterValues, options)); + } + + static getFilteredEntries_bySource(entries, pageFilter, filterValues) { + const isDisplayableEntry = ({entry, filterValues, pageFilter})=>{ + if (!entry.source){return true;} + + return this.isClassEntryFilterMatch(entry, pageFilter, filterValues); + }; + + return this._getFilteredEntries({ + entries, + pageFilter, + filterValues, + fnIsDisplayableEntry: isDisplayableEntry, + }, ); + } + static _getFilteredEntries({entries, pageFilter, filterValues, fnIsDisplayableEntry, }, ) { + const recursiveFilter = (entry)=>{ + if (entry == null) + return entry; + if (typeof entry !== "object") + return entry; + + if (entry instanceof Array) { + entry = entry.filter(it=>fnIsDisplayableEntry({ + entry: it, pageFilter, filterValues, })); + + return entry.map(it=>recursiveFilter(it)); + } + + Object.keys(entry).forEach(k=>{ + if (entry[k]instanceof Array) { + entry[k] = recursiveFilter(entry[k]); + if (!entry[k].length) + delete entry[k]; + } else + entry[k] = recursiveFilter(entry[k]); + } + ); + return entry; + }; + + entries = MiscUtil.copy(entries); + return recursiveFilter(entries); + } + + static async pGetPreparableSpells(spells, cls, spellLevelLow, spellLevelHigh) { + Renderer.spell.populatePrereleaseLookup(await PrereleaseUtil.pGetBrewProcessed(), { + isForce: true + }); + Renderer.spell.populateBrewLookup(await BrewUtil2.pGetBrewProcessed(), { + isForce: true + }); + + return spells.filter(it=>{ + if (!(it.level > 0 && it.level >= spellLevelLow && it.level <= spellLevelHigh)) + return false; + + Renderer.spell.uninitBrewSources(it); + Renderer.spell.initBrewSources(it); + + const fromClassList = Renderer.spell.getCombinedClasses(it, "fromClassList"); + return fromClassList.some(c=>(c.name || "").toLowerCase() === cls.name.toLowerCase() && (c.source || Parser.SRC_PHB).toLowerCase() === cls.source.toLowerCase()); + } + ); + } + + static getCasterProgression(cls, sc, {targetLevel, otherExistingClassItems=null, otherExistingSubclassItems=null}) { + otherExistingClassItems = otherExistingClassItems || []; + otherExistingSubclassItems = otherExistingSubclassItems || []; + + const isSpellcastingMulticlass = [...otherExistingClassItems.filter(it=>it.system?.spellcasting && it.system?.spellcasting !== "none"), ...otherExistingSubclassItems.filter(it=>it.system?.spellcasting && it.system?.spellcasting !== "none"), cls.casterProgression != null || sc?.casterProgression != null, ].filter(Boolean).length > 1; + + let {totalSpellcastingLevels, casterClassCount, maxPactCasterLevel, } = UtilActors.getActorSpellcastingInfo({ + sheetItems: [...otherExistingClassItems, ...otherExistingSubclassItems], + isForceSpellcastingMulticlass: isSpellcastingMulticlass, + }); + + maxPactCasterLevel = Math.max(maxPactCasterLevel, targetLevel); + + const casterProgression = sc?.casterProgression || cls.casterProgression; + const spellAbility = sc?.spellcastingAbility || cls.spellcastingAbility; + + if (casterProgression) { + const fnRound = casterClassCount ? Math.floor : Math.ceil; + switch (casterProgression) { + case "full": + totalSpellcastingLevels += targetLevel; + break; + case "1/2": + totalSpellcastingLevels += fnRound(targetLevel / 2); + break; + case "1/3": + totalSpellcastingLevels += fnRound(targetLevel / 3); + break; + } + } + + return { + casterProgression, + spellAbility, + totalSpellcastingLevels, + maxPactCasterLevel, + }; + } + + static getMysticProgression({cls=null, targetLevel=0, otherExistingClassItems=null, otherExistingSubclassItems=null}) { + otherExistingClassItems = otherExistingClassItems || []; + otherExistingSubclassItems = otherExistingSubclassItems || []; + let totalMysticLevels = 0; + + if (cls?.name === "Mystic" && cls?.source === Parser.SRC_UATMC) + totalMysticLevels += targetLevel; + + if (otherExistingClassItems) { + totalMysticLevels += otherExistingClassItems.filter(it=>it.name.toLowerCase().trim() === "mystic").map(it=>it.system.levels).sum(); + } + + return { + totalMysticLevels, + }; + } + + static addFauxOptionalFeatureFeatures(classList, optfeatList) { + for (const cls of classList) { + if (cls.classFeatures && cls.optionalfeatureProgression?.length) { + for (const optFeatProgression of cls.optionalfeatureProgression) { + this._addFauxOptionalFeatureFeatures_handleClassProgression(optfeatList, cls, null, optFeatProgression, ); + } + } + + for (const sc of cls.subclasses) { + if (sc.subclassFeatures && sc.optionalfeatureProgression?.length) { + for (const optFeatProgression of sc.optionalfeatureProgression) { + this._addFauxOptionalFeatureFeatures_handleClassProgression(optfeatList, cls, sc, optFeatProgression, ); + } + } + } + } + } + + static _addFauxOptionalFeatureFeatures_handleClassProgression(optfeatList, cls, sc, optFeatProgression) { + const fauxLoadeds = this._addFauxOptionalFeatureFeatures_getLoadeds(optfeatList, cls, optFeatProgression); + + let progression = optFeatProgression.progression; + if (!(progression instanceof Array)) { + if (progression["*"]) { + progression = MiscUtil.copy(progression); + progression[1] = progression["*"]; + } + + const populated = new Set(Object.keys(progression).map(it=>Number(it)).sort(SortUtil.ascSort)); + const nxt = []; + const lvlMax = Math.max(...populated, Consts.CHAR_MAX_LEVEL); + for (let i = 0; i < lvlMax; ++i) { + nxt[i] = populated.has(i + 1) ? progression[i + 1] : nxt.length ? nxt.last() : 0; + } + progression = nxt; + } + + let required = optFeatProgression.required; + if (required && !(required instanceof Array)) { + const populated = new Set(Object.keys(required).map(it=>Number(it)).sort(SortUtil.ascSort)); + const nxt = []; + const lvlMax = Math.max(...populated, Consts.CHAR_MAX_LEVEL); + for (let i = 0; i < lvlMax; ++i) { + nxt[i] = populated.has(i + 1) ? required[i + 1] : []; + } + required = nxt; + } + + const propFeatures = sc ? "subclassFeatures" : "classFeatures"; + const propFeature = sc ? "subclassFeature" : "classFeature"; + const fnUnpackUidFeature = sc ? DataUtil.class.unpackUidSubclassFeature : DataUtil.class.unpackUidClassFeature; + + let cntPrev = 0; + progression.forEach((cntOptFeats,ixLvl)=>{ + if (cntOptFeats === cntPrev) + return; + const cntDelta = cntOptFeats - cntPrev; + if (!~cntDelta) + return; + const lvl = ixLvl + 1; + const requiredUidsUnpacked = (required?.[ixLvl] || []).map(it=>DataUtil.proxy.unpackUid("optionalfeature", it, "optfeature", { + isLower: true + })); + + const feature = this._addFauxOptionalFeatureFeatures_getFauxFeature(cls, sc, optFeatProgression, lvl, fauxLoadeds, cntDelta, requiredUidsUnpacked); + + const ixInsertBefore = (sc || cls)[propFeatures].findIndex(it=>{ + return (it.level || fnUnpackUidFeature(it[propFeature] || it).level) > lvl; + } + ); + if (~ixInsertBefore) + (sc || cls)[propFeatures].splice(ixInsertBefore, 0, feature); + else + (sc || cls)[propFeatures].push(feature); + + cntPrev = cntOptFeats; + } + ); + } + + static _addFauxOptionalFeatureFeatures_getLoadeds(optfeatList, clsSc, optFeatProgression) { + const availOptFeats = optfeatList.filter(it=>optFeatProgression.featureType instanceof Array && (optFeatProgression.featureType || []).some(ft=>it.featureType.includes(ft))); + const optionsMeta = { + setId: CryptUtil.uid(), + name: optFeatProgression.name + }; + return availOptFeats.map(it=>{ + return { + type: "optionalfeature", + entry: `{@optfeature ${it.name}|${it.source}}`, + entity: MiscUtil.copy(it), + optionsMeta, + page: UrlUtil.PG_OPT_FEATURES, + source: it.source, + hash: UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_OPT_FEATURES](it), + isRequiredOption: false, + }; + } + ); + } + + static _addFauxOptionalFeatureFeatures_getFauxFeature(cls, sc, optFeatProgression, lvl, fauxLoadeds, cntOptions, requiredUidsUnpacked) { + const loadeds = MiscUtil.copy(fauxLoadeds).filter(l=>!ExcludeUtil.isExcluded(UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_OPT_FEATURES]({ + name: l.entity.name, + source: l.entity.source + }), "optionalfeature", l.entity.source, { + isNoCount: true + }, )); + + loadeds.forEach(l=>{ + l.isRequiredOption = requiredUidsUnpacked.some(it=>it.name === l.entity.name.toLowerCase() && it.source === l.entity.source.toLowerCase()); + l.optionsMeta.count = cntOptions; + PageFilterClassesFoundry.populateEntityTempData({ + entity: l.entity, + ancestorClassName: cls.name, + ancestorSubclassName: sc?.name, + level: lvl, + ancestorType: "optionalfeature", + displayName: `${optFeatProgression.name}: ${l.entity.name}`, + foundrySystem: { + requirements: cls.name ? `${cls.name}${sc ? ` (${sc.name})` : ""} ${lvl}` : null, + }, + }); + } + ); + + const out = { + name: optFeatProgression.name, + level: lvl, + loadeds: loadeds, + }; + + if (sc) { + Object.assign(out, { + source: sc.source, + subclassFeature: `${optFeatProgression.name}|${cls.name}|${cls.source}|${sc.shortName}|${sc.source}|${lvl}|${Parser.SRC_5ETOOLS_TMP}`, + hash: UrlUtil.URL_TO_HASH_BUILDER["subclassFeature"]({ + name: optFeatProgression.name, + subclassName: sc.name, + subclassSource: sc.source, + className: cls.name, + classSource: cls.source, + level: lvl, + source: Parser.SRC_5ETOOLS_TMP, + }), + }, ); + } else { + Object.assign(out, { + source: cls.source, + classFeature: `${optFeatProgression.name}|${cls.name}|${cls.source}|${lvl}|${Parser.SRC_5ETOOLS_TMP}`, + hash: UrlUtil.URL_TO_HASH_BUILDER["classFeature"]({ + name: optFeatProgression.name, + className: cls.name, + classSource: cls.source, + level: lvl, + source: Parser.SRC_5ETOOLS_TMP, + }), + }, ); + } + + return out; + } + + static getExistingClassItems(actor, cls) { + if (!cls || !actor?.items){return [];} + + return actor.items.filter(actItem=>{ + if (actItem.type !== "class"){return;} + + const {page, source, hash, propDroppable} = MiscUtil.get(actItem, "flags", SharedConsts.MODULE_ID) || {}; + if (page === UrlUtil.PG_CLASSES && propDroppable === "class" && source === cls.source && hash === UrlUtil.URL_TO_HASH_BUILDER["class"](cls)) + return true; + + return (actItem.name || "").toLowerCase().trim() === cls.name.toLowerCase().trim() && (!Config.get("import", "isStrictMatching") || (UtilDocumentSource.getDocumentSource(actItem).source || "").toLowerCase() === Parser.sourceJsonToAbv(cls.source).toLowerCase()); + } + ); + } + + static getExistingSubclassItems(actor, cls, sc) { + if (!cls || !sc) + return []; + + return actor.items.filter(actItem=>{ + if (actItem.type !== "subclass") + return false; + + const {page, source, hash, propDroppable} = MiscUtil.get(actItem, "flags", SharedConsts.MODULE_ID) || {}; + if (page === UrlUtil.PG_CLASSES && propDroppable === "subclass" && source === sc.source && hash === UrlUtil.URL_TO_HASH_BUILDER["subclass"](sc)) + return true; + + return (actItem.name || "").toLowerCase().trim() === sc.name.toLowerCase().trim() && (!Config.get("import", "isStrictMatching") || (UtilDocumentSource.getDocumentSource(actItem).source || "").toLowerCase() === Parser.sourceJsonToAbv(sc.source).toLowerCase()); + } + ); + } + + static getClassFromExistingClassItem(existingClassItem, classes) { + if (!existingClassItem || existingClassItem.type !== "class" || !classes?.length) + return null; + + classes = [...classes].sort(this._sortByOfficialAndRecent.bind(this)); + + return classes.find(cls=>cls.name.toLowerCase().trim() === existingClassItem.name.toLowerCase().trim() && (!Config.get("import", "isStrictMatching") || (UtilDocumentSource.getDocumentSource(existingClassItem).source || "").toLowerCase() === Parser.sourceJsonToAbv(cls.source).toLowerCase()), ); + } + + static getSubclassFromExistingSubclassItem(existingSubclassItem, cls, subclasses) { + if (!existingSubclassItem || existingSubclassItem.type !== "subclass" || !subclasses?.length) + return null; + + subclasses = subclasses.filter(it=>it.className === cls.name && it.classSource === cls.source); + + subclasses = [...subclasses].sort(this._sortByOfficialAndRecent.bind(this)); + + return subclasses.find(sc=>sc.name.toLowerCase().trim() === existingSubclassItem.name.toLowerCase().trim() || sc.shortName.toLowerCase().trim() === existingSubclassItem.name.toLowerCase().trim(), ); + } + + static _sortByOfficialAndRecent(a, b) { + const isNonStandardSourceA = SourceUtil.isNonstandardSource(a.source); + const isNonStandardSourceB = SourceUtil.isNonstandardSource(b.source); + + if (isNonStandardSourceA === isNonStandardSourceB) { + return SortUtil.ascSortDateString(Parser.sourceJsonToDate(a.source), Parser.sourceJsonToDate(b.source)) || SortUtil.ascSortLower(a.name, b.name); + } + + return isNonStandardSourceA ? 1 : -1; + } + + static getClassSubclassFeatureReferences(obj) { + const refsClassFeature = []; + const refsSubclassFeature = []; + + MiscUtil.getWalker({ + isNoModification: true + }).walk(obj, { + object: (obj)=>{ + if (obj.type === "refClassFeature") { + refsClassFeature.push(MiscUtil.copy(obj)); + return; + } + + if (obj.type === "refSubclassFeature") { + refsSubclassFeature.push(MiscUtil.copy(obj)); + } + } + , + }, ); + + return { + refsClassFeature, + refsSubclassFeature + }; + } + + static getClassSubclassItemTuples({classItems, subclassItems}) { + if (!classItems?.length) + return []; + + subclassItems = subclassItems || []; + + return classItems.map(classItem=>({ + classItem, + subclassItem: subclassItems.find(it=>it.system.classIdentifier === classItem.system.identifier), + })); + } + + static getToolProficiencyData(profs) { + if (!profs) + return null; + if (profs.toolProficiencies) + return profs.toolProficiencies; + if (!profs.tools) + return null; + + const out = {}; + profs.tools.forEach(str=>{ + const itemUid = UtilActors.getItemUIdFromToolProficiency(str); + if (!itemUid) + return; + const mappedTool = UtilActors.getMappedTool(itemUid); + if (!mappedTool) + return; + const unmappedTool = UtilActors.getUnmappedTool(mappedTool); + if (!unmappedTool) + return; + out[unmappedTool] = true; + } + ); + + return [out]; + } +} + +Charactermancer_Class_Util.ExistingFeatureChecker = class { + constructor(actor, cachedCharacter) { + this._actor = actor; + this._cachedCharacter = cachedCharacter; + + this._existingSheetFeatures = {}; + this._existingImportFeatures = {}; + + if(SETTINGS.USE_EXISTING_WEB){ + //TODO: Fix this + if(!cachedCharacter?.feats){return;} + cachedCharacter.feats.forEach(it=>{ + const cleanSource = (UtilDocumentSource.getDocumentSource(it).source || "").trim().toLowerCase(); + Charactermancer_Class_Util.ExistingFeatureChecker._getNameAliases(it.name).forEach(alias=>this._existingSheetFeatures[alias] = cleanSource); + + const {page, source, hash} = it.flags?.[SharedConsts.MODULE_ID] || {}; + if (page && source && hash) { this.addImportFeature(page, source, hash); } + }); + return; + } + if(SETTINGS.USE_EXISTING){ + actor.items.filter(it=>it.type === "feat").forEach(it=>{ + const cleanSource = (UtilDocumentSource.getDocumentSource(it).source || "").trim().toLowerCase(); + Charactermancer_Class_Util.ExistingFeatureChecker._getNameAliases(it.name).forEach(alias=>this._existingSheetFeatures[alias] = cleanSource); + + const {page, source, hash} = it.flags?.[SharedConsts.MODULE_ID] || {}; + if (page && source && hash) + this.addImportFeature(page, source, hash); + }); + } + } + + static _getNameAliases(name) { + const cleanName = name.trim().toLowerCase(); + const out = [cleanName, ]; + + const mTrailingParens = /^(.*?)\(.*\)$/.exec(cleanName); + if (mTrailingParens) + out.push(mTrailingParens[1].trim()); + + if (cleanName.includes(": ")) { + const cleanNamePostColon = cleanName.split(":").slice(1).join(":").trim(); + out.push(cleanNamePostColon); + const mTrailingParensPostColon = /^(.*?)\(.*\)$/.exec(cleanNamePostColon); + if (mTrailingParensPostColon) + out.push(mTrailingParensPostColon[1].trim()); + } + + return out; + } + + isExistingFeature(name, page, source, hash) { + if (MiscUtil.get(this._existingImportFeatures, page, source, hash)) + return true; + + const searchNameAliases = Charactermancer_Class_Util.ExistingFeatureChecker._getNameAliases(name); + if (!searchNameAliases.some(it=>this._existingSheetFeatures[it])) + return false; + + if (!Config.get("import", "isStrictMatching")) + return true; + + const searchSource = Parser.sourceJsonToAbv(source).trim().toLowerCase(); + return searchNameAliases.some(it=>this._existingSheetFeatures[it] === searchSource); + } + + addImportFeature(page, source, hash) { + MiscUtil.set(this._existingImportFeatures, page, source, hash, true); + } +}; + +class Charactermancer_ProficiencySelect extends BaseComponent { +} + +Charactermancer_ProficiencySelect.PropGroup = class { + constructor({prop, propTrackerPulse, propTracker}) { + this.prop = prop; + this.propTrackerPulse = propTrackerPulse; + this.propTracker = propTracker; + } +}; + +class Charactermancer_OtherProficiencySelect extends Charactermancer_ProficiencySelect { + + constructor(opts) { + opts = opts || {}; + super(); + + this._existing = opts.existing; //Just used to determine if ANYTHING else on our character already is giving us proficiency/expertise for something + this._available = Charactermancer_OtherProficiencySelect._getNormalizedAvailableProficiencies(opts.available); + this._titlePrefix = opts.titlePrefix; + this._featureSourceTracker = opts.featureSourceTracker || new Charactermancer_FeatureSourceTracker(); + this._$elesPreFromGroups = opts.$elesPreFromGroups; + this._$elesPostFromGroups = opts.$elesPostFromGroups; + + this._lastMetas = []; + this._hkExisting = null; + } + + static async pGetUserInput(opts) { + opts = opts || {}; + + if (!opts.available) + return { + isFormComplete: true, + data: {} + }; + + const comp = new this({ + ...opts, + existing: this.getExisting(opts.existingFvtt), + existingFvtt: opts.existingFvtt, + }); + if (comp.isNoChoice()) + return comp.pGetFormData(); + + return UtilApplications.pGetImportCompApplicationFormData({ + comp, + width: 640, + isAutoResize: true, + }); + } + + static getExistingFvttFromActor(actor) { + return { + skillProficiencies: MiscUtil.get(actor, "_source", "system", "skills"), + toolProficiencies: MiscUtil.get(actor, "_source", "system", "tools"), + languageProficiencies: MiscUtil.get(actor, "_source", "system", "traits", "languages"), + armorProficiencies: MiscUtil.get(actor, "_source", "system", "traits", "armorProf"), + weaponProficiencies: MiscUtil.get(actor, "_source", "system", "traits", "weaponProf"), + savingThrowProficiencies: MiscUtil.get(actor, "_source", "system", "abilities"), + }; + } + + static getExisting(existingFvtt) { + if(!SETTINGS.USE_EXISTING){return null;} //TEMPFIX + return { + skillProficiencies: this._getExistingSkillToolProficiencies({ + existingProficienciesSetFvtt: existingFvtt.skillProficiencies, + mapAbvToFull: UtilActors.SKILL_ABV_TO_FULL, + }), + toolProficiencies: this._getExistingSkillToolProficiencies({ + existingProficienciesSetFvtt: existingFvtt.toolProficiencies, + mapAbvToFull: UtilActors.SKILL_ABV_TO_FULL, + }), + languageProficiencies: this._getExistingProficiencies({ + existingProficienciesSetFvtt: existingFvtt?.languageProficiencies, + vetToFvttProfs: UtilActors.VALID_LANGUAGES, + allProfsVet: Parser.LANGUAGES_ALL, + }), + armorProficiencies: this._getExistingProficiencies({ + existingProficienciesSetFvtt: existingFvtt?.armorProficiencies, + vetToFvttProfs: UtilActors.VALID_ARMOR_PROFICIENCIES, + allProfsVet: UtilActors.ARMOR_PROFICIENCIES, + }), + weaponProficiencies: this._getExistingProficiencies({ + existingProficienciesSetFvtt: existingFvtt?.weaponProficiencies, + vetToFvttProfs: UtilActors.VALID_WEAPON_PROFICIENCIES, + allProfsVet: UtilActors.WEAPON_PROFICIENCIES, + }), + savingThrowProficiencies: this._getExistingSavingThrowProficiencies(existingFvtt), + }; + } + + static isNoChoice(available) { + return this._isNoChoice({ + available + }); + } + + static _isNoChoice({available, isAlreadyMapped}) { + if (!available?.length) + return true; + if (isAlreadyMapped && !this._isValidAvailableData(available)) + throw new Error(`Proficiency data was not valid! Data was:\n${JSON.stringify(available)}`); + + if (!isAlreadyMapped) + available = Charactermancer_OtherProficiencySelect._getNormalizedAvailableProficiencies(available); + + return available.length === 1 && !available[0].choose; + } + + static _isValidAvailableData(available) { + if (!(available instanceof Array)) + return false; + + for (const profSet of available) { + const badKeys = Object.keys(profSet).filter(it=>it !== "static" && it !== "choose"); + if (badKeys.length) + return false; + + if ((profSet.static || []).filter(it=>!it.prop).length) + return false; + if ((profSet.choose || []).filter(it=>it.from && it.from.some(from=>!from.prop)).length) + return false; + if ((profSet.choose || []).filter(it=>it.fromFilter && !it.prop).length) + return false; + } + + return true; + } + + static getMappedSkillProficiencies(skillProficiencies) { + if (!skillProficiencies) + return skillProficiencies; + return skillProficiencies.map(it=>{ + it = MiscUtil.copy(it); + if (it.any) { + it.anySkill = it.any; + delete it.any; + } + if (it.choose?.from && CollectionUtil.setEq(new Set(it.choose.from), new Set(Renderer.generic.FEATURE__ALL_SKILLS))) { + it.anySkill = it.choose.count ?? 1; + delete it.choose; + } + this._getMappedProficiencies_expandChoose({ + proficienciesSet: it, + prop: "skillProficiencies" + }); + return it; + } + ); + } + + static getMappedLanguageProficiencies(languageProficiencies) { + if (!languageProficiencies) + return languageProficiencies; + return languageProficiencies.map(it=>{ + it = MiscUtil.copy(it); + if (it.any) { + it.anyLanguage = it.any; + delete it.any; + } + if (it.anyStandard) { + it.anyStandardLanguage = it.anyStandard; + delete it.anyStandard; + } + if (it.anyExotic) { + it.anyExoticLanguage = it.anyExotic; + delete it.anyExotic; + } + this._getMappedProficiencies_expandChoose({ + proficienciesSet: it, + prop: "languageProficiencies" + }); + this._getMappedProficiencies_expandStatic({ + proficienciesSet: it, + prop: "languageProficiencies" + }); + return it; + } + ); + } + + static getMappedToolProficiencies(toolProficiencies) { + if (!toolProficiencies) + return toolProficiencies; + return toolProficiencies.map(it=>{ + it = MiscUtil.copy(it); + if (it.any) { + it.anyTool = it.any; + delete it.any; + } + if (it.anyArtisans) { + it.anyArtisansTool = it.anyArtisans; + delete it.anyArtisans; + } + this._getMappedProficiencies_expandChoose({ + proficienciesSet: it, + prop: "toolProficiencies" + }); + this._getMappedProficiencies_expandStatic({ + proficienciesSet: it, + prop: "toolProficiencies" + }); + return it; + } + ); + } + + static getMappedArmorProficiencies(armorProficiencies) { + if (!armorProficiencies) + return armorProficiencies; + return armorProficiencies.map(it=>{ + it = MiscUtil.copy(it); + if (it.any) { + it.anyArmor = it.any; + delete it.any; + } + this._getMappedProficiencies_expandChoose({ + proficienciesSet: it, + prop: "armorProficiencies" + }); + this._getMappedProficiencies_expandStatic({ + proficienciesSet: it, + prop: "armorProficiencies" + }); + return it; + } + ); + } + + static getMappedWeaponProficiencies(weaponProficiencies) { + if (!weaponProficiencies) + return weaponProficiencies; + return weaponProficiencies.map(it=>{ + it = MiscUtil.copy(it); + if (it.any) { + it.anyWeapon = it.any; + delete it.any; + } + this._getMappedProficiencies_expandChoose({ + proficienciesSet: it, + prop: "weaponProficiencies" + }); + this._getMappedProficiencies_expandStatic({ + proficienciesSet: it, + prop: "weaponProficiencies" + }); + return it; + } + ); + } + + static getMappedSavingThrowProficiencies(savingThrowProficiencies) { + if (!savingThrowProficiencies) + return savingThrowProficiencies; + return savingThrowProficiencies.map(it=>{ + it = MiscUtil.copy(it); + if (it.any) { + it.anySavingThrow = it.any; + delete it.any; + } + this._getMappedProficiencies_expandChoose({ + proficienciesSet: it, + prop: "savingThrowProficiencies" + }); + this._getMappedProficiencies_expandStatic({ + proficienciesSet: it, + prop: "savingThrowProficiencies" + }); + return it; + } + ); + } + + static _getMappedProficiencies_expandChoose({proficienciesSet, prop}) { + if (!proficienciesSet.choose) + return; + if (proficienciesSet.choose.fromFilter) + proficienciesSet.choose.prop = prop; + if (proficienciesSet.choose.from) { + proficienciesSet.choose.from = proficienciesSet.choose.from.map(it=>{ + if (typeof it !== "string") + return it; + return { + prop, + name: it + }; + } + ); + } + proficienciesSet.choose = [proficienciesSet.choose]; + } + + static _getMappedProficiencies_expandStatic({proficienciesSet, prop, ignoredKeys}) { + Object.entries(proficienciesSet).forEach(([k,v])=>{ + if ((ignoredKeys && ignoredKeys.has(k)) || Charactermancer_OtherProficiencySelect._MAPPED_IGNORE_KEYS.has(k)) + return; + + if (typeof v === "boolean") { + proficienciesSet[k] = { + prop + }; + return; + } + if (typeof v === "number") { + proficienciesSet[k] = { + prop, + count: v + }; + return; + } + + throw new Error(`Unhandled type "${typeof v}" for value of proficiency "${k}"`); + } + ); + } + + static _getExistingFvttProficiencySetsMeta(existingFvtt) { + return { + existingProficienciesFvttSet: new Set(existingFvtt?.value || []), + existingProficienciesFvttSetCustom: new Set((existingFvtt?.custom || "").split(";").map(it=>it.trim().toLowerCase()).filter(Boolean)), + }; + } + + + + static _getNormalizedAvailableProficiencies(availProfs) { + return availProfs.map(availProfSet=>{ + const out = {}; + + Object.entries(availProfSet).forEach(([k,v])=>{ + if (!v) + return; + + switch (k) { + case "choose": + { + v.forEach(choose=>{ + const mappedCount = choose.count != null && !isNaN(choose.count) ? Number(choose.count) : 1; + if (mappedCount <= 0) + return; + + const mappedFroms = (choose?.from || []).map(it=>Renderer.generic.getMappedAnyProficiency({ + keyAny: it, + countRaw: mappedCount + }) || this._getNormalizedProficiency(null, it)).filter(Boolean); + + const mappedFromFilter = (choose?.fromFilter || "").trim(); + + if (!mappedFroms.length && !mappedFromFilter) + return; + if (mappedFroms.length && mappedFromFilter) + throw new Error(`Invalid proficiencies! Only one of "from" and "fromFilter" may be provided. Data was:\n${JSON.stringify(choose)}`); + + const tgt = (out.choose = out.choose || []); + + if (mappedFromFilter) { + if (!choose.type && !choose.prop) + throw new Error(`"fromFilter" did not have an associated "type"!`); + tgt.push({ + fromFilter: mappedFromFilter, + count: mappedCount, + prop: choose.prop || this._getNormalizedProficiencyPropFromType(choose.type) + }); + return; + } + + if (!mappedFroms.length) + return; + + const subOut = { + from: [], + count: mappedCount + }; + mappedFroms.forEach(it=>{ + if (it.from) { + subOut.from = [...subOut.from, ...it.from]; + if (it.groups) + Object.assign((subOut.groups = subOut.groups || {}), it.groups); + return; + } + + subOut.from.push(it); + } + ); + tgt.push(subOut); + } + ); + + break; + } + + case "anySkill": + case "anyTool": + case "anyArtisansTool": + case "anyMusicalInstrument": + case "anyLanguage": + case "anyStandardLanguage": + case "anyExoticLanguage": + case "anyWeapon": + case "anyArmor": + case "anySavingThrow": + { + const mappedAny = Renderer.generic.getMappedAnyProficiency({ + keyAny: k, + countRaw: v + }); + if (!mappedAny) + break; + (out.choose = out.choose || []).push(mappedAny); + break; + } + + default: + { + if (k === "static") + throw new Error(`Property handling for "static" is unimplemented!`); + + if (v?.prop) { + (out.static = out.static || []).push({ + name: k, + prop: v.prop + }); + break; + } + if (v?.type) { + (out.static = out.static || []).push({ + name: k, + prop: this._getNormalizedProficiencyPropFromType(v.type) + }); + break; + } + + const normalized = this._getNormalizedProficiency(k, v); + if (normalized) + (out.static = out.static || []).push(normalized); + } + } + } + ); + + if (out.static && out.choose) { + out.choose.forEach(choose=>{ + if (choose.fromFilter) + return; + + choose.from = choose.from.filter(({name, prop})=>!out.static.some(({name: nameStatic, prop: propStatic})=>nameStatic === name && propStatic === prop)); + } + ); + } + + return out; + } + ); + } + + static _getNormalizedProficiency(k, v) { + if (!v){return null;} + + let name = v?.name ?? k ?? v; + if (!name || typeof name !== "string"){return null;} + name = name.trim(); + + if (v?.prop) { + return { + name, + prop: v.prop + }; + } + + if (v?.type) { + const prop = this._getNormalizedProficiencyPropFromType(v.type); + return { + name, + prop + }; + } + + if (Charactermancer_OtherProficiencySelect._VALID_SKILLS.has(name)) + return { + name, + prop: "skillProficiencies" + }; + if (Charactermancer_OtherProficiencySelect._VALID_TOOLS.has(name)) + return { + name, + prop: "toolProficiencies" + }; + if (Charactermancer_OtherProficiencySelect._VALID_LANGUAGES.has(name)) + return { + name, + prop: "languageProficiencies" + }; + if (Charactermancer_OtherProficiencySelect._VALID_WEAPONS.has(name)) + return { + name, + prop: "weaponProficiencies" + }; + if (Charactermancer_OtherProficiencySelect._VALID_ARMORS.has(name)) + return { + name, + prop: "armorProficiencies" + }; + if (Charactermancer_OtherProficiencySelect._VALID_SAVING_THROWS.has(name)) + return { + name, + prop: "savingThrowProficiencies" + }; + + console.warn(...LGT, `Could not discern the type of proficiency "${name}"\u2014you may need to specify it directly with "type".`); + + return null; + } + + static _getNormalizedProficiencyPropFromType(type) { + type = type.trim().toLowerCase(); + switch (type) { + case "skill": + return "skillProficiencies"; + case "tool": + return "toolProficiencies"; + case "language": + return "languageProficiencies"; + case "weapon": + return "weaponProficiencies"; + case "armor": + return "armorProficiencies"; + case "savingThrow": + return "savingThrowProficiencies"; + default: + throw new Error(`Type "${type}" did not have an associated proficiency property!`); + } + } + + static _getTagFromProp(prop) { + switch (prop) { + case "armorProficiencies": + return "@item"; + case "weaponProficiencies": + return "@item"; + default: + throw new Error(`Cannot get @tag from prop "${prop}"`); + } + } + + _getTitle() { + const props = this._getAllPossibleProps(); + return `${props.map(prop=>this.constructor._getPropDisplayName({ + prop + })).join("/")} Proficiency`; + } + + _getTitlePlural() { + const props = this._getAllPossibleProps(); + return `${props.map(prop=>this.constructor._getPropDisplayName({ + prop, + isPlural: true + })).join("/")} Proficiencies`; + } + + _getAllPossibleProps() { + const propSet = new Set(); + + this._available.forEach(profSet=>{ + const subSet = this.constructor._getAllPossiblePropsForProfSet(profSet); + subSet.forEach(prop=>propSet.add(prop)); + } + ); + + return [...propSet]; + } + + static _getAllPossiblePropsForProfSet(profSet) { + const out = new Set(); + (profSet.static || []).forEach(it=>out.add(it.prop)); + (profSet.choose || []).forEach(it=>{ + if (it.prop) + return out.add(it.prop); + it.from.forEach(from=>out.add(from.prop)); + } + ); + return out; + } + + get modalTitle() { + return this._getTitlePlural(); + } + + render($wrp) { + + const $stgSelGroup = this._render_$getStgSelGroup(); + + const $stgGroup = $$`
    `; + const hkIxSet = ()=>{ + $stgGroup.empty(); + + if (this._featureSourceTracker && this._hkExisting) { + Object.values(Charactermancer_OtherProficiencySelect._PROP_GROUPS).forEach(({propTrackerPulse})=>this._featureSourceTracker.removeHook(this, propTrackerPulse, this._hkExisting)); + } + this._lastMetas.forEach(it=>it.cleanup()); + this._lastMetas = []; + + //Get information about choices or if static + const selProfs = this._available[this._state.ixSet]; + + //This should only occur when we completely wipe the component state as part of 'remove class' button + if(!selProfs){ return; } + + if (this._featureSourceTracker){this._doSetTrackerState();} + + const $ptsExistingStatic = selProfs.static?.length ? this._render_renderPtStatic($stgGroup, selProfs.static) : null; + + if ($ptsExistingStatic && selProfs.choose?.length){$stgGroup.append(`
    `);} + + + const $ptsExistingChoose = (selProfs.choose || []).map(({count, from, groups, fromFilter, prop},i)=>{ + if (this._$elesPreFromGroups?.[i]) + $stgGroup.append(this._$elesPreFromGroups?.[i]); + + const $outPtsExisting = fromFilter ? this._render_renderPtChooseFromFilter($stgGroup, { ix: i, count, fromFilter, prop }) + : this._render_renderPtChooseFrom($stgGroup, { ix: i, count, from, groups }); + + if (this._$elesPostFromGroups?.[i]) + $stgGroup.append(this._$elesPostFromGroups?.[i]); + + if (selProfs.choose.length > 1 && (i < selProfs.choose.length - 1)) { + $stgGroup.append(`
    `); + } + + return $outPtsExisting; + }); + + this._hkExisting = ()=>this._hk_pUpdatePtsExisting($ptsExistingStatic, $ptsExistingChoose); + if (this._featureSourceTracker) { + Object.values(Charactermancer_OtherProficiencySelect._PROP_GROUPS).forEach(({propTrackerPulse})=>this._featureSourceTracker.addHook(this, propTrackerPulse, this._hkExisting)); + } + this._hkExisting(); + }; + this._addHookBase("ixSet", hkIxSet); + hkIxSet(); + + $$` + ${$stgSelGroup} + ${$stgGroup} + `.appendTo($wrp); + } + + _doSetTrackerState() { + const formData = this._getFormData(); + this._featureSourceTracker.setState(this, Object.keys(Charactermancer_OtherProficiencySelect._PROP_GROUPS).mergeMap(prop=>({ + [prop]: formData.data?.[prop] + })), ); + } + + static _render_getStaticKeyFullText({name, prop}) { + switch (prop) { + case "weaponProficiencies": + return name.split("|")[0].toTitleCase(); + + case "armorProficiencies": + { + switch (name) { + case "light": + case "medium": + case "heavy": + return name.toTitleCase(); + case "shield|phb": + return "Shields"; + default: + return name.split("|")[0].toTitleCase(); + } + } + + case "savingThrowProficiencies": + return Parser.attAbvToFull(name).toTitleCase(); + + default: + return name.toTitleCase(); + } + } + + static _render_getStaticKeyFullTextOther({prop}) { + switch (prop) { + case "skillProficiencies": + return "(Other skill proficiency)"; + case "toolProficiencies": + return "(Other tool proficiency)"; + case "languageProficiencies": + return "(Other language proficiency)"; + case "weaponProficiencies": + return "(Other weapon proficiency)"; + case "armorProficiencies": + return "(Other armor proficiency)"; + case "savingThrowProficiencies": + return "(Other saving throw proficiency)"; + default: + throw new Error(`Unhandled prop "${prop}"`); + } + } + + static async _pGetParentGroup({prop, name}) { + switch (prop) { + case "weaponProficiencies": + return UtilDataConverter.pGetItemWeaponType(name); + default: + return null; + } + } + + static _getRenderedStatic({prop, name}) { + switch (prop) { + case "skillProficiencies": + return this._getRenderedStatic_skillProficiencies(name); + case "languageProficiencies": + return this._getRenderedStatic_languageProficiencies(name); + case "toolProficiencies": + return this._getRenderedStatic_toolProficiencies(name); + case "armorProficiencies": + return this._getRenderedStatic_armorProficiencies(name); + case "weaponProficiencies": + return Renderer.get().render(`{@item ${name.split("|").map(sub=>sub.toTitleCase()).join("|")}}`); + case "savingThrowProficiencies": + return Parser.attAbvToFull(name).toTitleCase(); + default: + return name.toTitleCase(); + } + } + + static _getRenderedStatic_skillProficiencies(name) { + const atb = Parser.skillToAbilityAbv(name); + const ptAbility = `
    (${atb.toTitleCase()})
    `; + + return `
    ${Renderer.get().render(`{@skill ${name.toTitleCase()}}`)}${ptAbility}
    `; + } + + static _getRenderedStatic_languageProficiencies(name) { + if (name === "other"){return name.toTitleCase();} + if (UtilActors.LANGUAGES_PRIMORDIAL.includes(name)) + {return Renderer.get().render(`{@language primordial||${name.toTitleCase()}}`);} + return Renderer.get().render(`{@language ${name.toTitleCase()}}`); + } + + static _getRenderedStatic_toolProficiencies(name) { + if (UtilActors.TOOL_PROFICIENCIES_TO_UID[name]) + return Renderer.get().render(`{@item ${UtilActors.TOOL_PROFICIENCIES_TO_UID[name].toTitleCase()}}`); + return name.toTitleCase(); + } + + static _getRenderedStatic_armorProficiencies(key) { + if (key === "light" || key === "medium" || key === "heavy") + return key.toTitleCase(); + if (key === "shield|phb") + return Renderer.get().render(`{@item shield|phb|Shields}`); + return Renderer.get().render(`{@item ${key.split("|").map(sub=>sub.toTitleCase()).join("|")}}`); + } + + static _getPropDisplayName({prop}) { + switch (prop) { + case "skillProficiencies": + return `Skill`; + case "toolProficiencies": + return `Tool`; + case "languageProficiencies": + return `Language`; + case "weaponProficiencies": + return `Weapon`; + case "armorProficiencies": + return `Armor`; + case "savingThrowProficiencies": + return `Saving Throw`; + default: + throw new Error(`Unhandled prop "${prop}"`); + } + } + + _render_$getStgSelGroup() { + if (this._available.length <= 1) + return null; + + const $selIxSet = ComponentUiUtil.$getSelEnum(this, "ixSet", { + placeholder: `Select ${this._getTitle()} Set`, + values: this._available.map((_,i)=>i), + fnDisplay: ix=>{ + const selProfs = this._available[ix]; + + const out = []; + + if (selProfs.static) { + const pt = MiscUtil.copy(selProfs.static).sort((a,b)=>SortUtil.ascSortLower(a.name, b.name)).map(({name, prop})=>{ + if (name === "other") + return this.constructor._render_getStaticKeyFullTextOther({ + prop + }); + return this.constructor._render_getStaticKeyFullText({ + name, + prop + }); + } + ).join(", "); + out.push(pt); + } + + if (selProfs.choose) { + selProfs.choose.forEach(fromBlock=>{ + if (fromBlock.name) { + out.push(`Choose ${fromBlock.name.toLowerCase()}`); + return; + } + + if (fromBlock.fromFilter) { + out.push(`Choose ${Parser.numberToText(fromBlock.count)} from filtered selection`); + return; + } + + if (fromBlock.groups) { + out.push(`Choose ${Parser.numberToText(fromBlock.count)} from ${Object.values(fromBlock.groups).map(({name})=>name).joinConjunct(", ", " or ")}`); + return; + } + + out.push(`Choose ${Parser.numberToText(fromBlock.count || 1)} from ${fromBlock.from.map(({name})=>name.toTitleCase()).join(", ")}`); + } + ); + } + + return out.filter(Boolean).join("; ") || "(Nothing)"; + } + , + }, ); + + if (this._featureSourceTracker) { + const hk = ()=>{ + const formData = this._getFormData().data; + const trackerState = Object.keys(formData.data || {}).filter(k=>Charactermancer_OtherProficiencySelect._PROP_GROUPS[k]).mergeMap(it=>it); + this._featureSourceTracker.setState(this, trackerState); + } + ; + this._addHookBase("ixSet", hk); + } + + return $$`
    + ${$selIxSet} +
    `; + } + + _getAllValuesMaybeInUseLookup() { + const out = {}; + + const activeSet = this._available[this._state.ixSet] || {}; + + if (activeSet.static) { + activeSet.static.forEach(({name, prop})=>{ + out[prop] = out[prop] || new Set(); + out[prop].add(name); + } + ); + } + + if (activeSet.choose) { + activeSet.choose.forEach(({from, fromFilter})=>{ + if (fromFilter) { + const prefix = `${this._getStateKeyPrefix()}_chooseFilter_`; + Object.entries(this._state).filter(([k,v])=>k.startsWith(prefix) && v).forEach(([,{prop, name}])=>{ + if (!name) + throw new Error(`"fromFilter" choice had no "name"--this should never occur!`); + out[prop] = out[prop] || new Set(); + out[prop].add(name); + } + ); + return; + } + + from.forEach(({name, prop})=>{ + out[prop] = out[prop] || new Set(); + out[prop].add(name); + } + ); + } + ); + } + + return out; + } + + _getStateKeyPrefix() { + return "otherProfSelect"; + } + + _getPropsChooseFromFilter({ixChoose, ixCount}) { + return { + propState: `${this._getStateKeyPrefix()}_chooseFilter_${ixChoose}_${ixCount}`, + }; + } + + _getPropsChooseFrom({ixChoose}) { + return { + propState: `${this._getStateKeyPrefix()}_${ixChoose}`, + }; + } + + async _hk_pUpdatePtsExisting($ptsExistingStatic, $ptsExistingChooseFrom) { + try { + await this._pLock("updateExisting"); + await this._hk_pUpdatePtsExisting_({ $ptsExistingStatic, $ptsExistingChooseFrom }); + } finally { + this._unlock("updateExisting"); + } + } + + async _hk_pUpdatePtsExisting_({$ptsExistingStatic, $ptsExistingChooseFrom}) { + const allValueLookupEntries = Object.entries(this._getAllValuesMaybeInUseLookup()); + + if ($ptsExistingStatic) + await this._hk_pUpdatePtsExisting_part({ allValueLookupEntries, $ptsExisting: $ptsExistingStatic }); + if (!$ptsExistingChooseFrom) + return; + for (const $ptsExisting of $ptsExistingChooseFrom) + await this._hk_pUpdatePtsExisting_part({ allValueLookupEntries, $ptsExisting }); + } + + async _hk_pUpdatePtsExisting_part({allValueLookupEntries, $ptsExisting}) { + for (const [prop,allProfs] of allValueLookupEntries) { + const otherStates = this._featureSourceTracker ? this._featureSourceTracker.getStatesForKey(prop, { ignore: this }) : null; + + for (const v of allProfs) { + const parentGroup = await this.constructor._pGetParentGroup({ prop, name: v }); + + if (!$ptsExisting[prop]?.[v] && !parentGroup){continue;} + + //Check our actor already has proficiency or expertise in this skill from anywhere else + let maxExisting = this._existing?.[prop]?.[v] || (parentGroup && this._existing?.[prop]?.[parentGroup]) || 0; + + if (otherStates) + otherStates.forEach(otherState=>maxExisting = Math.max(maxExisting, otherState[v] || 0, (parentGroup ? otherState[parentGroup] : 0) || 0)); + + //1 is proficiency, 2 is expertise + const helpText = maxExisting === 0 ? "" : `${UtilActors.PROF_TO_TEXT[maxExisting]} from Another Source`; + + //Show a warning text + $ptsExisting[prop][v].title(helpText).toggleClass("ml-1", !!maxExisting).html(maxExisting ? `()` : ""); + } + } + } + + _render_renderPtStatic($stgGroup, profsStatic) { + const $ptsExisting = {}; + + const byProp = {}; + profsStatic.forEach(({prop, name})=>MiscUtil.set(byProp, prop, name, true)); + const isMultiProp = this.constructor._getAllPossiblePropsForProfSet(this._available[this._state.ixSet]).size > 1; + + const $wrps = Object.entries(byProp).map(([prop,profsStaticSet])=>{ + const ptPropType = isMultiProp ? ` (${this.constructor._getPropDisplayName({ + prop + })} Proficiency)` : ""; + const profsStaticSetKeys = Object.keys(profsStaticSet); + return profsStaticSetKeys.sort(SortUtil.ascSortLower).map((name,i)=>{ + const $ptExisting = $(`
    `); + MiscUtil.set($ptsExisting, prop, name, $ptExisting); + const isNotLast = i < profsStaticSetKeys.length - 1; + return $$`
    ${this.constructor._getRenderedStatic({ + prop, + name + })}${ptPropType}${$ptExisting}${isNotLast ? `,` : ""}
    `; + } + ); + } + ).flat(); + + $$`
    + ${$wrps} +
    `.appendTo($stgGroup); + + return $ptsExisting; + } + + _render_renderPtChooseFrom($stgGroup, {ix, count, from, groups}) { + const {propState} = this._getPropsChooseFrom({ ixChoose: ix }); + const $ptsExisting = {}; + const compOpts = { + count, + fnDisplay: ({prop, name})=>{ + const $ptExisting = $(`
    `); + MiscUtil.set($ptsExisting, prop, name, $ptExisting); + + return $$`
    +
    ${this.constructor._getRenderedStatic({ + prop, + name + })}
    + ${$ptExisting} +
    `; + } + , + }; + + const fromProps = new Set(from.map(({prop})=>prop)); + + const byPropThenGroup = {}; + + from.forEach(({name, prop, group})=>{ + group = group ?? "_"; + MiscUtil.set(byPropThenGroup, prop, group, name, Charactermancer_OtherProficiencySelect._PROFICIENT); + }); + + const isMultiProp = Object.keys(byPropThenGroup).length > 1; + const isGrouped = Object.values(byPropThenGroup).some(groupMeta=>Object.keys(groupMeta).some(group=>group !== "_")); + + if (isMultiProp || isGrouped) { + const valueGroups = []; + Object.entries(byPropThenGroup).forEach(([prop,groupMeta])=>{ + Object.entries(groupMeta).forEach(([groupId,names])=>{ + const groupDetails = groups?.[groupId]; + + valueGroups.push({ + name: [(isMultiProp ? `${this.constructor._getPropDisplayName({ + prop + })} Proficiencies` : ""), groupDetails?.name, ].filter(Boolean).join(""), + text: groupDetails?.hint, + values: Object.keys(names).map(name=>({ prop, name })), + }); + } + ); + }); + + compOpts.valueGroups = valueGroups; + } + else { compOpts.values = from; } + + //Create the UI element + const meta = ComponentUiUtil.getMetaWrpMultipleChoice(this, propState, compOpts, ); + + let hkSetTrackerInfo = null; + if (this._featureSourceTracker) { + hkSetTrackerInfo = ()=>this._doSetTrackerState(); + this._addHookBase(meta.propPulse, hkSetTrackerInfo); + } + + this._lastMetas.push({ + cleanup: ()=>{ + meta.cleanup(); + if (hkSetTrackerInfo) + this._removeHookBase(meta.propPulse, hkSetTrackerInfo); + }, + }); + + const header = fromProps.size === 1 ? (`${this.constructor._getPropDisplayName({ + prop: [...fromProps][0] + })} ${count === 1 ? "Proficiency" : "Proficiencies"}`) : (count === 1 ? this._getTitle() : this._getTitlePlural()); + $stgGroup.append(`
    ${this._titlePrefix ? `${this._titlePrefix}: ` : ""}Choose ${Parser.numberToText(count)} ${header}:
    `); + meta.$ele.appendTo($stgGroup); + + return $ptsExisting; + } + + _render_renderPtChooseFromFilter($stgGroup, {ix, fromFilter, count, prop}) { + const $ptsExisting = {}; + + const $row = $(`
    `); + + [...new Array(count)].forEach((_,i)=>{ + const {propState} = this._getPropsChooseFromFilter({ + ixChoose: ix, + ixCount: i + }); + + const $ptExisting = $(`
    `); + + const $disp = $(`
    `); + const hkChosen = (propHk,valueHk,prevValueHk)=>{ + const isFirstRun = !propHk; + if (!isFirstRun) { + if (prevValueHk) { + const {prop: propPrev, name: namePrev} = prevValueHk; + const uidPrev = (namePrev || "").toLowerCase(); + MiscUtil.delete($ptsExisting, propPrev, uidPrev, $ptExisting); + } + + if (valueHk) { + const {prop, name} = valueHk || {}; + const uid = (name || "").toLowerCase(); + MiscUtil.set($ptsExisting, prop, uid, $ptExisting); + } + } + + $disp.html(this._state[propState] != null ? `
    ${Renderer.get().render(`{${this.constructor._getTagFromProp(prop)} ${this._state[propState].name.toLowerCase()}}`)}
    ` : `
    (select a ${this.constructor._getPropDisplayName({ + prop + }).toLowerCase()} proficiency)
    `, ); + + if (!isFirstRun && this._featureSourceTracker) + this._doSetTrackerState(); + }; + this._addHookBase(propState, hkChosen); + this._lastMetas.push({ + cleanup: ()=>this._removeHookBase(propState, hkChosen) + }); + hkChosen(); + + const $btnFilter = $(``).click(async()=>{ + const selecteds = await this._pGetFilterChoice({ + prop, + fromFilter + }); + if (selecteds == null || !selecteds.length) + return; + + const selected = selecteds[0]; + this._state[propState] = { + prop, + name: `${selected.name}|${selected.values.sourceJson}`.toLowerCase() + }; + } + ); + + $$`
    ${$btnFilter}${$disp}${$ptExisting}
    `.appendTo($row); + } + ); + + $$`
    + ${$row} +
    `.appendTo($stgGroup); + + return $ptsExisting; + } + + _pGetFilterChoice({prop, fromFilter}) { + switch (prop) { + case "armorProficiencies": + case "weaponProficiencies": + { + const modalFilterItems = new ModalFilterItemsFvtt({ + filterExpression: fromFilter, + namespace: "Charactermancer_OtherProficiencySelect.items", + isRadio: true, + }); + return modalFilterItems.pGetUserSelection({ + filterExpression: fromFilter + }); + } + + default: + throw new Error(`Filter choices for "${prop}" are unimplemented!`); + } + } + + isNoChoice() { + return this.constructor._isNoChoice({ + available: this._available, + isAlreadyMapped: true + }); + } + + _getFormData() { + let isFormComplete = true; + const out = {}; + + const selProfs = this._available[this._state.ixSet]; + + //This should only occur when we completely wipe the component state as part of 'remove class' button + if(!selProfs){ return {isFormComplete:false, data:null};} + + (selProfs.static || []).forEach(({prop, name})=>MiscUtil.set(out, prop, name, Charactermancer_OtherProficiencySelect._PROFICIENT)); + + (selProfs.choose || []).forEach(({count, from, groups, fromFilter, prop},ixChoose)=>{ + if (fromFilter) { + [...new Array(count)].forEach((_,ixCount)=>{ + const {propState} = this._getPropsChooseFromFilter({ + ixChoose, + ixCount + }); + + if (!this._state[propState]) + return isFormComplete = false; + + const {prop, name} = this._state[propState]; + MiscUtil.set(out, prop, name, Charactermancer_OtherProficiencySelect._PROFICIENT); + } + ); + + return; + } + + const {propState} = this._getPropsChooseFrom({ ixChoose }); + + const ixs = ComponentUiUtil.getMetaWrpMultipleChoice_getSelectedIxs(this, propState); + ixs.map(ix=>from[ix]).forEach(({prop, name})=>MiscUtil.set(out, prop, name, Charactermancer_OtherProficiencySelect._PROFICIENT)); + + if (!this._state[ComponentUiUtil.getMetaWrpMultipleChoice_getPropIsAcceptable(propState)]) + isFormComplete = false; + }); + + return { isFormComplete, data: out, }; + } + + pGetFormData() { + return this._getFormData(); + } + + _getDefaultState() { + return { + ixSet: 0, + }; + } + + static _getExistingProficiencies({existingProficienciesSetFvtt, vetToFvttProfs, allProfsVet}) { + const {existingProficienciesFvttSet, existingProficienciesFvttSetCustom} = this._getExistingFvttProficiencySetsMeta(existingProficienciesSetFvtt); + + const existing = {}; + + Object.entries(vetToFvttProfs).filter(([_,fvtt])=>existingProficienciesFvttSet.has(fvtt)).forEach(([vet,fvtt])=>{ + existing[vet] = Charactermancer_OtherProficiencySelect._PROFICIENT; + existingProficienciesFvttSet.delete(fvtt); + } + ); + + allProfsVet.forEach(vet=>{ + if (existingProficienciesFvttSet.has(vet)) { + existing[vet] = Charactermancer_OtherProficiencySelect._PROFICIENT; + existingProficienciesFvttSet.delete(vet); + } else if (existingProficienciesFvttSetCustom.has(vet)) { + existing[vet] = Charactermancer_OtherProficiencySelect._PROFICIENT; + existingProficienciesFvttSetCustom.delete(vet); + } + } + ); + + if (existingProficienciesFvttSet.size || existingProficienciesFvttSetCustom.size) { + existing.other = existingProficienciesFvttSet.size + existingProficienciesFvttSetCustom.size; + } + + return existing; + } + + static _getExistingSkillToolProficiencies({existingProficienciesSetFvtt, mapAbvToFull}) { + const existing = {}; + + Object.entries(existingProficienciesSetFvtt || {}).forEach(([abv,data])=>{ + if (!data.value) + return; + existing[mapAbvToFull[abv]] = data.value; + }); + + return existing; + } + + static _getExistingSavingThrowProficiencies(existingFvtt) { + const existing = {}; + + Object.entries(existingFvtt?.savingThrowProficiencies || {}).forEach(([ab,data])=>{ + if (!data.proficient) + return; + existing[ab] = data.proficient; + } + ); + + return existing; + } +} + +Charactermancer_OtherProficiencySelect._PROFICIENT = 1; +Charactermancer_OtherProficiencySelect._PROP_GROUPS = { + "skillProficiencies": { + propTrackerPulse: "pulseSkillProficiencies", + }, + "toolProficiencies": { + propTrackerPulse: "pulseToolProficiencies", + }, + "languageProficiencies": { + propTrackerPulse: "pulseLanguageProficiencies", + }, + "weaponProficiencies": { + propTrackerPulse: "pulseWeaponProficiencies", + }, + "armorProficiencies": { + propTrackerPulse: "pulseArmorProficiencies", + }, + "savingThrowProficiencies": { + propTrackerPulse: "pulseSavingThrowProficiencies", + }, +}; + +Charactermancer_OtherProficiencySelect._MAPPED_IGNORE_KEYS = new Set(["choose", "any", "anySkill", "anyTool", "anyArtisansTool", "anyMusicalInstrument", "anyLanguage", "anyStandardLanguage", "anyExoticLanguage", "anyWeapon", "anyArmor", "anySavingThrow", ]); + +Charactermancer_OtherProficiencySelect._VALID_SKILLS = new Set([...Renderer.generic.FEATURE__SKILLS_ALL, "anySkill", ]); +Charactermancer_OtherProficiencySelect._VALID_TOOLS = new Set([...Renderer.generic.FEATURE__TOOLS_ALL, "anyTool", "anyArtisansTool", "anyMusicalInstrument", ]); +Charactermancer_OtherProficiencySelect._VALID_LANGUAGES = new Set([...Renderer.generic.FEATURE__LANGUAGES_ALL, "anyLanguage", "anyStandardLanguage", "anyExoticLanguage", ]); +Charactermancer_OtherProficiencySelect._VALID_WEAPONS = new Set([...UtilActors.WEAPON_PROFICIENCIES, "anyWeapon", ]); +Charactermancer_OtherProficiencySelect._VALID_ARMORS = new Set([...UtilActors.ARMOR_PROFICIENCIES, "anyArmor", ]); +Charactermancer_OtherProficiencySelect._VALID_SAVING_THROWS = new Set([...Parser.ABIL_ABVS, "anySavingThrow", ]); + +class Charactermancer_SkillSaveProficiencySelect extends Charactermancer_ProficiencySelect { + static async pGetUserInput(opts) { + opts = opts || {}; + + if (!opts.available) + return { + isFormComplete: true, + data: {} + }; + + const comp = new this({ + ...opts, + existing: this.getExisting(opts.existingFvtt), + existingFvtt: opts.existingFvtt, + }); + if (comp.isNoChoice()) + return comp.pGetFormData(); + + return UtilApplications.pGetImportCompApplicationFormData({ + comp, + isAutoResize: true + }); + } + + static getExisting(existingFvtt) { + throw new Error(`Unimplemented!`); + } + + static isNoChoice(available) { + if (!available?.length) + return true; + return available.length === 1 && !available[0].choose; + } + + constructor(opts) { + opts = opts || {}; + super(); + + this._propGroup = opts.propGroup; + this._existing = opts.existing; + this._existingFvtt = opts.existingFvtt; + this._available = opts.available; + this._titlePrefix = opts.titlePrefix; + this._featureSourceTracker = opts.featureSourceTracker; + this._modalTitle = opts.modalTitle; + this._title = opts.title; + this._titlePlural = opts.titlePlural; + + this._hkUpdateExisting = null; + this._$stgGroup = null; + this._lastMeta = null; + } + + get modalTitle() { + return this._modalTitle; + } + + _getStaticDisplay(prof, {isPlainText=false}={}) { + throw new Error(`Unimplemented!`); + } + _getMultiChoiceDisplay($ptsExisting, profOrObj) { + throw new Error(`Unimplemented!`); + } + _getMultiChoiceTitle(cpyProfSet, count) { + throw new Error(`Unimplemented!`); + } + + _getNonStaticDisplay(key, value, {isPlainText=false}={}) { + switch (key) { + case "choose": + return this._getChooseFromDisplay(key, value, { + isPlainText + }); + default: + throw new Error(`Unhandled non-static key "${key}" (value was ${JSON.stringify(value)})`); + } + } + + _getChooseFromDisplay(key, value, {isPlainText=false}={}) { + return `Choose ${value.count || 1} from ${value.from.map(it=>this._getStaticDisplay(it, { + isPlainText + })).join(", ")}`; + } + + render($wrp) { + const $stgSelGroup = this._render_$getStgSelGroup(); + this._$stgGroup = $$`
    `; + + this._addHookBase("ixSet", this._hk_ixSet.bind(this)); + this._hk_ixSet(); + + $$` + ${$stgSelGroup} + ${this._$stgGroup} + `.appendTo($wrp); + } + + _render_$getStgSelGroup() { + if (this._available.length <= 1) + return null; + + const $selIxSet = ComponentUiUtil.$getSelEnum(this, "ixSet", { + values: this._available.map((_,i)=>i), + fnDisplay: ix=>{ + const v = this._available[ix]; + + const out = []; + + out.push(Object.keys(v).sort(SortUtil.ascSortLower).filter(it=>this._isStaticKey(it)).map(k=>this._getStaticDisplay(k, { + isPlainText: true + })).join(", "), ); + + Object.keys(v).filter(it=>!this._isStaticKey(it)).forEach(k=>out.push(this._getNonStaticDisplay(k, v[k], { + isPlainText: true + }))); + + return out.filter(Boolean).join("; ") || "(Nothing)"; + } + , + }, ); + + if (this._featureSourceTracker) + this._addHookBase("ixSet", ()=>this._doSetTrackerState()); + + return $$`
    + ${$selIxSet} +
    `; + } + + _doSetTrackerState() { + this._featureSourceTracker.setState(this, { + [this._propGroup.propTracker]: this._getFormData().data?.[this._propGroup.prop] + }); + } + + static _getSortedProfSet(profSet) { + if (!profSet) + return profSet; + + profSet = MiscUtil.copy(profSet); + + if (profSet.choose?.from) { + profSet.choose.from.sort((a,b)=>{ + if (typeof a === "object" && typeof b === "object") + return 0; + if (typeof a === "object") + return 1; + if (typeof b === "object") + return -1; + return SortUtil.ascSortLower(a, b); + } + ); + } + + return profSet; + } + + _render_renderPtStatic($stgGroup, profSet) { + const $ptsExisting = {}; + + const profList = this._getStaticKeys_profSet().filter(key=>profSet[key]); + + const $wrps = profList.map((it,i)=>{ + const $ptExisting = $(`
    `); + ($ptsExisting[it] = $ptsExisting[it] || []).push($ptExisting); + const isNotLast = i < profList.length - 1; + return $$`
    ${this._getStaticDisplay(it)}${$ptExisting}${isNotLast ? `,` : ""}
    `; + } + ); + + $$`
    + ${$wrps} +
    `.appendTo($stgGroup); + + return $ptsExisting; + } + + _getStaticKeys_all() { + throw new Error("Unimplemented!"); + } + + _getStaticKeys_profSet() { + throw new Error("Unimplemented!"); + } + + _hk_ixSet() { + this._$stgGroup.empty(); + + if (this._featureSourceTracker && this._hkUpdateExisting) + this._featureSourceTracker.removeHook(this, this._propGroup.propTrackerPulse, this._hkUpdateExisting); + if (this._lastMeta) + this._lastMeta.cleanup(); + + const profSet = this._available[this._state.ixSet]; + + if (this._featureSourceTracker) + this._doSetTrackerState(); + + this._hk_ixSet_renderPts(profSet); + + if (this._featureSourceTracker) + this._featureSourceTracker.addHook(this, this._propGroup.propTrackerPulse, this._hkUpdateExisting); + this._hkUpdateExisting(); + } + + _hk_ixSet_renderPts(profSet) { + const $ptsExistingStatic = Object.keys(profSet).some(it=>this._isStaticKey(it)) ? this._render_renderPtStatic(this._$stgGroup, profSet) : null; + + if ($ptsExistingStatic && profSet.choose) + this._$stgGroup.append(`
    `); + const $ptsExistingChooseFrom = profSet.choose ? this._render_renderPtChooseFrom(this._$stgGroup, profSet) : null; + + this._hkUpdateExisting = ()=>this._hk_updatePtsExisting($ptsExistingStatic, $ptsExistingChooseFrom); + } + + _isStaticKey(key) { + return this._getStaticKeys_all().includes(key); + } + + _hk_updatePtsExisting($ptsExistingStatic, $ptsExistingChoose) { + const otherStates = this._featureSourceTracker ? this._featureSourceTracker.getStatesForKey(this._propGroup.propTracker, { + ignore: this + }) : null; + + const $ptsExistings = [$ptsExistingStatic, $ptsExistingChoose].filter(Boolean); + + this._getStaticKeys_all().forEach(prof=>{ + $ptsExistings.forEach($ptsExisting=>{ + if (!$ptsExisting[prof]) + return; + + let maxExisting = this._existing?.[prof] || 0; + + if (otherStates) + otherStates.forEach(otherState=>maxExisting = Math.max(maxExisting, otherState[prof] || 0)); + + if (maxExisting) { + const helpText = maxExisting === 1 ? `Proficient from Another Source` : maxExisting === 2 ? `Proficient with Expertise from Another Source` : `Half-Proficient from Another Source`; + + $ptsExisting[prof].forEach($ptExisting=>{ + $ptExisting.title(helpText).addClass("ml-1").html(`()`); + } + ); + } + else { + $ptsExisting[prof].forEach($ptExisting=>{ + $ptExisting.title("").removeClass("ml-1").html(""); + } + ); + } + }); + }); + } + + _render_renderPtChooseFrom($stgGroup, profSet) { + const count = profSet.choose.count || 1; + + const cpyProfSet = this.constructor._getSortedProfSet(profSet); + + const $ptsExisting = {}; + const multiChoiceMeta = ComponentUiUtil.getMetaWrpMultipleChoice(this, "proficiencyChoice", { + count, + values: cpyProfSet.choose.from, + fnDisplay: profOrObj=>this._getMultiChoiceDisplay($ptsExisting, profOrObj), + }, ); + + let hkSetTrackerInfo = null; + if (this._featureSourceTracker) { + hkSetTrackerInfo = ()=>this._doSetTrackerState(); + this._addHookBase(multiChoiceMeta.propPulse, hkSetTrackerInfo); + } + + $stgGroup.append(`
    ${this._getMultiChoiceTitle(cpyProfSet, count)}:
    `); + multiChoiceMeta.$ele.appendTo($stgGroup); + + this._lastMeta = { + cleanup: ()=>{ + multiChoiceMeta.cleanup(); + if (hkSetTrackerInfo) + this._removeHookBase(multiChoiceMeta.propPulse, hkSetTrackerInfo); + } + , + }; + + return $ptsExisting; + } + + isNoChoice() { + return this.constructor.isNoChoice(this._available); + } + + _getFormData() { + const out = {}; + + const profSet = this._available[this._state.ixSet]; + + const cpyProfSet = this.constructor._getSortedProfSet(profSet); + + this._getStaticKeys_all().filter(name=>cpyProfSet[name]).map(name=>out[name] = 1); + + if (cpyProfSet.choose) { + const ixs = ComponentUiUtil.getMetaWrpMultipleChoice_getSelectedIxs(this, "proficiencyChoice"); + ixs.map(it=>cpyProfSet.choose.from[it]).forEach(name=>out[name] = 1); + } + + return { + isFormComplete: !!this._state[ComponentUiUtil.getMetaWrpMultipleChoice_getPropIsAcceptable("proficiencyChoice")], + data: { + [this._propGroup.prop]: out, + }, + }; + } + + pGetFormData() { + return this._getFormData(); + } + + _getDefaultState() { + return { + ixSet: 0, + }; + } +} +class Charactermancer_ImmResVulnSelect extends BaseComponent { + static async pGetUserInput(opts) { + opts = opts || {}; + + if (!opts.available) + return { + isFormComplete: true, + data: {} + }; + + const comp = new this({ + ...opts, + existing: this.getExisting(opts.existingFvtt), + existingFvtt: opts.existingFvtt, + }); + if (comp.isNoChoice()) + return comp.pGetFormData(); + + return UtilApplications.pGetImportCompApplicationFormData({ + comp, + isAutoResize: true + }); + } + + static getExisting() { + throw new TypeError(`Unimplemented!`); + } + + static isNoChoice(available) { + let cntChoices = 0; + UtilDataConverter.WALKER_READONLY_GENERIC.walk(available, { + object: (obj)=>{ + if (obj.choose) + cntChoices++; + } + }); + return cntChoices === 0; + } + + constructor(opts) { + opts = opts || {}; + super(); + + this._existing = opts.existing; + this._available = opts.available; + this._prop = opts.prop; + this._modalTitle = opts.modalTitle; + this._titlePlural = opts.titlePlural; + this._titleSingle = opts.titleSingle; + + this._lastChoiceMeta = null; + + Object.assign(this.__state.readonly_selectedValues, this._getOutputObject()); + } + + get modalTitle() { + return this._modalTitle; + } + + render($wrp) { + this._lastChoiceMeta = { + isActive: true, + children: [] + }; + this._render_recurse($wrp, MiscUtil.copy(this._available), this._lastChoiceMeta, false); + } + + _render_recurse($wrp, arr, outMeta, isChoices) { + const arrStrings = arr.filter(it=>typeof it === "string").sort(SortUtil.ascSortLower); + + if (!isChoices) { + const staticValues = arrStrings.map(it=>{ + outMeta.children.push({ + isActive: true, + value: it + }); + return it.toTitleCase(); + } + ); + $wrp.append(`
    ${staticValues.join(", ")}
    `); + } else { + arrStrings.forEach(it=>{ + const $cb = $(``).change(()=>{ + if ($cb.prop("checked")) { + const numChecked = outMeta.children.filter(it=>it.isChoosable && it.isActive()).length; + if (numChecked > outMeta.count) { + const toDeActive = outMeta.lastChecked || outMeta.children.filter(it=>it.isChoosable).last(); + toDeActive.setActive(false); + } + outMeta.lastChecked = node; + } else { + if (outMeta.lastChecked === node) + outMeta.lastChecked = null; + } + + this._state.readonly_selectedValues = this._getOutputObject(); + } + ); + + const node = { + isActive: ()=>$cb.prop("checked") ? it : null, + value: it, + isChoosable: true, + setActive: (val)=>$cb.prop("checked", val), + }; + outMeta.children.push(node); + + return $$``.appendTo($wrp); + } + ); + } + + arr.filter(it=>typeof it !== "string").forEach((it,i)=>{ + if (!it.choose) + throw new Error(`Unhandled immune/resist/vulnerability properties "${Object.keys(it).join(", ")}"`); + + if (isChoices) { + + const $btnSetActive = $(``).click(()=>{ + outMeta.children.forEach(it=>it.isActive = false); + nxtMeta.isActive = true; + this._state.readonly_selectedValues = this._getOutputObject(); + } + ); + + const nxtMeta = { + isActive: false, + children: [] + }; + + const $wrpChoice = $(`
    `); + this._render_recurse($wrpChoice, it.choose.from, nxtMeta, true); + + $$`
    +
    ${$btnSetActive}
    + ${$wrpChoice} +
    `; + + return; + } + + const count = it.choose.count || 1; + const nxtMeta = { + isActive: true, + children: [], + count, + lastChecked: null + }; + outMeta.children.push(nxtMeta); + + const $wrpChoice = $(`
    + ${arrStrings.length || i > 0 ? `
    ` : ""} +
    Choose ${count} ${count === 1 ? this._titleSingle : this._titlePlural}:
    +
    `).appendTo($wrp); + this._render_recurse($wrpChoice, it.choose.from, nxtMeta, true); + } + ); + } + + isNoChoice() { + return this.constructor.isNoChoice(this._available); + } + + _getOutputSet() { + const outSet = new Set(this._existing[this._prop] || []); + if (this._lastChoiceMeta) + this._getOutputSet_recurse(outSet, this._lastChoiceMeta); + else + UtilDataConverter.WALKER_READONLY_GENERIC.walk(this._available, { + string: (str)=>{ + outSet.add(str); + } + }); + + return outSet; + } + + _getOutputSet_recurse(outSet, node) { + if (!node.isActive) + return; + const isNodeActive = node.isActive === true || node.isActive(); + if (!isNodeActive) + return; + + if (node.value) + outSet.add(node.value); + if (node.children) + node.children.forEach(it=>this._getOutputSet_recurse(outSet, it)); + } + + _getOutputObject() { + return [...this._getOutputSet()].sort(SortUtil.ascSortLower).mergeMap(it=>({ + [it]: true + })); + } + + pGetFormData() { + let isFormComplete = true; + + return { + isFormComplete, + data: { + [this._prop]: MiscUtil.copy(this._state.readonly_selectedValues), + }, + }; + } + + _getDefaultState() { + return { + readonly_selectedValues: {}, + }; + } +} + +class Charactermancer_DamageImmunitySelect extends Charactermancer_ImmResVulnSelect { + static getExisting(existingFvtt) { + return MiscUtil.copy([existingFvtt?.immune?.value || []]); + } + + constructor(opts) { + opts = opts || {}; + super({ + ...opts, + modalTitle: `Damage Immunities`, + titlePlural: `Damage Immunities`, + titleSingle: `Damage Immunity`, + prop: "immune", + }); + } +} + +class Charactermancer_DamageResistanceSelect extends Charactermancer_ImmResVulnSelect { + static getExisting(existingFvtt) { + return MiscUtil.copy([existingFvtt?.resist?.value || []]); + } + + constructor(opts) { + opts = opts || {}; + super({ + ...opts, + modalTitle: `Damage Resistances`, + titlePlural: `Damage Resistances`, + titleSingle: `Damage Resistance`, + prop: "resist", + }); + } +} + +class Charactermancer_DamageVulnerabilitySelect extends Charactermancer_ImmResVulnSelect { + static getExisting(existingFvtt) { + return MiscUtil.copy([existingFvtt?.vulnerable?.value || []]); + } + + constructor(opts) { + opts = opts || {}; + super({ + ...opts, + modalTitle: `Damage Vulnerabilities`, + titlePlural: `Damage Vulnerabilities`, + titleSingle: `Damage Vulnerability`, + prop: "vulnerable", + }); + } +} + +class Charactermancer_ConditionImmunitySelect extends Charactermancer_ImmResVulnSelect { + static getExisting(existingFvtt) { + return [existingFvtt?.conditionImmune?.value || []].map(it=>it === "diseased" ? "disease" : it); + } + + constructor(opts) { + opts = opts || {}; + super({ + ...opts, + modalTitle: `Condition Immunities`, + titlePlural: `Condition Immunities`, + titleSingle: `Condition Immunity`, + prop: "conditionImmune", + }); + } +} + +class Charactermancer_ExpertiseSelect extends Charactermancer_SkillSaveProficiencySelect { + constructor(opts) { + super({ + ...opts, + propGroup: new Charactermancer_ProficiencySelect.PropGroup({ + prop: "expertise", + propTrackerPulse: "pulseExpertise", + propTracker: "expertise", + }), + modalTitle: "Expertise", + title: "Expertise", + titlePlural: "Expertise", + }); + } + + static getExisting(existingFvtt) { + const existingSkills = Object.entries(Charactermancer_OtherProficiencySelect.getExisting({ + skillProficiencies: existingFvtt.skillProficiencies + })?.skillProficiencies || {}).filter(([,profLevel])=>Number(profLevel) === 2).mergeMap(([prof,profLevel])=>({ + [prof]: profLevel + })); + + const existingTools = Object.entries(Charactermancer_OtherProficiencySelect.getExisting({ + skillProficiencies: existingFvtt.toolProficiencies + })?.toolProficiencies || {}).filter(([,profLevel])=>Number(profLevel) === 2).mergeMap(([prof,profLevel])=>({ + [prof]: profLevel + })); + + return { + ...existingSkills, + ...existingTools + }; + } + + static getExistingFvttFromActor(actor) { + return { + skillProficiencies: MiscUtil.get(actor, "_source", "system", "skills"), + toolProficiencies: MiscUtil.get(actor, "_source", "system", "tools"), + }; + } + + static isNoChoice(available) { + if (!available?.length) + return true; + return available.length === 1 && !available[0].choose && !available[0].anyProficientSkill && !available[0].anyProficientTool; + } + + + + _getStaticDisplay(key, {isPlainText=false}={}) { + if (isPlainText) + return key.toTitleCase(); + + if (Parser.SKILL_TO_ATB_ABV[key]) + return Renderer.get().render(`{@skill ${key.toTitleCase()}}`); + return key.toTitleCase(); + } + + _getNonStaticDisplay(key, value, {isPlainText=false}={}) { + switch (key) { + case "anyProficientSkill": + return `Choose ${value || 1} existing skill ${value > 1 ? "proficiencies" : "proficiency"}`; + case "anyProficientTool": + return `Choose ${value || 1} existing tool ${value > 1 ? "proficiencies" : "proficiency"}`; + default: + return super._getNonStaticDisplay(key, value, { + isPlainText + }); + } + } + + _getStaticKeys_all() { + return this._available.map(profSet=>this._getStaticKeys_profSet({ + profSet + })).flat().unique(); + } + + _getStaticKeys_profSet({profSet=null}={}) { + profSet = profSet || this._available[this._state.ixSet]; + return Object.keys(profSet).filter(it=>this._isStaticKey(it)); + } + + _isStaticKey(key) { + return !["anyProficientSkill", "anyProficientTool"].includes(key); + } + + _isSkillKey(key) { + return key === "anyProficientSkill" || Object.keys(Parser.SKILL_TO_ATB_ABV).includes(key); + } + + _hk_ixSet_renderPts(profSet) { + this._lastMeta = { + cleanup: ()=>{ + this._lastMeta._fnsCleanup.forEach(fn=>fn()); + } + , + _fnsCleanup: [], + }; + + const $ptsExistingStatic = Object.keys(profSet).some(it=>this._isStaticKey(it)) ? this._render_renderPtStatic(this._$stgGroup, profSet) : null; + let needsHr = $ptsExistingStatic != null; + + if (needsHr && profSet.anyProficientSkill) { + needsHr = false; + this._$stgGroup.append(`
    `); + } + const $ptsExistingChooseAnyProficientSkill = profSet.anyProficientSkill ? this._render_renderPtChooseAnyProficientSkill(this._$stgGroup, profSet) : null; + needsHr = needsHr || $ptsExistingChooseAnyProficientSkill != null; + + if (needsHr && profSet.anyProficientTool) { + needsHr = false; + this._$stgGroup.append(`
    `); + } + const $ptsExistingChooseAnyProficientTool = profSet.anyProficientTool ? this._render_renderPtChooseAnyProficientTool(this._$stgGroup, profSet) : null; + + this._hkUpdateExisting = ()=>this._hk_updatePtsExisting($ptsExistingStatic, $ptsExistingChooseAnyProficientSkill, $ptsExistingChooseAnyProficientTool); + } + + _getProps(prop, ix) { + return { + propAnyProficientSkill: `${prop}_ix_skill_${ix}`, + propAnyProficientTool: `${prop}_ix_tool_${ix}`, + }; + } + + _render_$getPtExisting() { + return $(`
    ()
    `); + } + + _render_renderPtStatic($stgGroup, profSet) { + const $ptsExisting = []; + + const profList = this._getStaticKeys_profSet().filter(key=>profSet[key]); + + const $wrps = profList.map((it,i)=>{ + const $ptExisting = this._render_$getPtExisting(); + + $ptsExisting.push({ + prof: it, + $ptExisting, + }); + + const isNotLast = i < profList.length - 1; + return $$`
    ${this._getStaticDisplay(it)}${$ptExisting}${isNotLast ? `,` : ""}
    `; + } + ); + + $$`
    + ${$wrps} +
    `.appendTo($stgGroup); + + return $ptsExisting; + } + + _render_renderPtChooseAnyProficientSkill($stgGroup, profSet) { + return this._render_renderPtChooseAnyProficient({ + $stgGroup, + profSet, + propProfSet: "anyProficientSkill", + propIxProps: "propAnyProficientSkill", + fnGetValues: this._getAvailableSkills.bind(this), + propPulse: "pulseSkillProficiencies", + titleRow: "Existing Skill", + }); + } + + _render_renderPtChooseAnyProficientTool($stgGroup, profSet) { + return this._render_renderPtChooseAnyProficient({ + $stgGroup, + profSet, + propProfSet: "anyProficientTool", + propIxProps: "propAnyProficientTool", + fnGetValues: this._getAvailableTools.bind(this), + propPulse: "pulseToolProficiencies", + titleRow: "Existing Tool", + }); + } + + _render_renderPtChooseAnyProficient({$stgGroup, profSet, propProfSet, propIxProps, fnGetValues, propPulse, titleRow, }, ) { + const numChoices = Number(profSet[propProfSet] || 1); + + const $wrp = $(`
    `).appendTo($stgGroup); + + const $ptsExisting = []; + + for (let i = 0; i < numChoices; ++i) { + const ixProps = this._getProps(propProfSet, i); + + const selMeta = ComponentUiUtil.$getSelEnum(this, ixProps[propIxProps], { + values: fnGetValues(), + isAllowNull: true, + asMeta: true, + fnDisplay: it=>it.toTitleCase(), + }, ); + this._lastMeta._fnsCleanup.push(selMeta.unhook); + + const $ptExisting = this._render_$getPtExisting(); + $ptsExisting.push({ + prop: ixProps[propIxProps], + $ptExisting, + }); + + const hk = ()=>selMeta.setValues(fnGetValues(), { + isResetOnMissing: true + }); + if (this._featureSourceTracker) { + this._featureSourceTracker.addHook(this, propPulse, hk); + this._lastMeta._fnsCleanup.push(()=>this._featureSourceTracker.removeHook(this, propPulse, hk)); + + const hkSetTrackerInfo = ()=>this._doSetTrackerState(); + this._addHookBase(ixProps[propIxProps], hkSetTrackerInfo); + this._lastMeta._fnsCleanup.push(()=>this._removeHookBase(ixProps[propIxProps], hkSetTrackerInfo)); + } + hk(); + + this._lastMeta._fnsCleanup.push(()=>delete this._state[ixProps[propIxProps]]); + + $$`
    +
    ${titleRow}:
    + ${selMeta.$sel} + ${$ptExisting} +
    `.appendTo($wrp); + } + + return $ptsExisting; + } + + _getAvailableSkills() { + return this._getAvailableByType({ + propExistingFvtt: "skillProficiencies", + propFeatureTracker: "skillProficiencies", + }); + } + + _getAvailableTools() { + return this._getAvailableByType({ + propExistingFvtt: "toolProficiencies", + propFeatureTracker: "toolProficiencies", + }); + } + + _getAvailableByType({propExistingFvtt, propFeatureTracker, }, ) { + //Read existing proficencies on the foundry character + /* const existingAnyProfLevel = Charactermancer_OtherProficiencySelect.getExisting({ + [propExistingFvtt]: this._existingFvtt[propExistingFvtt], + }); + const out = new Set(Object.entries(existingAnyProfLevel[propExistingFvtt]).filter(([,profLevel])=>profLevel >= 1).map(([prof])=>prof)); */ + + const out = new Set(); + if (this.featureSourceTracker) { //this is a Charactermancer_FeatureSourceTracker + (this.featureSourceTracker.getStatesForKey(propFeatureTracker, { + ignore: this + }) || []).forEach(otherState=>{ + Object.entries(otherState).filter(([,isAvailable])=>isAvailable).forEach(([prof])=>out.add(prof)); + }); + } + else{ + console.error("No FeatureSourceTracker provided. Could not read existing proficiencies and learn what expertise options are available"); + } + + return [...out].sort(SortUtil.ascSortLower); + } + + _hk_updatePtsExisting($ptsExistingStatic, $ptsExistingChooseAnyProficientSkill, $ptsExistingChooseAnyProficientTool) { + const otherStates = this._featureSourceTracker ? this._featureSourceTracker.getStatesForKey(this._propGroup.propTracker, { + ignore: this + }) : null; + + const ptsExistingMetas = [$ptsExistingStatic, $ptsExistingChooseAnyProficientSkill, $ptsExistingChooseAnyProficientTool].filter(Boolean).flat(); + + ptsExistingMetas.forEach(ptExistingMeta=>{ + const prof = ptExistingMeta.prof ?? this._state[ptExistingMeta.prop]; + + if (prof == null) { + ptExistingMeta.$ptExisting.hideVe(); + return; + } + + let maxExisting = this._existing?.[prof] || 0; + + if (otherStates) + otherStates.forEach(otherState=>maxExisting = Math.max(maxExisting, otherState[prof] || 0)); + + ptExistingMeta.$ptExisting.toggleVe(maxExisting === 2); + } + ); + } + + _doSetTrackerState() { + const formData = this._getFormData(); + this._featureSourceTracker.setState(this, { + [this._propGroup.propTracker]: formData.data?.[this._propGroup.prop], + "skillProficiencies": formData.data?.skillProficiencies, + "toolProficiencies": formData.data?.toolProficiencies, + }); + } + + _getFormData() { + const outSkills = {}; + const outTools = {}; + const outExpertise = {}; + + let isFormComplete = true; + + const profSet = this._available[this._state.ixSet]; + + Object.entries(profSet).forEach(([k,v])=>{ + if (k === "anyProficientSkill" || k === "anyProficientTool") { + const numChoices = Number(v || 1); + for (let i = 0; i < numChoices; ++i) { + const {propAnyProficientSkill, propAnyProficientTool} = this._getProps(k, i); + const prop = this._isSkillKey(k) ? propAnyProficientSkill : propAnyProficientTool; + const chosenProf = this._state[prop]; + if (chosenProf == null) { + isFormComplete = false; + continue; + } + (this._isSkillKey(k) ? outSkills : outTools)[chosenProf] = outExpertise[chosenProf] = 2; + } + return; + } + + (this._isSkillKey(k) ? outSkills : outTools)[k] = outExpertise[k] = 2; + }); + + return { + isFormComplete, + data: { + skillProficiencies: outSkills, + toolProficiencies: outTools, + expertise: outExpertise, + }, + }; + } + + pGetFormData() { + return this._getFormData(); + } + + _getDefaultState() { + return { + ixSet: 0, + }; + } + + /** + * @returns {Charactermancer_FeatureSourceTracker} + */ + get featureSourceTracker(){ + return this._featureSourceTracker; + } +} + +class Charactermancer_ResourceSelect extends BaseComponent { + static isNoChoice() { + return true; + } + + static async pApplyFormDataToActor(actor, formData) { + if (!formData?.data?.length) + return; + + const itemLookup = {}; + actor.items.contents.forEach(it=>itemLookup[it.name.toLowerCase().trim()] = it); + + const toCreate = []; + + formData.data.forEach(res=>{ + const existing = itemLookup[res.name.toLowerCase().trim()]; + + if (existing) + return; + + toCreate.push({ + name: res.name, + type: "feat", + data: this._getItemDataData({ + res + }), + img: this._getItemDataImg({ + res + }), + }); + } + ); + + await UtilDocuments.pCreateEmbeddedDocuments(actor, toCreate, { + ClsEmbed: Item, + isRender: false, + }, ); + } + + render() {} + + static _getItemDataData({res}) { + switch (res.type) { + case "dicePool": + return this._getItemDataData_dicePool({ + res + }); + default: + throw new Error(`Unhandled resource type "${res.type}"`); + } + } + + static _getItemDataData_dicePool({res}) { + return { + actionType: "other", + formula: `${res.number}d${res.faces}`, + activation: { + type: "special", + }, + uses: { + value: 0, + max: res.count, + per: UtilDataConverter.getFvttUsesPer(res.recharge), + }, + }; + } + + static _IMAGES = { + "Superiority Die": `icons/sundries/gaming/dice-runed-brown.webp`, + "Psionic Energy Die": "icons/sundries/gaming/dice-pair-white-green.webp", + }; + static _getItemDataImg({res}) { + if (this._IMAGES[res.name]) + return this._IMAGES[res.name]; + + if (/\b(?:dice|die)\b/i.test((res.name || ""))) + return `icons/sundries/gaming/dice-runed-brown.webp`; + + return `modules/${SharedConsts.MODULE_ID}/media/icon/mighty-force.svg`; + } + + constructor({resources, className, classSource, subclassShortName, subclassSource}) { + super(); + this._resources = resources; + this._className = className; + this._classSource = classSource; + this._subclassShortName = subclassShortName; + this._subclassSource = subclassSource; + + this._mappedResources = this._getMappedResources(); + } + + _getMappedResources() { + return (this._resources || []).map(res=>{ + switch (res.type) { + case "dicePool": + return this._getMappedResources_dicePool({ + res + }); + default: + throw new Error(`Unhandled resource type "${res.type}"`); + } + } + ); + } + + _getMappedResources_dicePool({res}) { + res = MiscUtil.copy(res); + res.number = this._getMappedResources_getReplacedVars(res.number || 1); + res.faces = this._getMappedResources_getReplacedVars(res.faces); + res.count = this._getMappedResources_getReplacedVars(res.count || 1); + return res; + } + + _getMappedResources_getReplacedVars(val) { + return `${val}`.replace(/\bPB\b/g, "@attributes.prof").replace(/<\$(?[^$]+)\$>/g, (...m)=>{ + switch (m.last().variable) { + case "level": + return `@classes.${Parser.stringToSlug(this._className || "unknown")}.levels`; + default: + return m[0]; + } + } + ); + } + + pGetFormData() { + return { + isFormComplete: true, + data: MiscUtil.copy(this._mappedResources || []), + }; + } +} + +class Charactermancer_SenseSelect extends BaseComponent { + static isNoChoice() { + return true; + } + + static getExistingFvttFromActor(actor) { + return { + senses: MiscUtil.get(actor, "_source", "system", "attributes", "senses"), + }; + } + + static getExisting(existingFvtt) { + return Object.keys(CONFIG.DND5E.senses).filter(sense=>existingFvtt?.senses[sense]).mergeMap(sense=>({ + [sense]: existingFvtt?.senses[sense] + })); + } + + render() {} + + constructor({senses, existing, existingFvtt}) { + super(); + this._senses = senses; + this._existing = existing; + this._existingFvtt = existingFvtt; + } + + static getFormDataFromRace(race) { + return { + isFormComplete: true, + data: { + darkvision: race.darkvision, + blindsight: race.blindsight, + truesight: race.truesight, + tremorsense: race.tremorsense, + }, + }; + } + + pGetFormData() { + return { + isFormComplete: true, + data: MiscUtil.copy(this._senses[0] || {}), + }; + } +} + +/** + * This component handles choices presented by class features and feats. + * It can create several sub-components that handle specific choices like expertise, language proficiencies, etc + * The state information is kept within these sub-components themselves. + */ +class Charactermancer_FeatureOptionsSelect extends BaseComponent { + constructor(opts) { + super(); + + this._optionsSet = opts.optionsSet; + this._actor = opts.actor; + this._level = opts.level; + this._existingFeatureChecker = opts.existingFeatureChecker; + this._featureSourceTracker = opts.featureSourceTracker; + this._isModal = !!opts.isModal; + this._modalFilterSpells = opts.modalFilterSpells; + this._isSkipCharactermancerHandled = !!opts.isSkipCharactermancerHandled; + this._isSkipRenderingFirstFeatureTitle = !!opts.isSkipRenderingFirstFeatureTitle; + + if (this._isOptions()) { + this._optionsSet.sort((a,b)=>SortUtil.ascSortLower(a.entity.name, b.entity.name) + || SortUtil.ascSortLower(Parser.sourceJsonToAbv(a.entity.source), Parser.sourceJsonToAbv(b.entity.source))); + } + + this._lastMeta = null; + this._lastSubMetas = []; + + this._subCompsSkillToolLanguageProficiencies = []; + this._subCompsSkillProficiencies = []; + this._subCompsLanguageProficiencies = []; + this._subCompsToolProficiencies = []; + this._subCompsWeaponProficiencies = []; + this._subCompsArmorProficiencies = []; + this._subCompsSavingThrowProficiencies = []; + this._subCompsDamageImmunities = []; + this._subCompsDamageResistances = []; + this._subCompsDamageVulnerabilities = []; + this._subCompsConditionImmunities = []; + this._subCompsExpertise = []; + this._subCompsResources = []; + this._subCompsSenses = []; + this._subCompsAdditionalSpells = []; + + //Arrays to store previous sub-components. We will pull state from them when creating new ones at re-render + this._prevSubCompsSkillToolLanguageProficiencies = null; + this._prevSubCompsSkillProficiencies = null; + this._prevSubCompsLanguageProficiencies = null; + this._prevSubCompsToolProficiencies = null; + this._prevSubCompsWeaponProficiencies = null; + this._prevSubCompsArmorProficiencies = null; + this._prevSubCompsSavingThrowProficiencies = null; + this._prevSubCompsDamageImmunities = []; + this._prevSubCompsDamageResistances = []; + this._prevSubCompsDamageVulnerabilities = []; + this._prevSubCompsConditionImmunities = []; + this._prevSubCompsExpertise = []; + this._prevSubCompsResources = []; + this._prevSubCompsSenses = null; + this._prevSubCompsAdditionalSpells = null; + } + + /** + * @returns {string[]} + */ + get allSubComponentNames(){ + return [ + "_subCompsSkillToolLanguageProficiencies", + "_subCompsSkillProficiencies", + "_subCompsLanguageProficiencies", + "_subCompsToolProficiencies", + "_subCompsWeaponProficiencies", + "_subCompsArmorProficiencies", + "_subCompsSavingThrowProficiencies", + "_subCompsDamageImmunities", + "_subCompsDamageResistances", + "_subCompsDamageVulnerabilities", + "_subCompsConditionImmunities", + "_subCompsExpertise", + "_subCompsResources", + "_subCompsSenses", + "_subCompsAdditionalSpells", + ]; + } + /** + * @returns {BaseComponent[]} + */ + allSubComponents(){ + const names = this.allSubComponentNames; + let arr = []; + for(let n of names){ + let a = this[n]; + if(a && a.length > 0){arr = arr.concat(a);} + } + return arr; + } + + render($wrp) { + const $stgSubChoiceData = $$`
    `.hideVe(); + this.$stgSubChoiceData = $stgSubChoiceData; //need this to be publicly accessible so we can call _render_pHkIxsChosen elsewhere + + this._render_options(); + + $$`
    + ${this._lastMeta?.$ele} + ${$stgSubChoiceData} +
    `.appendTo($wrp); + + this._addHookBase(ComponentUiUtil.getMetaWrpMultipleChoice_getPropPulse("ixsChosen"), ()=>this._render_pHkIxsChosen({ + $stgSubChoiceData + }), ); + + return this._render_pHkIxsChosen({$stgSubChoiceData}); + } + + async pRender($wrp) { + return this.render($wrp); + } + + async _render_pHkIxsChosen({$stgSubChoiceData}) { + try { + await this._pLock("ixsChosen"); + await this._render_pHkIxsChosen_({ $stgSubChoiceData }); + } + finally { this._unlock("ixsChosen"); } + } + + async _render_pHkIxsChosen_({$stgSubChoiceData}) { + const {prefixSubComps} = this._getProps(); + Object.keys(this._state).filter(k=>k.startsWith(prefixSubComps)).forEach(k=>delete this._state[k]); + + const selectedLoadeds = this._getSelectedLoadeds(); + + //If no loadeds are found, just don't render any subcomponents + if (!selectedLoadeds.length){return this._render_noSubChoices({ $stgSubChoiceData });} + + const isSubChoiceForceDisplay = await this._pIsSubChoiceForceDisplay(selectedLoadeds); + const isSubChoiceAvailable = await this._pIsSubChoiceAvailable(selectedLoadeds); + //Or if no choices are available for display, also dont render any subcomponents + if (!isSubChoiceForceDisplay && !isSubChoiceAvailable){return this._render_noSubChoices({ $stgSubChoiceData });} + + $stgSubChoiceData.empty(); + this._unregisterSubComps(); + + //TEMPFIX + const sideDataRaws = null;//await this._pGetLoadedsSideDataRaws(selectedLoadeds); + const ptrIsFirstSection = {_: true }; + + for (let i = 0; i < selectedLoadeds.length; ++i) { + const loaded = selectedLoadeds[i]; + + if (!(await this._pIsSubChoiceForceDisplay([selectedLoadeds[i]]) || await this._pIsSubChoiceAvailable([selectedLoadeds[i]]))) + continue; + + //TEMPFIX + const isSubChoice_sideDataChooseSystem = false; //await this._pHasChoiceInSideData_chooseSystem([selectedLoadeds[i]]); + const isSubChoice_sideDataChooseFlags = false; //await this._pHasChoiceInSideData_chooseFlags([selectedLoadeds[i]]); + + //Check if force display + const isForceDisplay_entryDataSkillToolLanguageProficiencies = await this._pIsForceDisplay_skillToolLanguageProficiencies([selectedLoadeds[i]]); + const isForceDisplay_entryDataSkillProficiencies = await this._pIsForceDisplay_skillProficiencies([selectedLoadeds[i]]); + const isForceDisplay_entryDataLanguageProficiencies = await this._pIsForceDisplay_languageProficiencies([selectedLoadeds[i]]); + const isForceDisplay_entryDataToolProficiencies = await this._pIsForceDisplay_toolProficiencies([selectedLoadeds[i]]); + const isForceDisplay_entryDataWeaponProficiencies = await this._pIsForceDisplay_weaponProficiencies([selectedLoadeds[i]]); + const isForceDisplay_entryDataArmorProficiencies = await this._pIsForceDisplay_armorProficiencies([selectedLoadeds[i]]); + const isForceDisplay_entryDataSavingThrowProficiencies = await this._pIsForceDisplay_savingThrowProficiencies([selectedLoadeds[i]]); + const isForceDisplay_entryDataDamageImmunities = await this._pIsForceDisplay_damageImmunities([selectedLoadeds[i]]); + const isForceDisplay_entryDataDamageResistances = await this._pIsForceDisplay_damageResistances([selectedLoadeds[i]]); + const isForceDisplay_entryDataDamageVulnerabilities = await this._pIsForceDisplay_damageVulnerabilities([selectedLoadeds[i]]); + const isForceDisplay_entryDataConditionImmunities = await this._pIsForceDisplay_conditionImmunities([selectedLoadeds[i]]); + const isForceDisplay_entryDataExpertise = await this._pIsForceDisplay_expertise([selectedLoadeds[i]]); + const isForceDisplay_entryDataResources = await this._pIsForceDisplay_resources([selectedLoadeds[i]]); + const isForceDisplay_entryDataSenses = await this._pIsForceDisplay_senses([selectedLoadeds[i]]); + const isForceDisplay_entryDataAdditionalSpells = await this._pIsForceDisplay_additionalSpells([selectedLoadeds[i]]); + + //Check if available + const isAvailable_entryDataSkillToolLanguageProficiencies = await this._pIsAvailable_skillToolLanguageProficiencies([selectedLoadeds[i]]); + const isAvailable_entryDataSkillProficiencies = await this._pIsAvailable_skillProficiencies([selectedLoadeds[i]]); + const isAvailable_entryDataLanguageProficiencies = await this._pIsAvailable_languageProficiencies([selectedLoadeds[i]]); + const isAvailable_entryDataToolProficiencies = await this._pIsAvailable_toolProficiencies([selectedLoadeds[i]]); + const isAvailable_entryDataWeaponProficiencies = await this._pIsAvailable_weaponProficiencies([selectedLoadeds[i]]); + const isAvailable_entryDataArmorProficiencies = await this._pIsAvailable_armorProficiencies([selectedLoadeds[i]]); + const isAvailable_entryDataSavingThrowProficiencies = await this._pIsAvailable_savingThrowProficiencies([selectedLoadeds[i]]); + const isAvailable_entryDataDamageImmunities = await this._pIsAvailable_damageImmunities([selectedLoadeds[i]]); + const isAvailable_entryDataDamageResistances = await this._pIsAvailable_damageResistances([selectedLoadeds[i]]); + const isAvailable_entryDataDamageVulnerabilities = await this._pIsAvailable_damageVulnerabilities([selectedLoadeds[i]]); + const isAvailable_entryDataConditionImmunities = await this._pIsAvailable_conditionImmunities([selectedLoadeds[i]]); + const isAvailable_entryDataExpertise = await this._pIsAvailable_expertise([selectedLoadeds[i]]); + const isAvailable_entryDataResources = await this._pIsAvailable_resources([selectedLoadeds[i]]); + const isAvailable_entryDataSenses = await this._pIsAvailable_senses([selectedLoadeds[i]]); + const isAvailable_entryDataAdditionalSpells = await this._pIsAvailable_additionalSpells([selectedLoadeds[i]]); + + const {entity, type} = loaded; + + if (i !== 0 || !this._isSkipRenderingFirstFeatureTitle) + $stgSubChoiceData.append(this._render_getSubCompTitle(entity)); + + //Try to render any subcomponent possible (will self-cancel if requirements are not met) + + //TEMPFIX + /* if (isSubChoice_sideDataChooseSystem) { + const sideDataRaw = sideDataRaws[i]; + if (sideDataRaw?.chooseSystem) { + ptrIsFirstSection._ = false; + this._render_renderSubComp_chooseSystem(i, $stgSubChoiceData, entity, type, sideDataRaw); + } + } + + if (isSubChoice_sideDataChooseFlags) { + const sideDataRaw = sideDataRaws[i]; + if (sideDataRaw?.chooseFlags) { + ptrIsFirstSection._ = false; + this._render_renderSubComp_chooseFlags(i, $stgSubChoiceData, entity, type, sideDataRaw); + } + } */ + + this._render_pHkIxsChosen_comp({ + ix: i, + $stgSubChoiceData, + selectedLoadeds, + propSubComps: "_subCompsSkillToolLanguageProficiencies", + propPrevSubComps: "_prevSubCompsSkillToolLanguageProficiencies", + isAvailable: isAvailable_entryDataSkillToolLanguageProficiencies, + isForceDisplay: isForceDisplay_entryDataSkillToolLanguageProficiencies, + prop: "skillToolLanguageProficiencies", + ptrIsFirstSection, + CompClass: Charactermancer_OtherProficiencySelect, + fnGetExistingFvtt: Charactermancer_OtherProficiencySelect.getExistingFvttFromActor.bind(Charactermancer_OtherProficiencySelect), + fnSetComp: this._render_pHkIxsChosen_setCompOtherProficiencies.bind(this), + }); + + this._render_pHkIxsChosen_comp({ + ix: i, + $stgSubChoiceData, + selectedLoadeds, + propSubComps: "_subCompsSkillProficiencies", + propPrevSubComps: "_prevSubCompsSkillProficiencies", + isAvailable: isAvailable_entryDataSkillProficiencies, + isForceDisplay: isForceDisplay_entryDataSkillProficiencies, + prop: "skillProficiencies", + title: "Skill Proficiencies", + ptrIsFirstSection, + CompClass: Charactermancer_OtherProficiencySelect, + propPathActorExistingProficiencies: ["system", "skills"], + fnSetComp: this._render_pHkIxsChosen_setCompOtherProficiencies.bind(this), + fnGetMappedProficiencies: Charactermancer_OtherProficiencySelect.getMappedSkillProficiencies.bind(Charactermancer_OtherProficiencySelect), + }); + + this._render_pHkIxsChosen_comp({ + ix: i, + $stgSubChoiceData, + selectedLoadeds, + propSubComps: "_subCompsLanguageProficiencies", + propPrevSubComps: "_prevSubCompsLanguageProficiencies", + isAvailable: isAvailable_entryDataLanguageProficiencies, + isForceDisplay: isForceDisplay_entryDataLanguageProficiencies, + prop: "languageProficiencies", + title: "Language Proficiencies", + ptrIsFirstSection, + CompClass: Charactermancer_OtherProficiencySelect, + propPathActorExistingProficiencies: ["system", "traits", "languages"], + fnSetComp: this._render_pHkIxsChosen_setCompOtherProficiencies.bind(this), + fnGetMappedProficiencies: Charactermancer_OtherProficiencySelect.getMappedLanguageProficiencies.bind(Charactermancer_OtherProficiencySelect), + }); + + this._render_pHkIxsChosen_comp({ + ix: i, + $stgSubChoiceData, + selectedLoadeds, + propSubComps: "_subCompsToolProficiencies", + propPrevSubComps: "_prevSubCompsToolProficiencies", + isAvailable: isAvailable_entryDataToolProficiencies, + isForceDisplay: isForceDisplay_entryDataToolProficiencies, + prop: "toolProficiencies", + title: "Tool Proficiencies", + ptrIsFirstSection, + CompClass: Charactermancer_OtherProficiencySelect, + propPathActorExistingProficiencies: ["system", "tools"], + fnSetComp: this._render_pHkIxsChosen_setCompOtherProficiencies.bind(this), + fnGetMappedProficiencies: Charactermancer_OtherProficiencySelect.getMappedToolProficiencies.bind(Charactermancer_OtherProficiencySelect), + }); + + this._render_pHkIxsChosen_comp({ + ix: i, + $stgSubChoiceData, + selectedLoadeds, + propSubComps: "_subCompsWeaponProficiencies", + propPrevSubComps: "_prevSubCompsWeaponProficiencies", + isAvailable: isAvailable_entryDataWeaponProficiencies, + isForceDisplay: isForceDisplay_entryDataWeaponProficiencies, + prop: "weaponProficiencies", + title: "Weapon Proficiencies", + ptrIsFirstSection, + CompClass: Charactermancer_OtherProficiencySelect, + propPathActorExistingProficiencies: ["system", "traits", "weaponProf"], + fnSetComp: this._render_pHkIxsChosen_setCompOtherProficiencies.bind(this), + fnGetMappedProficiencies: Charactermancer_OtherProficiencySelect.getMappedWeaponProficiencies.bind(Charactermancer_OtherProficiencySelect), + }); + + this._render_pHkIxsChosen_comp({ + ix: i, + $stgSubChoiceData, + selectedLoadeds, + propSubComps: "_subCompsArmorProficiencies", + propPrevSubComps: "_prevSubCompsArmorProficiencies", + isAvailable: isAvailable_entryDataArmorProficiencies, + isForceDisplay: isForceDisplay_entryDataArmorProficiencies, + prop: "armorProficiencies", + title: "Armor Proficiencies", + ptrIsFirstSection, + CompClass: Charactermancer_OtherProficiencySelect, + propPathActorExistingProficiencies: ["system", "traits", "armorProf"], + fnSetComp: this._render_pHkIxsChosen_setCompOtherProficiencies.bind(this), + fnGetMappedProficiencies: Charactermancer_OtherProficiencySelect.getMappedArmorProficiencies.bind(Charactermancer_OtherProficiencySelect), + }); + + this._render_pHkIxsChosen_comp({ + ix: i, + $stgSubChoiceData, + selectedLoadeds, + propSubComps: "_subCompsSavingThrowProficiencies", + propPrevSubComps: "_prevSubCompsSavingThrowProficiencies", + isAvailable: isAvailable_entryDataSavingThrowProficiencies, + isForceDisplay: isForceDisplay_entryDataSavingThrowProficiencies, + prop: "savingThrowProficiencies", + title: "Saving Throw Proficiencies", + ptrIsFirstSection, + CompClass: Charactermancer_OtherProficiencySelect, + propPathActorExistingProficiencies: ["system", "abilities"], + fnSetComp: this._render_pHkIxsChosen_setCompOtherProficiencies.bind(this), + fnGetMappedProficiencies: Charactermancer_OtherProficiencySelect.getMappedSavingThrowProficiencies.bind(Charactermancer_OtherProficiencySelect), + }); + + this._render_pHkIxsChosen_comp({ + ix: i, + $stgSubChoiceData, + selectedLoadeds, + propSubComps: "_subCompsDamageImmunities", + propPrevSubComps: "_prevSubCompsDamageImmunities", + isAvailable: isAvailable_entryDataDamageImmunities, + isForceDisplay: isForceDisplay_entryDataDamageImmunities, + prop: "immune", + title: "Damage Immunities", + ptrIsFirstSection, + CompClass: Charactermancer_DamageImmunitySelect, + propPathActorExistingProficiencies: ["system", "traits", "di"], + fnSetComp: this._render_pHkIxsChosen_setCompOtherProficiencies.bind(this), + }); + + this._render_pHkIxsChosen_comp({ + ix: i, + $stgSubChoiceData, + selectedLoadeds, + propSubComps: "_subCompsDamageResistances", + propPrevSubComps: "_prevSubCompsDamageResistances", + isAvailable: isAvailable_entryDataDamageResistances, + isForceDisplay: isForceDisplay_entryDataDamageResistances, + prop: "resist", + title: "Damage Resistances", + ptrIsFirstSection, + CompClass: Charactermancer_DamageResistanceSelect, + propPathActorExistingProficiencies: ["system", "traits", "dr"], + fnSetComp: this._render_pHkIxsChosen_setCompOtherProficiencies.bind(this), + }); + + this._render_pHkIxsChosen_comp({ + ix: i, + $stgSubChoiceData, + selectedLoadeds, + propSubComps: "_subCompsDamageVulnerabilities", + propPrevSubComps: "_prevSubCompsDamageVulnerabilities", + isAvailable: isAvailable_entryDataDamageVulnerabilities, + isForceDisplay: isForceDisplay_entryDataDamageVulnerabilities, + prop: "vulnerable", + title: "Damage Vulnerabilities", + ptrIsFirstSection, + CompClass: Charactermancer_DamageVulnerabilitySelect, + propPathActorExistingProficiencies: ["system", "traits", "dv"], + fnSetComp: this._render_pHkIxsChosen_setCompOtherProficiencies.bind(this), + }); + + this._render_pHkIxsChosen_comp({ + ix: i, + $stgSubChoiceData, + selectedLoadeds, + propSubComps: "_subCompsConditionImmunities", + propPrevSubComps: "_prevSubCompsConditionImmunities", + isAvailable: isAvailable_entryDataConditionImmunities, + isForceDisplay: isForceDisplay_entryDataConditionImmunities, + prop: "conditionImmune", + title: "Condition Immunities", + CompClass: Charactermancer_ConditionImmunitySelect, + propPathActorExistingProficiencies: ["system", "traits", "ci"], + ptrIsFirstSection, + fnSetComp: this._render_pHkIxsChosen_setCompOtherProficiencies.bind(this), + }); + + this._render_pHkIxsChosen_comp({ + ix: i, + $stgSubChoiceData, + selectedLoadeds, + propSubComps: "_subCompsExpertise", + propPrevSubComps: "_prevSubCompsExpertise", + isAvailable: isAvailable_entryDataExpertise, + isForceDisplay: isForceDisplay_entryDataExpertise, + prop: "expertise", + title: "Expertise", + ptrIsFirstSection, + fnSetComp: this._render_pHkIxsChosen_setCompExpertise.bind(this), + }); + + this._render_pHkIxsChosen_comp({ + ix: i, + $stgSubChoiceData, + selectedLoadeds, + propSubComps: "_subCompsResources", + propPrevSubComps: "_prevSubCompsResources", + isAvailable: isAvailable_entryDataResources, + isForceDisplay: isForceDisplay_entryDataResources, + prop: "resources", + ptrIsFirstSection, + fnSetComp: this._render_pHkIxsChosen_setCompResources.bind(this), + }); + + this._render_pHkIxsChosen_comp({ + ix: i, + $stgSubChoiceData, + selectedLoadeds, + propSubComps: "_subCompsSenses", + propPrevSubComps: "_prevSubCompsSenses", + isAvailable: isAvailable_entryDataSenses, + isForceDisplay: isForceDisplay_entryDataSenses, + prop: "senses", + ptrIsFirstSection, + fnSetComp: this._render_pHkIxsChosen_setCompSenses.bind(this), + }); + + this._render_pHkIxsChosen_comp({ + ix: i, + $stgSubChoiceData, + selectedLoadeds, + propSubComps: "_subCompsAdditionalSpells", + propPrevSubComps: "_prevSubCompsAdditionalSpells", + isAvailable: isAvailable_entryDataAdditionalSpells, + isForceDisplay: isForceDisplay_entryDataAdditionalSpells, + prop: "additionalSpells", + ptrIsFirstSection, + fnSetComp: this._render_pHkIxsChosen_setCompAdditionalSpells.bind(this), + }); + } + + this._prevSubCompsSkillToolLanguageProficiencies = null; + this._prevSubCompsSkillProficiencies = null; + this._prevSubCompsLanguageProficiencies = null; + this._prevSubCompsToolProficiencies = null; + this._prevSubCompsWeaponProficiencies = null; + this._prevSubCompsArmorProficiencies = null; + this._prevSubCompsSavingThrowProficiencies = null; + this._prevSubCompsDamageImmunities = null; + this._prevSubCompsDamageResistances = null; + this._prevSubCompsDamageVulnerabilities = null; + this._prevSubCompsConditionImmunities = null; + this._prevSubCompsExpertise = null; + this._prevSubCompsResources = null; + this._prevSubCompsSenses = null; + this._prevSubCompsAdditionalSpells = null; + + $stgSubChoiceData.toggleVe(isSubChoiceForceDisplay); + } + + /** + * Try to render a subcomponent. Will self-cancel if isAvailable is false + * @param {{ix:number, isAvailable:boolean, isForceDisplay:boolean, propSubComps:string, propPrevSubComps:string, prop:string, propPathActorExistingProficiencies:string[], + * ptrIsFirstSection: {_:boolean,}}} + */ + _render_pHkIxsChosen_comp({ix, $stgSubChoiceData, propSubComps, propPrevSubComps, isAvailable, isForceDisplay, selectedLoadeds, prop, title, CompClass, propPathActorExistingProficiencies, ptrIsFirstSection, fnSetComp, fnGetMappedProficiencies, fnGetExistingFvtt, }, ) { + this[propSubComps][ix] = null; + if (!isAvailable){return;} + + const {entity} = selectedLoadeds[ix]; + + if (!entity?.[prop] && !entity?.entryData?.[prop]){return;} + + //Create the sub-component (can be found at this[propSubComps][ix]) + //Use the function we were passed + fnSetComp({ + ix, + propSubComps, + prop, + CompClass, + propPathActorExistingProficiencies, + entity, + fnGetMappedProficiencies, + fnGetExistingFvtt, + }); + + //Copy the state over from previous sub-components + if (this[propPrevSubComps] && this[propPrevSubComps][ix]) { + this[propSubComps][ix]._proxyAssignSimple("state", MiscUtil.copy(this[propPrevSubComps][ix].__state)); + } + + if (!isForceDisplay){return;} + + if (!title){title = this[propSubComps][ix]?.modalTitle;} + + if (title) + $stgSubChoiceData.append(`${ptrIsFirstSection._ ? "" : `
    `}
    ${title}
    `); + + //Render the subcomponent + this[propSubComps][ix].render($stgSubChoiceData); + ptrIsFirstSection._ = false; + } + + /** + * @param {{ix:number, prop:string, propPathActorExistingProficiencies:string[]}} + */ + _render_pHkIxsChosen_setCompOtherProficiencies({ix, propSubComps, prop, CompClass, propPathActorExistingProficiencies, entity, fnGetMappedProficiencies, fnGetExistingFvtt, }, ) { + const availableRaw = entity[prop] || entity.entryData[prop]; + //If existingFvtt is null, we can try to pull the info we need from _actor by using the fnGetExistingFvtt function that was passed to us + const existingFvtt = fnGetExistingFvtt ? fnGetExistingFvtt() : { + [prop]: MiscUtil.get(this._actor, ...propPathActorExistingProficiencies) + }; + this[propSubComps][ix] = new CompClass({ + featureSourceTracker: this._featureSourceTracker, + existing: CompClass.getExisting(existingFvtt), + existingFvtt, + available: fnGetMappedProficiencies ? fnGetMappedProficiencies(availableRaw) : availableRaw, + }); + } + + _render_pHkIxsChosen_setCompExpertise({ix, propSubComps, prop, entity, }, ) { + const existingFvtt = Charactermancer_ExpertiseSelect.getExistingFvttFromActor(this._actor); + this[propSubComps][ix] = new Charactermancer_ExpertiseSelect({ + featureSourceTracker: this._featureSourceTracker, + existing: Charactermancer_ExpertiseSelect.getExisting(existingFvtt), + existingFvtt, + available: entity[prop] || entity.entryData[prop], + }); + } + + _render_pHkIxsChosen_setCompResources({ix, propSubComps, prop, entity, }, ) { + this[propSubComps][ix] = new Charactermancer_ResourceSelect({ + resources: entity[prop] || entity.entryData[prop], + className: entity.className, + classSource: entity.classSource, + subclassShortName: entity.subclassShortName, + subclassSource: entity.subclassSource, + }); + } + + _render_pHkIxsChosen_setCompSenses({ix, propSubComps, prop, entity, }, ) { + const existingFvtt = Charactermancer_SenseSelect.getExistingFvttFromActor(this._actor); + this[propSubComps][ix] = new Charactermancer_SenseSelect({ + //existing: Charactermancer_SenseSelect.getExisting(existingFvtt), + existingFvtt, + senses: entity[prop] || entity.entryData[prop], + }); + } + + _render_pHkIxsChosen_setCompAdditionalSpells({ix, propSubComps, prop, entity, }, ) { + this[propSubComps][ix] = Charactermancer_AdditionalSpellsSelect.getComp({ + additionalSpells: entity[prop] || entity.entryData[prop], + modalFilterSpells: this._modalFilterSpells, + curLevel: 0, + targetLevel: Consts.CHAR_MAX_LEVEL, + spellLevelLow: 0, + spellLevelHigh: 9, + }); + } + + get optionSet_() { + return this._optionsSet; + } + + async pIsNoChoice() { + if (this._isOptions()) + return false; + //TEMPFIX + /* if (await this._pHasChoiceInSideData_chooseSystem()) + return false; + if (await this._pHasChoiceInSideData_chooseFlags()) + return false; */ + if (await this._pHasSubChoice_entryData_skillToolLanguageProficiencies()) + return false; + if (await this._pHasSubChoice_entryData_skillProficiencies()) + return false; + if (await this._pHasSubChoice_entryData_languageProficiencies()) + return false; + if (await this._pHasSubChoice_entryData_toolProficiencies()) + return false; + if (await this._pHasSubChoice_entryData_weaponProficiencies()) + return false; + if (await this._pHasSubChoice_entryData_armorProficiencies()) + return false; + if (await this._pHasSubChoice_entryData_savingThrowProficiencies()) + return false; + if (await this._pHasSubChoice_damageImmunities()) + return false; + if (await this._pHasSubChoice_damageResistances()) + return false; + if (await this._pHasSubChoice_damageVulnerabilities()) + return false; + if (await this._pHasSubChoice_conditionImmunities()) + return false; + if (await this._pHasSubChoice_expertise()) + return false; + if (await this._pHasSubChoice_resources()) + return false; + if (await this._pHasSubChoice_entryData_senses()) + return false; + if (await this._pHasSubChoice_entryData_additionalSpells()) + return false; + return true; + } + + async pIsForceDisplay() { + if (await this._pIsForceDisplay_skillToolLanguageProficiencies()) + return true; + if (await this._pIsForceDisplay_skillProficiencies()) + return true; + if (await this._pIsForceDisplay_languageProficiencies()) + return true; + if (await this._pIsForceDisplay_toolProficiencies()) + return true; + if (await this._pIsForceDisplay_weaponProficiencies()) + return true; + if (await this._pIsForceDisplay_armorProficiencies()) + return true; + if (await this._pIsForceDisplay_savingThrowProficiencies()) + return true; + if (await this._pIsForceDisplay_damageImmunities()) + return true; + if (await this._pIsForceDisplay_damageResistances()) + return true; + if (await this._pIsForceDisplay_damageVulnerabilities()) + return true; + if (await this._pIsForceDisplay_conditionImmunities()) + return true; + if (await this._pIsForceDisplay_expertise()) + return true; + if (await this._pIsForceDisplay_resources()) + return true; + if (await this._pIsForceDisplay_senses()) + return true; + if (await this._pIsForceDisplay_additionalSpells()) + return true; + return false; + } + + async pIsAvailable() { + if (await this._pIsAvailable_skillToolLanguageProficiencies()) + return true; + if (await this._pIsAvailable_skillProficiencies()) + return true; + if (await this._pIsAvailable_languageProficiencies()) + return true; + if (await this._pIsAvailable_toolProficiencies()) + return true; + if (await this._pIsAvailable_weaponProficiencies()) + return true; + if (await this._pIsAvailable_armorProficiencies()) + return true; + if (await this._pIsAvailable_savingThrowProficiencies()) + return true; + if (await this._pIsAvailable_damageImmunities()) + return true; + if (await this._pIsAvailable_damageResistances()) + return true; + if (await this._pIsAvailable_damageVulnerabilities()) + return true; + if (await this._pIsAvailable_conditionImmunities()) + return true; + if (await this._pIsAvailable_expertise()) + return true; + if (await this._pIsAvailable_resources()) + return true; + if (await this._pIsAvailable_senses()) + return true; + if (await this._pIsAvailable_additionalSpells()) + return true; + return false; + } + + _isOptions() { + return !!(this._optionsSet[0] && this._optionsSet[0].optionsMeta); + } + + unregisterFeatureSourceTracking() { + if (this._featureSourceTracker) + this._featureSourceTracker.unregister(this); + this._unregisterSubComps(); + } + + async _pIsSubChoiceForceDisplay(selectedLoadeds) { + //TEMPFIX + const isSubChoice_sideDataChooseSystem = false;//await this._pHasChoiceInSideData_chooseSystem(selectedLoadeds); + const isSubChoice_sideDataChooseFlags = false; //await this._pHasChoiceInSideData_chooseFlags(selectedLoadeds); + const isForceDisplay_entryDataSkillToolLanguageProficiencies = await this._pIsForceDisplay_skillToolLanguageProficiencies(selectedLoadeds); + const isForceDisplay_entryDataSkillProficiencies = await this._pIsForceDisplay_skillProficiencies(selectedLoadeds); + const isForceDisplay_entryDataLanguageProficiencies = await this._pIsForceDisplay_languageProficiencies(selectedLoadeds); + const isForceDisplay_entryDataToolProficiencies = await this._pIsForceDisplay_toolProficiencies(selectedLoadeds); + const isForceDisplay_entryDataWeaponProficiencies = await this._pIsForceDisplay_weaponProficiencies(selectedLoadeds); + const isForceDisplay_entryDataArmorProficiencies = await this._pIsForceDisplay_armorProficiencies(selectedLoadeds); + const isForceDisplay_entryDataSavingThrowProficiencies = await this._pIsForceDisplay_savingThrowProficiencies(selectedLoadeds); + const isForceDisplay_entryDataDamageImmunities = await this._pIsForceDisplay_damageImmunities(selectedLoadeds); + const isForceDisplay_entryDataDamageResistances = await this._pIsForceDisplay_damageResistances(selectedLoadeds); + const isForceDisplay_entryDataDamageVulnerabilities = await this._pIsForceDisplay_damageVulnerabilities(selectedLoadeds); + const isForceDisplay_entryDataConditionImmunities = await this._pIsForceDisplay_conditionImmunities(selectedLoadeds); + const isForceDisplay_entryDataExpertise = await this._pIsForceDisplay_expertise(selectedLoadeds); + const isForceDisplay_entryDataResources = await this._pIsForceDisplay_resources(selectedLoadeds); + const isForceDisplay_entryDataSenses = await this._pIsForceDisplay_senses(selectedLoadeds); + const isForceDisplay_entryDataAdditionalSpells = await this._pIsForceDisplay_additionalSpells(selectedLoadeds); + + return [isSubChoice_sideDataChooseSystem, isSubChoice_sideDataChooseFlags, isForceDisplay_entryDataSkillToolLanguageProficiencies, isForceDisplay_entryDataSkillProficiencies, isForceDisplay_entryDataLanguageProficiencies, isForceDisplay_entryDataToolProficiencies, isForceDisplay_entryDataWeaponProficiencies, isForceDisplay_entryDataArmorProficiencies, isForceDisplay_entryDataSavingThrowProficiencies, isForceDisplay_entryDataDamageImmunities, isForceDisplay_entryDataDamageResistances, isForceDisplay_entryDataDamageVulnerabilities, isForceDisplay_entryDataConditionImmunities, isForceDisplay_entryDataExpertise, isForceDisplay_entryDataResources, isForceDisplay_entryDataSenses, isForceDisplay_entryDataAdditionalSpells, ].some(Boolean); + } + async _pIsSubChoiceAvailable(selectedLoadeds) { + //TEMPFIX + const isSubChoice_sideDataChooseSystem = false; //await this._pHasChoiceInSideData_chooseSystem(selectedLoadeds); + const isSubChoice_sideDataChooseFlags = false; //await this._pHasChoiceInSideData_chooseFlags(selectedLoadeds); + const isAvailable_entryDataSkillToolLanguageProficiencies = await this._pIsAvailable_skillToolLanguageProficiencies(selectedLoadeds); + const isAvailable_entryDataSkillProficiencies = await this._pIsAvailable_skillProficiencies(selectedLoadeds); + const isAvailable_entryDataLanguageProficiencies = await this._pIsAvailable_languageProficiencies(selectedLoadeds); + const isAvailable_entryDataToolProficiencies = await this._pIsAvailable_toolProficiencies(selectedLoadeds); + const isAvailable_entryDataWeaponProficiencies = await this._pIsAvailable_weaponProficiencies(selectedLoadeds); + const isAvailable_entryDataArmorProficiencies = await this._pIsAvailable_armorProficiencies(selectedLoadeds); + const isAvailable_entryDataSavingThrowProficiencies = await this._pIsAvailable_savingThrowProficiencies(selectedLoadeds); + const isAvailable_entryDataDamageImmunities = await this._pIsAvailable_damageImmunities(selectedLoadeds); + const isAvailable_entryDataDamageResistances = await this._pIsAvailable_damageResistances(selectedLoadeds); + const isAvailable_entryDataDamageVulnerabilities = await this._pIsAvailable_damageVulnerabilities(selectedLoadeds); + const isAvailable_entryDataConditionImmunities = await this._pIsAvailable_conditionImmunities(selectedLoadeds); + const isAvailable_entryDataExpertise = await this._pIsAvailable_expertise(selectedLoadeds); + const isAvailable_entryDataResources = await this._pIsAvailable_resources(selectedLoadeds); + const isAvailable_entryDataSenses = await this._pIsAvailable_senses(selectedLoadeds); + const isAvailable_entryDataAdditionalSpells = await this._pIsAvailable_additionalSpells(selectedLoadeds); + + return [isSubChoice_sideDataChooseSystem, isSubChoice_sideDataChooseFlags, isAvailable_entryDataSkillToolLanguageProficiencies, isAvailable_entryDataSkillProficiencies, isAvailable_entryDataLanguageProficiencies, isAvailable_entryDataToolProficiencies, isAvailable_entryDataWeaponProficiencies, isAvailable_entryDataArmorProficiencies, isAvailable_entryDataSavingThrowProficiencies, isAvailable_entryDataDamageImmunities, isAvailable_entryDataDamageResistances, isAvailable_entryDataDamageVulnerabilities, isAvailable_entryDataConditionImmunities, isAvailable_entryDataExpertise, isAvailable_entryDataResources, isAvailable_entryDataSenses, isAvailable_entryDataAdditionalSpells, ].some(Boolean); + } + + + + async _pHasChoiceInSideData_chooseSystem(optionsSet) { + return this._pHasChoiceInSideData_chooseSystemOrFlags({ + optionsSet, + propChoose: "chooseSystem" + }); + } + async _pHasChoiceInSideData_chooseFlags(optionsSet) { + return this._pHasChoiceInSideData_chooseSystemOrFlags({ + optionsSet, + propChoose: "chooseFlags" + }); + } + async _pHasChoiceInSideData_chooseSystemOrFlags({optionsSet, propChoose}) { + optionsSet = optionsSet || this._optionsSet; + + if (this._isSkipCharactermancerHandled) + return false; + + for (const loaded of optionsSet) { + const {entity, type} = loaded; + + const sideDataConverterMeta = this.constructor._ENTITY_TYPE_TO_SIDE_DATA_META[type]; + + if (sideDataConverterMeta) { + if (!sideDataConverterMeta.file.startsWith("SideDataInterface")) + throw new Error(`Expected side-data interface to start with "SideDataInterface"!`); + const mod = await __variableDynamicImportRuntime2__(`./SideDataInterface/SideDataInterface${sideDataConverterMeta.file.replace(/^SideDataInterface/, "")}.js`); + + const sideData = await mod[sideDataConverterMeta.sideDataInterface].pGetSideLoaded(entity); + if (sideData?.[propChoose]?.length) + return true; + } + } + return false; + } + static _ENTITY_TYPE_TO_SIDE_DATA_META = { + "backgroundFeature": { + file: "SideDataInterfaceBackgroundFeature", + sideDataInterface: "SideDataInterfaceBackgroundFeature" + }, + "charoption": { + file: "SideDataInterfaceCharCreationOption", + sideDataInterface: "SideDataInterfaceCharCreationOption" + }, + "classFeature": { + file: "SideDataInterfaceClassSubclassFeature", + sideDataInterface: "SideDataInterfaceClassSubclassFeature" + }, + "subclassFeature": { + file: "SideDataInterfaceClassSubclassFeature", + sideDataInterface: "SideDataInterfaceClassSubclassFeature" + }, + "feat": { + file: "SideDataInterfaceFeat", + sideDataInterface: "SideDataInterfaceFeat" + }, + "optionalfeature": { + file: "SideDataInterfaceOptionalfeature", + sideDataInterface: "SideDataInterfaceOptionalfeature" + }, + "raceFeature": { + file: "SideDataInterfaceRaceFeature", + sideDataInterface: "SideDataInterfaceRaceFeature" + }, + "reward": { + file: "SideDataInterfaceReward", + sideDataInterface: "SideDataInterfaceReward" + }, + "vehicleUpgrade": { + file: "SideDataInterfaceVehicleUpgrade", + sideDataInterface: "SideDataInterfaceVehicleUpgrade" + }, + }; + + async _pHasSubChoice_entryData_skillToolLanguageProficiencies(optionsSet) { + return this._pHasEntryData_prop({ + optionsSet, + CompClass: Charactermancer_OtherProficiencySelect, + prop: "skillToolLanguageProficiencies", + isRequireChoice: true, + }); + } + + async _pHasSubChoice_entryData_skillProficiencies(optionsSet) { + //Check if optionsSet has the property "skillProficiencies" in its entryData (or in the root object), and checks if its a choice between several + //Also provides it a function to map the choices + return this._pHasEntryData_prop({ + optionsSet, + CompClass: Charactermancer_OtherProficiencySelect, + prop: "skillProficiencies", + isRequireChoice: true, + fnGetMappedProficiencies: Charactermancer_OtherProficiencySelect.getMappedSkillProficiencies.bind(Charactermancer_OtherProficiencySelect), + }); + } + + async _pHasSubChoice_entryData_languageProficiencies(optionsSet) { + return this._pHasEntryData_prop({ + optionsSet, + CompClass: Charactermancer_OtherProficiencySelect, + prop: "languageProficiencies", + isRequireChoice: true, + fnGetMappedProficiencies: Charactermancer_OtherProficiencySelect.getMappedLanguageProficiencies.bind(Charactermancer_OtherProficiencySelect), + }); + } + + async _pHasSubChoice_entryData_toolProficiencies(optionsSet) { + return this._pHasEntryData_prop({ + optionsSet, + CompClass: Charactermancer_OtherProficiencySelect, + prop: "toolProficiencies", + isRequireChoice: true, + fnGetMappedProficiencies: Charactermancer_OtherProficiencySelect.getMappedToolProficiencies.bind(Charactermancer_OtherProficiencySelect), + }); + } + + async _pHasSubChoice_entryData_weaponProficiencies(optionsSet) { + return this._pHasEntryData_prop({ + optionsSet, + CompClass: Charactermancer_OtherProficiencySelect, + prop: "weaponProficiencies", + isRequireChoice: true, + fnGetMappedProficiencies: Charactermancer_OtherProficiencySelect.getMappedWeaponProficiencies.bind(Charactermancer_OtherProficiencySelect), + }); + } + + async _pHasSubChoice_entryData_armorProficiencies(optionsSet) { + return this._pHasEntryData_prop({ + optionsSet, + CompClass: Charactermancer_OtherProficiencySelect, + prop: "armorProficiencies", + isRequireChoice: true, + fnGetMappedProficiencies: Charactermancer_OtherProficiencySelect.getMappedArmorProficiencies.bind(Charactermancer_OtherProficiencySelect), + }); + } + + async _pHasSubChoice_entryData_savingThrowProficiencies(optionsSet) { + return this._pHasEntryData_prop({ + optionsSet, + CompClass: Charactermancer_OtherProficiencySelect, + prop: "savingThrowProficiencies", + isRequireChoice: true, + fnGetMappedProficiencies: Charactermancer_OtherProficiencySelect.getMappedSavingThrowProficiencies.bind(Charactermancer_OtherProficiencySelect), + }); + } + + async _pHasSubChoice_damageImmunities(optionsSet) { + return this._pHasEntryData_prop({ + optionsSet, + CompClass: Charactermancer_DamageImmunitySelect, + prop: "immune", + isRequireChoice: true, + }); + } + + async _pHasSubChoice_damageResistances(optionsSet) { + return this._pHasEntryData_prop({ + optionsSet, + CompClass: Charactermancer_DamageResistanceSelect, + prop: "resist", + isRequireChoice: true, + }); + } + + async _pHasSubChoice_damageVulnerabilities(optionsSet) { + return this._pHasEntryData_prop({ + optionsSet, + CompClass: Charactermancer_DamageVulnerabilitySelect, + prop: "vulnerable", + isRequireChoice: true, + }); + } + + async _pHasSubChoice_conditionImmunities(optionsSet) { + return this._pHasEntryData_prop({ + optionsSet, + CompClass: Charactermancer_ConditionImmunitySelect, + prop: "conditionImmune", + isRequireChoice: true, + }); + } + + async _pHasSubChoice_expertise(optionsSet) { + return this._pHasEntryData_prop({ + optionsSet, + CompClass: Charactermancer_ExpertiseSelect, + prop: "expertise", + isRequireChoice: true, + }); + } + + async _pHasSubChoice_resources(optionsSet) { + return this._pHasEntryData_prop({ + optionsSet, + CompClass: Charactermancer_ResourceSelect, + prop: "resources", + isRequireChoice: true, + }); + } + + async _pHasSubChoice_entryData_senses(optionsSet) { + return this._pHasEntryData_prop({ + optionsSet, + CompClass: Charactermancer_SenseSelect, + prop: "senses", + isRequireChoice: true, + }); + } + + async _pHasSubChoice_entryData_additionalSpells(optionsSet) { + return this._pHasEntryData_prop({ + optionsSet, + CompClass: Charactermancer_AdditionalSpellsSelect, + prop: "additionalSpells", + isRequireChoice: true, + }); + } + + /**Checks if an object has a property in their entryData called 'prop'*/ + async _pHasEntryData_prop({optionsSet, CompClass, prop, isRequireChoice, fnGetMappedProficiencies}) { + optionsSet = optionsSet || this._optionsSet; + + if (this._isSkipCharactermancerHandled){return false;} + + for (const loaded of optionsSet) { + const {entity} = loaded; + + let proficiencies = entity?.[prop] || entity?.entryData?.[prop]; + if (proficiencies) { + if (fnGetMappedProficiencies){proficiencies = fnGetMappedProficiencies(proficiencies);} + + if (!isRequireChoice){return true;} + else { + const isNoChoice = CompClass.isNoChoice(proficiencies); + if (!isNoChoice){return true;} + } + } + } + return false; + } + + async _pIsForceDisplay_skillToolLanguageProficiencies(optionsSet) { + return this._pHasEntryData_prop({ + optionsSet, + CompClass: Charactermancer_OtherProficiencySelect, + prop: "skillToolLanguageProficiencies", + }); + } + + async _pIsForceDisplay_skillProficiencies(optionsSet) { + return this._pHasEntryData_prop({ + optionsSet, + CompClass: Charactermancer_OtherProficiencySelect, + prop: "skillProficiencies", + fnGetMappedProficiencies: Charactermancer_OtherProficiencySelect.getMappedSkillProficiencies.bind(Charactermancer_OtherProficiencySelect), + }); + } + + async _pIsForceDisplay_languageProficiencies(optionsSet) { + return this._pHasEntryData_prop({ + optionsSet, + CompClass: Charactermancer_OtherProficiencySelect, + prop: "languageProficiencies", + fnGetMappedProficiencies: Charactermancer_OtherProficiencySelect.getMappedLanguageProficiencies.bind(Charactermancer_OtherProficiencySelect), + }); + } + + async _pIsForceDisplay_toolProficiencies(optionsSet) { + return this._pHasEntryData_prop({ + optionsSet, + CompClass: Charactermancer_OtherProficiencySelect, + prop: "toolProficiencies", + fnGetMappedProficiencies: Charactermancer_OtherProficiencySelect.getMappedToolProficiencies.bind(Charactermancer_OtherProficiencySelect), + }); + } + + async _pIsForceDisplay_weaponProficiencies(optionsSet) { + return this._pHasEntryData_prop({ + optionsSet, + CompClass: Charactermancer_OtherProficiencySelect, + prop: "weaponProficiencies", + fnGetMappedProficiencies: Charactermancer_OtherProficiencySelect.getMappedWeaponProficiencies.bind(Charactermancer_OtherProficiencySelect), + }); + } + + async _pIsForceDisplay_armorProficiencies(optionsSet) { + return this._pHasEntryData_prop({ + optionsSet, + CompClass: Charactermancer_OtherProficiencySelect, + prop: "armorProficiencies", + fnGetMappedProficiencies: Charactermancer_OtherProficiencySelect.getMappedArmorProficiencies.bind(Charactermancer_OtherProficiencySelect), + }); + } + + async _pIsForceDisplay_savingThrowProficiencies(optionsSet) { + return this._pHasEntryData_prop({ + optionsSet, + CompClass: Charactermancer_OtherProficiencySelect, + prop: "savingThrowProficiencies", + fnGetMappedProficiencies: Charactermancer_OtherProficiencySelect.getMappedSavingThrowProficiencies.bind(Charactermancer_OtherProficiencySelect), + }); + } + + async _pIsForceDisplay_damageImmunities(optionsSet) { + return this._pHasEntryData_prop({ + optionsSet, + CompClass: Charactermancer_ConditionImmunitySelect, + prop: "immune", + }); + } + + async _pIsForceDisplay_damageResistances(optionsSet) { + return this._pHasEntryData_prop({ + optionsSet, + CompClass: Charactermancer_DamageResistanceSelect, + prop: "resist", + }); + } + + async _pIsForceDisplay_damageVulnerabilities(optionsSet) { + return this._pHasEntryData_prop({ + optionsSet, + CompClass: Charactermancer_DamageVulnerabilitySelect, + prop: "vulnerable", + }); + } + + async _pIsForceDisplay_conditionImmunities(optionsSet) { + return this._pHasEntryData_prop({ + optionsSet, + CompClass: Charactermancer_ConditionImmunitySelect, + prop: "conditionImmune", + }); + } + + async _pIsForceDisplay_expertise(optionsSet) { + return this._pHasEntryData_prop({ + optionsSet, + CompClass: Charactermancer_ExpertiseSelect, + prop: "expertise", + }); + } + + async _pIsForceDisplay_resources(optionsSet) { + return this._pHasEntryData_prop({ + optionsSet, + CompClass: Charactermancer_ResourceSelect, + prop: "resources", + isRequireChoice: true, + }); + } + + _pIsForceDisplay_senses(optionsSet) { + return this._pHasEntryData_prop({ + optionsSet, + CompClass: Charactermancer_SenseSelect, + prop: "senses", + isRequireChoice: true, + }); + } + + _pIsForceDisplay_additionalSpells(optionsSet) { + return this._pHasEntryData_prop({ + optionsSet, + CompClass: Charactermancer_AdditionalSpellsSelect, + prop: "additionalSpells", + }); + } + + _pIsAvailable_skillToolLanguageProficiencies(...args) { + return this._pIsForceDisplay_skillToolLanguageProficiencies(...args); + } + _pIsAvailable_skillProficiencies(...args) { + return this._pIsForceDisplay_skillProficiencies(...args); + } + _pIsAvailable_languageProficiencies(...args) { + return this._pIsForceDisplay_languageProficiencies(...args); + } + _pIsAvailable_toolProficiencies(...args) { + return this._pIsForceDisplay_toolProficiencies(...args); + } + _pIsAvailable_weaponProficiencies(...args) { + return this._pIsForceDisplay_weaponProficiencies(...args); + } + _pIsAvailable_armorProficiencies(...args) { + return this._pIsForceDisplay_armorProficiencies(...args); + } + _pIsAvailable_savingThrowProficiencies(...args) { + return this._pIsForceDisplay_savingThrowProficiencies(...args); + } + _pIsAvailable_damageImmunities(...args) { + return this._pIsForceDisplay_damageImmunities(...args); + } + _pIsAvailable_damageResistances(...args) { + return this._pIsForceDisplay_damageResistances(...args); + } + _pIsAvailable_damageVulnerabilities(...args) { + return this._pIsForceDisplay_damageVulnerabilities(...args); + } + _pIsAvailable_conditionImmunities(...args) { + return this._pIsForceDisplay_conditionImmunities(...args); + } + _pIsAvailable_expertise(...args) { + return this._pIsForceDisplay_expertise(...args); + } + + async _pIsAvailable_resources(optionsSet) { + return this._pHasEntryData_prop({ + optionsSet, + CompClass: Charactermancer_ResourceSelect, + prop: "resources", + }); + } + + _pIsAvailable_senses(optionsSet) { + return this._pHasEntryData_prop({ + optionsSet, + CompClass: Charactermancer_SenseSelect, + prop: "senses", + }); + } + + _pIsAvailable_additionalSpells(...args) { + return this._pIsForceDisplay_additionalSpells(...args); + } + + async _pGetLoadedsSideDataRaws(optionsSet) { + optionsSet = optionsSet || this._optionsSet; + const out = []; + for (const loaded of optionsSet) { + const {entity, type} = loaded; + + switch (type) { + case "classFeature": + case "subclassFeature": + { + /* const {SideDataInterfaceClassSubclassFeature} = await Promise.resolve().then(function() { + return SideDataInterfaceClassSubclassFeature; + }); */ + const sideData = await SideDataInterfaceClassSubclassFeature.pGetSideLoaded(entity); + out.push(sideData); + break; + } + + default: { out.push(null); break; } + } + } + return out; + } + + + + _getTrackableFeatures() { + const ixs = ComponentUiUtil.getMetaWrpMultipleChoice_getSelectedIxs(this, "ixsChosen"); + const selectedLoadeds = ixs.map(ix=>this._optionsSet[ix]); + + return selectedLoadeds.map(({page, hash})=>({ + page, + hash + })); + } + + /** + * Copies over state from any component within 'comps' that has a matching optionset to ours + * @param {any[]} comps + */ + findAndCopyStateFrom(comps) { + if (!comps?.length){ return;} + + const match = comps.find(it=>CollectionUtil.deepEquals(it.optionSet_, this.optionSet_)); + if (match) { + this._proxyAssignSimple("state", MiscUtil.copy(match.__state)); + this._prevSubCompsSkillToolLanguageProficiencies = match._subCompsSkillToolLanguageProficiencies; + this._prevSubCompsSkillProficiencies = match._subCompsSkillProficiencies; + this._prevSubCompsLanguageProficiencies = match._subCompsLanguageProficiencies; + this._prevSubCompsToolProficiencies = match._subCompsToolProficiencies; + this._prevSubCompsWeaponProficiencies = match._subCompsWeaponProficiencies; + this._prevSubCompsArmorProficiencies = match._subCompsArmorProficiencies; + this._prevSubCompsSavingThrowProficiencies = match._subCompsSavingThrowProficiencies; + this._prevSubCompsDamageImmunities = match._prevSubCompsDamageImmunities; + this._prevSubCompsDamageResistances = match._prevSubCompsDamageResistances; + this._prevSubCompsDamageVulnerabilities = match._prevSubCompsDamageVulnerabilities; + this._prevSubCompsConditionImmunities = match._prevSubCompsConditionImmunities; + this._prevSubCompsExpertise = match._prevSubCompsExpertise; + this._prevSubCompsResources = match._prevSubCompsResources; + this._prevSubCompsSenses = match._subCompsSenses; + this._prevSubCompsAdditionalSpells = match._subCompsAdditionalSpells; + } + } + + async pGetFormData() { + if (await this.pIsNoChoice() && !await this.pIsAvailable()) { + const sideDatas = await this._pGetLoadedsSideDataRaws(); + const cpyOptionsSet = MiscUtil.copy(this._optionsSet); + cpyOptionsSet.forEach((loaded,i)=>{ + const sideData = sideDatas[i]; + if (!sideData) + return; + + const {entity} = loaded; + if (sideData.data) + entity.foundryAdditionalSystem = MiscUtil.copy(sideData.data); + if (sideData.flags) + entity.foundryAdditionalFlags = MiscUtil.copy(sideData.flags); + if (sideData.effects) + entity.effectsRaw = MiscUtil.copy(sideData.effects); + } + ); + + return { + isFormComplete: true, + data: { + features: cpyOptionsSet, + }, + }; + } + + await this._pGate("ixsChosen"); + + const selectedLoadeds = this._getSelectedLoadeds(); + + const sideDatas = await this._pGetLoadedsSideDataRaws(selectedLoadeds); + const cpySelectedLoadeds = MiscUtil.copy(selectedLoadeds); + + const outSkillToolLanguageProficiencies = []; + const outSkillProficiencies = []; + const outLanguageProficiencies = []; + const outToolProficiencies = []; + const outWeaponProficiencies = []; + const outArmorProficiencies = []; + const outSavingThrowProficiencies = []; + const outDamageImmunities = []; + const outDamageResistances = []; + const outDamageVulnerabilities = []; + const outConditionImmunities = []; + const outExpertise = []; + const outResources = []; + const outSenses = []; + const outAdditionalSpells = []; + + for (let i = 0; i < cpySelectedLoadeds.length; ++i) { + const loaded = cpySelectedLoadeds[i]; + + const sideData = sideDatas[i]; + + const {entity} = loaded; + + if (sideData) { + if (sideData.data) + entity.foundryAdditionalSystem = MiscUtil.copy(sideData.data); + if (sideData.flags) + entity.foundryAdditionalFlags = MiscUtil.copy(sideData.flags); + if (sideData.effects) + entity.effectsRaw = MiscUtil.copy(sideData.effects); + + const selectedChooseDataSystem = this._getFormData_getChooseSystemOrChooseFlags({ + sideData, + ixCpySelectedLoaded: i, + propChoose: "chooseSystem", + propCompProp: "propChooseSystem", + }); + if (selectedChooseDataSystem) { + entity.foundryAdditionalSystem = entity.foundryAdditionalSystem || {}; + Object.assign(entity.foundryAdditionalSystem, MiscUtil.copy(selectedChooseDataSystem.system)); + } + + const selectedChooseDataFlags = this._getFormData_getChooseSystemOrChooseFlags({ + sideData, + ixCpySelectedLoaded: i, + propChoose: "chooseFlags", + propCompProp: "propChooseFlags", + }); + if (selectedChooseDataFlags) { + entity.foundryAdditionalFlags = entity.foundryAdditionalFlags || {}; + foundry.utils.mergeObject(entity.foundryAdditionalFlags, MiscUtil.copy(selectedChooseDataFlags.flags)); + } + } + + if (!this._isSkipCharactermancerHandled) { + if ((entity?.skillToolLanguageProficiencies || entity?.entryData?.skillToolLanguageProficiencies) && this._subCompsSkillToolLanguageProficiencies[i]) { + const formData = await this._subCompsSkillToolLanguageProficiencies[i].pGetFormData(); + outSkillToolLanguageProficiencies.push(formData); + } + + if ((entity?.skillProficiencies || entity?.entryData?.skillProficiencies) && this._subCompsSkillProficiencies[i]) { + const formData = await this._subCompsSkillProficiencies[i].pGetFormData(); + outSkillProficiencies.push(formData); + } + + if ((entity?.languageProficiencies || entity?.entryData?.languageProficiencies) && this._subCompsLanguageProficiencies[i]) { + const formData = await this._subCompsLanguageProficiencies[i].pGetFormData(); + outLanguageProficiencies.push(formData); + } + + if ((entity?.toolProficiencies || entity?.entryData?.toolProficiencies) && this._subCompsToolProficiencies[i]) { + const formData = await this._subCompsToolProficiencies[i].pGetFormData(); + outToolProficiencies.push(formData); + } + + if ((entity?.weaponProficiencies || entity?.entryData?.weaponProficiencies) && this._subCompsWeaponProficiencies[i]) { + const formData = await this._subCompsWeaponProficiencies[i].pGetFormData(); + outWeaponProficiencies.push(formData); + } + + if ((entity?.armorProficiencies || entity?.entryData?.armorProficiencies) && this._subCompsArmorProficiencies[i]) { + const formData = await this._subCompsArmorProficiencies[i].pGetFormData(); + outArmorProficiencies.push(formData); + } + + if ((entity?.savingThrowProficiencies || entity?.entryData?.savingThrowProficiencies) && this._subCompsSavingThrowProficiencies[i]) { + const formData = await this._subCompsSavingThrowProficiencies[i].pGetFormData(); + outSavingThrowProficiencies.push(formData); + } + + if ((entity?.immune || entity?.entryData?.immune) && this._subCompsDamageImmunities[i]) { + const formData = await this._subCompsDamageImmunities[i].pGetFormData(); + outDamageImmunities.push(formData); + } + + if ((entity?.resist || entity?.entryData?.resist) && this._subCompsDamageResistances[i]) { + const formData = await this._subCompsDamageResistances[i].pGetFormData(); + outDamageResistances.push(formData); + } + + if ((entity?.vulnerable || entity?.entryData?.vulnerable) && this._subCompsDamageVulnerabilities[i]) { + const formData = await this._subCompsDamageVulnerabilities[i].pGetFormData(); + outDamageVulnerabilities.push(formData); + } + + if ((entity?.conditionImmune || entity?.entryData?.conditionImmune) && this._subCompsConditionImmunities[i]) { + const formData = await this._subCompsConditionImmunities[i].pGetFormData(); + outConditionImmunities.push(formData); + } + + if ((entity?.expertise || entity?.entryData?.expertise) && this._subCompsExpertise[i]) { + const formData = await this._subCompsExpertise[i].pGetFormData(); + outExpertise.push(formData); + } + + if ((entity?.resources || entity?.entryData?.resources) && this._subCompsResources[i]) { + const formData = await this._subCompsResources[i].pGetFormData(); + outResources.push(formData); + } + + if ((entity?.senses || entity?.entryData?.senses) && this._subCompsSenses[i]) { + const formData = await this._subCompsSenses[i].pGetFormData(); + outSenses.push(formData); + } + + if ((entity?.additionalSpells || entity?.entryData?.additionalSpells) && this._subCompsAdditionalSpells[i]) { + const formData = await this._subCompsAdditionalSpells[i].pGetFormData(); + outAdditionalSpells.push(formData); + } + } + } + + return { + isFormComplete: true, + data: { + features: cpySelectedLoadeds, + formDatasSkillToolLanguageProficiencies: outSkillToolLanguageProficiencies, + formDatasSkillProficiencies: outSkillProficiencies, + formDatasLanguageProficiencies: outLanguageProficiencies, + formDatasToolProficiencies: outToolProficiencies, + formDatasWeaponProficiencies: outWeaponProficiencies, + formDatasArmorProficiencies: outArmorProficiencies, + formDatasSavingThrowProficiencies: outSavingThrowProficiencies, + formDatasDamageImmunities: outDamageImmunities, + formDatasDamageResistances: outDamageResistances, + formDatasDamageVulnerabilities: outDamageVulnerabilities, + formDatasConditionImmunities: outConditionImmunities, + formDatasExpertise: outExpertise, + formDatasResources: outResources, + formDatasSenses: outSenses, + formDatasAdditionalSpells: outAdditionalSpells, + }, + }; + } + + _getFormData_getChooseSystemOrChooseFlags({sideData, ixCpySelectedLoaded, propChoose, propCompProp}) { + if (!sideData[propChoose]) + return null; + + const compProps = this._getProps(ixCpySelectedLoaded); + + const ixs = ComponentUiUtil.getMetaWrpMultipleChoice_getSelectedIxs(this, compProps[propCompProp]); + const selectedChoose = ixs.map(ix=>sideData[propChoose][ix]); + + if (!selectedChoose.length) + return null; + + return selectedChoose[0]; + } + + _getOptionsNameAndCount() { + const {name, count} = this._optionsSet[0].optionsMeta; + const required = this._optionsSet.map((it,ix)=>({ + it, + ix + })).filter(({it})=>it.isRequiredOption).map(({ix})=>ix); + const dispCount = count - required.length; + + return { + name, + count, + dispCount, + required + }; + } + + get modalTitle() { + if (!this._isOptions()) + return null; + + const {dispCount, name} = this._getOptionsNameAndCount(); + return `Choose ${dispCount === 1 ? "" : `${dispCount} `}Option${dispCount === 1 ? "" : "s"}: ${name}${this._level != null ? ` (Level ${this._level})` : ""}`; + } + + static _getLoadedTmpUid(loaded) { + return `${loaded.page}__${loaded.hash}`; + } + + _getSelectedLoadeds() { + if (this._isOptions()) { + const ixs = ComponentUiUtil.getMetaWrpMultipleChoice_getSelectedIxs(this, "ixsChosen"); + const {required} = this._getOptionsNameAndCount(); + return [...ixs, ...required].map(ix=>this._optionsSet[ix]); + } + else { return this._optionsSet; } + } + + _getProps(ix) { + return { + prefixSubComps: "subComp_", + propChooseSystem: `subComp_${ix}_chooseSystem`, + propChooseFlags: `subComp_${ix}_chooseFlags`, + }; + } + + /** + * Unregister all existing subcomponents from the featureSourceTracker. + * Doesn't clear our cache of existing subcomponents. + */ + _unregisterSubComps() { + if (!this._featureSourceTracker){return;} + + this._subCompsSkillToolLanguageProficiencies.filter(Boolean).forEach(comp=>this._featureSourceTracker.unregister(comp)); + this._subCompsSkillProficiencies.filter(Boolean).forEach(comp=>this._featureSourceTracker.unregister(comp)); + this._subCompsLanguageProficiencies.filter(Boolean).forEach(comp=>this._featureSourceTracker.unregister(comp)); + this._subCompsToolProficiencies.filter(Boolean).forEach(comp=>this._featureSourceTracker.unregister(comp)); + this._subCompsWeaponProficiencies.filter(Boolean).forEach(comp=>this._featureSourceTracker.unregister(comp)); + this._subCompsArmorProficiencies.filter(Boolean).forEach(comp=>this._featureSourceTracker.unregister(comp)); + this._subCompsSavingThrowProficiencies.filter(Boolean).forEach(comp=>this._featureSourceTracker.unregister(comp)); + this._subCompsDamageImmunities.filter(Boolean).forEach(comp=>this._featureSourceTracker.unregister(comp)); + this._subCompsDamageResistances.filter(Boolean).forEach(comp=>this._featureSourceTracker.unregister(comp)); + this._subCompsDamageVulnerabilities.filter(Boolean).forEach(comp=>this._featureSourceTracker.unregister(comp)); + this._subCompsConditionImmunities.filter(Boolean).forEach(comp=>this._featureSourceTracker.unregister(comp)); + this._subCompsExpertise.filter(Boolean).forEach(comp=>this._featureSourceTracker.unregister(comp)); + this._subCompsResources.filter(Boolean).forEach(comp=>this._featureSourceTracker.unregister(comp)); + this._subCompsSenses.filter(Boolean).forEach(comp=>this._featureSourceTracker.unregister(comp)); + this._subCompsAdditionalSpells.filter(Boolean).forEach(comp=>this._featureSourceTracker.unregister(comp)); + } + + /** + * Render this FeatureOptionsSelect without any visible subcomponents. + * Unregisters and clears cache of existing subcomponents. + * @param {any} {$stgSubChoiceData} + */ + _render_noSubChoices({$stgSubChoiceData}) { + this._lastSubMetas.forEach(it=>it.unhook()); + this._lastSubMetas = []; + + this._unregisterSubComps(); + + this._subCompsSkillToolLanguageProficiencies = []; + this._subCompsSkillProficiencies = []; + this._subCompsLanguageProficiencies = []; + this._subCompsToolProficiencies = []; + this._subCompsWeaponProficiencies = []; + this._subCompsArmorProficiencies = []; + this._subCompsSavingThrowProficiencies = []; + this._subCompsDamageImmunities = []; + this._subCompsDamageResistances = []; + this._subCompsDamageVulnerabilities = []; + this._subCompsConditionImmunities = []; + this._subCompsExpertise = []; + this._subCompsResources = []; + this._subCompsSenses = []; + this._subCompsAdditionalSpells = []; + + $stgSubChoiceData.empty().hideVe(); + } + + _render_options() { + if (!this._isOptions()) + return; + + const {count, required} = this._getOptionsNameAndCount(); + + const $ptsExisting = {}; + this._lastMeta = ComponentUiUtil.getMetaWrpMultipleChoice(this, "ixsChosen", { + values: this._optionsSet, + ixsRequired: required, + count, + fnDisplay: v=>{ + const ptName = Renderer.get().render(v.entry); + + const $ptExisting = $(`
    `); + $ptsExisting[this.constructor._getLoadedTmpUid(v)] = $ptExisting; + + return $$`
    +
    ${ptName}${$ptExisting}
    +
    ${Parser.sourceJsonToAbv(v.entity.source)}
    +
    `; + }, + }, ); + + const hkUpdatePtsExisting = ()=>{ + const otherStates = this._featureSourceTracker ? this._featureSourceTracker.getStatesForKey("features", { + ignore: this + }) : null; + + //Check if we already gain this feature from somewhere else + this._optionsSet.forEach(v=>{ + const tmpUid = this.constructor._getLoadedTmpUid(v); + + if (!$ptsExisting[tmpUid]) + return; + + let isExists = this._existingFeatureChecker + && this._existingFeatureChecker.isExistingFeature(UtilEntityGeneric.getName(v.entity), v.page, v.source, v.hash); + + if (otherStates) + isExists = isExists || otherStates.some(arr=>arr.some(it=>it.page === v.page && it.hash === v.hash)); + + $ptsExisting[tmpUid].title(isExists ? `Gained from Another Source` : "").html(isExists ? `()` : "").toggleClass("ml-1", isExists); + }); + }; + if (this._featureSourceTracker){this._featureSourceTracker.addHook(this, "pulseFeatures", hkUpdatePtsExisting);} + hkUpdatePtsExisting(); + + if (this._featureSourceTracker) { + const hkSetTrackerState = ()=>this._featureSourceTracker.setState(this, { features: this._getTrackableFeatures() }); + this._addHookBase(this._lastMeta.propPulse, hkSetTrackerState); + hkSetTrackerState(); + } + } + + _render_getSubCompTitle(entity) { + const titleIntro = [entity.className, entity.subclassShortName ? `(${entity.subclassShortName})` : "", entity.level ? `Level ${entity.level}` : "", ].filter(Boolean).join(" "); + const title = `${titleIntro}${titleIntro ? ": " : ""}${entity.name}`; + return `${this._isModal ? "" : `
    `}
    ${title}
    `; + } + + _render_renderSubComp_chooseSystem(ix, $stgSubChoice, entity, type, sideData) { + return this._render_renderSubComp_chooseSystemChooseFlags({ + ix, + $stgSubChoice, + entity, + type, + sideData, + propChoose: "chooseSystem", + propCompProp: "propChooseSystem", + propIsRenderEntries: "isChooseSystemRenderEntries", + }); + } + + _render_renderSubComp_chooseFlags(ix, $stgSubChoice, entity, type, sideData) { + return this._render_renderSubComp_chooseSystemChooseFlags({ + ix, + $stgSubChoice, + entity, + type, + sideData, + propChoose: "chooseFlags", + propCompProp: "propChooseFlags", + propIsRenderEntries: "isChooseFlagsRenderEntries", + }); + } + + _render_renderSubComp_chooseSystemChooseFlags({ix, $stgSubChoice, entity, type, sideData, propChoose, propCompProp, propIsRenderEntries}) { + const compProps = this._getProps(ix); + + const htmlDescription = sideData[propIsRenderEntries] ? Vetools.withUnpatchedDiceRendering(()=>`${(entity.entries || []).map(ent=>`
    ${Renderer.get().render(ent)}
    `).join("")}`) : null; + + const choiceMeta = ComponentUiUtil.getMetaWrpMultipleChoice(this, compProps[propCompProp], { + count: 1, + fnDisplay: val=>val.name, + values: sideData[propChoose], + }, ); + + this._lastSubMetas.push(choiceMeta); + + $$`
    + ${htmlDescription} + ${choiceMeta.$ele} +
    `.appendTo($stgSubChoice); + } + + _getDefaultState() { + return { + ixsChosen: [], + }; + } + + static async pGetUserInput(opts) { + const comp = new this({ + ...opts, + featureSourceTracker: opts.featureSourceTracker || new Charactermancer_FeatureSourceTracker(), + isModal: true, + }); + if (await comp.pIsNoChoice()) { + comp.render($(document.createElement("div"))); + return comp.pGetFormData(); + } + + return UtilApplications.pGetImportCompApplicationFormData({ + comp, + width: 640, + height: Util.getMaxWindowHeight(), + isAutoResize: true, + }); + } + + static async pDoApplyProficiencyFormDataToActorUpdate(actor, actorUpdate, formData) { + const formDataData = formData.data; + if (!formDataData) + return; + + const {DataConverter} = await Promise.resolve().then(function() { + return DataConverter$1; + }); + + actorUpdate.system = actorUpdate.system || {}; + + for (const formData of formDataData.formDatasSkillToolLanguageProficiencies || []) { + DataConverter.doApplySkillFormDataToActorUpdate({ + existingProfsActor: MiscUtil.get(actor, "_source", "system", "skills"), + formData: formData, + actorData: actorUpdate.system, + }); + + DataConverter.doApplyLanguageProficienciesFormDataToActorUpdate({ + existingProfsActor: MiscUtil.get(actor, "_source", "system", "traits", "languages"), + formData, + actorData: actorUpdate.system, + }); + + DataConverter.doApplyToolProficienciesFormDataToActorUpdate({ + existingProfsActor: MiscUtil.get(actor, "_source", "system", "tools"), + formData, + actorData: actorUpdate.system, + }); + } + + for (const formData of formDataData.formDatasSkillProficiencies || []) { + DataConverter.doApplySkillFormDataToActorUpdate({ + existingProfsActor: MiscUtil.get(actor, "_source", "system", "skills"), + formData: formData, + actorData: actorUpdate.system, + }); + } + + for (const formData of formDataData.formDatasLanguageProficiencies || []) { + DataConverter.doApplyLanguageProficienciesFormDataToActorUpdate({ + existingProfsActor: MiscUtil.get(actor, "_source", "system", "traits", "languages"), + formData, + actorData: actorUpdate.system, + }); + } + + for (const formData of formDataData.formDatasToolProficiencies || []) { + DataConverter.doApplyToolProficienciesFormDataToActorUpdate({ + existingProfsActor: MiscUtil.get(actor, "_source", "system", "tools"), + formData, + actorData: actorUpdate.system, + }); + } + + for (const formData of formDataData.formDatasWeaponProficiencies || []) { + DataConverter.doApplyWeaponProficienciesFormDataToActorUpdate({ + existingProfsActor: MiscUtil.get(actor, "_source", "system", "traits", "weaponProf"), + formData, + actorData: actorUpdate.system, + }); + } + + for (const formData of formDataData.formDatasArmorProficiencies || []) { + DataConverter.doApplyArmorProficienciesFormDataToActorUpdate({ + existingProfsActor: MiscUtil.get(actor, "_source", "system", "traits", "armorProf"), + formData, + actorData: actorUpdate.system, + }); + } + + for (const formData of formDataData.formDatasSavingThrowProficiencies || []) { + DataConverter.doApplySavingThrowProficienciesFormDataToActorUpdate({ + existingProfsActor: MiscUtil.get(actor, "_source", "system", "abilities"), + formData, + actorData: actorUpdate.system, + }); + } + + for (const formData of formDataData.formDatasDamageImmunities || []) { + DataConverter.doApplyDamageImmunityFormDataToActorUpdate({ + existingProfsActor: MiscUtil.get(actor, "_source", "system", "traits", "di"), + formData, + actorData: actorUpdate.system, + }); + } + + for (const formData of formDataData.formDatasDamageResistances || []) { + DataConverter.doApplyDamageResistanceFormDataToActorUpdate({ + existingProfsActor: MiscUtil.get(actor, "_source", "system", "traits", "dr"), + formData, + actorData: actorUpdate.system, + }); + } + + for (const formData of formDataData.formDatasDamageVulnerabilities || []) { + DataConverter.doApplyDamageVulnerabilityFormDataToActorUpdate({ + existingProfsActor: MiscUtil.get(actor, "_source", "system", "traits", "dv"), + formData, + actorData: actorUpdate.system, + }); + } + + for (const formData of formDataData.formDatasConditionImmunities || []) { + DataConverter.doApplyConditionImmunityFormDataToActorUpdate({ + existingProfsActor: MiscUtil.get(actor, "_source", "system", "traits", "ci"), + formData, + actorData: actorUpdate.system, + }); + } + + for (const formData of formDataData.formDatasExpertise || []) { + DataConverter.doApplyExpertiseFormDataToActorUpdate({ + existingProfsActor: { + skillProficiencies: MiscUtil.get(actor, "_source", "system", "skills"), + toolProficiencies: MiscUtil.get(actor, "_source", "system", "tools"), + }, + formData: formData, + actorData: actorUpdate.system, + }); + } + } + + static async pDoApplyResourcesFormDataToActor({actor, formData}) { + const formDataData = formData.data; + + if (!formDataData?.formDatasResources?.length) + return; + + for (const formDataResources of formDataData.formDatasResources) { + await Charactermancer_ResourceSelect.pApplyFormDataToActor(actor, formDataResources, ); + } + } + + static async pDoApplySensesFormDataToActor({actor, actorUpdate, formData, configGroup}) { + const formDataData = formData.data; + if (!formDataData || !formDataData.formDatasSenses?.length) + return; + + const {DataConverter} = await Promise.resolve().then(function() { + return DataConverter$1; + }); + + actorUpdate.prototypeToken = actorUpdate.prototypeToken || {}; + actorUpdate.system = actorUpdate.system || {}; + + for (const formData of formDataData.formDatasSenses || []) { + DataConverter.doApplySensesFormDataToActorUpdate({ + existingSensesActor: MiscUtil.get(actor, "_source", "system", "attributes", "senses"), + existingTokenActor: MiscUtil.get(actor, "_source", "prototypeToken"), + formData: formData, + actorData: actorUpdate.system, + actorToken: actorUpdate.prototypeToken, + configGroup, + }); + } + } + + static async pDoApplyAdditionalSpellsFormDataToActor({actor, formData, abilityAbv, parentAbilityAbv=null, taskRunner=null}) { + const formDataData = formData.data; + if (!formDataData || !formDataData.formDatasAdditionalSpells?.length) + return; + + for (const formDataAdditionalSpells of formDataData.formDatasAdditionalSpells) { + await Charactermancer_AdditionalSpellsSelect.pApplyFormDataToActor(actor, formDataAdditionalSpells, { + taskRunner, + abilityAbv, + parentAbilityAbv, + }, ); + } + } +} + +class Charactermancer_Feature_Util { + static addFauxOptionalFeatureEntries(featureList, optfeatList) { + if (!featureList || !optfeatList) + return; + + Object.values(featureList).forEach(arr=>{ + if (!(arr instanceof Array)) + return; + + for (const feature of arr) { + if (!feature.entries?.length || !feature.optionalfeatureProgression) + continue; + + for (const optFeatProgression of feature.optionalfeatureProgression) { + this._addFauxOptionalFeatureFeatures_handleFeatProgression(optfeatList, feature, optFeatProgression, ); + } + } + } + ); + } + + static _addFauxOptionalFeatureFeatures_handleFeatProgression(optfeatList, feature, optFeatProgression) { + if (optFeatProgression.progression instanceof Array) + return; + + if (!optFeatProgression.progression["*"]) + return; + + const availOptFeats = optfeatList.filter(it=>optFeatProgression.featureType instanceof Array && (optFeatProgression.featureType || []).some(ft=>it.featureType.includes(ft))).filter(it=>!ExcludeUtil.isExcluded(UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_OPT_FEATURES](it), "optionalfeature", it.source, { + isNoCount: true + }, )); + + feature.entries.push({ + type: "options", + count: optFeatProgression.progression["*"], + entries: availOptFeats.map(it=>({ + type: "refOptionalfeature", + optionalfeature: DataUtil.proxy.getUid("optionalfeature", it, { + isMaintainCase: true + }), + })), + data: { + _plut_tmpOptionalfeatureList: true, + }, + }); + } + + static getCleanedFeature_tmpOptionalfeatureList(feature) { + const cpyFeature = MiscUtil.copy(feature); + MiscUtil.getWalker().walk(cpyFeature, { + array: (arr)=>{ + return arr.filter(it=>it.data == null || !it.data._plut_tmpOptionalfeatureList); + } + , + }, ); + return cpyFeature; + } +} +//#endregion \ No newline at end of file diff --git a/charbuilder/js/plutonium/config.js b/charbuilder/js/plutonium/config.js new file mode 100644 index 0000000..222abfa --- /dev/null +++ b/charbuilder/js/plutonium/config.js @@ -0,0 +1,3869 @@ +//#region SharedConsts +class SharedConsts { + static MODULE_TITLE = "Plutonium"; + static MODULE_TITLE_FAKE = "SRD: Enhanced"; + static MODULE_ID = "plutonium"; + static MODULE_ID_FAKE = "srd5e"; + + static PACK_NAME_CREATURES = "creatures"; + static PACK_NAME_SPELLS = "spells"; + static PACK_NAME_ITEMS = "items"; + + static MODULE_LOCATION = `modules/${SharedConsts.MODULE_ID}`; + + static SYSTEM_ID_DND5E = "dnd5e"; +} +//#endregion + +//#region ConfigConsts +class ConfigConsts { + static ["_flushCaches"]() { + this._DEFAULT_CONFIG = null; + this._DEFAULT_CONFIG_SORTED = null; + this._DEFAULT_CONFIG_SORTED_FLAT = null; + } + static ['_IMPORTERS'] = {}; + static ["registerImporter"]({ + id: _0x17cd9e, + name: _0x948c81 + }) { + this._IMPORTERS[_0x17cd9e] = _0x948c81; + this._flushCaches(); + } + static ["_template_getImporterToggles"]() { + return { + 'hiddenImporterIds': { + 'name': "Hidden Importers", + 'help': "Importers which should not be shown in the Import Wizard UI.", + 'default': { + 'background-features': true, + 'race-and-subrace-features': true + }, + 'type': 'multipleChoice', + 'choices': Object.entries(this._IMPORTERS).map(([_0x40fcfd, _0x286521]) => ({ + 'name': _0x286521, + 'value': _0x40fcfd + })).sort(({ + name: _0x5ca503 + }, { + name: _0x44f71d + }) => SortUtil.ascSortLower(_0x5ca503, _0x44f71d)) + } + }; + } + static ['_getModelBarAttributes'](_0x2c6d2c) { + if (!_0x2c6d2c) { + return []; + } + return Object.values(TokenDocument.implementation.getTrackedAttributeChoices(TokenDocument.implementation.getTrackedAttributes(_0x2c6d2c))).flat(); + } + static ['_template_getEntityOwnership'](_0x1b27c6) { + const _0x2971c8 = MiscUtil.copy(ConfigConsts._TEMPLATE_ENTITY_OWNERSHIP); + _0x2971c8.values = Util.Fvtt.getOwnershipEnum(); + _0x2971c8.help = _0x1b27c6; + return _0x2971c8; + } + static ["_template_getTokenSettings"]({ + actorType: _0x1701bd + }) { + return { + 'tokenNameDisplay': { + 'name': "Token Name Display Mode", + 'help': "The default Display Name mode for imported tokens.", + 'default': 0x14, + 'type': "enum", + 'values': [{ + 'value': ConfigConsts.C_USE_GAME_DEFAULT, + 'name': "Use game setting" + }, ...Object.entries({ + ...CONST.TOKEN_DISPLAY_MODES + }).sort(([, _0x11b4dd], [, _0x418ae3]) => SortUtil.ascSort(_0x11b4dd, _0x418ae3)).map(([_0x1c91ac, _0x536776]) => ({ + 'value': _0x536776, + 'fnGetName': () => game.i18n.localize('TOKEN.DISPLAY_' + _0x1c91ac) + }))] + }, + 'tokenDisposition': { + 'name': "Token Disposition", + 'help': "The default Token Disposition mode for imported tokens.", + 'default': -0x1, + 'type': "enum", + 'values': [{ + 'value': ConfigConsts.C_USE_GAME_DEFAULT, + 'name': "Use game setting" + }, ...Object.entries(CONST.TOKEN_DISPOSITIONS).sort(([, _0x153b2c], [, _0x397446]) => SortUtil.ascSort(_0x153b2c, _0x397446)).map(([_0x2e1787, _0x29a452]) => ({ + 'value': _0x29a452, + 'fnGetName': () => game.i18n.localize("TOKEN.DISPOSITION." + _0x2e1787) + }))] + }, + 'tokenLockRotation': { + 'name': "Token Lock Rotation", + 'help': "The default Lock Rotation mode for imported tokens.", + 'default': ConfigConsts.C_USE_PLUT_VALUE, + 'type': 'enum', + 'values': [{ + 'value': ConfigConsts.C_USE_GAME_DEFAULT, + 'name': "Use game setting" + }, { + 'value': ConfigConsts.C_USE_PLUT_VALUE, + 'name': "Allow importer to set" + }] + }, + 'tokenIsAddVision': { + 'name': "Enable Token Vision", + 'help': "Enable vision for tokens.", + 'default': ConfigConsts.C_BOOL_ENABLED, + 'type': "enum", + 'values': [{ + 'value': ConfigConsts.C_USE_GAME_DEFAULT, + 'name': "Use game setting" + }, { + 'value': ConfigConsts.C_BOOL_DISABLED, + 'name': "Disabled" + }, { + 'value': ConfigConsts.C_BOOL_ENABLED, + 'name': 'Enabled' + }] + }, + 'tokenSightRange': { + 'name': "Token Vision Range", + 'help': "How token Vision Range should be set.", + 'default': ConfigConsts.C_USE_PLUT_VALUE, + 'type': 'enum', + 'values': [{ + 'value': ConfigConsts.C_USE_GAME_DEFAULT, + 'name': "Use game setting" + }, { + 'value': ConfigConsts.C_USE_PLUT_VALUE, + 'name': "Allow importer to set" + }] + }, + 'tokenSightVisionMode': { + 'name': "Token Vision Mode", + 'help': "How token Vision Mode should be set.", + 'default': ConfigConsts.C_USE_PLUT_VALUE, + 'type': "enum", + 'values': [{ + 'value': ConfigConsts.C_USE_GAME_DEFAULT, + 'name': "Use game setting" + }, { + 'value': ConfigConsts.C_USE_PLUT_VALUE, + 'name': "Allow importer to set" + }] + }, + 'tokenSightAngle': { + 'name': "Token Sight Angle", + 'help': "How token Sight Angle (Degrees) should be set.", + 'default': ConfigConsts.C_USE_PLUT_VALUE, + 'type': "enum", + 'values': [{ + 'value': ConfigConsts.C_USE_GAME_DEFAULT, + 'name': "Use game setting" + }, { + 'value': ConfigConsts.C_USE_PLUT_VALUE, + 'name': "Allow importer to set" + }] + }, + 'tokenDetectionModes': { + 'name': "Token Detection Modes", + 'help': "How token Detection Modes should be set.", + 'default': ConfigConsts.C_USE_PLUT_VALUE, + 'type': "enum", + 'values': [{ + 'value': ConfigConsts.C_USE_GAME_DEFAULT, + 'name': "Use game setting" + }, { + 'value': ConfigConsts.C_USE_PLUT_VALUE, + 'name': "Allow importer to set" + }] + }, + 'tokenVisionSaturation': { + 'name': "Token Vision Saturation", + 'help': "How token vision Saturation should be set.", + 'default': ConfigConsts.C_USE_PLUT_VALUE, + 'type': "enum", + 'values': [{ + 'value': ConfigConsts.C_USE_GAME_DEFAULT, + 'name': "Use game setting" + }, { + 'value': ConfigConsts.C_USE_PLUT_VALUE, + 'name': "Allow importer to set" + }] + }, + 'tokenDimLight': { + 'name': "Token Dim Light Radius", + 'help': "How token Dim Light Radius (Distance) should be set.", + 'default': ConfigConsts.C_USE_PLUT_VALUE, + 'type': "enum", + 'values': [{ + 'value': ConfigConsts.C_USE_GAME_DEFAULT, + 'name': "Use game setting" + }, { + 'value': ConfigConsts.C_USE_PLUT_VALUE, + 'name': "Allow importer to set" + }] + }, + 'tokenBrightLight': { + 'name': "Token Bright Light Radius", + 'help': "How token Bright Light Radius (Distance) should be set.", + 'default': ConfigConsts.C_USE_PLUT_VALUE, + 'type': "enum", + 'values': [{ + 'value': ConfigConsts.C_USE_GAME_DEFAULT, + 'name': "Use game setting" + }, { + 'value': ConfigConsts.C_USE_PLUT_VALUE, + 'name': "Allow importer to set" + }] + }, + 'tokenLightAngle': { + 'name': "Token Light Emission Angle", + 'help': "How token Light Emission (Angle) should be set.", + 'default': ConfigConsts.C_USE_PLUT_VALUE, + 'type': 'enum', + 'values': [{ + 'value': ConfigConsts.C_USE_GAME_DEFAULT, + 'name': "Use game setting" + }, { + 'value': ConfigConsts.C_USE_PLUT_VALUE, + 'name': "Allow importer to set" + }] + }, + 'tokenLightColor': { + 'name': "Token Light Color", + 'help': "How token Light Color should be set.", + 'default': ConfigConsts.C_USE_PLUT_VALUE, + 'type': "enum", + 'values': [{ + 'value': ConfigConsts.C_USE_GAME_DEFAULT, + 'name': "Use game setting" + }, { + 'value': ConfigConsts.C_USE_PLUT_VALUE, + 'name': "Allow importer to set" + }] + }, + 'tokenLightAlpha': { + 'name': "Token Light Intensity", + 'help': "How token Color Intensity should be set.", + 'default': ConfigConsts.C_USE_PLUT_VALUE, + 'type': "enum", + 'values': [{ + 'value': ConfigConsts.C_USE_GAME_DEFAULT, + 'name': "Use game setting" + }, { + 'value': ConfigConsts.C_USE_PLUT_VALUE, + 'name': "Allow importer to set" + }] + }, + 'tokenLightAnimationType': { + 'name': "Token Light Animation Type", + 'help': "How token Light Animation Type should be set.", + 'default': ConfigConsts.C_USE_PLUT_VALUE, + 'type': 'enum', + 'values': [{ + 'value': ConfigConsts.C_USE_GAME_DEFAULT, + 'name': "Use game setting" + }, { + 'value': ConfigConsts.C_USE_PLUT_VALUE, + 'name': "Allow importer to set" + }] + }, + 'tokenLightAnimationSpeed': { + 'name': "Token Light Animation Speed", + 'help': "How token Light Animation Speed should be set.", + 'default': ConfigConsts.C_USE_PLUT_VALUE, + 'type': "enum", + 'values': [{ + 'value': ConfigConsts.C_USE_GAME_DEFAULT, + 'name': "Use game setting" + }, { + 'value': ConfigConsts.C_USE_PLUT_VALUE, + 'name': "Allow importer to set" + }] + }, + 'tokenLightAnimationIntensity': { + 'name': "Token Light Animation Intensity", + 'help': "How token Light Animation Intensity should be set.", + 'default': ConfigConsts.C_USE_PLUT_VALUE, + 'type': 'enum', + 'values': [{ + 'value': ConfigConsts.C_USE_GAME_DEFAULT, + 'name': "Use game setting" + }, { + 'value': ConfigConsts.C_USE_PLUT_VALUE, + 'name': "Allow importer to set" + }] + }, + 'tokenBarDisplay': { + 'name': "Token Bar Display Mode", + 'help': "The default Display Bars mode for imported tokens.", + 'default': 0x28, + 'type': 'enum', + 'values': [{ + 'value': ConfigConsts.C_USE_GAME_DEFAULT, + 'name': "Use game setting" + }, { + 'value': 0x0, + 'name': "None" + }, { + 'value': 0xa, + 'name': 'Control' + }, { + 'value': 0x14, + 'name': "Owner Hover" + }, { + 'value': 0x1e, + 'name': 'Hover' + }, { + 'value': 0x28, + 'name': 'Owner' + }, { + 'value': 0x32, + 'name': "Always" + }] + }, + 'tokenBar1Attribute': { + 'name': "Token Bar 1 Attribute", + 'help': "The default token bar 1 attribute for imported tokens.", + 'default': "attributes.hp", + 'type': "enum", + 'values': () => [{ + 'value': ConfigConsts.C_USE_GAME_DEFAULT, + 'name': "Use game setting" + }, ...ConfigConsts._getModelBarAttributes(_0x1701bd)], + 'isNullable': true + }, + 'tokenBar2Attribute': { + 'name': "Token Bar 2 Attribute", + 'help': "The default token bar 2 attribute for imported tokens.", + 'default': null, + 'type': "enum", + 'values': () => [{ + 'value': ConfigConsts.C_USE_GAME_DEFAULT, + 'name': "Use game setting" + }, ...ConfigConsts._getModelBarAttributes(_0x1701bd)], + 'isNullable': true + }, + 'tokenScale': { + 'name': "Token Scale", + 'help': "The default token scale for imported tokens.", + 'default': null, + 'type': "number", + 'placeholder': "(Use default)", + 'min': 0.2, + 'max': 0x3, + 'isNullable': true + }, + 'isTokenMetric': { + 'name': "Convert Token Vision Ranges to Metric", + 'help': "Whether or not token vision range units should be converted to an approximate metric equivalent (5 feet โ‰ˆ 1.5 metres).", + 'default': false, + 'type': "boolean" + } + }; + } + static ["_template_getSceneImportSettings"]() { + return { + 'scenePadding': { + 'name': "Scene Padding", + 'help': "The amount of scene padding to apply when creating a scene.", + 'default': 0x0, + 'type': "number", + 'min': 0x0, + 'max': 0.5 + }, + 'sceneBackgroundColor': { + 'name': "Scene Background Color", + 'help': "The background color to apply when creating a scene.", + 'default': "#222222", + 'type': "color" + }, + 'isSceneTokenVision': { + 'name': "Scene Token Vision", + 'help': "Whether or not token vision should be enabled for a created scene.", + 'default': true, + 'type': "boolean" + }, + 'isSceneFogExploration': { + 'name': "Scene Fog Exploration", + 'help': "Whether or not fog exploration should be enabled for a created scene.", + 'default': true, + 'type': "boolean" + }, + 'isSceneAddToNavigation': { + 'name': "Add Scenes to Navigation", + 'help': "Whether or not a created scene should be added to the navigation bar.", + 'default': false, + 'type': "boolean" + }, + 'isSceneGenerateThumbnail': { + 'name': "Generate Scene Thumbnails", + 'help': "Whether or not a thumbnail should be generated for a created scene. Note that this greatly slows down the scene creation process.", + 'default': true, + 'type': "boolean" + }, + 'isSceneGridMetric': { + 'name': "Convert Scene Grid Distances to Metric", + 'help': "Whether or not scene grid distances should be converted to an approximate metric equivalent (" + ConfigConsts._DISP_METRIC_FEET + "; " + ConfigConsts._DISP_METRIC_MILES + ').', + 'default': false, + 'type': "boolean" + } + }; + } + static ["_template_getActiveEffectsDisabledTransferSettings"]({ + name: _0x535a83 + }) { + return { + 'setEffectDisabled': { + 'name': "Override Effect "Disabled" Value", + 'help': "If set, overrides the \"Disabled\" value present on any effects tied to imported " + _0x535a83 + '.', + 'type': 'enum', + 'default': ConfigConsts.C_USE_PLUT_VALUE, + 'compatibilityModeValues': { + [UtilCompat.MODULE_MIDI_QOL]: { + 'value': ConfigConsts.C_USE_PLUT_VALUE, + 'name': "Allow importer to set" + } + }, + 'values': [{ + 'value': ConfigConsts.C_USE_PLUT_VALUE, + 'name': "Allow importer to set" + }, { + 'value': ConfigConsts.C_BOOL_DISABLED, + 'name': "Set to \"False\"" + }, { + 'value': ConfigConsts.C_BOOL_ENABLED, + 'name': "Set to \"True\"" + }] + }, + 'setEffectTransfer': { + 'name': "Override Effect "Transfer" Value", + 'help': "If set, overrides the \"Transfer to Actor\" value present on any effects tied to imported " + _0x535a83 + '.', + 'type': 'enum', + 'default': ConfigConsts.C_USE_PLUT_VALUE, + 'compatibilityModeValues': { + [UtilCompat.MODULE_MIDI_QOL]: { + 'value': ConfigConsts.C_USE_PLUT_VALUE, + 'name': "Allow importer to set" + } + }, + 'values': [{ + 'value': ConfigConsts.C_USE_PLUT_VALUE, + 'name': "Allow importer to set" + }, { + 'value': ConfigConsts.C_BOOL_DISABLED, + 'name': "Set to \"False\"" + }, { + 'value': ConfigConsts.C_BOOL_ENABLED, + 'name': "Set to \"True\"" + }] + } + }; + } + static ["_template_getMinimumRole"]({ + name: _0x2cb847, + help: _0x713a + }) { + const _0xdadcbd = MiscUtil.copy(ConfigConsts._TEMPALTE_MINIMUM_ROLE); + _0xdadcbd.values = Util.Fvtt.getMinimumRolesEnum(); + _0xdadcbd.name = _0x2cb847; + _0xdadcbd.help = _0x713a; + return _0xdadcbd; + } + static ['_template_getModuleFauxCompendiumIndexSettings']({ + moduleName: _0x5afcb7 + }) { + return { + 'isEnabled': { + 'name': "Enabled", + 'help': "If enabled, and the " + _0x5afcb7 + " module is active, Plutonium content will be indexed by " + _0x5afcb7 + '.', + 'default': true, + 'type': "boolean", + 'isReloadRequired': true + }, + 'isFilterSourcesUa': { + 'name': "Exclude UA/etc.", + 'help': "If Unearthed Arcana and other unofficial source content should be excluded from the index.", + 'default': true, + 'type': "boolean", + 'isReloadRequired': true + } + }; + } + static ["_template_getActorImportOverwriteSettings"]() { + return { + 'isDisableActorOverwriteWarning': { + 'name': "Disable Actor Overwrite Warning", + 'help': "Disable the warning confirmation dialogue shown when importing to an existing actor.", + 'default': false, + 'type': "boolean", + 'isPlayerEditable': true + } + }; + } + static ["_template_getTargetTemplatePrompt"]({ + namePlural: _0x1f80cf + }) { + return { + 'isTargetTemplatePrompt': { + 'name': "Enable "Template Prompt"s", + 'help': "If enabled, the \"Template Prompt\" option will be set on imported " + _0x1f80cf + '.', + 'default': true, + 'type': "boolean", + 'isPlayerEditable': true + } + }; + } + static _DEFAULT_CONFIG = null; + static getDefaultConfig_() { + return this._DEFAULT_CONFIG = this._DEFAULT_CONFIG || { + 'ui': { + 'name': 'UI', + 'settings': { + 'isStreamerMode': { + 'name': "Streamer Mode", + 'help': "Remove identifiable 5etools/Plutonium references from the UI, and replaces them with \"SRD Enhanced.\"", + 'default': false, + 'type': "boolean", + 'isReloadRequired': true, + 'isPlayerEditable': true + }, + 'isShowPopout': { + 'name': "Enable Sheet Popout Buttons", + 'help': "Add a \"Popout\" button to sheet headers, which opens the sheet as a popup browser window.", + 'default': true, + 'type': 'boolean', + 'isPlayerEditable': true + }, + 'isCompactWindowBar': { + 'name': "Compact Header Buttons", + 'help': "Re-style header buttons to better support the compact, no-text buttons used by Plutonium.", + 'default': true, + 'type': "boolean", + 'isPlayerEditable': true + }, + 'isCompactDirectoryButtons': { + 'name': "Compact Directory Buttons", + 'help': "Reduce the height of \"Create X\"/\"Create Folder\" buttons in the directory, to offset the additional space requirements of Plutonium's UI.", + 'default': true, + 'type': "boolean", + 'isPlayerEditable': true + }, + 'isCompactChat': { + 'name': "Compact Chat", + 'help': "Make various tweaks to the appearance of chat, in order to fit more on-screen. Hold down SHIFT while hovering over a message to expand it, revealing its header and delete button.", + 'default': true, + 'type': "boolean", + 'isPlayerEditable': true + }, + 'isCompactScenes': { + 'name': "Compact Scenes Directory", + 'help': "Reduce the height of scene thumbnails in the Scenes Directory, to fit more on-screen.", + 'default': true, + 'type': "boolean", + 'isPlayerEditable': true + }, + 'isCompactActors': { + 'name': "Compact Actors Directory", + 'help': "Reduce the height of Actors Directory directory items, to fit more on-screen.", + 'default': true, + 'type': "boolean", + 'isPlayerEditable': true + }, + 'isCompactItems': { + 'name': "Compact Items Directory", + 'help': "Reduce the height of Items Directory directory items, to fit more on-screen.", + 'default': true, + 'type': "boolean", + 'isPlayerEditable': true + }, + 'isCompactJournal': { + 'name': "Compact Journal Entries", + 'help': "Reduce the height of Journal Entries directory items, to fit more on-screen.", + 'default': true, + 'type': "boolean", + 'isPlayerEditable': true + }, + 'isCompactTables': { + 'name': "Compact Rollable Tables", + 'help': "Reduce the height of Rollable Tables directory items, to fit more on-screen.", + 'default': true, + 'type': 'boolean', + 'isPlayerEditable': true + }, + 'isCompactCards': { + 'name': "Compact Card Stacks", + 'help': "Reduce the height of Card Stacks directory items, to fit more on-screen.", + 'default': true, + 'type': 'boolean', + 'isPlayerEditable': true + }, + 'isCompactCompendiums': { + 'name': "Compact Compendium Packs", + 'help': "Reduce the height of Compendium Packs directory items, to fit more on-screen.", + 'default': true, + 'type': "boolean", + 'isPlayerEditable': true + }, + 'isCompactMacros': { + 'name': "Compact Macros", + 'help': "Reduce the height of Macro directory items, to fit more on-screen.", + 'default': true, + 'type': "boolean", + 'isPlayerEditable': true + }, + 'isHidePlutoniumDirectoryButtons': { + 'name': "Hide Directory Buttons", + 'help': "Hide the Plutonium directory buttons.", + 'default': false, + 'type': 'boolean' + }, + 'isNameTabFromScene': { + 'name': "Prepend Active Scene Name to Browser Tab Name", + 'help': "Sets the browser tab name to be that of the currently-active scene.", + 'default': true, + 'type': 'boolean' + }, + 'tabNameSuffix': { + 'name': "Tab Name Suffix", + 'help': "Requires the \"Name Browser Tab After Active Scene\" option to be enabled. A custom name suffix to append to the scene name displayed in the tab (separated by a Foundry-style bullet character).", + 'default': null, + 'isNullable': true, + 'type': "string" + }, + 'isDisplayBackendStatus': { + 'name': "Display Detected Backend", + 'help': "Adds a cool green hacker tint to the Foundry \"anvil\" logo in the top-left corner of the screen if Plutonium's backend is detected.", + 'default': true, + 'type': "boolean", + 'isPlayerEditable': true + }, + 'isExpandActiveEffectConfig': { + 'name': "Enhance Active Effect Config UI", + 'help': "Adds a list of potential active effect attribute keys to the Configure Active Effect window's \"Effects\" tab, and a field for configuring priority.", + 'default': true, + 'type': "boolean", + 'compatibilityModeValues': { + [UtilCompat.MODULE_DAE]: false + } + }, + 'isAddDeleteToSceneNavOptions': { + 'name': "Add \"Delete\" to Navbar Scene Context Menu", + 'help': "Adds a \"Delete\" option to the context menu found when right-clicking a scene in the navigation bar. Note that this does not include the currently-active scene.", + 'default': true, + 'type': "boolean" + } + }, + 'settingsAdvanced': { + 'isHideGmOnlyConfig': { + 'name': "Hide GM-Only Config", + 'help': "If enabled, a player viewing the config will see only the limited subset of settings they are allowed to modify. If disabled, a player viewing the config will see all settings, regardless of whether or not they can modify those settings.", + 'default': true, + 'type': 'boolean' + }, + 'isDisableLargeImportWarning': { + 'name': "Disable Large Import Warning", + 'help': "Disable the warning confirmation dialogue shown when importing a large number of entities.", + 'default': false, + 'type': "boolean", + 'isPlayerEditable': true + } + }, + 'settingsHacks': { + 'isFastAnimations': { + 'name': "Fast Animations", + 'help': "Increase the speed of various UI animations.", + 'default': false, + 'type': 'boolean', + 'isPlayerEditable': true + }, + 'isFastTooltips': { + 'name': "Fast Tooltips", + 'help': "Increase the speed of tooltip animations, and reduce the delay before tooltips appear.", + 'default': false, + 'type': "boolean", + 'isPlayerEditable': true + }, + 'isFixEscapeKey': { + 'name': "Fix ESC Key", + 'help': "Bind the \"Escape\" key to (in this order): de-select active input fields; de-select selected canvas elements; close context menus; close individual windows in most-recently-active-first order; toggle the main menu.", + 'default': true, + 'type': "boolean", + 'isPlayerEditable': true + }, + 'isAddOpenMainMenuButtonToSettings': { + 'name': "Add \"Open Game Menu\" Button if "Fix ESC Key" Is Enabled", + 'help': "Add an alternate \"Open Game Menu\" button to the Settings tab if the \"Fix ESC Key\" Config option is enabled. This allows you to quickly open the main menu without first having to close all open windows.", + 'default': true, + 'type': "boolean", + 'isPlayerEditable': true + }, + 'isFixDrawingFreehandMinDistance': { + 'name': "Fix Freehand Drawing Minimum Distance", + 'help': "Reduce the minimum mouse movement distance required to start a freehand drawing.", + 'default': true, + 'type': 'boolean', + 'isPlayerEditable': true + }, + 'isEnableIncreasedFolderDepth': { + 'name': "Render >3 Levels of Folder Nesting", + 'help': "If enabled, Foundry's default folder nesting limit (of 3) will be bypassed, for the purpose of rendering directories. Note that this does not necessarily allow you to create additionally-nested folders without using the game API.", + 'default': true, + 'type': "boolean", + 'compatibilityModeValues': { + [UtilCompat.MODULE_BETTER_ROLLTABLES]: false + } + }, + 'isEnableFolderNameWrap': { + 'name': "Wrap Long Folder Names", + 'help': "Wrap long folder names over multiple lines, instead of clipping the name.", + 'default': false, + 'type': "boolean", + 'isPlayerEditable': true + }, + 'isEnableSubPopouts': { + 'name': "Allow Popout Chaining", + 'help': "Automatically pop out apps opened from within popped-out apps. If disabled, apps opened from within popped-out apps will appear in the main window, instead.", + 'default': true, + 'type': 'boolean', + 'isPlayerEditable': true + }, + 'isSuppressMissingRollDataNotifications': { + 'name': "Suppress "Missing Roll Data" Notifications", + 'help': "If enabled, notification warning messages of the form \"The attribute was not present in the provided roll data.\" will be suppressed, and logged as console warnings instead.", + 'default': true, + 'type': "boolean", + 'isPlayerEditable': true + }, + 'isLazyActorAndItemRendering': { + 'name': "Minimize Actor/Item Re-Renders", + 'help': "If enabled, actor/item sheet re-rendering will be skipped where possible. This may reduce UI flickering, and may reduce unexpected input deselection when tabbing or clicking through fields. It may also horribly break your game, and is not expected to work with anything except default dnd5e sheets. Use with caution.", + 'default': false, + 'type': "boolean", + 'isPlayerEditable': true, + 'isReloadRequired': true + }, + 'isAlwaysResizableApps': { + 'name': "Default Resizeable Applications", + 'help': "If enabled, applications will be resizeable by default. Note that specific applications may still override this.", + 'default': false, + 'type': "boolean", + 'isPlayerEditable': true + } + } + }, + 'tokens': { + 'name': 'Tokens', + 'settings': { + 'isDisplayDamageDealt': { + 'name': "Display Missing Health", + 'help': "This allows players to see \"damage dealt\" to a token, without revealing the token's total health. If enabled, each token's missing health is displayed as a number in the bottom-right corner of the token.", + 'default': false, + 'type': "boolean" + }, + 'damageDealtBloodiedThreshold': { + 'name': "Display Missing Health "Wounded" Threshold", + 'help': "The health-loss threshold at which the Missing Health text turns red.", + 'default': 0.5, + 'type': "percentage", + 'min': 0x0, + 'max': 0x1 + }, + 'isDamageDealtBelowToken': { + 'name': "Missing Health Below Token", + 'help': "If the Missing Health text should be displayed beneath a token, rather than as an overlay.", + 'default': false, + 'type': 'boolean' + }, + 'nameplateFontSizeMultiplier': { + 'name': "Font Size Multiplier", + 'help': "A multiplier which is applied to token nameplate/tooltip font size, e.g. a value of \"0.5\" will decrease token nameplate/tooltip font size by half.", + 'default': null, + 'type': "number", + 'placeholder': "(Use default)", + 'min': 0.1, + 'max': 0xa, + 'isNullable': true + }, + 'isAllowNameplateFontWrap': { + 'name': "Allow Text Wrap", + 'help': "If enabled, token nameplate/tooltip text will wrap.", + 'default': ConfigConsts.C_USE_GAME_DEFAULT, + 'type': "enum", + 'values': [{ + 'value': ConfigConsts.C_USE_GAME_DEFAULT, + 'name': "Use Foundry default" + }, { + 'value': false, + 'name': "Disabled" + }, { + 'value': true, + 'name': 'Enabled' + }] + }, + 'nameplateFontWrapWidthMultiplier': { + 'name': "Text Wrap Max Width Multiplier", + 'help': "A multiplier which is applied to token nameplate/tooltip text wrapping maximum size, e.g. a value of \"0.5\" will force token nameplates/tooltips to wrap at half their usual length. The base value to which this multiplier is applied is: \"2.5 ร— token width\".", + 'default': null, + 'type': "number", + 'placeholder': "(Use default)", + 'min': 0.1, + 'max': 0xa, + 'isNullable': true + }, + 'isNameplateOnToken': { + 'name': "Move Token Name Onto Token", + 'help': "If a token's name should be displayed on the token, rather than below it.", + 'default': false, + 'type': "boolean" + }, + 'npcHpRollMode': { + 'name': "NPC HP Roll Mode", + 'help': "Determines whether or not token HP, for NPC tokens which are not linked to their actor's data, should be rolled upon token creation. If a mode other than \"None\" is selected, and the token has a valid HP dice formula, the token will roll for HP. For example, a Goblin (7 HP; formula is 2d6) could be created with anywhere between 2 and 12 HP (inclusive).", + 'default': ConfigConsts.C_TOKEN_NPC_HP_ROLL_MODE_NONE, + 'type': 'enum', + 'values': [{ + 'value': ConfigConsts.C_TOKEN_NPC_HP_ROLL_MODE_NONE, + 'name': "None", + 'help': "Do not roll NPC token health." + }, { + 'value': ConfigConsts.C_TOKEN_NPC_HP_ROLL_MODE_STANDARD, + 'name': "Standard Roll" + }, { + 'value': ConfigConsts.C_TOKEN_NPC_HP_ROLL_MODE_GM, + 'name': "GM Roll" + }, { + 'value': ConfigConsts.C_TOKEN_NPC_HP_ROLL_MODE_BLIND, + 'name': "Blind Roll" + }, { + 'value': ConfigConsts.C_TOKEN_NPC_HP_ROLL_MODE_SELF, + 'name': "Self Roll" + }, { + 'value': ConfigConsts.C_TOKEN_NPC_HP_ROLL_MODE_HIDDEN, + 'name': "Hidden Roll", + 'help': "Roll NPC token health, but do not post the result to chat." + }, { + 'value': ConfigConsts.C_TOKEN_NPC_HP_ROLL_MODE_MIN, + 'name': "Minimum Value", + 'help': "Use the minimum possible roll value." + }, { + 'value': ConfigConsts.C_TOKEN_NPC_HP_ROLL_MODE_MAX, + 'name': "Maximum Value", + 'help': "Use the maximum possible roll value." + }] + }, + 'isDisableAnimations': { + 'name': "Disable Animations", + 'help': "Disable token animations.", + 'default': false, + 'type': "boolean" + }, + 'animationSpeedMultiplier': { + 'name': "Animation Speed", + 'help': "Multiplies token animation movement speed by the factor provided.", + 'default': null, + 'type': "number", + 'isNullable': true, + 'min': 0.1, + 'max': 0xa + } + }, + 'settingsAdvanced': { + 'missingHealthAttribute': { + 'name': "Health Attribute", + 'help': "The sheet attribute used to fetch current/max health when the \"Display Missing Health\" option is enabled.", + 'default': "attributes.hp", + 'type': "string", + 'additionalStyleClasses': "code" + } + }, + 'settingsHacks': { + 'isIgnoreDisableAnimationsForWaypointMovement': { + 'name': "Avoid Disabling Animations for Ruler Movement", + 'help': "Suppresses the \"Disable Animations\" option for a token being moved via ruler waypoints (i.e. when CTRL-dragging from a token and pressing SPACE). Note that dismissing the ruler during the move will end this suppression.", + 'default': true, + 'type': "boolean" + } + } + }, + 'import': { + 'name': "Import", + 'settings': { + 'isAddSourceToName': { + 'name': "Add Source to Names", + 'help': "If the source of each imported entry (e.g. \"MM\" for Monster Manual) should be appended to the name of the entry.", + 'default': false, + 'type': 'boolean', + 'isPlayerEditable': true + }, + 'isRenderLinksAsTags': { + 'name': "Render Links as "@tag"s", + 'help': "If links found in description text should be rendered as Plutonium-specific @tag syntax, e.g. a link to \"goblin\" would be rendered as \"@creature[goblin|mm]\". (By default, a link to the 5etools page will be rendered instead.)", + 'default': true, + 'type': 'boolean' + }, + 'isRendererLinksDisabled': { + 'name': "Disable 5etools Links", + 'help': "Prevents links to other 5etools content from being added to the text of imported 5etools content.", + 'default': false, + 'type': "boolean", + 'isPlayerEditable': true + }, + 'isRendererDiceDisabled': { + 'name': "Render Dice as Plain Text", + 'help': "Forces dice expressions, usually rendered as \"[[/r XdY + Z ...]]\", to be rendered as plain text when importing 5etools content.", + 'default': false, + 'type': "boolean", + 'isPlayerEditable': true + }, + 'isRenderCustomDiceEnrichers': { + 'name': "Render Dice as Custom Enrichers", + 'help': "If enabled, importers will make use of dnd5e-specific custom enrichers when rendering dice. For example, damage rolls may be rendered as \"[[/damage ...]]\" instead of \"[[/r ...]]\", changing the on-click behaviour.", + 'default': true, + 'type': 'boolean' + }, + 'deduplicationMode': { + 'name': "Duplicate Handling Mode", + 'help': "Determines what action is taken when importing duplicate content to a directory or compendium. An entity is considered a duplicate if and only if its name and source match an existing entity. Note that this does not function when importing to actor sheets.", + 'default': ConfigConsts.C_IMPORT_DEDUPE_MODE_NONE, + 'type': "enum", + 'values': [{ + 'value': ConfigConsts.C_IMPORT_DEDUPE_MODE_NONE, + 'name': "None", + 'help': "No deduplication is done." + }, { + 'value': ConfigConsts.C_IMPORT_DEDUPE_MODE_SKIP, + 'name': "Skip duplicates", + 'help': "If a duplicate is found for a would-be imported entity, that entity is not imported." + }, { + 'value': ConfigConsts.C_IMPORT_DEDUPE_MODE_OVERWRITE, + 'name': "Update existing", + 'help': "If a duplicate is found for a would-be import entity, the existing entity is updated." + }] + }, + 'isDuplicateHandlingMaintainImage': { + 'name': "Maintain Images when Overwriting Duplicates", + 'help': "If enabled, sheet and token images will be maintained when overwriting an existing document in \"Update Existing\" Duplicate Handling Mode.", + 'default': false, + 'type': 'boolean' + }, + /* 'minimumRole': ConfigConsts._template_getMinimumRole({ + 'name': "Minimum Permission Level for Import", + 'help': "\"Import\" buttons will be hidden for any user with a role less than the chosen role." + }), */ + 'dragDropMode': { + 'name': "Use Importer when Drag-Dropping Items to Actors", + 'help': "Some Foundry items (backgrounds, races, spells, items, etc.), when imported via Plutonium and later drag-dropped to an actor sheet, have special handling allowing for greater functionality (such as populating skills and features). This allows you to control whether or not that special handling is used, rather than the baseline Foundry drag-drop. Note that if you modify an item, the changes will not be reflected in the version imported to the sheet by Plutonium.", + 'default': ConfigConsts.C_IMPORT_DRAG_DROP_MODE_PROMPT, + 'type': "enum", + 'values': [{ + 'value': ConfigConsts.C_IMPORT_DRAG_DROP_MODE_NEVER, + 'name': "Never" + }, { + 'value': ConfigConsts.C_IMPORT_DRAG_DROP_MODE_PROMPT, + 'name': "Prompt" + }, { + 'value': ConfigConsts.C_IMPORT_DRAG_DROP_MODE_ALWAYS, + 'name': "Always" + }], + 'isPlayerEditable': true + }, + 'isUseOtherFormulaFieldForSaveHalvesDamage': { + 'name': "Treat "Save Halves" Additional Attack Damage as "Other Formula"", + 'help': "This moves extra attack damage rolls (for example, the poison damage done by a Giant Spider's bite) to the \"Other Formula\" dice field, which can improve compatibility with some modules.", + 'default': false, + 'type': 'boolean', + 'compatibilityModeValues': { + [UtilCompat.MODULE_PLUTONIUM_ADDON_AUTOMATION]: true + } + }, + 'isUseOtherFormulaFieldForOtherDamage': { + 'name': "Treat "Alternate" Attack Damage as "Other Formula"", + 'help': "This moves alternate non-versatile attack damage rolls (for example, Egg Hunter Hatchling's "Egg Tooth" damage when targeting an object) to the \"Other Formula\" dice field, which can improve compatibility with some modules.", + 'default': false, + 'type': 'boolean', + 'compatibilityModeValues': { + [UtilCompat.MODULE_PLUTONIUM_ADDON_AUTOMATION]: true + } + }, + 'isGlobalMetricDistance': { + 'name': "Prefer Metric Distance/Speed (Where Available)", + 'help': "If enabled, metric distance/speed units will be preferred, where the importer supports them. Enabling this option effectively overrides all other metric distance/speed options, causing the importer to treat each as though it was enabled.", + 'default': false, + 'type': 'boolean' + }, + 'isGlobalMetricWeight': { + 'name': "Prefer Metric Weight (Where Available)", + 'help': "If enabled, metric weight units will be preferred, where the importer supports them. Enabling this option effectively overrides all other metric weight options, causing the importer to treat each as though it was enabled.", + 'default': false, + 'type': "boolean" + }, + 'isShowVariantsInLists': { + 'name': "Show Variants/Versions", + 'help': "If variants/versions of base entries should be shown in list views (with grayed-out names).", + 'default': true, + 'type': "boolean" + }, + 'isSaveImagesToServer': { + 'name': "Save Imported Images to Server", + 'help': "If images referenced in imported content should be saved to your server files, rather than referenced from an external server.", + 'default': false, + 'type': "boolean" + }, + 'isSaveTokensToServer': { + 'name': "Save Imported Tokens to Server", + 'help': "If tokens for imported actors should be saved to your server files, rather than referenced from an external server.", + 'default': true, + 'type': "boolean" + }, + 'localImageDirectoryPath': { + 'name': "Image/Token Directory", + 'help': "The sub-directory of the \"User Data\" directory where imported images/tokens will be saved to when using the \"Save Imported Images to Server\" option or the \"Save Imported Tokens to Server\" option. If the \"Use Local Images\" option is enabled, images will be loaded from this directory by default.", + 'default': "assets/" + SharedConsts.MODULE_ID_FAKE, + 'type': "string", + 'additionalStyleClasses': 'code' + }, + 'isPreferFoundryImages': { + 'name': "Prefer Foundry/System Images", + 'help': "If enabled, portraits for actors and images for items will be sourced from built-in compendiums first, then Plutonium second. If disabled, portraits/images will be sourced from Plutonium first, then built-in compendiums second.", + 'default': false, + 'type': "boolean" + }, + 'isPreferFoundryTokens': { + 'name': "Prefer Foundry/System Tokens", + 'help': "If enabled, tokens will be sourced from built-in compendiums first, then Plutonium second. If disabled, tokens will be sourced from Plutonium first, then built-in compendiums second.", + 'default': false, + 'type': 'boolean' + } + }, + 'settingsAdvanced': { + ...ConfigConsts._template_getImporterToggles(), + 'isTreatJournalEntriesAsFolders': { + 'name': "Treat Journal Entries as Folders", + 'help': "If enabled, Journal Entries are treated as an additional folder level for the purpose of organising imports, etc.", + 'default': true, + 'type': "boolean", + 'isReloadRequired': true + }, + 'isUseLocalImages': { + 'name': "Use Local Images", + 'help': "If enabled, images will be sourced from the \"Image/Token Directory\" directory, defined above.", + 'default': false, + 'type': 'boolean' + }, + 'isStrictMatching': { + 'name': "Use Strict Entity Matching", + 'help': "If enabled, any Plutonium feature which searches for existing data (for example, the class importer attempting to find existing class levels in a given class) will match by name and source. If disabled, only name is used.", + 'default': false, + 'type': 'boolean', + 'isPlayerEditable': true + }, + 'tempFolderName': { + 'name': "Temp Folder Name", + 'help': "The name of a temporary folder created/deleted by some operations. Note that the importer will delete this folder regardless of its contents, as anything contained within it is assumed to be a temporary entity created by the importer.", + 'type': 'string', + 'default': "Temp" + }, + 'isAutoAddAdditionalFonts': { + 'name': "Automatically Add Extra Fonts", + 'help': "If enabled, and you import content which requires additional fonts, these fonts will be added to your game's \"Additional Fonts\" setting.", + 'default': true, + 'type': "boolean" + } + } + }, + 'importCreature': { + 'name': "Import (Creatures)", + 'settings': { + //'ownership': ConfigConsts._template_getEntityOwnership("The default (i.e. used for all players unless a player-specific ownership level is set) ownership for an imported creature."), + 'isImportBio': { + 'name': "Import Fluff to Biography", + 'help': "If enabled, any fluff text which is available for a creature will be imported into that creature's biography.", + 'default': true, + 'type': "boolean" + }, + 'isImportBioImages': { + 'name': "Include Fluff Image in Biography", + 'help': "If enabled, any fluff image which is available for a creature will be imported into that creature's biography.", + 'default': false, + 'type': 'boolean' + }, + 'isImportBioVariants': { + 'name': "Include Variants in Biography", + 'help': "If enabled, any inset variant boxes associated with a creature will be imported into that creature's biography.", + 'default': true, + 'type': "boolean" + }, + 'isImportVariantsAsFeatures': { + 'name': "Import Variants as Features", + 'help': "If enabled, any inset variant boxes associated with a creature will be imported into that creature's features.", + 'default': false, + 'type': "boolean" + }, + /* ...ConfigConsts._template_getTokenSettings({ + 'actorType': "npc" + }), */ + 'itemWeightAndValueSizeScaling': { + 'name': "Item Weight & Value Scaling", + 'help': "The method by which to scale the weights and values of non-standard-sizes items carried by creatures.", + 'default': 0x1, + 'type': 'enum', + 'values': [{ + 'value': 0x1, + 'name': "No scaling" + }, { + 'value': 0x2, + 'name': "\"Barding\" scaling (multiplicative)", + 'help': "Based on the rules for calculating the weight and cost of barding, as presented in the Player's Handbook (p. 155)." + }, { + 'value': 0x3, + 'name': "\"Gurt's Greataxe\" scaling (exponential)", + 'help': "Based on the giant-size greateaxe of the same name found in Storm King's Thunder (p. 234)." + }] + }, + 'isMetricDistance': { + 'name': "Convert Speeds to Metric", + 'help': "Whether or not creature speed units should be converted to an approximate metric equivalent (" + ConfigConsts._DISP_METRIC_FEET + ').', + 'default': false, + 'type': "boolean" + }, + 'spellcastingPrimaryTraitMode': { + 'name': "Spellcasting Primary Trait Selection Method", + 'help': "The method by which a primary spellcasting trait (i.e., the spellcasting trait used to set spellcasting ability, spell DC, and spell attack bonus) is selected if a creature has multiple spellcasting traits with associated ability scores.", + 'default': 0x1, + 'type': 'enum', + 'values': [{ + 'value': 0x1, + 'name': "Highest spell count", + 'help': "Use whichever spellcasting trait has the most spells listed." + }, { + 'value': 0x2, + 'name': "Highest ability score", + 'help': "Use whichever spellcasting trait has the highest associated ability score. Note that this may prefer innate spellcasting traits over spellcasting class levels." + }] + }, + 'nameTags': { + 'name': "Add Tag Suffixes to Names", + 'help': "Add tags to an imported creature's name, to allow easier searching (especially within compendiums).", + 'default': { + [ConfigConsts.C_CREATURE_NAMETAGS_CR]: false, + [ConfigConsts.C_CREATURE_NAMETAGS_TYPE]: false, + [ConfigConsts.C_CREATURE_NAMETAGS_TYPE_WITH_TAGS]: false + }, + 'type': 'multipleChoice', + 'choices': [{ + 'value': ConfigConsts.C_CREATURE_NAMETAGS_CR, + 'name': "Add [CR] tag" + }, { + 'value': ConfigConsts.C_CREATURE_NAMETAGS_TYPE, + 'name': "Add [type] tag" + }, { + 'value': ConfigConsts.C_CREATURE_NAMETAGS_TYPE_WITH_TAGS, + 'name': "Add [type (with tags)] tag" + }] + }, + 'isAddSoundEffect': { + 'name': "MLD: Add Audio as Sound Effect", + 'help': "If, when the Monk's Little Details module is active, an imported creature should have its sound effect set, where an audio clip is available (for official data, this will usually be an audio clip of the creature's name being pronounced).", + 'default': false, + 'type': "boolean" + } + }, + 'settingsAdvanced': { + 'additionalDataCompendium': { + 'name': "Additional Data Compendiums", + 'help': "A comma-separated list of compendiums that the Creature Importer will attempt to pull additional data (including art) from rather than use the default Plutonium icons.", + 'default': ConfigConsts.SRD_COMPENDIUMS_CREATURES.join(", "), + 'type': "string", + 'typeSub': "compendiums", + 'additionalStyleClasses': "code", + 'isNullable': true + }, + 'additionalDataCompendiumFeatures': { + 'name': "Additional Data Compendiums (Features)", + 'help': "A comma-separated list of compendiums that the Creature Importer will attempt to pull additional data (including art) from rather than use the default Plutonium icons.", + 'default': ConfigConsts.SRD_COMPENDIUMS_CREATURE_FEATURES.join(", "), + 'type': 'string', + 'typeSub': "compendiums", + 'additionalStyleClasses': "code", + 'isNullable': true + }, + 'isUseTokenImageAsPortrait': { + 'name': "Use Token Image as Portrait", + 'help': "If enabled, a creature's token image will be preferred over its portrait image when populating its sheet portrait during import.", + 'default': false, + 'type': "boolean" + }, + ...ConfigConsts._template_getActorImportOverwriteSettings(), + 'isAddFakeClassToCharacter': { + 'name': "Add Class to Creatures Imported as Player Characters", + 'help': "If enabled, when importing a creature as a Player Character (\"character\"-type actor) a class item will be added to the actor's sheet, in order to set proficiency bonus and spellcasting levels.", + 'default': true, + 'type': 'boolean' + }, + 'isUseStaticAc': { + 'name': "Use Static AC Values", + 'help': "If enabled, creature AC will be imported as a static number (rather than relying on the sheet's formula calculation), and creature armor will be imported as unequipped.", + 'default': false, + 'type': "boolean" + }, + 'isUseCustomNaturalAc': { + 'name': "Use Custom Natural Armor Formula", + 'help': "If enabled, creatures with natural armor will have their armor formula broken down as \"@attributes.ac.armor + @attributes.ac.dex + \", allowing any later Dexterity score changes to be reflected in the creatures AC.", + 'default': false, + 'type': 'boolean' + } + }, + 'settingsHacks': { + 'isUsePathfinderTokenPackBestiariesImages': { + 'name': "Use "Pathfinder Token Pack: Bestiaries" Tokens/Portraits", + 'help': "If enabled, and the \"Pathfinder Token Pack: Bestiaries\" module is installed and enabled, the importer will attempt to use token and portrait art from the \"Pathfinder Token Pack: Bestiaries\" module.", + 'default': false, + 'type': "boolean" + } + } + }, + 'importCreatureFeature': { + 'name': "Import (Creature Features)", + 'settings': { + /* 'ownership': ConfigConsts._template_getEntityOwnership("The default (i.e. used for all players unless a player-specific ownership level is set) ownership for an imported creature feature."), + ...ConfigConsts._template_getTargetTemplatePrompt({ + 'namePlural': "creature features" + }), */ + 'isSecretWrapAttacks': { + 'name': ""Secret" Attack Descriptions", + 'help': "If enabled, creature attack descriptions will be wrapped in \"Secret\" blocks, which are not shown when rolling.", + 'default': false, + 'type': "boolean" + }, + 'isScaleToTargetActor': { + 'name': "Scale to Target Actor CR", + 'help': "If enabled, creature features imported to existing NPC actors will be automatically scaled (altering to-hit bonuses, damage rolls, DCs, etc.) based on the difference between the original creature's CR and the target actor's CR.", + 'default': true, + 'type': 'boolean' + }, + 'isMetricDistance': { + 'name': "Convert Ranges to Metric", + 'help': "Whether or not creature feature range units should be converted to an approximate metric equivalent (" + ConfigConsts._DISP_METRIC_FEET + ').', + 'default': false, + 'type': "boolean" + } + }, + 'settingsAdvanced': { + ...ConfigConsts._template_getActiveEffectsDisabledTransferSettings({ + 'name': "creature features" + }), + 'isSplitMeleeRangedAttack': { + 'name': "Split "Melee or Ranged Attack" Actions", + 'help': "If enabled, the importer will create two sheet items per \"Melee or Ranged Attack\" action, each with the appropriate range set.", + 'default': true, + 'type': "boolean", + 'compatibilityModeValues': { + [UtilCompat.MODULE_PLUTONIUM_ADDON_AUTOMATION]: true + } + }, + 'isSplitConditionalDamageAttack': { + 'name': "Split Conditional Damage Actions", + 'help': "If enabled, the importer will create two sheet items (\"Base\" and \"Full\") per \"... plus damage if \" action, where the \"base\" item does not include the conditional damage, and the \"full\" item does include the conditional damage.", + 'default': true, + 'type': "boolean", + 'compatibilityModeValues': { + [UtilCompat.MODULE_PLUTONIUM_ADDON_AUTOMATION]: true + } + }, + 'isPreferFlatSavingThrows': { + 'name': "Prefer Flat Saving Throws", + 'help': "If enabled, a saving throw for a sheet item will always have \"flat\" scaling, with the flat DC value set to match the number in the creature's stat block. If disabled, a sheet item's saving throw scaling may be set as an ability score, provided that doing so produces the same value for the DC as is listed in the creature's stat block.", + 'default': false, + 'type': "boolean" + } + } + }, + 'importVehicle': { + 'name': "Import (Vehicles)", + 'settings': { + /* 'ownership': ConfigConsts._template_getEntityOwnership("The default (i.e. used for all players unless a player-specific ownership level is set) ownership for an imported vehicle."), + ...ConfigConsts._template_getTokenSettings({ + 'actorType': "vehicle" + }), */ + 'isMetricDistance': { + 'name': "Convert Speeds to Metric", + 'help': "Whether or not vehicle speed units should be converted to an approximate metric equivalent (" + ConfigConsts._DISP_METRIC_FEET + "; " + ConfigConsts._DISP_METRIC_MILES + ').', + 'default': false, + 'type': "boolean" + }, + 'isImportBio': { + 'name': "Import Fluff to Description", + 'help': "If enabled, any fluff text which is available for a vehicle will be imported into that vehicle's description.", + 'default': true, + 'type': 'boolean' + }, + 'isImportBioImages': { + 'name': "Include Fluff Image in Description", + 'help': "If enabled, any fluff image which is available for a vehicle will be imported into that vehicle's description.", + 'default': false, + 'type': "boolean" + } + }, + 'settingsAdvanced': { + 'additionalDataCompendium': { + 'name': "Additional Data Compendiums", + 'help': "A comma-separated list of compendiums that the vehicle importer will attempt to pull additional data (including art) from rather than use the default Plutonium icons.", + 'default': '', + 'type': "string", + 'typeSub': "compendiums", + 'additionalStyleClasses': "code", + 'isNullable': true + }, + 'isUseTokenImageAsPortrait': { + 'name': "Use Token Image as Portrait", + 'help': "If enabled, a vehicle's token image will be preferred over its portrait image when populating its sheet portrait during import.", + 'default': false, + 'type': 'boolean' + }, + ...ConfigConsts._template_getActorImportOverwriteSettings(), + ...ConfigConsts._template_getActiveEffectsDisabledTransferSettings({ + 'name': 'vehicles' + }) + } + }, + 'importVehicleUpgrade': { + 'name': "Import (Vehicle Upgrades)", + 'settings': { + /* 'ownership': ConfigConsts._template_getEntityOwnership("The default (i.e. used for all players unless a player-specific ownership level is set) ownership for an imported vehicle upgrades."), + ...ConfigConsts._template_getTargetTemplatePrompt({ + 'namePlural': "vehicle upgrades" + }), */ + 'isMetricDistance': { + 'name': "Convert Speeds to Metric", + 'help': "Whether or not vehicle upgrade speed units should be converted to an approximate metric equivalent (" + ConfigConsts._DISP_METRIC_FEET + ').', + 'default': false, + 'type': 'boolean' + } + }, + 'settingsAdvanced': { + ...ConfigConsts._template_getActiveEffectsDisabledTransferSettings({ + 'name': "vehicle upgrades" + }), + 'isImportDescription': { + 'name': "Import Text as Description", + 'help': "If enabled, a vehicle upgrade's text will be imported as item description.", + 'default': true, + 'type': "boolean" + } + } + }, + 'importObject': { + 'name': "Import (Objects)", + 'settings': { + /* 'ownership': ConfigConsts._template_getEntityOwnership("The default (i.e. used for all players unless a player-specific ownership level is set) ownership for an imported object."), + ...ConfigConsts._template_getTokenSettings({ + 'actorType': 'vehicle' + }), */ + 'isMetricDistance': { + 'name': "Convert Speeds to Metric", + 'help': "Whether or not object speed units should be converted to an approximate metric equivalent (" + ConfigConsts._DISP_METRIC_FEET + ').', + 'default': false, + 'type': "boolean" + }, + 'isImportBio': { + 'name': "Import Fluff to Description", + 'help': "If enabled, any fluff text which is available for an object will be imported into that object's description.", + 'default': true, + 'type': "boolean" + }, + 'isImportBioImages': { + 'name': "Include Fluff Image in Description", + 'help': "If enabled, any fluff image which is available for an object will be imported into that object's description.", + 'default': false, + 'type': "boolean" + } + }, + 'settingsAdvanced': { + 'isUseTokenImageAsPortrait': { + 'name': "Use Token Image as Portrait", + 'help': "If enabled, an object's token image will be preferred over its portrait image when populating its sheet portrait during import.", + 'default': false, + 'type': "boolean" + }, + ...ConfigConsts._template_getActorImportOverwriteSettings() + } + }, + 'importObjectFeature': { + 'name': "Import (Object Features)", + 'settings': { + /* 'ownership': ConfigConsts._template_getEntityOwnership("The default (i.e. used for all players unless a player-specific ownership level is set) ownership for an imported object feature."), + ...ConfigConsts._template_getTargetTemplatePrompt({ + 'namePlural': "object features" + }), */ + 'isMetricDistance': { + 'name': "Convert Ranges to Metric", + 'help': "Whether or not object feature range units should be converted to an approximate metric equivalent (" + ConfigConsts._DISP_METRIC_FEET + ').', + 'default': false, + 'type': 'boolean' + } + }, + 'settingsAdvanced': { + ...ConfigConsts._template_getActiveEffectsDisabledTransferSettings({ + 'name': "object features" + }) + } + }, + 'importFeat': { + 'name': "Import (Feats)", + 'settings': { + /* 'ownership': ConfigConsts._template_getEntityOwnership("The default (i.e. used for all players unless a player-specific ownership level is set) ownership for an imported feat."), + ...ConfigConsts._template_getTargetTemplatePrompt({ + 'namePlural': 'feats' + }), */ + 'isMetricDistance': { + 'name': "Convert Speeds to Metric", + 'help': "Whether or not feat speed units should be converted to an approximate metric equivalent (" + ConfigConsts._DISP_METRIC_FEET + ').', + 'default': false, + 'type': "boolean" + } + }, + 'settingsAdvanced': { + ...ConfigConsts._template_getActiveEffectsDisabledTransferSettings({ + 'name': "feats" + }), + 'isImportDescription': { + 'name': "Import Text as Description", + 'help': "If enabled, a feat's text will be imported as item description.", + 'default': true, + 'type': "boolean" + } + } + }, + 'importBackground': { + 'name': "Import (Backgrounds)", + 'settings': { + //'ownership': ConfigConsts._template_getEntityOwnership("The default (i.e. used for all players unless a player-specific ownership level is set) ownership for an imported background.") + }, + 'settingsAdvanced': { + 'additionalDataCompendium': { + 'name': "Additional Data Compendiums (Backgrounds)", + 'help': "A comma-separated list of compendiums that the background importer will attempt to pull additional data (including art) from rather than use the default Plutonium icons.", + 'default': ConfigConsts.SRD_COMPENDIUMS_BACKGROUNDS_AND_FEATURES.join(", "), + 'type': "string", + 'typeSub': 'compendiums', + 'additionalStyleClasses': "code", + 'isNullable': true + }, + 'additionalDataCompendiumFeatures': { + 'name': "Additional Data Compendiums (Features)", + 'help': "A comma-separated list of compendiums that the background importer will attempt to pull additional data (including art) from rather than use the default Plutonium icons.", + 'default': ConfigConsts.SRD_COMPENDIUMS_BACKGROUNDS_AND_FEATURES.join(", "), + 'type': "string", + 'typeSub': "compendiums", + 'additionalStyleClasses': "code", + 'isNullable': true + }, + ...ConfigConsts._template_getActiveEffectsDisabledTransferSettings({ + 'name': "backgrounds" + }), + 'isImportDescription': { + 'name': "Import Text as Description", + 'help': "If enabled, a background's text will be imported as item description.", + 'default': true, + 'type': 'boolean' + } + } + }, + 'importBackgroundFeature': { + 'name': "Import (Background Features)", + 'settings': { + /* 'ownership': ConfigConsts._template_getEntityOwnership("The default (i.e. used for all players unless a player-specific ownership level is set) ownership for an imported background feature."), + ...ConfigConsts._template_getTargetTemplatePrompt({ + 'namePlural': "background features" + }) */ + } + }, + 'importClass': { + 'name': "Import (Classes & Subclasses)", + 'settings': { + //'ownership': ConfigConsts._template_getEntityOwnership("The default (i.e. used for all players unless a player-specific ownership level is set) ownership for an imported class or subclass."), + 'isAddUnarmedStrike': { + 'name': "Add Unarmed Strike", + 'help': "If enabled, importing a class to an actor will create an \"Unarmed Strike\" weapon, unless one already exists.", + 'default': false, + 'type': "boolean", + 'isPlayerEditable': true + }, + 'isImportClassTable': { + 'name': "Import Class Table to Description", + 'help': "If enabled, a class's table will be imported as part of the class item's description.", + 'default': true, + 'type': 'boolean', + 'isPlayerEditable': true + }, + 'isAddLevelUpButton': { + 'name': "Add "Level Up" Button to Character Sheets", + 'help': "If enabled, a \"Level Up\" button will be displayed in the top-right corner of a character's sheet (assuming the default dnd5e sheet is used).", + 'default': true, + 'type': 'boolean', + 'isPlayerEditable': true + }, + 'isSetXp': { + 'name': "Set Minimum Actor XP on Class Import", + 'help': "If enabled, during class import, actor XP will be set to the minimum XP value required for the actor's new level, if the actor's current XP is insufficient for them to reach their new level.", + 'default': false, + 'type': "boolean", + 'isPlayerEditable': true + }, + 'hpIncreaseMode': { + 'name': "Hit Points Increase Mode", + 'help': "Determines how Hit Points are calculated when using the Class Importer to level up. If left unspecified, a user will be prompted to choose the mode each time their Hit Points are increased by the Class Importer.", + 'type': "enum", + 'values': [{ + 'value': ConfigConsts.C_IMPORT_CLASS_HP_INCREASE_MODE__TAKE_AVERAGE, + 'name': ConfigConsts.C_IMPORT_CLASS_HP_INCREASE_MODE___NAMES[ConfigConsts.C_IMPORT_CLASS_HP_INCREASE_MODE__TAKE_AVERAGE] + }, { + 'value': ConfigConsts.C_IMPORT_CLASS_HP_INCREASE_MODE__MIN, + 'name': ConfigConsts.C_IMPORT_CLASS_HP_INCREASE_MODE___NAMES[ConfigConsts.C_IMPORT_CLASS_HP_INCREASE_MODE__MIN] + }, { + 'value': ConfigConsts.C_IMPORT_CLASS_HP_INCREASE_MODE__MAX, + 'name': ConfigConsts.C_IMPORT_CLASS_HP_INCREASE_MODE___NAMES[ConfigConsts.C_IMPORT_CLASS_HP_INCREASE_MODE__MAX] + }, { + 'value': ConfigConsts.C_IMPORT_CLASS_HP_INCREASE_MODE__ROLL, + 'name': ConfigConsts.C_IMPORT_CLASS_HP_INCREASE_MODE___NAMES[ConfigConsts.C_IMPORT_CLASS_HP_INCREASE_MODE__ROLL] + }, { + 'value': ConfigConsts.C_IMPORT_CLASS_HP_INCREASE_MODE__ROLL_CUSTOM, + 'name': ConfigConsts.C_IMPORT_CLASS_HP_INCREASE_MODE___NAMES[ConfigConsts.C_IMPORT_CLASS_HP_INCREASE_MODE__ROLL_CUSTOM] + }, { + 'value': ConfigConsts.C_IMPORT_CLASS_HP_INCREASE_MODE__DO_NOT_INCREASE, + 'name': ConfigConsts.C_IMPORT_CLASS_HP_INCREASE_MODE___NAMES[ConfigConsts.C_IMPORT_CLASS_HP_INCREASE_MODE__DO_NOT_INCREASE] + }], + 'default': null, + 'isNullable': true + }, + 'hpIncreaseModeCustomRollFormula': { + 'name': "Hit Points Increase Custom Roll Formula", + 'help': "A custom roll formula to be used when gaining HP on level up. Used if either the \"Hit Points Increase Mode\" option is set to \"" + ConfigConsts.C_IMPORT_CLASS_HP_INCREASE_MODE___NAMES[ConfigConsts.C_IMPORT_CLASS_HP_INCREASE_MODE__ROLL_CUSTOM] + "\", or if a player chooses \"" + ConfigConsts.C_IMPORT_CLASS_HP_INCREASE_MODE___NAMES[ConfigConsts.C_IMPORT_CLASS_HP_INCREASE_MODE__ROLL_CUSTOM] + "\" when prompted to select their Hit Points Increase Mode. Use \"@hd.faces\" for the type of dice (i.e., the \"8\" in \"1d8\"), and \"@hd.number\" for \"number of dice\" (i.e., the \"1\" in \"1d8\"). Note that backticks (`) around an expression will also be replaced so \"`@hd.number`d`@hd.faces`\" will produce e.g. \"1d8\", should you need to avoid using brackets.", + 'placeholder': "(@hd.number)d(@hd.faces)", + 'type': "string", + 'additionalStyleClasses': "code", + 'default': null, + 'isNullable': true + } + }, + 'settingsAdvanced': { + 'isDisplayOnLevelZeroCharacters': { + 'name': "Display "Level Up" Button on New Characters", + 'help': "If enabled, the \"Level Up\" button will be displayed on character actors with no levels.", + 'default': true, + 'type': "boolean", + 'isPlayerEditable': true + }, + 'isLevelUpButtonDisabledUntilEnoughExperience': { + 'name': "Disable the "Level Up" Button Until Character Has Enough XP", + 'help': "If enabled, the \"Level Up\" button will be disabled (though still visible) on characters who do not have sufficient XP to level up.", + 'default': true, + 'type': 'boolean' + }, + 'isLegacyLevelUpButton': { + 'name': "Prefer legacy "Level Up" Button", + 'help': "If disabled, the \"Level Up\" button will attempt to open the Charactermancer, a Patron-only feature which requires you to log in. If enabled, a dialogue of options will be presented, via which the Class Importer can be directly invoked.", + 'default': true, + 'type': "boolean" + }, + 'additionalDataCompendiumClasses': { + 'name': "Additional Data Compendiums (Classes)", + 'help': "A comma-separated list of compendiums that the class importer will attempt to pull additional data (including art) from rather than use the default Plutonium icons.", + 'default': ConfigConsts.SRD_COMPENDIUMS_CLASSES.join(", "), + 'type': "string", + 'typeSub': "compendiums", + 'additionalStyleClasses': "code", + 'isNullable': true + }, + 'additionalDataCompendiumSubclasses': { + 'name': "Additional Data Compendiums (Subclasses)", + 'help': "A comma-separated list of compendiums that the class importer will attempt to pull additional data (including art) from rather than use the default Plutonium icons.", + 'default': ConfigConsts.SRD_COMPENDIUMS_SUBCLASSES.join(", "), + 'type': "string", + 'typeSub': "compendiums", + 'additionalStyleClasses': "code", + 'isNullable': true + }, + 'additionalDataCompendiumFeatures': { + 'name': "Additional Data Compendiums (Features)", + 'help': "A comma-separated list of compendiums that the class importer will attempt to pull additional data (including art) from rather than use the default Plutonium icons.", + 'default': ConfigConsts.SRD_COMPENDIUMS_CLASS_FEATURES.join(", "), + 'type': "string", + 'typeSub': 'compendiums', + 'additionalStyleClasses': 'code', + 'isNullable': true + }, + ...ConfigConsts._template_getActiveEffectsDisabledTransferSettings({ + 'name': "class" + }), + 'isImportDescription': { + 'name': "Import Text as Description", + 'help': "If enabled, a class's text will be imported as item description.", + 'default': true, + 'type': "boolean" + }, + 'isUseDefaultSubclassImage': { + 'name': "Subclass Default Image Fallback", + 'help': "If enabled, when importing a subclass which has no well-defined image, use a default image based on class. If disabled, a generic black and white image will be used as a fallback instead.", + 'default': true, + 'type': "boolean", + 'isPlayerEditable': true + }, + 'isHideSubclassRows': { + 'name': "Hide Subclasses in Class Importer", + 'help': "If enabled, the class/subclass list in the Class Importer will only show classes.", + 'default': false, + 'type': 'boolean', + 'isPlayerEditable': true + } + } + }, + 'importClassSubclassFeature': { + 'name': "Import (Class & Sub. Features)", + 'help': "Import (Class & Subclass Features)", + 'settings': { + /* 'ownership': ConfigConsts._template_getEntityOwnership("The default (i.e. used for all players unless a player-specific ownership level is set) ownership for an imported class/subclass feature."), + ...ConfigConsts._template_getTargetTemplatePrompt({ + 'namePlural': "class/subclass features" + }), */ + 'isMetricDistance': { + 'name': "Convert Speeds to Metric", + 'help': "Whether or not class/subclass feature speed units should be converted to an approximate metric equivalent (" + ConfigConsts._DISP_METRIC_FEET + ').', + 'default': false, + 'type': 'boolean' + } + }, + 'settingsAdvanced': { + ...ConfigConsts._template_getActiveEffectsDisabledTransferSettings({ + 'name': "class features" + }), + 'isImportDescription': { + 'name': "Import Text as Description", + 'help': "If enabled, a class/subclass feature's text will be imported as item description.", + 'default': true, + 'type': 'boolean' + } + } + }, + 'importItem': { + 'name': "Import (Items)", + 'settings': { + //'ownership': ConfigConsts._template_getEntityOwnership("The default (i.e. used for all players unless a player-specific ownership level is set) ownership for an imported item."), + 'isAddActiveEffects': { + 'name': "Populate Active Effects", + 'help': "If items should have active effects created during import.", + 'default': true, + 'type': 'boolean' + }, + 'isMetricDistance': { + 'name': "Convert Ranges to Metric", + 'help': "Whether or not item range units should be converted to an approximate metric equivalent (" + ConfigConsts._DISP_METRIC_FEET + ').', + 'default': false, + 'type': "boolean" + }, + 'isMetricWeight': { + 'name': "Convert Item Weights to Metric", + 'help': "Whether or not item weight units should be converted to an approximate metric equivalent (" + ConfigConsts._DISP_METRIC_POUNDS + ').', + 'default': false, + 'type': 'boolean' + }, + 'inventoryStackingMode': { + 'name': "Inventory Stacking Mode", + 'help': "If imported items should \"stack\" with existing items when imported to an actor's inventory. If stacking is allowed, the importer will check for an existing item when importing an item to an actor's sheet. If the item already exists, the importer will increase the quantity of that item in the actor's inventory, rather than create a new copy of the item in the actor's inventory.", + 'default': ConfigConsts.C_ITEM_ATTUNEMENT_SMART, + 'type': "enum", + 'values': [{ + 'value': ConfigConsts.C_ITEM_ATTUNEMENT_NEVER, + 'name': "Never Stack" + }, { + 'value': ConfigConsts.C_ITEM_ATTUNEMENT_SMART, + 'name': "Sometimes Stack (e.g. consumables, throwables)" + }, { + 'value': ConfigConsts.C_ITEM_ATTUNEMENT_ALWAYS, + 'name': "Always Stack" + }] + }, + 'isSplitPacksActor': { + 'name': "Import Packs to Actors as Constituent Items", + 'help': "If \"pack\" items (explorer's pack, dungeoneer's pack) should be broken down and imported as their constituent items when importing to an actor's items.", + 'default': true, + 'type': "boolean", + 'isPlayerEditable': true + }, + 'isSplitAtomicPacksActor': { + 'name': "Import Item Stacks to Actors as Constituent Items", + 'help': "If an item which is formed of multiple constituent items of the same type, such as \"Bag of Ball Bearings (1,000)\", should be split up into its constituent items (a \"Ball Bearing\" item with its sheet quantity set to 1,000, in this example).", + 'default': false, + 'type': "boolean", + 'isPlayerEditable': true + }, + 'throwables': { + 'name': "Throwing Items", + 'help': "A list of items which are imported with their usage set to deplete their own quantity when used.", + 'default': ["Handaxe", "Javelin", "Light Hammer", "Dart", 'Net'], + 'type': 'arrayStringShort', + 'isPlayerEditable': true + }, + 'altAbilityScoreByClass': { + 'name': "Alt Ability Scores by Class", + 'help': "A list of -- mappings, an entry in which, when importing an item, will change the default ability score used by an item for a member of that class.", + 'default': ['monk:club:dex', "monk:dagger:dex", 'monk:handaxe:dex', "monk:javelin:dex", "monk:light hammer:dex", "monk:mace:dex", "monk:quarterstaff:dex", 'monk:shortsword:dex', 'monk:sickle:dex', "monk:spear:dex"], + 'type': "arrayStringShort", + 'isPlayerEditable': true + }, + 'attunementType': { + 'name': "Attunement when Importing to Directory/Compendium", + 'help': "The attunement type to use when importing an item which can be attuned.", + 'default': ConfigConsts.C_ITEM_ATTUNEMENT_REQUIRED, + 'type': 'enum', + 'values': [{ + 'value': ConfigConsts.C_ITEM_ATTUNEMENT_NONE, + 'name': "None" + }, { + 'value': ConfigConsts.C_ITEM_ATTUNEMENT_REQUIRED, + 'name': "Attunement required" + }, { + 'value': ConfigConsts.C_ITEM_ATTUNEMENT_ATTUNED, + 'name': "Attuned" + }] + }, + 'attunementTypeActor': { + 'name': "Attunement when Importing to Actors", + 'help': "The attunement type to use when importing an item which can be attuned.", + 'default': ConfigConsts.C_ITEM_ATTUNEMENT_ATTUNED, + 'type': "enum", + 'values': [{ + 'value': ConfigConsts.C_ITEM_ATTUNEMENT_NONE, + 'name': "None" + }, { + 'value': ConfigConsts.C_ITEM_ATTUNEMENT_REQUIRED, + 'name': "Attunement required" + }, { + 'value': ConfigConsts.C_ITEM_ATTUNEMENT_ATTUNED, + 'name': "Attuned" + }] + }, + 'isImportDescriptionHeader': { + 'name': "Include Damage, Properties, Rarity, and Attunement in Description", + 'help': "If enabled, an imported item's description will include text generated from its rarity, attunement requirements, damage, and other properties.", + 'default': false, + 'type': 'boolean', + 'isPlayerEditable': true + }, + 'isUseOtherFormulaFieldForExtraDamage': { + 'name': "Treat Extra Damage as "Other Formula"", + 'help': "This moves extra damage rolls to the \"Other Formula\" dice field, which can improve compatibility with some modules.", + 'default': false, + 'type': "boolean", + 'compatibilityModeValues': { + [UtilCompat.MODULE_PLUTONIUM_ADDON_AUTOMATION]: true + } + } + }, + 'settingsAdvanced': { + 'additionalDataCompendium': { + 'name': "Additional Data Compendiums", + 'help': "A comma-separated list of compendiums that the Item Importer will attempt to pull additional data (including art) from rather than use the default Plutonium icons.", + 'default': ConfigConsts.SRD_COMPENDIUMS_ITEMS.join(", "), + 'type': "string", + 'typeSub': 'compendiums', + 'additionalStyleClasses': 'code', + 'isNullable': true + }, + 'replacementDataCompendium': { + 'name': "Replacement Data Compendiums", + 'help': "A comma-separated list of compendiums that the Item Importer will attempt to pull items from, rather than using the data Plutonium would otherwise generate. This is useful when the Item Importer is used by other importers, e.g. when the Creature Importer is adding items to newly-created actors.", + 'default': '', + 'type': "string", + 'typeSub': "compendiums", + 'additionalStyleClasses': "code", + 'isNullable': true + }, + ...ConfigConsts._template_getActiveEffectsDisabledTransferSettings({ + 'name': "items" + }), + 'isImportDescription': { + 'name': "Import Text as Description", + 'help': "If enabled, an item's text will be imported as item description.", + 'default': true, + 'type': 'boolean' + } + } + }, + 'importPsionic': { + 'name': "Import (Psionics)", + 'settings': { + //'ownership': ConfigConsts._template_getEntityOwnership("The default (i.e. used for all players unless a player-specific ownership level is set) ownership for an imported psionic."), + 'psiPointsResource': { + 'name': "Psi Points Resource", + 'help': "The resource consumed by psionics.", + 'default': "resources.primary", + 'type': "enum", + 'values': [{ + 'value': "resources.primary" + }, { + 'value': 'resources.secondary' + }, { + 'value': "resources.tertiary" + }, { + 'value': ConfigConsts.C_SPELL_POINTS_RESOURCE__SHEET_ITEM, + 'name': "\"Psi Points\" sheet item" + }, { + 'value': ConfigConsts.C_SPELL_POINTS_RESOURCE__ATTRIBUTE_CUSTOM, + 'name': "Custom (see below)" + }], + 'isPlayerEditable': true + }, + 'psiPointsResourceCustom': { + 'name': "Psi Points Custom Resource", + 'help': "The name of the custom resource to use if \"Custom\" is selected for \"Psi Points Resource\", above. This supports modules that expand the number of available sheet resources, such as \"5e-Sheet Resources Plus\" (which adds e.g. \"resources.fourth\", \"resources.fifth\", ...).", + 'type': 'string', + 'additionalStyleClasses': 'code', + 'default': null, + 'isNullable': true, + 'isPlayerEditable': true + }, + 'isImportAsSpell': { + 'name': "Import as Spells", + 'help': "If enabled, psionics will be imported as spells, rather than features.", + 'default': false, + 'type': "boolean" + }, + ...ConfigConsts._template_getTargetTemplatePrompt({ + 'namePlural': "psionics" + }) + }, + 'settingsAdvanced': { + ...ConfigConsts._template_getActiveEffectsDisabledTransferSettings({ + 'name': 'psionic' + }), + 'isImportDescription': { + 'name': "Import Text as Description", + 'help': "If enabled, a psionic's text will be imported as item description.", + 'default': true, + 'type': 'boolean' + } + } + }, + 'importRace': { + 'name': "Import (Races)", + 'settings': { + //'ownership': ConfigConsts._template_getEntityOwnership("The default (i.e. used for all players unless a player-specific ownership level is set) ownership for an imported race."), + /* ...ConfigConsts._template_getTokenSettings({ + 'actorType': "character" + }), */ + 'isMetricDistance': { + 'name': "Convert Speeds to Metric", + 'help': "Whether or not race speed units should be converted to an approximate metric equivalent (" + ConfigConsts._DISP_METRIC_FEET + ').', + 'default': false, + 'type': 'boolean' + } + }, + 'settingsAdvanced': { + 'additionalDataCompendium': { + 'name': "Additional Data Compendiums", + 'help': "A comma-separated list of compendiums that the race importer will attempt to pull additional data (including art) from rather than use the default Plutonium icons.", + 'default': ConfigConsts.SRD_COMPENDIUMS_RACES_AND_FEATURES.join(", "), + 'type': "string", + 'typeSub': "compendiums", + 'additionalStyleClasses': "code", + 'isNullable': true + }, + ...ConfigConsts._template_getActiveEffectsDisabledTransferSettings({ + 'name': "races" + }), + 'isImportDescription': { + 'name': "Import Text as Description", + 'help': "If enabled, a race's text will be imported as item description.", + 'default': true, + 'type': "boolean" + } + } + }, + 'importRaceFeature': { + 'name': "Import (Race Features)", + 'settings': { + /* 'ownership': ConfigConsts._template_getEntityOwnership("The default (i.e. used for all players unless a player-specific ownership level is set) ownership for an imported race feature."), + ...ConfigConsts._template_getTargetTemplatePrompt({ + 'namePlural': "race features" + }) */ + }, + 'settingsAdvanced': { + 'additionalDataCompendiumFeatures': { + 'name': "Additional Data Compendiums", + 'help': "A comma-separated list of compendiums that the race feature importer will attempt to pull additional data (including art) from rather than use the default Plutonium icons.", + 'default': ConfigConsts.SRD_COMPENDIUMS_RACES_AND_FEATURES.join(", "), + 'type': "string", + 'typeSub': 'compendiums', + 'additionalStyleClasses': 'code', + 'isNullable': true + }, + ...ConfigConsts._template_getActiveEffectsDisabledTransferSettings({ + 'name': "race features" + }) + } + }, + 'importTable': { + 'name': "Import (Table)", + 'settings': { + //'ownership': ConfigConsts._template_getEntityOwnership("The default (i.e. used for all players unless a player-specific ownership level is set) ownership for an imported table.") + }, + 'settingsAdvanced': { + 'additionalDataCompendium': { + 'name': "Additional Data Compendiums", + 'help': "A comma-separated list of compendiums that the Table Importer will attempt to pull additional data (including art) from rather than use the default Plutonium icons.", + 'default': ConfigConsts.SRD_COMPENDIUMS_TABLES.join(", "), + 'type': "string", + 'typeSub': "compendiums", + 'additionalStyleClasses': 'code', + 'isNullable': true + } + } + }, + 'importSpell': { + 'name': "Import (Spells)", + 'settings': { + //'ownership': ConfigConsts._template_getEntityOwnership("The default (i.e. used for all players unless a player-specific ownership level is set) ownership for an imported spell."), + 'prepareActorSpells': { + 'name': "Prepare Actor Spells", + 'help': "Whether or not spells that are imported to actor sheets should be prepared by default.", + 'default': true, + 'type': "boolean", + 'isPlayerEditable': true + }, + 'prepareSpellItems': { + 'name': "Prepare Spell Items", + 'help': "Whether or not spells that are imported to the items directory should be prepared by default.", + 'default': false, + 'type': 'boolean' + }, + 'actorSpellPreparationMode': { + 'name': "Actor Spell Preparation Mode", + 'help': "The default spell preparation mode for spells imported to actor sheets.", + 'default': "prepared", + 'type': "enum", + 'values': [{ + 'value': '', + 'name': "(None)" + }, { + 'value': "always", + 'name': "Always Prepared" + }, { + 'value': "prepared", + 'name': "Prepared" + }, { + 'value': "innate", + 'name': "Innate Spellcasting" + }, { + 'value': "pact", + 'name': "Pact Magic" + }], + 'isPlayerEditable': true + }, + 'isAutoDetectActorSpellPreparationMode': { + 'name': "Auto-Detect Actor Spell Preparation Mode", + 'help': "If enabled, the default spell preparation mode for spells imported to actor sheets (as defined by \"Actor Spell Preparation Mode\") may be automatically overridden, e.g. \"pact magic\" is automatically used when importing to a warlock.", + 'default': true, + 'type': "boolean", + 'isPlayerEditable': true + }, + 'spellItemPreparationMode': { + 'name': "Spell Item Preparation Mode", + 'help': "The default spell preparation mode for spells imported to the items directory.", + 'default': "prepared", + 'type': "enum", + 'values': [{ + 'value': '', + 'name': '(None)' + }, { + 'value': "always", + 'name': "Always Prepared" + }, { + 'value': "prepared", + 'name': "Prepared" + }, { + 'value': 'innate', + 'name': "Innate Spellcasting" + }, { + 'value': "pact", + 'name': "Pact Magic" + }] + }, + 'spellPointsMode': { + 'name': "Use Spell Points", + 'help': "If enabled, imported spells which would use spell slots will instead be marked as \"at will\" and set to consume an a sheet or feature resource. (The \"Spell Points\" variant rule can be found in the DMG, page 288.)", + 'default': ConfigConsts.C_SPELL_POINTS_MODE__DISABLED, + 'type': 'enum', + 'values': [{ + 'name': "Disabled", + 'value': ConfigConsts.C_SPELL_POINTS_MODE__DISABLED + }, { + 'name': 'Enabled', + 'value': ConfigConsts.C_SPELL_POINTS_MODE__ENABLED + }, { + 'name': "Enabled, and Use 99 Slots", + 'value': ConfigConsts.C_SPELL_POINTS_MODE__ENABLED_AND_UNLIMITED_SLOTS, + 'help': "If enabled, an imported spells will retain its \"Spell Preparation Mode\" in addition to consuming a \"Spell Points\" sheet/feature resource. This improves compatibility with many sheets and modules. To allow \"unlimited\" spellcasting at each spell level, a character's spell slots for each level will be set to 99." + }], + 'isPlayerEditable': true + }, + 'spellPointsResource': { + 'name': "Spell Points Resource", + 'help': "The resource consumed by spells imported with \"Use Spell Points\" enabled.", + 'default': "resources.primary.value", + 'type': "enum", + 'values': [{ + 'value': "resources.primary" + }, { + 'value': 'resources.secondary' + }, { + 'value': 'resources.tertiary' + }, { + 'value': ConfigConsts.C_SPELL_POINTS_RESOURCE__SHEET_ITEM, + 'name': "\"Spell Points\" sheet item" + }, { + 'value': ConfigConsts.C_SPELL_POINTS_RESOURCE__ATTRIBUTE_CUSTOM, + 'name': "Custom (see below)" + }], + 'isPlayerEditable': true + }, + 'spellPointsResourceCustom': { + 'name': "Spell Points Custom Resource", + 'help': "The name of the custom resource to use if \"Custom\" is selected for \"Spell Points Resource\", above. This supports modules that expand the number of available sheet resources, such as \"5e-Sheet Resources Plus\" (which adds e.g. \"resources.fourth\", \"resources.fifth\", ...).", + 'type': 'string', + 'additionalStyleClasses': "code", + 'default': null, + 'isNullable': true, + 'isPlayerEditable': true + }, + 'isIncludeClassesInDescription': { + 'name': "Include Caster Classes in Spell Description", + 'help': "If enabled, an imported spell's description will include the list of classes which have the spell on their spell list.", + 'default': false, + 'type': "boolean" + }, + ...ConfigConsts._template_getTargetTemplatePrompt({ + 'namePlural': 'spells' + }), + 'isMetricDistance': { + 'name': "Convert Ranges and Areas to Metric", + 'help': "Whether or not spell range/area units should be converted to an approximate metric equivalent (" + ConfigConsts._DISP_METRIC_FEET + "; " + ConfigConsts._DISP_METRIC_MILES + ').', + 'default': false, + 'type': "boolean" + }, + 'isFilterOnOpen': { + 'name': "Apply Class Filter when Opening on Actor", + 'help': "If enabled, and the importer is opened from an actor, the spell list will be filtered according to that actor's current class(es).", + 'default': true, + 'type': "boolean", + 'isPlayerEditable': true + } + }, + 'settingsAdvanced': { + 'additionalDataCompendium': { + 'name': "Additional Data Compendiums", + 'help': "A comma-separated list of compendiums that the Spell Importer will attempt to pull additional data (including art) from rather than use the default Plutonium icons.", + 'default': ConfigConsts.SRD_COMPENDIUMS_SPELLS.join(", "), + 'type': 'string', + 'typeSub': "compendiums", + 'additionalStyleClasses': "code", + 'isNullable': true + }, + 'replacementDataCompendium': { + 'name': "Replacement Data Compendiums", + 'help': "A comma-separated list of compendiums that the Spell Importer will attempt to pull spells from, rather than using the data Plutonium would otherwise generate. This is useful when the Spell Importer is used by other importers, e.g. when the Creature Importer is adding spells to newly-created actors.", + 'default': '', + 'type': "string", + 'typeSub': "compendiums", + 'additionalStyleClasses': "code", + 'isNullable': true + }, + ...ConfigConsts._template_getActiveEffectsDisabledTransferSettings({ + 'name': 'spells' + }), + 'isImportDescription': { + 'name': "Import Text as Description", + 'help': "If enabled, a spell's text will be imported as item description.", + 'default': true, + 'type': 'boolean' + }, + 'isUseCustomSrdIcons': { + 'name': "Use Custom Icons for SRD Spells", + 'help': "If enabled, imported SRD spells will use an alternate icon set, as curated by the community.", + 'default': true, + 'type': 'boolean', + 'isPlayerEditable': true + }, + 'isUseDefaultSchoolImage': { + 'name': "School Default Image Fallback", + 'help': "If enabled, when importing a spell which has no well-defined image, use a default image based on the school of the spell. If disabled, a generic black and white image will be used as a fallback instead.", + 'default': true, + 'type': "boolean", + 'isPlayerEditable': true + }, + 'spellPointsModeNpc': { + 'name': "Use Spell Points (NPCs)", + 'help': "If enabled, a spell imported to an NPC which would use spell slots will instead be marked as \"at will\" and set to consume an a sheet or feature resource. (The \"Spell Points\" variant rule can be found in the DMG, page 288.)", + 'default': ConfigConsts.C_SPELL_POINTS_MODE__DISABLED, + 'type': "enum", + 'values': [{ + 'name': "Disabled", + 'value': ConfigConsts.C_SPELL_POINTS_MODE__DISABLED + }, { + 'name': "Enabled", + 'value': ConfigConsts.C_SPELL_POINTS_MODE__ENABLED + }, { + 'name': "Enabled, but Use 99 Slots", + 'value': ConfigConsts.C_SPELL_POINTS_MODE__ENABLED_AND_UNLIMITED_SLOTS, + 'help': "If enabled, imported spells will retain their \"prepared\"/etc. types in addition to consuming a \"Spell Points\" sheet/feature resource. This allows easier organisation of spells, and better compatibility with many modules. To allow \"unlimited\" spellcasting at each spell level, a character's spell slots for each level will be set to 99." + }] + } + } + }, + 'importRule': { + 'name': "Import (Rules)", + 'settings': { + //'ownership': ConfigConsts._template_getEntityOwnership("The default (i.e. used for all players unless a player-specific ownership level is set) ownership for an imported rule.") + } + }, + 'importLanguage': { + 'name': "Import (Languages)", + 'settings': { + //'ownership': ConfigConsts._template_getEntityOwnership("The default (i.e. used for all players unless a player-specific ownership level is set) ownership for an imported language.") + } + }, + 'importOptionalFeature': { + 'name': "Import (Options & Features)", + 'settings': { + //'ownership': ConfigConsts._template_getEntityOwnership("The default (i.e. used for all players unless a player-specific ownership level is set) ownership for an imported option/feature."), + ...ConfigConsts._template_getTargetTemplatePrompt({ + 'namePlural': "optional features" + }), + 'isMetricDistance': { + 'name': "Convert Speeds to Metric", + 'help': "Whether or not optional feature speed units should be converted to an approximate metric equivalent (" + ConfigConsts._DISP_METRIC_FEET + ').', + 'default': false, + 'type': "boolean" + } + }, + 'settingsAdvanced': { + 'additionalDataCompendium': { + 'name': "Additional Data Compendiums", + 'help': "A comma-separated list of compendiums that the optional feature importer will attempt to pull additional data (including art) from rather than use the default Plutonium icons.", + 'default': ConfigConsts.SRD_COMPENDIUMS_OPTIONAL_FEATURES.join(", "), + 'type': "string", + 'typeSub': "compendiums", + 'additionalStyleClasses': 'code', + 'isNullable': true + }, + ...ConfigConsts._template_getActiveEffectsDisabledTransferSettings({ + 'name': "optional features" + }), + 'isImportDescription': { + 'name': "Import Text as Description", + 'help': "If enabled, an optional feature's text will be imported as item description.", + 'default': true, + 'type': 'boolean' + } + } + }, + 'importConditionDisease': { + 'name': "Import (Conditions & Diseases)", + 'settings': { + //'ownership': ConfigConsts._template_getEntityOwnership("The default (i.e. used for all players unless a player-specific ownership level is set) ownership for an imported condition/diseases.") + }, + 'settingsAdvanced': { + ...ConfigConsts._template_getActiveEffectsDisabledTransferSettings({ + 'name': "conditions/diseases" + }), + 'isImportDescription': { + 'name': "Import Text as Description", + 'help': "If enabled, a condition/disease's text will be imported as item description.", + 'default': true, + 'type': 'boolean' + } + } + }, + 'importCultBoon': { + 'name': "Import (Cults & Supernatural Boons)", + 'settings': { + //'ownership': ConfigConsts._template_getEntityOwnership("The default (i.e. used for all players unless a player-specific ownership level is set) ownership for an imported cult/boon.") + }, + 'settingsAdvanced': { + ...ConfigConsts._template_getActiveEffectsDisabledTransferSettings({ + 'name': "cults/boons" + }), + 'isImportDescription': { + 'name': "Import Text as Description", + 'help': "If enabled, a cult/boon's text will be imported as item description.", + 'default': true, + 'type': "boolean" + } + } + }, + 'importAction': { + 'name': "Import (Actions)", + 'settings': { + //'ownership': ConfigConsts._template_getEntityOwnership("The default (i.e. used for all players unless a player-specific ownership level is set) ownership for an imported action.") + }, + 'settingsAdvanced': { + ...ConfigConsts._template_getActiveEffectsDisabledTransferSettings({ + 'name': "actions" + }), + 'isImportDescription': { + 'name': "Import Text as Description", + 'help': "If enabled, a action's text will be imported as item description.", + 'default': true, + 'type': "boolean" + } + } + }, + 'importReward': { + 'name': "Import (Gifts & Rewards)", + 'settings': { + /* 'ownership': ConfigConsts._template_getEntityOwnership("The default (i.e. used for all players unless a player-specific ownership level is set) ownership for an imported supernatural gift/reward."), + ...ConfigConsts._template_getTargetTemplatePrompt({ + 'namePlural': "supernatural gift/rewards" + }), */ + 'isMetricDistance': { + 'name': "Convert Speeds to Metric", + 'help': "Whether or not gift/reward speed units should be converted to an approximate metric equivalent (" + ConfigConsts._DISP_METRIC_FEET + ').', + 'default': false, + 'type': 'boolean' + } + }, + 'settingsAdvanced': { + ...ConfigConsts._template_getActiveEffectsDisabledTransferSettings({ + 'name': "gift/rewards" + }), + 'isImportDescription': { + 'name': "Import Text as Description", + 'help': "If enabled, a supernatural gift/reward's text will be imported as item description.", + 'default': true, + 'type': "boolean" + } + } + }, + 'importCharCreationOption': { + 'name': "Import (Char. Creation Options)", + 'help': "Import (Character Creation Options)", + 'settings': { + /* 'ownership': ConfigConsts._template_getEntityOwnership("The default (i.e. used for all players unless a player-specific ownership level is set) ownership for an imported character creation option."), + ...ConfigConsts._template_getTargetTemplatePrompt({ + 'namePlural': "character creation options" + }), */ + 'isMetricDistance': { + 'name': "Convert Speeds to Metric", + 'help': "Whether or not character creation option speed units should be converted to an approximate metric equivalent (" + ConfigConsts._DISP_METRIC_FEET + ').', + 'default': false, + 'type': "boolean" + } + }, + 'settingsAdvanced': { + ...ConfigConsts._template_getActiveEffectsDisabledTransferSettings({ + 'name': "character creation options" + }), + 'isImportDescription': { + 'name': "Import Text as Description", + 'help': "If enabled, a character creation option's text will be imported as item description.", + 'default': true, + 'type': 'boolean' + } + } + }, + 'importDeity': { + 'name': "Import (Deities)", + 'settings': { + //'ownership': ConfigConsts._template_getEntityOwnership("The default (i.e. used for all players unless a player-specific ownership level is set) ownership for an imported deity.") + } + }, + 'importRecipe': { + 'name': "Import (Recipes)", + 'settings': { + //'ownership': ConfigConsts._template_getEntityOwnership("The default (i.e. used for all players unless a player-specific ownership level is set) ownership for an imported recipe.") + } + }, + 'importTrap': { + 'name': "Import (Traps)", + 'settings': { + /* 'ownership': ConfigConsts._template_getEntityOwnership("The default (i.e. used for all players unless a player-specific ownership level is set) ownership for an imported trap."), + ...ConfigConsts._template_getTokenSettings({ + 'actorType': "npc" + }), */ + 'isImportBio': { + 'name': "Import Fluff to Description", + 'help': "If enabled, any fluff text which is available for a trap will be imported into that trap's description.", + 'default': true, + 'type': 'boolean' + }, + 'isImportBioImages': { + 'name': "Include Fluff Image in Description", + 'help': "If enabled, any fluff image which is available for a trap will be imported into that trap's description.", + 'default': false, + 'type': "boolean" + } + }, + 'settingsAdvanced': { + ...ConfigConsts._template_getActorImportOverwriteSettings() + } + }, + 'importTrapFeature': { + 'name': "Import (Trap Features)", + 'settings': { + /* 'ownership': ConfigConsts._template_getEntityOwnership("The default (i.e. used for all players unless a player-specific ownership level is set) ownership for an imported trap feature."), + ...ConfigConsts._template_getTargetTemplatePrompt({ + 'namePlural': "trap features" + }), */ + 'isMetricDistance': { + 'name': "Convert Ranges to Metric", + 'help': "Whether or not trap feature range units should be converted to an approximate metric equivalent (" + ConfigConsts._DISP_METRIC_FEET + ').', + 'default': false, + 'type': "boolean" + } + }, + 'settingsAdvanced': { + ...ConfigConsts._template_getActiveEffectsDisabledTransferSettings({ + 'name': "trap features" + }) + } + }, + 'importHazard': { + 'name': "Import (Hazards)", + 'settings': { + //'ownership': ConfigConsts._template_getEntityOwnership("The default (i.e. used for all players unless a player-specific ownership level is set) ownership for an imported hazard.") + } + }, + 'importAdventure': { + 'name': "Import (Adventures)", + 'settings': { + //'ownership': ConfigConsts._template_getEntityOwnership("The default (i.e. used for all players unless a player-specific ownership level is set) ownership for an imported adventure."), + 'isUseModdedInstaller': { + 'name': "Use Modded Package Installer", + 'help': "If the modded Plutonium backend is installed, adventure packages (modules/worlds) will be installed, automatically, using the mod, rather than providing you with a list of links to copy-paste into Foundry's \"Setup\".", + 'type': 'boolean', + 'default': false + }, + 'isUseLegacyImporter': { + 'name': "Enable Legacy Package Importer", + 'help': "If Plutonium should allow adventure packages (modules/worlds) to be imported directly, rather than providing references for the user to investigate themselves.", + 'type': "boolean", + 'default': false, + 'unlockCode': "unlock" + }, + 'indexUrl': { + 'name': "Package Index URL", + 'help': "The URL of the index file from which world/module package metadata is loaded.", + 'type': 'url', + 'default': "https://raw.githubusercontent.com/DMsGuild201/Foundry_Resources/master/worlds/index.json", + 'additionalStyleClasses': 'code', + 'isReloadRequired': true + } + } + }, + 'importBook': { + 'name': "Import (Books)", + 'settings': { + //'ownership': ConfigConsts._template_getEntityOwnership("The default (i.e. used for all players unless a player-specific ownership level is set) ownership for an imported book.") + } + }, + 'importMap': { + 'name': "Import (Maps)", + 'settings': { + //...ConfigConsts._template_getSceneImportSettings() + } + }, + 'importDeck': { + 'name': "Import (Decks)", + 'settings': { + //'ownership': ConfigConsts._template_getEntityOwnership("The default (i.e. used for all players unless a player-specific ownership level is set) ownership for an imported deck.") + } + }, + 'actor': { + 'name': 'Actors', + 'settings': { + 'isRefreshOtherOwnedSheets': { + 'name': "Refresh Sheets using "@" + SharedConsts.MODULE_ID_FAKE + ".userchar" when Updating Player Character", + 'help': "Player only. If enabled, when you update your character, the sheets of other actors you control which use \"@" + SharedConsts.MODULE_ID_FAKE + ".userchar. ...\" attributes will be automatically refreshed to reflect any changes made to your character. If disabled, you may notice a \"lag\" between updating your character and seeing the changes reflected in other sheets (a refresh can be forced manually by editing any field on the other sheet, or refreshing your browser tab).", + 'default': true, + 'type': 'boolean', + 'isPlayerEditable': true + } + }, + 'settingsAdvanced': { + 'isAddRollDataItemsFeat': { + 'name': "Add "@items" to Roll Data (Features)", + 'help': "If actor roll data should be modified to allow access owned items, via data paths of the form \"@items.. ...\" (for example, \"@items.big-sword.system.attackBonus\" would be substituted with the attack bonus of the owned item \"Big Sword\").", + 'default': false, + 'type': "boolean", + 'compatibilityModeValues': { + [UtilCompat.MODULE_PLUTONIUM_ADDON_AUTOMATION]: true + } + }, + 'isAddRollDataItemsItem': { + 'name': "Add "@items" to Roll Data (Inventory)", + 'help': "If actor roll data should be modified to allow access owned items, via data paths of the form \"@items.. ...\" (for example, \"@items.big-sword.system.attackBonus\" would be substituted with the attack bonus of the owned item \"Big Sword\").", + 'default': false, + 'type': 'boolean' + }, + 'isAddRollDataItemsSpell': { + 'name': "Add "@items" to Roll Data (Spells)", + 'help': "If actor roll data should be modified to allow access owned items, via data paths of the form \"@items.. ...\" (for example, \"@items.big-sword.system.attackBonus\" would be substituted with the attack bonus of the owned item \"Big Sword\").", + 'default': false, + 'type': "boolean" + }, + 'isAddRollDataItemsOther': { + 'name': "Add "@items" to Roll Data (Other)", + 'help': "If actor roll data should be modified to allow access owned items, via data paths of the form \"@items.. ...\" (for example, \"@items.big-sword.system.attackBonus\" would be substituted with the attack bonus of the owned item \"Big Sword\").", + 'default': false, + 'type': 'boolean' + } + }, + 'settingsHacks': { + 'isAutoMultiattack': { + 'name': "Auto-Roll Multiattacks", + 'help': "Attempt to detect and automatically roll components of a creature's \"Multiattack\" sheet item on activation.", + 'default': false, + 'type': "boolean" + }, + 'autoMultiattackDelay': { + 'name': "Time Between Multiattack Rolls (ms)", + 'help': "A number of milliseconds to wait between each roll of a multiattack when using the \"Auto-Roll Multiattacks\" option. A value of 2000-2500 is recommended when using the \"Automated Animations\" module.", + 'default': null, + 'type': 'number', + 'min': 0x0, + 'isNullable': true + }, + 'isUseExtendedActiveEffectsParser': { + 'name': "Support Variables in Active Effect Values", + 'help': "Allows the use of roll syntax, and notably variables (such as \"@abilities.dex.mod\"), in active effect values.", + 'default': true, + 'type': "boolean", + 'compatibilityModeValues': { + [UtilCompat.MODULE_DAE]: false, + [UtilCompat.MODULE_ROLLDATA_AWARE_ACTIVE_EFFECTS]: false + } + } + } + }, + 'item': { + 'name': "Items", + 'settingsHacks': { + 'isSuppressAdvancementsOnImportedDrop': { + 'name': "Suppress Advancements During Drop Flow", + 'help': "If enabled, dropping a Plutonium-imported item to a sheet will briefly disable the default advancement workflow, potentially allowing Plutonium's importer to run instead.", + 'default': true, + 'type': 'boolean' + } + } + }, + 'rivet': { + 'name': "Rivet", + 'settings': { + 'targetDocumentId': { + 'name': "Target Document", + 'help': "The ID of an actor or compendium to which Rivet content should be imported.", + 'default': '', + 'type': "string", + 'additionalStyleClasses': "code", + 'isPlayerEditable': true + }, + 'isDisplayStatus': { + 'name': "Display Extension Detected", + 'help': "Adds a \"paper plane\" icon to the Foundry \"anvil\" logo in the top-left corner of the screen if Rivet is detected.", + 'default': true, + 'type': "boolean", + 'isPlayerEditable': true + }, + /* 'minimumRole': ConfigConsts._template_getMinimumRole({ + 'name': "Minimum Permission Level", + 'help': "Rivet will cease to function for any user user with a role less than the chosen role. Directory \"Set as Rivet Target\" context menu option will also be hidden for any user with a role less than the chosen role." + }) */ + } + }, + 'artBrowser': { + 'name': "Art Browser", + 'settings': { + 'importImagesAs': { + 'name': "Drag-Drop Images As", + 'help': "The type of canvas object that should be created when drag-dropping images from the art browser to the canvas.", + 'default': ConfigConsts.C_ART_IMAGE_MODE_TOKEN, + 'type': "enum", + 'values': [{ + 'value': ConfigConsts.C_ART_IMAGE_MODE_TOKEN, + 'name': 'Tokens' + }, { + 'value': ConfigConsts.C_ART_IMAGE_MODE_TILE, + 'name': "Tiles" + }, { + 'value': ConfigConsts.C_ART_IMAGE_MODE_NOTE, + 'name': "Journal notes" + }, { + 'value': ConfigConsts.C_ART_IMAGE_MODE_SCENE, + 'name': "Scenes" + }] + }, + 'dropAnchor': { + 'name': "Drag-Drop Position Anchor", + 'help': "The origin point of the image used for the purpose of dropping it to the canvas. \"Center\" will place the center of the image at the drop position, whereas \"Top-Left Corner\" will place the top-left corner of the image at the drop position.", + 'default': 0x0, + 'type': "enum", + 'values': [{ + 'value': ConfigConsts.C_ART_DROP_ANCHOR_CENTER, + 'name': "Center" + }, { + 'value': ConfigConsts.C_ART_DROP_ANCHOR_TOP_LEFT, + 'name': "Top-Left Corner" + }] + }, + 'scale': { + 'name': "Tile/Scene Scaling", + 'help': "A factor by which to scale placed tiles, and by which to scale scene backgrounds.", + 'default': 0x1, + 'type': 'number', + 'min': 0.01, + 'max': 0x64 + }, + ...ConfigConsts._template_getSceneImportSettings(), + 'tokenSize': { + 'name': "Token Size", + 'help': "The default size of placed tokens.", + 'default': 0x1, + 'type': "enum", + 'values': [{ + 'value': 0x1, + 'name': "Medium or smaller" + }, { + 'value': 0x2, + 'name': 'Large' + }, { + 'value': 0x3, + 'name': "Huge" + }, { + 'value': 0x4, + 'name': "Gargantuan or larger" + }] + }, + 'isSwitchToCreatedScene': { + 'name': "Activate Scenes on Creation", + 'help': "If enabled, a scene will be activated upon creation (by drag-dropping an image to the canvas).", + 'default': true, + 'type': "boolean" + }, + 'isDisplaySheetCreatedScene': { + 'name': "Display Scene Sheets on Creation", + 'help': "If enabled, the \"sheet\" (i.e., configuration UI) for a scene will be shown upon creation (by drag-dropping an image to the canvas).", + 'default': true, + 'type': 'boolean' + }, + 'artDirectoryPath': { + 'name': "User Art Directory", + 'help': "The sub-directory of the \"User Data\" directory where downloaded images and image packs will be saved.", + 'default': "assets/art", + 'type': 'string', + 'additionalStyleClasses': "code", + 'isNullable': true + }, + 'buttonDisplay': { + 'name': "Add Button To", + 'help': "The place(s) where the Art Browser button should be visible.", + 'default': { + [ConfigConsts.C_ART_IMAGE_MODE_TOKEN]: false, + [ConfigConsts.C_ART_IMAGE_MODE_TILE]: true, + [ConfigConsts.C_ART_IMAGE_MODE_NOTE]: false, + [ConfigConsts.C_ART_IMAGE_MODE_SCENE]: true + }, + 'type': "multipleChoice", + 'choices': [{ + 'value': ConfigConsts.C_ART_IMAGE_MODE_TOKEN, + 'name': "Token scene controls" + }, { + 'value': ConfigConsts.C_ART_IMAGE_MODE_TILE, + 'name': "Tile scene controls" + }, { + 'value': ConfigConsts.C_ART_IMAGE_MODE_NOTE, + 'name': "Note scene controls" + }, { + 'value': ConfigConsts.C_ART_IMAGE_MODE_SCENE, + 'name': "Scene controls" + }] + }, + 'imageSaveMode': { + 'name': "Image Saving Mode", + 'help': "How images should be saved to the server. If \"Default\" is selected, an imported image will only be saved if it cannot be referenced via URL. If \"Always\" is selected, an imported image will be saved to the server, regardless of whether or not it can be referenced via URL. If \"Never\" is selected, an imported image will only be referenced by URL; if it cannot be referenced via URL, the import will fail. Note that saving images requires the Plutonium backend mod to be installed.", + 'default': ConfigConsts.C_ART_IMAGE_SAVE_MODE__DEFAULT, + 'type': "enum", + 'values': [{ + 'value': ConfigConsts.C_ART_IMAGE_SAVE_MODE__DEFAULT, + 'name': "Default" + }, { + 'value': ConfigConsts.C_ART_IMAGE_SAVE_MODE__ALWAYS, + 'name': 'Always' + }, { + 'value': ConfigConsts.C_ART_IMAGE_SAVE_MODE__NEVER, + 'name': "Never" + }] + } + }, + 'settingsAdvanced': { + 'isSwitchLayerOnDrop': { + 'name': "Switch to Layer on Drop", + 'help': "If, when dropping an image into a given layer, the canvas should switch to that layer.", + 'default': true, + 'type': "boolean" + }, + 'isShowMissingBackendWarning': { + 'name': "Show "Missing Backend" Warning", + 'help': "If enabled, and the Plutonium backend mod is not installed, a warning will be shown in the Art Browser.", + 'default': true, + 'type': "boolean" + } + } + }, + 'journalEntries': { + 'name': "Journal Entries", + 'settings': { + 'isAutoExpandJournalEmbeds': { + 'name': "Auto-Expand Page Embeds", + 'help': "If enabled, journal pages embedded using \"@EmbedUUID[JournalEntry. ... JournalEntryPage. ...]{...}\" will be expanded by default.", + 'default': true, + 'type': "boolean" + }, + 'isEnableNoteHeaderAnchor': { + 'name': "Allow "Header Anchors" in Notes", + 'help': "If enabled, a \"Header Anchor\" may be specified when creating or editing a map note. When opening a journal entry via a map note with a Header Anchor set, the journal entry will scroll to that header.", + 'default': true, + 'type': "boolean" + } + } + }, + 'tools': { + 'name': "Tools", + 'settings': { + 'isDeduplicateIgnoreType': { + 'name': "Ignore Types When Deduplicating", + 'help': "If enabled, the Collection Deduplicator will ignore entity types, treating e.g. a PC sheet and an NPC sheet with the same name as a set of duplicates.", + 'default': false, + 'type': "boolean" + }, + /* 'minimumRolePolymorph': ConfigConsts._template_getMinimumRole({ + 'name': "Minimum Permission Level for Polymorph Tool", + 'help': "Actor \"Polymorph\" buttons will be hidden for any user with a role less than the chosen role." + }), + 'minimumRoleActorTools': ConfigConsts._template_getMinimumRole({ + 'name': "Minimum Permission Level for Other Actor Tools", + 'help': "Actor \"Feature/Spell Cleaner,\" \"Prepared Spell Mass-Toggler,\" etc. buttons will be hidden for any user with a role less than the chosen role." + }), + 'minimumRoleTableTools': ConfigConsts._template_getMinimumRole({ + 'name': "Minimum Permission Level for Other Table Tools", + 'help': "Table \"Row Cleaner\" button will be hidden for any user with a role less than the chosen role." + }), */ + 'isAddClearFlagsContextMenu': { + 'name': "Add "Clear Flags" Context Option", + 'help': "If enabled a \"Clear Flags\" option will be added to directory document context menus. This option will clear all \"plutonium\" flags from a document, and the document's embedded documents. Note that this will negatively impact Plutonium functionality for the document.", + 'default': false, + 'type': "boolean", + 'isReloadRequired': true + } + } + }, + 'text': { + 'name': "Text and Tags", + 'settings': { + 'isEnableHoverForLinkTags': { + 'name': "Enable Hover Popups for "@tag" Links", + 'help': "If links rendered from @tag syntax should display popups when hovered.", + 'default': false, + 'type': "boolean", + 'isReloadRequired': true + }, + 'isAutoRollActorItemTags': { + 'name': "Roll Items Linked by @UUID[Actor.Item.] on Click", + 'help': "If enabled, clicking a rendered @UUID[Actor. ... Item. ...] tag will roll the linked embedded item. If disabled, or on SHIFT-click, the default action (opening the item's sheet) is taken.", + 'default': false, + 'type': "boolean", + 'isPlayerEditable': true + }, + 'isJumpToFolderTags': { + 'name': "Show Folder Linked by @UUID[Folder.] on Click", + 'help': "If enabled, clicking a rendered @UUID[Folder. ...] tag will switch to that folder's tab and scroll the folder into view. If disabled, or on SHIFT-click, the default action (opening the folder's sheet) is taken.", + 'default': true, + 'type': "boolean", + 'isPlayerEditable': true + }, + 'isShowLinkParent': { + 'name': "Show Parent Icon/Name For Child @UUIDs", + 'help': "If enabled, a rendered @UUID[...] tag will display the icon of the parent document type and the name of the parent document, in addition to the usual icon of the document type and the name of the document.", + 'default': false, + 'type': "boolean", + 'isPlayerEditable': true + } + } + }, + 'misc': { + 'name': "Miscellaneous", + 'settings': { + 'isSkipAddonAutomationCheck': { + 'name': "Skip Addon: Automation Check", + 'help': "Avoid posting to chat if the Addon: Automation companion model is not installed.", + 'default': false, + 'type': "boolean" + }, + 'isSkipBackendCheck': { + 'name': "Skip Backend Check", + 'help': "Avoid sending a network request during module initialisation to check if the modded Plutonium backend is installed.", + 'default': false, + 'type': "boolean", + 'isPlayerEditable': true + } + }, + 'settingsAdvanced': { + 'baseSiteUrl': { + 'name': "Master of Ceremonies Server URL", + 'help': "The root server URL for the Mater of Ceremonies app, used to verify and unlock Patron benefits.", + 'type': 'url', + 'default': "https://plutonium.giddy.cyou", + 'isNullable': true, + 'isReloadRequired': true, + 'unlockCode': 'unlock' + }, + 'backendEndpoint': { + 'name': "Custom Backend Endpoint", + 'help': "The API endpoint used to make calls to the modded Plutonium backend, if available. Note that this API is considered \"internal,\" and is therefore undocumented, and may change on a per-version basis.", + 'default': null, + 'placeholder': "(Use default)", + 'type': "url", + 'additionalStyleClasses': "code", + 'isNullable': true + }, + 'isPatchFromUuid': { + 'name': "Patch fromUuid", + 'help': "Patch the built-in Foundry function \"fromUuid\" to allow Plutonium-specific UUIDs to be processed. This improves compatibility with some modules.", + 'default': true, + 'type': "boolean" + } + } + }, + 'equipmentShop': { + 'name': "Equipment Shop", + 'settings': { + 'priceMultiplier': { + 'name': "Price Multiplier", + 'help': "A factor by which the prices in the equipment shop are multiplied.", + 'default': 0x1, + 'type': 'percentage', + 'min': 0.0001 + }, + 'startingGold': { + 'name': "Class Starting Gold", + 'help': "A starting gold amount to use instead of a class's starting gold, when using the equipment shop during class creation.", + 'default': null, + 'type': "number", + 'isNullable': true + }, + /* 'minimumRole': ConfigConsts._template_getMinimumRole({ + 'name': "Minimum Permission Level", + 'help': "\"Equipment Shop\" button will be hidden for any user with a role less than the chosen role." + }) */ + } + }, + 'currency': { + 'name': 'Currency', + 'settingsAdvanced': { + 'isNpcUseCurrencySheetItems': { + 'name': "Import Currency as Sheet Item for NPCs", + 'help': "If enabled, the currency component of loot drag-dropped to an NPC sheet will be added as a sheet item. If disabled, it will be added as \"currency\" data instead, which the default " + SharedConsts.SYSTEM_ID_DND5E + " sheet does not display.", + 'default': true, + 'type': "boolean" + } + } + }, + 'dataSources': { + 'name': "Data Sources", + 'btnsAdditional': [{ + 'name': "World Data Source Selector", + 'icon': "fas fa-fw fa-globe-africa", + 'onClick': async () => { + const { + WorldDataSourceSelector: _0x3787e7 + } = await Promise.resolve().then(function () { + return WorldDataSourceSelector$1; + }); + _0x3787e7.pHandleButtonClick().then(null); + } + }, { + 'name': "World Content Blocklist", + 'icon': "fas fa-fw fa-ban", + 'onClick': async () => { + const { + WorldContentBlocklistSourceSelector: _0x2b04fc + } = await Promise.resolve().then(function () { + return WorldContentBlocklist$1; + }); + _0x2b04fc.pHandleButtonClick().then(null); + } + }], + 'settings': { + 'isPlayerEnableSourceSelection': { + 'name': "Enable Data Source Filtering for Players", + 'help': "Whether or not " + ConfigConsts._STR_DATA_SOURCES + " are filtered down to only those chosen in the \"World Data Source Selector\" application. Applies to players only.", + 'default': false, + 'type': 'boolean', + 'isReloadRequired': true + }, + 'isGmEnableSourceSelection': { + 'name': "Enable Data Source Filtering for GMs", + 'help': "Whether or not " + ConfigConsts._STR_DATA_SOURCES + " are filtered down to only those chosen in the \"World Data Source Selector\" application. Applies to GMs only.", + 'default': false, + 'type': 'boolean', + 'isReloadRequired': true + }, + 'isPlayerForceSelectAllowedSources': { + 'name': "Force Select All for Players", + 'help': "Whether or not all available " + ConfigConsts._STR_DATA_SOURCES + " are forcibly selected for players. Note that this can seriously degrade performance for players if data source filtering is not also enabled.", + 'default': false, + 'type': "boolean", + 'isReloadRequired': true + }, + 'isGmForceSelectAllowedSources': { + 'name': "Force Select All for GMs", + 'help': "Whether or not all available " + ConfigConsts._STR_DATA_SOURCES + " are forcibly selected for GMs. Note that this can seriously degrade performance for GMs if data source filtering is not also enabled.", + 'default': false, + 'type': "boolean", + 'isReloadRequired': true + }, + 'isLoadLocalPrereleaseIndex': { + 'name': "Load Local Prerelease Content", + 'help': "If enabled, the directory specified by the \"Local Prerelease Content Directory\" option will be read, and its contents added to the list of available sources.", + 'default': false, + 'type': "boolean" + }, + 'localPrereleaseDirectoryPath': { + 'name': "Local Prerelease Content Directory", + 'help': "The sub-directory of the \"User Data\" directory from which prerelease content should be automatically loaded if the \"Load Local Prerelease\" option is enabled.", + 'default': "assets/prerelease", + 'type': "string", + 'additionalStyleClasses': "code" + }, + 'isUseLocalPrereleaseIndexJson': { + 'name': "Use index.json for Local Prerelease Content", + 'help': "If, rather than read the local prerelease content directory directly, an \"index.json\" file should be read when loading local prerelease content. This file should be of the form: {\"toImport\": [ ... list of filenames ... ]}. Note that this is required if players do not have \"Use File Browser\" permissions.", + 'default': false, + 'type': "boolean" + }, + 'localPrerelease': { + 'name': "Additional Prerelease Files", + 'help': "Prerelease files which should be automatically loaded and added to the list of available sources.", + 'default': [], + 'type': "arrayStringShort", + 'isCaseSensitive': true + }, + 'isLoadLocalHomebrewIndex': { + 'name': "Load Local Homebrew", + 'help': "If enabled, the directory specified by the \"Local Homebrew Directory\" option will be read, and its contents added to the list of available sources.", + 'default': false, + 'type': "boolean" + }, + 'localHomebrewDirectoryPath': { + 'name': "Local Homebrew Directory", + 'help': "The sub-directory of the \"User Data\" directory from which homebrew should be automatically loaded if the \"Load Local Homebrew\" option is enabled.", + 'default': "assets/homebrew", + 'type': "string", + 'additionalStyleClasses': "code" + }, + 'isUseLocalHomebrewIndexJson': { + 'name': "Use index.json for Local Homebrew", + 'help': "If, rather than read the local homebrew directory directly, an \"index.json\" file should be read when loading local homebrew. This file should be of the form: {\"toImport\": [ ... list of filenames ... ]}. Note that this is required if players do not have \"Use File Browser\" permissions.", + 'default': false, + 'type': "boolean" + }, + 'localHomebrew': { + 'name': "Additional Homebrew Files", + 'help': "Homebrew files which should be automatically loaded and added to the list of available sources.", + 'default': [], + 'type': "arrayStringShort", + 'isCaseSensitive': true + } + }, + 'settingsAdvanced': { + 'tooManySourcesWarningThreshold': { + 'name': "Auto-Selected Source Count Warning Threshold", + 'help': "If set, a warning will be shown when auto-selecting a number of sources greater than this value, which usually occurs if a \"Force Select All...\" option is set, without also \"Enabl[ing] Data Source Filtering.\"", + 'default': 0x32, + 'type': "integer", + 'isNullable': true + }, + 'baseSiteUrl': { + 'name': "Base Site URL", + 'help': "The root server URL from which to load data and source images, and to link in rendered text. Note that, where possible, the module will use its own built-in data files, rather than call out to a remote server.", + 'type': "url", + 'additionalStyleClasses': "code", + 'default': null, + 'isNullable': true, + 'isReloadRequired': true + }, + 'isNoLocalData': { + 'name': "Avoid Loading Local Data", + 'help': "If enabled, any data which would normally be loaded from the module's local copies is instead loaded from the sites URL (which may be customised by editing the \"Base Site Url\" config option).", + 'default': false, + 'type': "boolean" + }, + 'isNoPrereleaseBrewIndexes': { + 'name': "Avoid Loading Prerelease/Homebrew Indexes on Startup", + 'help': "If enabled, prerelease/homebrew repository indexes won't be loaded during initial module load. This will effectively prevent any prerelease/homebrew sources from appearing in source listings. Note that these indexes are loaded in the background/asynchronously during normal operation, so should not negatively impact game load times, unless you have a particularly terrible internet connection.", + 'default': false, + 'type': "boolean" + }, + 'basePrereleaseUrl': { + 'name': "Base Prerelease Repository URL", + 'help': "The root GitHub repository URL from which to load data and source images, and to link in rendered text, when importing prerelease content. URLs should be of the form \"https://raw.githubusercontent.com/[username]/[repository name]/master\".", + 'type': "url", + 'additionalStyleClasses': 'code', + 'default': null, + 'isNullable': true, + 'isReloadRequired': true + }, + 'baseBrewUrl': { + 'name': "Base Homebrew Repository URL", + 'help': "The root GitHub repository URL from which to load data and source images, and to link in rendered text, when importing homebrew content. URLs should be of the form \"https://raw.githubusercontent.com/[username]/[repository name]/master\".", + 'type': "url", + 'additionalStyleClasses': 'code', + 'default': null, + 'isNullable': true, + 'isReloadRequired': true + } + } + }, + /* 'integrationQuickInsert': { + 'name': "Integrations (Quick Insert)", + 'settings': { + ...ConfigConsts._template_getModuleFauxCompendiumIndexSettings({ + 'moduleName': "Quick Insert" + }), + 'pagesHidden': { + 'name': "Hidden Categories", + 'help': "Categories of entity which should not be indexed.", + 'default': ConfigConsts._QUICK_INSERT_PAGE_METAS.mergeMap(({ + page: _0x25cba7 + }) => ({ + [_0x25cba7]: _0x25cba7 === UrlUtil.PG_RECIPES + })), + 'type': "multipleChoice", + 'choices': ConfigConsts._QUICK_INSERT_PAGE_METAS.map(({ + page: _0x365d30, + displayPage: _0x31cbd1 + }) => ({ + 'value': _0x365d30, + 'name': _0x31cbd1 + })) + }, + 'isDisplaySource': { + 'name': "Display Sources", + 'help': "If enabled, a source abbreviation will be displayed on each result. If disabled, the module name will be shown instead.", + 'default': true, + 'type': 'boolean' + } + } + }, + 'integrationFoundrySummons': { + 'name': "Integrations (Foundry Summons)", + 'settings': { + ...ConfigConsts._template_getModuleFauxCompendiumIndexSettings({ + 'moduleName': "Foundry Summons" + }) + } + }, + 'integrationBabele': { + 'name': "Integrations (Babele)", + 'settings': { + 'isEnabled': { + 'name': "Enabled", + 'help': "If enabled, and the Babele module is active, Plutonium will attempt to translate parts of imported content.", + 'default': true, + 'type': "boolean" + }, + 'isUseTranslatedDescriptions': { + 'name': "Use Translated Descriptions", + 'help': "If enabled, and a translated description is found for a document during import, that description will be used instead of the Plutonium default. Note that this may result in embedded functionality (for example, links between documents) being removed.", + 'default': true, + 'type': "boolean" + } + } + }, + 'integrationThreeDiCanvas': { + 'name': "Integrations (3D Canvas)", + 'settings': { + 'isSetThreeDiModels': { + 'name': "Allow Importer to Set 3D Models", + 'help': "If enabled, and the 3D Canvas, 3D Canvas Mapmaking Pack, and 3D Canvas Token Collection modules are active, Plutonium will attempt to set the \"3D Model\" field on imported tokens.", + 'default': true, + 'type': 'boolean', + 'isReloadRequired': true + } + } + }, */ + 'charactermancer': { + 'name': "Charactermancer", + 'settings': { + /* 'minimumRole': ConfigConsts._template_getMinimumRole({ + 'name': "Minimum Permission Level", + 'help': "Actor \"Charactermancer\" buttons will be hidden for any user with a role less than the chosen role." + }) */ + } + } + }; + } + static _DEFAULT_CONFIG_SORTED = null; + static getDefaultConfigSorted_() { + //Returns _DEFAULT_CONFIG_SORTED if it has already been set, if not, it sorts and sets _DEFAULT_CONFIG_SORTED + return this._DEFAULT_CONFIG_SORTED = this._DEFAULT_CONFIG_SORTED + || Object.entries(this.getDefaultConfig_()).sort(([, entry1], [, entry2]) => SortUtil.ascSortLower(entry1.name, entry2.name)); + } + static _DEFAULT_CONFIG_SORTED_FLAT = null; + static getDefaultConfigSortedFlat_() { + //Returns _DEFAULT_CONFIG_SORTED_FLAT if it has already been set + if (this._DEFAULT_CONFIG_SORTED_FLAT) { return this._DEFAULT_CONFIG_SORTED_FLAT; } + + //If not, it sets _DEFAULT_CONFIG_SORTED_FLAT and returns itself + return this._DEFAULT_CONFIG_SORTED_FLAT = this._DEFAULT_CONFIG_SORTED_FLAT + || this.getDefaultConfigSorted_().map(([_0x102f4b, _0x540fe5]) => { + const _0x417b84 = {}; + this._KEYS_SETTINGS_METAS.forEach(meta => { + Object.entries(_0x540fe5[meta] || {}).forEach(([_0x70d0f8, _0x41ed31]) => { + _0x417b84[_0x70d0f8] = _0x41ed31; + }); + }); + return [_0x102f4b, _0x417b84]; + }); + } + static ["getCompendiumPaths"]() { + const _0x525cb1 = []; + Object.entries(this.getDefaultConfig_()).forEach(([_0x3661aa, _0x57ce92]) => { + this._KEYS_SETTINGS_METAS.forEach(_0x435517 => { + if (!_0x57ce92[_0x435517]) { + return; + } + Object.entries(_0x57ce92[_0x435517]).forEach(([_0x57b4d8, _0x1b0cb0]) => { + if (_0x1b0cb0.typeSub !== "compendiums") { + return; + } + _0x525cb1.push([_0x3661aa, _0x57b4d8]); + }); + }); + }); + return _0x525cb1; + } +} +ConfigConsts._STR_DATA_SOURCES = "\"data sources\" (e.g. those displayed in the Import Wizard)"; +ConfigConsts._KEYS_SETTINGS_METAS = ["settings", "settingsHacks", "settingsAdvanced"]; +ConfigConsts._TEMPLATE_ENTITY_OWNERSHIP = { + 'name': "Default Ownership", + 'default': 0x0, + 'type': "enum" +}; +ConfigConsts._TEMPALTE_MINIMUM_ROLE = { + 'default': 0x0, + 'type': "enum", + 'isReloadRequired': true +}; +ConfigConsts._DISP_METRIC_POUNDS = "1 pound โ‰ˆ 0.5 kilograms"; +ConfigConsts._DISP_METRIC_FEET = "5 feet โ‰ˆ 1.5 metres"; +ConfigConsts._DISP_METRIC_MILES = "1 mile โ‰ˆ 1.6 kilometres"; +ConfigConsts.SRD_COMPENDIUMS_CREATURES = [SharedConsts.SYSTEM_ID_DND5E + ".monsters"]; +ConfigConsts.SRD_COMPENDIUMS_CREATURE_FEATURES = [SharedConsts.SYSTEM_ID_DND5E + ".monsterfeatures"]; +ConfigConsts.SRD_COMPENDIUMS_CLASSES = [SharedConsts.SYSTEM_ID_DND5E + ".classes"]; +ConfigConsts.SRD_COMPENDIUMS_SUBCLASSES = [SharedConsts.SYSTEM_ID_DND5E + ".subclasses"]; +ConfigConsts.SRD_COMPENDIUMS_CLASS_FEATURES = [SharedConsts.SYSTEM_ID_DND5E + ".classfeatures"]; +ConfigConsts.SRD_COMPENDIUMS_ITEMS = [SharedConsts.SYSTEM_ID_DND5E + '.items', SharedConsts.SYSTEM_ID_DND5E + ".tradegoods"]; +ConfigConsts.SRD_COMPENDIUMS_SPELLS = [SharedConsts.SYSTEM_ID_DND5E + '.spells']; +ConfigConsts.SRD_COMPENDIUMS_OPTIONAL_FEATURES = [SharedConsts.SYSTEM_ID_DND5E + ".classfeatures"]; +ConfigConsts.SRD_COMPENDIUMS_RACES_AND_FEATURES = [SharedConsts.SYSTEM_ID_DND5E + ".races"]; +ConfigConsts.SRD_COMPENDIUMS_BACKGROUNDS_AND_FEATURES = [SharedConsts.SYSTEM_ID_DND5E + ".backgrounds"]; +ConfigConsts.SRD_COMPENDIUMS_TABLES = [SharedConsts.SYSTEM_ID_DND5E + ".tables"]; +/* ConfigConsts._QUICK_INSERT_PAGE_METAS = [...new Set(Renderer.tag.TAGS.filter(_0x244883 => _0x244883.page).map(_0x2e0a71 => _0x2e0a71.page).filter(_0x5b9350 => ![UrlUtil.PG_QUICKREF, "skill", "sense", "card", 'legroup'].includes(_0x5b9350)))].map(_0x207d8d => { + let _0x1333af = UrlUtil.pageToDisplayPage(_0x207d8d); + if (_0x1333af === _0x207d8d) { + _0x1333af = Parser.getPropDisplayName(_0x207d8d); + } + return { + 'page': _0x207d8d, + 'displayPage': _0x1333af + }; +}).sort(({ + displayPage: _0xb12442 +}, { + displayPage: _0xb84237 +}) => SortUtil.ascSortLower(_0xb12442, _0xb84237)); */ +ConfigConsts.C_ART_IMAGE_MODE_TOKEN = 0x0; +ConfigConsts.C_ART_IMAGE_MODE_TILE = 0x1; +ConfigConsts.C_ART_IMAGE_MODE_NOTE = 0x2; +ConfigConsts.C_ART_IMAGE_MODE_SCENE = 0x3; +ConfigConsts.C_ART_DROP_ANCHOR_CENTER = 0x0; +ConfigConsts.C_ART_DROP_ANCHOR_TOP_LEFT = 0x1; +ConfigConsts.C_ART_IMAGE_SAVE_MODE__DEFAULT = 0x0; +ConfigConsts.C_ART_IMAGE_SAVE_MODE__ALWAYS = 0x1; +ConfigConsts.C_ART_IMAGE_SAVE_MODE__NEVER = 0x2; +ConfigConsts.C_IMPORT_DEDUPE_MODE_NONE = 0x0; +ConfigConsts.C_IMPORT_DEDUPE_MODE_SKIP = 0x1; +ConfigConsts.C_IMPORT_DEDUPE_MODE_OVERWRITE = 0x2; +ConfigConsts.C_IMPORT_DRAG_DROP_MODE_NEVER = 0x0; +ConfigConsts.C_IMPORT_DRAG_DROP_MODE_PROMPT = 0x1; +ConfigConsts.C_IMPORT_DRAG_DROP_MODE_ALWAYS = 0x2; +ConfigConsts.C_CREATURE_NAMETAGS_CR = 0x0; +ConfigConsts.C_CREATURE_NAMETAGS_TYPE = 0x1; +ConfigConsts.C_CREATURE_NAMETAGS_TYPE_WITH_TAGS = 0x2; +ConfigConsts.C_SPELL_POINTS_MODE__DISABLED = 0x0; +ConfigConsts.C_SPELL_POINTS_MODE__ENABLED = 0x1; +ConfigConsts.C_SPELL_POINTS_MODE__ENABLED_AND_UNLIMITED_SLOTS = 0x2; +ConfigConsts.C_SPELL_POINTS_RESOURCE__SHEET_ITEM = "sheetItem"; +ConfigConsts.C_SPELL_POINTS_RESOURCE__ATTRIBUTE_CUSTOM = "attributeCustom"; +ConfigConsts.C_ITEM_ATTUNEMENT_NONE = 0x0; +ConfigConsts.C_ITEM_ATTUNEMENT_REQUIRED = 0x1; +ConfigConsts.C_ITEM_ATTUNEMENT_ATTUNED = 0x2; +ConfigConsts.C_ITEM_ATTUNEMENT_NEVER = 0x0; +ConfigConsts.C_ITEM_ATTUNEMENT_SMART = 0x1; +ConfigConsts.C_ITEM_ATTUNEMENT_ALWAYS = 0x2; +ConfigConsts.C_USE_GAME_DEFAULT = 'VE_USE_GAME_DEFAULT'; +ConfigConsts.C_USE_PLUT_VALUE = "VE_USE_MODULE_VALUE"; +ConfigConsts.C_BOOL_DISABLED = 0x0; +ConfigConsts.C_BOOL_ENABLED = 0x1; +ConfigConsts.C_TOKEN_NPC_HP_ROLL_MODE_NONE = 0x0; +ConfigConsts.C_TOKEN_NPC_HP_ROLL_MODE_STANDARD = 0x1; +ConfigConsts.C_TOKEN_NPC_HP_ROLL_MODE_GM = 0x2; +ConfigConsts.C_TOKEN_NPC_HP_ROLL_MODE_BLIND = 0x3; +ConfigConsts.C_TOKEN_NPC_HP_ROLL_MODE_SELF = 0x4; +ConfigConsts.C_TOKEN_NPC_HP_ROLL_MODE_HIDDEN = 0x5; +ConfigConsts.C_TOKEN_NPC_HP_ROLL_MODE_MIN = 0x6; +ConfigConsts.C_TOKEN_NPC_HP_ROLL_MODE_MAX = 0x7; +ConfigConsts.C_IMPORT_CLASS_HP_INCREASE_MODE__TAKE_AVERAGE = 0x0; +ConfigConsts.C_IMPORT_CLASS_HP_INCREASE_MODE__MIN = 0x1; +ConfigConsts.C_IMPORT_CLASS_HP_INCREASE_MODE__MAX = 0x2; +ConfigConsts.C_IMPORT_CLASS_HP_INCREASE_MODE__ROLL = 0x3; +ConfigConsts.C_IMPORT_CLASS_HP_INCREASE_MODE__ROLL_CUSTOM = 0x4; +ConfigConsts.C_IMPORT_CLASS_HP_INCREASE_MODE__DO_NOT_INCREASE = 0x5; +ConfigConsts.C_IMPORT_CLASS_HP_INCREASE_MODE___NAMES = { + [ConfigConsts.C_IMPORT_CLASS_HP_INCREASE_MODE__TAKE_AVERAGE]: "Take Average", + [ConfigConsts.C_IMPORT_CLASS_HP_INCREASE_MODE__MIN]: "Minimum Value", + [ConfigConsts.C_IMPORT_CLASS_HP_INCREASE_MODE__MAX]: "Maximum Value", + [ConfigConsts.C_IMPORT_CLASS_HP_INCREASE_MODE__ROLL]: "Roll", + [ConfigConsts.C_IMPORT_CLASS_HP_INCREASE_MODE__ROLL_CUSTOM]: "Roll (Custom Formula)", + [ConfigConsts.C_IMPORT_CLASS_HP_INCREASE_MODE__DO_NOT_INCREASE]: "Do Not Increase HP" +}; +//#endregion + +//#region Consts +class Consts { + static RUN_TIME = `${Date.now()}`; + static FLAG_IFRAME_URL = "iframe_url"; + + static TERMS_COUNT = [{ + tokens: ["once"], + count: 1 + }, { + tokens: ["twice"], + count: 2 + }, { + tokens: ["thrice"], + count: 3 + }, { + tokens: ["three", " ", "times"], + count: 3 + }, { + tokens: ["four", " ", "times"], + count: 4 + }, ]; + + static Z_INDEX_MAX_FOUNDRY = 9999; + + static ACTOR_TEMP_NAME = "Importing..."; + + static CHAR_MAX_LEVEL = 20; + + static RE_ID_STR = `[A-Za-z0-9]{16}`; + static RE_ID = new RegExp(`^${this.RE_ID_STR}$`); + + static FLAG_IS_DEV_CLEANUP = "isDevCleanup"; + + static USER_DATA_TRACKING_KEYS__ACTOR = ["system.details.biography.value", + "system.attributes.hp.value", "system.attributes.death.success", "system.attributes.death.failure", "system.attributes.exhaustion", "system.attributes.inspiration", + "system.details.xp.value", + "system.resources.primary.value", "system.resources.secondary.value", "system.resources.tertiary.value", "system.resources.legact.value", "system.resources.legres.value", "system.resources.lair.value", + "system.currency.cp", "system.currency.sp", "system.currency.ep", "system.currency.gp", "system.currency.pp", ]; +} +//#endregion +//#region Config +class Config { + + static _IS_INIT = false; + static _IS_INIT_SAVE_REQUIRED = false; + static get backendEndpoint() { + const _0xa1e040 = Config.get("misc", "backendEndpoint"); + if (_0xa1e040) { + return _0xa1e040; + } + return ROUTE_PREFIX ? '/' + ROUTE_PREFIX + "/api/plutonium" : "/api/plutonium"; + } + + static get isInit() { return this._IS_INIT; } + + static prePreInit() { this._preInit_doLoadConfig(); } + + static _preInit_getLoadedConfig() { + //Tries to ask foundry for existing game settings + let existingConfig = UtilGameSettings.getSafe(SharedConsts.MODULE_ID, Config._SETTINGS_KEY); + //If none are found, we create some new ones + if (existingConfig == null || !Object.keys(existingConfig).length) { + return { 'isLoaded': false, 'config': Config._getDefaultGmConfig() }; //Using the default gm config + } + //Or we load the config that was found, and make sure to migrate it incase it is an out of date version + return { 'isLoaded': true, 'config': ConfigMigration.getMigrated({ 'config': existingConfig }) }; + } + + static _preInit_doLoadConfig() { + this._pPrePreInit_registerSettings(); //Registers some settings into foundry + //Tries to load an existing config + const { isLoaded: isLoaded, config: config } = this._preInit_getLoadedConfig(); + Config._CONFIG = config; + if (isLoaded) { //If we managed to load an existing config + const isSaveRequired = this._populateMissingConfigValues(Config._CONFIG, { 'isPlayer': false }); + this._IS_INIT_SAVE_REQUIRED = this._IS_INIT_SAVE_REQUIRED || isSaveRequired; + }//TEMPFIX some boring foundry stuff + /* game.socket.on(this._SOCKET_ID, _0x11513f => { + switch (_0x11513f.type) { + case "config.update": + { + const _0x2829e6 = _0x11513f.config; + const _0x218c03 = MiscUtil.copy(Config._CONFIG); + Object.assign(Config._CONFIG, _0x2829e6); + if (!UtilPrePreInit.isGM()) { + ConfigApp.handleGmConfigUpdate(_0x2829e6); + } + UtilHooks.callAll(UtilHooks.HK_CONFIG_UPDATE, { + 'previous': _0x218c03, + 'current': MiscUtil.copy(Config._CONFIG) + }); + break; + } + } + }); + if (!UtilPrePreInit.isGM()) { + const _0x2f835b = GameStorage.getClient(Config._CLIENT_SETTINGS_KEY); + if (_0x2f835b == null) { + Config._CONFIG_PLAYER = Config._getDefaultPlayerConfig(); + } else { + Config._CONFIG_PLAYER = _0x2f835b; + const _0x2803d0 = this._populateMissingConfigValues(Config._CONFIG_PLAYER, { + 'isPlayer': true + }); + this._IS_INIT_SAVE_REQUIRED = this._IS_INIT_SAVE_REQUIRED || _0x2803d0; + } + } */ + + //Do some check on compability to other modules + this._pInit_initCompatibilityTempOverrides(); + this._IS_INIT = true; + } + + static ['_COMPATIBILITY_TEMP_OVERRIDES'] = null; + static _pInit_initCompatibilityTempOverrides() { + + ConfigConsts.getDefaultConfigSortedFlat_().forEach(([propName, propValue]) => { + Object.entries(propValue).forEach(([k, v]) => { + if (!v.compatibilityModeValues) { return; } + Object.entries(v.compatibilityModeValues).find(([key, val]) => { + const enumVal = v.type === "enum" ? ConfigUtilsSettings.getEnumValueValue(val) : val; + const enumName = v.type === "enum" ? val.name || enumVal : enumVal; + if (!UtilCompat.isModuleActive(key)) { return false; } + + const configVal = Config.get(propName, k); + const isEquals = !CollectionUtil.deepEquals(enumVal, configVal); + Config.setTemp(propName, k, enumVal, { 'isSkipPermissionCheck': true }); + if (isEquals) { + const { displayGroup, displayKey } = Config._getDisplayLabels(propName, k); + const jsonConfig = configVal != null ? JSON.stringify(configVal) : configVal; + const jsonEnum = enumName != null ? JSON.stringify(enumName) : enumName; + this._COMPATIBILITY_TEMP_OVERRIDES = this._COMPATIBILITY_TEMP_OVERRIDES || {}; + MiscUtil.set(this._COMPATIBILITY_TEMP_OVERRIDES, propName, k, { + value: enumVal, + message: "\"" + displayGroup + " -> " + displayKey + "\" value `" + jsonConfig + "` has compatibility issues with module \"" + game.modules.get(key).title + "\" (must be set to `" + jsonEnum + '`)' + }); + console.warn(...LGT, game.modules.get(key).title + " detected! Setting compatibility config: " + + propName + '.' + k + " = " + jsonEnum + " (was " + jsonConfig + "). If you encounter unexpected issues, consider disabling either module."); + } + }); + }); + }); + } + static _hasCompatibilityWarnings() { + return this._COMPATIBILITY_TEMP_OVERRIDES != null; + } + static _getCompatibilityWarnings() { + if (!this._COMPATIBILITY_TEMP_OVERRIDES) { + return ''; + } + const _0x35b741 = Object.values(this._COMPATIBILITY_TEMP_OVERRIDES).map(_0x152dda => Object.values(_0x152dda).map(_0x120f14 => _0x120f14.message)).flat().map(_0xcf5598 => " - " + _0xcf5598).join("\n"); + return "Click to resolve config module compatibility issues. Issues detected:\n" + _0x35b741 + '.'; + } + static ["_doResolveCompatibility"]() { + Object.entries(this._COMPATIBILITY_TEMP_OVERRIDES).forEach(([_0xf82a5d, _0x3d417f]) => { + Object.entries(_0x3d417f).forEach(([_0x10d0e6, _0x5141bd]) => { + Config.set(_0xf82a5d, _0x10d0e6, _0x5141bd.value); + }); + }); + this._COMPATIBILITY_TEMP_OVERRIDES = null; + } + static _pPrePreInit_registerSettings() { + //TEMPFIX + /* game.settings.register(SharedConsts.MODULE_ID, Config._SETTINGS_KEY, { + 'name': 'Config', + 'default': {}, + 'type': Object, + 'scope': "world", + 'onChange': _0x2cf485 => {} + }); */ + } + static pOpen({ + evt = null, + initialVisibleGroup = null + } = {}) { + return ConfigApp.pOpen({ + 'evt': evt, + 'initialVisibleGroup': initialVisibleGroup, + 'backend': this + }); + } + static _populateMissingConfigValues(config, opts) { + opts = opts || {}; + const isPlayer = !!opts.isPlayer; + let _0x811367 = false; + Object.entries(this._getDefaultConfig({ + 'isPlayer': isPlayer + })).forEach(([_0x159930, _0x40e344]) => { + if (!config[_0x159930]) { + config[_0x159930] = _0x40e344; + _0x811367 = true; + } + else { + Object.entries(_0x40e344).forEach(([_0xfb319f, _0x2cb8c5]) => { + if (config[_0x159930][_0xfb319f] === undefined) { + config[_0x159930][_0xfb319f] = _0x2cb8c5; + _0x811367 = true; + } + }); + } + }); + return _0x811367; + } + static async pInit() { + if (this._IS_INIT_SAVE_REQUIRED) { + Config._saveConfigDebounced(); + } + this._IS_INIT_SAVE_REQUIRED = false; + } + static _getDefaultGmConfig() { + return this._getDefaultConfig({ 'isPlayer': false }); + } + static ["_getDefaultPlayerConfig"]() { + return this._getDefaultConfig({ + 'isPlayer': true + }); + } + static _getDefaultConfig(opts) { + opts = opts || {}; + const isPlayer = opts.isPlayer; + const configConsts = MiscUtil.copy(ConfigConsts.getDefaultConfigSorted_()); + const outputConfig = {}; + configConsts.forEach(([propName, propValue]) => { + const _0x5a892a = outputConfig[propName] = {}; + const _0x2a5160 = _0x1fa78d => Object.entries(_0x1fa78d).forEach(([_0x249b71, _0x38a295]) => { + if (isPlayer) { + if (_0x38a295.isPlayerEditable) { + _0x5a892a[_0x249b71] = null; + } + } else { + _0x5a892a[_0x249b71] = _0x38a295["default"]; + } + }); + if (propValue.settings) { + _0x2a5160(propValue.settings); + } + if (propValue.settingsAdvanced) { + _0x2a5160(propValue.settingsAdvanced); + } + if (propValue.settingsHacks) { + _0x2a5160(propValue.settingsHacks); + } + }); + outputConfig.version = ConfigMigration.CURRENT_VERSION; + return outputConfig; + } + static ['set'](_0x4f9d2a, _0x1112ce, _0xc4fc91) { + if (!this._isCanSetConfig(_0x4f9d2a, _0x1112ce)) { + return; + } + const _0x122bb6 = Config.get(_0x4f9d2a, _0x1112ce); + const _0x5ededb = UtilPrePreInit.isGM() ? Config._CONFIG : Config._CONFIG_PLAYER; + (_0x5ededb[_0x4f9d2a] = _0x5ededb[_0x4f9d2a] || {})[_0x1112ce] = _0xc4fc91; + Config._saveConfigDebounced(); + this._fireConfigUpdateHook(_0x4f9d2a, _0x1112ce, _0x122bb6, _0xc4fc91); + } + static ['setTemp'](_0x43e89c, _0x14b600, _0x30dc13, { + isSkipPermissionCheck = false + } = {}) { + if (!isSkipPermissionCheck && !this._isCanSetConfig(_0x43e89c, _0x14b600)) { + return; + } + const _0x10d336 = Config.get(_0x43e89c, _0x14b600); + (Config._CONFIG_TEMP[_0x43e89c] = Config._CONFIG_TEMP[_0x43e89c] || {})[_0x14b600] = _0x30dc13; + this._fireConfigUpdateHook(_0x43e89c, _0x14b600, _0x10d336, _0x30dc13); + } + static ["setRivetTargetDocument"]({ + actor: _0x7aa906, + pack: _0x11cb33 + } = {}) { + if (_0x7aa906 && _0x11cb33) { + throw new Error("Only one of \"actor\" or \"pack\" may be specified!"); + } + if (!_0x7aa906 && !_0x11cb33) { + ui.notifications.info("Cleared Rivet import target. Rivet will now import to an appropriate directory."); + Config.set("rivet", "targetDocumentId", null); + return; + } + if (_0x7aa906) { + const _0x4dae8a = _0x7aa906.isToken ? _0x7aa906.uuid : _0x7aa906.id; + if (Config.get("rivet", "targetDocumentId") === _0x4dae8a) { + Config.set("rivet", "targetDocumentId", null); + ui.notifications.warn("Cleared Rivet import target. Rivet will now import to an appropriate directory."); + return; + } + Config.set("rivet", 'targetDocumentId', _0x4dae8a); + ui.notifications.info("Set Rivet import target. Rivet will now import to Actor \"" + _0x7aa906.name + "\" (" + _0x4dae8a + "). This can be changed in the Config."); + return; + } + if (_0x11cb33) { + const _0xef065a = 'Compendium.' + _0x11cb33.metadata.id; + if (Config.get('rivet', 'targetDocumentId') === _0xef065a) { + Config.set("rivet", "targetDocumentId", null); + ui.notifications.warn("Cleared Rivet import target. Rivet will now import to an appropriate directory."); + return; + } + Config.set('rivet', "targetDocumentId", _0xef065a); + ui.notifications.info("Set Rivet import target. Rivet will now import to Compendium \"" + _0x11cb33.metadata.label + "\" (" + _0x11cb33.metadata.id + "). This can be changed in the Config."); + } + } + static ["_fireConfigUpdateHook"](_0x374034, _0xb156d3, _0x305b29, _0xea010c) { + UtilHooks.callAll(UtilHooks.HK_CONFIG_UPDATE, { + 'previous': { + [_0x374034]: { + [_0xb156d3]: _0x305b29 + } + }, + 'current': { + [_0x374034]: { + [_0xb156d3]: _0xea010c + } + } + }); + } + static ["_isCanSetConfig"](_0x347dfb, _0x3965df) { + return UtilPrePreInit.isGM() || ConfigUtilsSettings.isPlayerEditable(_0x347dfb, _0x3965df); + } + static ["_LOCK_SAVE_CONFIG"] = new VeLock({ + 'name': "save config" + }); + static async ["_pSaveConfig"]() { + try { + await this._LOCK_SAVE_CONFIG.pLock(); + await this._pSaveConfig_(); + } finally { + this._LOCK_SAVE_CONFIG.unlock(); + } + } + static async ["_pSaveConfig_"]() { + if (!UtilPrePreInit.isGM()) { + await GameStorage.pSetClient(Config._CLIENT_SETTINGS_KEY, MiscUtil.copy(Config._CONFIG_PLAYER)); + return; + } + await game.settings.set(SharedConsts.MODULE_ID, Config._SETTINGS_KEY, MiscUtil.copy(Config._CONFIG)); + const _0x473106 = { + 'type': "config.update", + 'config': MiscUtil.copy(this._CONFIG) + }; + game.socket.emit(Config._SOCKET_ID, _0x473106); + } + static ["_saveConfigDebounced"] = MiscUtil.throttle(Config._pSaveConfig, 0x64); + + static get(namespace, prop, {isIgnorePlayer = false} = {}) { + if (Config._CONFIG_TEMP[namespace]?.[prop] !== undefined) { + return Config._CONFIG_TEMP[namespace][prop]; + } + if (!UtilPrePreInit.isGM() && ConfigUtilsSettings.isPlayerEditable(namespace, prop) && !isIgnorePlayer) { + const val = (Config._CONFIG_PLAYER[namespace] || {})[prop]; + if (ConfigUtilsSettings.isNullable(namespace, prop) && val === null || val != null) { + return this._get_getValidValue(namespace, prop, val); + } + } + const out = (Config._CONFIG[namespace] || {})[prop]; + return this._get_getValidValue(namespace, prop, out); + } + static _get_getValidValue(namespace, prop, val) { + const match = ConfigConsts.getDefaultConfigSortedFlat_().find(([ns]) => ns === namespace)[1][prop]; + if (match.type !== "enum") { return val; } + if (match.isNullable && val == null) { return val; } + const enumValues = ConfigUtilsSettings.getEnumValues(match); + if (val == null || !enumValues.some(enumVal => (enumVal.value ?? enumVal) === val)) { + return match["default"] ?? enumValues[0].value ?? enumValues[0]; + } + return val; + } + static ["_getDisplayLabels"](_0x288279, _0xf85c74) { + const _0x6262b3 = ConfigConsts.getDefaultConfig_(); + const _0x5cebfe = _0x6262b3[_0x288279]?.["name"]; + const _0x956382 = _0x6262b3[_0x288279]?.["settings"]?.[_0xf85c74]?.['name'] || _0x6262b3[_0x288279]?.['settingsAdvanced']?.[_0xf85c74]?.["name"] || _0x6262b3[_0x288279]?.['settingsHacks']?.[_0xf85c74]?.['name']; + return { + 'displayGroup': _0x5cebfe, + 'displayKey': _0x956382 + }; + } + static ['has'](_0x23c99c, _0x413db8) { + return !!ConfigConsts.getDefaultConfigSortedFlat_().find(([_0x24a9a0]) => _0x24a9a0 === _0x23c99c)?.[0x1]?.[_0x413db8]; + } + static ["getSafe"](_0x48e921, _0x55bcb1) { + try { + return this.get(_0x48e921, _0x55bcb1); + } catch (_0x3f1440) { + return undefined; + } + } + static ["handleFailedInitConfigApplication"](_0x26ed85, _0x525fb9, _0x1a25c6) { + const { + displayGroup: _0x576794, + displayKey: _0x1c32ad + } = Config._getDisplayLabels(_0x26ed85, _0x525fb9); + ui.notifications.error("Failed to apply Config \"" + _0x1c32ad + "\" -> \"" + _0x576794 + "\" during initial load! " + VeCt.STR_SEE_CONSOLE); + if (_0x1a25c6) { + console.error(...LGT, _0x1a25c6); + } + } + static ["isUseMetricDistance"]({ + configGroup: _0x4095b3, + configKey = "isMetricDistance" + }) { + return Config.get("import", "isGlobalMetricDistance") || Config.has(_0x4095b3, configKey) && Config.get(_0x4095b3, configKey); + } + static ["isUseMetricWeight"]({ + configGroup: _0x335df5, + configKey = "isMetricWeight" + }) { + if (UtilGameSettings.getSafe(game.system.id, "metricWeightUnits")) { + return true; + } + return Config.get('import', "isGlobalMetricWeight") || Config.has(_0x335df5, configKey) && Config.get(_0x335df5, configKey); + } + static ["getMetricNumberDistance"]({ + configGroup: _0x287496, + originalValue: _0x2525c8, + originalUnit: _0x15cbf9, + configKey = "isMetricDistance", + toFixed: _0x4780c3 + }) { + return this._getMetricNumber({ + 'configGroup': _0x287496, + 'originalValue': _0x2525c8, + 'originalUnit': _0x15cbf9, + 'configKey': configKey, + 'fnIsUse': Config.isUseMetricDistance.bind(Config), + 'toFixed': _0x4780c3 + }); + } + static ['getMetricNumberWeight']({ + configGroup: _0x472563, + originalValue: _0x3bcac6, + originalUnit: _0x2715b2, + configKey = "isMetricWeight", + toFixed: _0x371dc8 + }) { + return this._getMetricNumber({ + 'configGroup': _0x472563, + 'originalValue': _0x3bcac6, + 'originalUnit': _0x2715b2, + 'configKey': configKey, + 'fnIsUse': Config.isUseMetricWeight.bind(Config), + 'toFixed': _0x371dc8 + }); + } + static ["_getMetricNumber"]({ + configGroup: _0x3f59a7, + originalValue: _0xb3e2df, + originalUnit: _0x48ea42, + configKey: _0x3f6086, + fnIsUse: _0x4e4c24, + toFixed: _0x13e977 + }) { + if (!_0x4e4c24({ + 'configGroup': _0x3f59a7, + 'configKey': _0x3f6086 + })) { + if (_0x13e977) { + return NumberUtil.toFixedNumber(_0xb3e2df, _0x13e977); + } + return _0xb3e2df; + } + return Parser.metric.getMetricNumber({ + 'originalValue': _0xb3e2df, + 'originalUnit': _0x48ea42, + 'toFixed': _0x13e977 ?? 0x3 + }); + } + static ['getMetricUnitDistance']({ + configGroup: _0x293bb4, + originalUnit: _0x3ed5b6, + configKey = "isMetricDistance", + isShortForm = true, + isPlural = false + }) { + return this._getMetricUnit({ + 'configGroup': _0x293bb4, + 'originalUnit': _0x3ed5b6, + 'configKey': configKey, + 'isShortForm': isShortForm, + 'isPlural': isPlural, + 'fnIsUse': Config.isUseMetricDistance.bind(Config) + }); + } + static ["getMetricUnitWeight"]({ + configGroup: _0x486606, + originalUnit: _0x50698d, + configKey = "isMetricWeight", + isShortForm = true, + isPlural = false + }) { + return this._getMetricUnit({ + 'configGroup': _0x486606, + 'originalUnit': _0x50698d, + 'configKey': configKey, + 'isShortForm': isShortForm, + 'isPlural': isPlural, + 'fnIsUse': Config.isUseMetricWeight.bind(Config) + }); + } + static ["_getMetricUnit"]({ + configGroup: _0x50bdc7, + originalUnit: _0x5f0dc8, + configKey: _0x4280fe, + isShortForm: _0x45e3bf, + isPlural: _0x44cab6, + fnIsUse: _0x2933c2 + }) { + if (!_0x2933c2({ + 'configGroup': _0x50bdc7, + 'configKey': _0x4280fe + })) { + if (!_0x45e3bf) { + return _0x5f0dc8; + } + switch (_0x5f0dc8) { + case Parser.UNT_FEET: + return 'ft'; + case Parser.UNT_YARDS: + return 'yd'; + case Parser.UNT_MILES: + return 'mi'; + default: + return _0x5f0dc8; + } + } + return Parser.metric.getMetricUnit({ + 'originalUnit': _0x5f0dc8, + 'isShortForm': _0x45e3bf, + 'isPlural': _0x44cab6 + }); + } + static ["getSpellPointsKey"]({ + actorType: _0x34101f + }) { + return _0x34101f === 'character' ? "spellPointsMode" : "spellPointsModeNpc"; + } + static ["getSpellPointsResource"]({ + isValueKey = false, + isMaxKey = false + } = {}) { + return this._getSpellPsiPointsResource({ + 'configGroup': "importSpell", + 'configKey': "spellPointsResource", + 'configKeyCustom': "spellPointsResourceCustom", + 'isValueKey': isValueKey, + 'isMaxKey': isMaxKey + }); + } + static ["getPsiPointsResource"]({ + isValueKey = false, + isMaxKey = false + } = {}) { + return this._getSpellPsiPointsResource({ + 'configGroup': "importPsionic", + 'configKey': "psiPointsResource", + 'configKeyCustom': "psiPointsResourceCustom", + 'isValueKey': isValueKey, + 'isMaxKey': isMaxKey + }); + } + static ["_getSpellPsiPointsResource"]({ + configGroup: _0x5ff393, + configKey: _0x61a78, + configKeyCustom: _0x27b383, + isValueKey = false, + isMaxKey = false + } = {}) { + if (Config.get(_0x5ff393, _0x61a78) === ConfigConsts.C_SPELL_POINTS_RESOURCE__SHEET_ITEM) { + return ConfigConsts.C_SPELL_POINTS_RESOURCE__SHEET_ITEM; + } + if (isValueKey && isMaxKey) { + throw new Error("Only one of \"isValue\" and \"isMax\" may be specified!"); + } + const _0x265e6e = Config.get(_0x5ff393, _0x61a78) === ConfigConsts.C_SPELL_POINTS_RESOURCE__ATTRIBUTE_CUSTOM ? Config.get(_0x5ff393, _0x27b383) : Config.get(_0x5ff393, _0x61a78); + return isValueKey ? _0x265e6e + ".value" : isMaxKey ? _0x265e6e + ".max" : _0x265e6e; + } +} +Config._SETTINGS_KEY = "config"; +Config._CLIENT_SETTINGS_KEY = SharedConsts.MODULE_ID + '_config'; +Config._SOCKET_ID = "module." + SharedConsts.MODULE_ID; +Config._CONFIG = {}; +Config._CONFIG_PLAYER = {}; +Config._CONFIG_TEMP = {}; +//#endregion + +//#region ConfigMigration +class _ConfigMigratorBase { + _versionFrom; + _versionTo; + + get versionFrom() { + return this._versionFrom; + } + get versionTo() { + return this._versionTo; + } + + getMigratedForward({config, versionCurrent, versionTarget}) { + if (versionCurrent !== this._versionFrom) + return config; + if (this._versionTo > versionTarget) + return config; + return this._getMigratedForward({ + config + }); + } + + _getMigratedForward({config}) { + throw new Error("Unimplemented!"); + } + + _mutMoveProp({config, groupSource, groupDestination, prop}) { + if (!(prop in config[groupSource] || {})) + return; + + (config[groupDestination] ||= {})[prop] = config[groupSource][prop]; + delete config[groupSource][prop]; + + if (!Object.keys(config[groupSource]).length) + delete config[groupSource]; + } +} + +class _ConfigMigratorZeroToOne extends _ConfigMigratorBase { + _versionFrom = 0; + _versionTo = 1; + + _getMigratedForward({config}) { + config = MiscUtil.copyFast(config); + + ["isLoadLocalPrereleaseIndex", "localPrereleaseDirectoryPath", "isUseLocalPrereleaseIndexJson", "localPrerelease", "isLoadLocalHomebrewIndex", "localHomebrewDirectoryPath", "isUseLocalHomebrewIndexJson", "localHomebrew", "baseSiteUrl", "isNoLocalData", "isNoPrereleaseBrewIndexes", "basePrereleaseUrl", "baseBrewUrl", ].forEach(prop=>{ + this._mutMoveProp({ + config, + groupSource: "import", + groupDestination: "dataSources", + prop, + }); + } + ); + + return config; + } +} +class ConfigMigration { + static _MIGRATORS = [new _ConfigMigratorZeroToOne(), ]; + + static get CURRENT_VERSION() { + return Math.max(...this._MIGRATORS.map(it=>it.versionTo)); + } + + static _IS_INIT = false; + static _init() { + if (this._IS_INIT) + return; + this._IS_INIT = true; + + const cnts = {}; + this._MIGRATORS.forEach(({versionFrom, versionTo})=>{ + cnts[versionFrom] = (cnts[versionFrom] || 0) + 1; + cnts[versionTo] = (cnts[versionTo] || 0) + 1; + } + ); + if (Object.values(cnts).some(it=>it > 0)) + throw new Error(`Multiple Config migrations defined for one or more versions! This is a bug!`); + } + + static getMigrated({config}) { + if (!config) + return config; + + const versionFrom = config.version ?? 0; + const versionTarget = this.CURRENT_VERSION; + + const migrators = this._MIGRATORS.slice(versionFrom); + if (!migrators.length) + return config; + + let versionCurrent = versionFrom; + for (const migrator of migrators) { + config = migrator.getMigratedForward({ + config, + versionCurrent, + versionTarget + }); + versionCurrent = migrator.versionTo; + } + + config.version = versionCurrent; + + return config; + } +} +//#endregion + +//#region ConfigUtilSettings +class ConfigUtilsSettings { + static getEnumValues(meta) { + return typeof meta.values === "function" ? meta.values() : meta.values; + } + + static getEnumValueValue(val) { + return val.value !== undefined ? val.value : val; + } + + static isPlayerEditable(group, key) { + const meta = this._is_getKeyMeta(group, key); + return !!meta?.isPlayerEditable; + } + + static isNullable(group, key) { + const meta = this._is_getKeyMeta(group, key); + return !!meta?.isNullable; + } + + static _is_getKeyMeta(groupKey, key) { + return ConfigConsts.getDefaultConfigSortedFlat_().find(([groupKey_])=>groupKey_ === groupKey)[1][key]; + } +} + +//#endregion \ No newline at end of file diff --git a/charbuilder/js/plutonium/filter.js b/charbuilder/js/plutonium/filter.js new file mode 100644 index 0000000..8a627f7 --- /dev/null +++ b/charbuilder/js/plutonium/filter.js @@ -0,0 +1,9504 @@ +//#region FilterBox +//TEMP ProxyBase seems to just be a MixedProxyBase +class FilterBox extends ProxyBase +//extends ProxyBase +{ + static TITLE_BTN_RESET = "Reset filters. SHIFT to reset everything."; + + static selectFirstVisible(entryList) { + if (Hist.lastLoadedId == null && !Hist.initialLoad) { + Hist._freshLoad(); + } + + } + + constructor(opts) { + super(); + + this._$iptSearch = opts.$iptSearch; + this._$wrpFormTop = opts.$wrpFormTop; + this._$btnReset = opts.$btnReset; + this._$btnOpen = opts.$btnOpen; + this._$wrpMiniPills = opts.$wrpMiniPills; + this._$btnToggleSummaryHidden = opts.$btnToggleSummaryHidden; + this._filters = opts.filters; + this._isCompact = opts.isCompact; + this._namespace = opts.namespace; + + this._doSaveStateThrottled = MiscUtil.throttle(()=>this._pDoSaveState(), 50); + this.__meta = this._getDefaultMeta(); + if (this._isCompact) + this.__meta.isSummaryHidden = true; + + this._meta = this._getProxy("meta", this.__meta); + this.__minisHidden = {}; + this._minisHidden = this._getProxy("minisHidden", this.__minisHidden); + this.__combineAs = {}; + this._combineAs = this._getProxy("combineAs", this.__combineAs); + this._modalMeta = null; + this._isRendered = false; + + this._cachedState = null; + + this._compSearch = BaseComponent.fromObject({ + search: "" + }); + this._metaIptSearch = null; + + this._filters.forEach(f=>f.filterBox = this); + + this._eventListeners = {}; + } + + get filters() { + return this._filters; + } + + teardown() { + this._filters.forEach(f=>f._doTeardown()); + if (this._modalMeta) + this._modalMeta.doTeardown(); + } + + on(identifier, fn) { + const [eventName,namespace] = identifier.split("."); + (this._eventListeners[eventName] = this._eventListeners[eventName] || []).push({ + namespace, + fn + }); + return this; + } + + off(identifier, fn=null) { + const [eventName,namespace] = identifier.split("."); + this._eventListeners[eventName] = (this._eventListeners[eventName] || []).filter(it=>{ + if (fn != null) + return it.namespace !== namespace || it.fn !== fn; + return it.namespace !== namespace; + } + ); + if (!this._eventListeners[eventName].length) + delete this._eventListeners[eventName]; + return this; + } + + fireChangeEvent() { + this._doSaveStateThrottled(); + this.fireEvent(FilterBox.EVNT_VALCHANGE); + } + + fireEvent(eventName) { + (this._eventListeners[eventName] || []).forEach(it=>it.fn()); + } + + _getNamespacedStorageKey() { + return `${FilterBox._STORAGE_KEY}${this._namespace ? `.${this._namespace}` : ""}`; + } + getNamespacedHashKey(k) { + return `${k || "_".repeat(FilterUtil.SUB_HASH_PREFIX_LENGTH)}${this._namespace ? `.${this._namespace}` : ""}`; + } + + async pGetStoredActiveSources() { + const stored = await StorageUtil.pGetForPage(this._getNamespacedStorageKey()); + if (stored) { + const sourceFilterData = stored.filters[FilterBox.SOURCE_HEADER]; + if (sourceFilterData) { + const state = sourceFilterData.state; + const blue = []; + const white = []; + Object.entries(state).forEach(([src,mode])=>{ + if (mode === 1) + blue.push(src); + else if (mode !== -1) + white.push(src); + } + ); + if (blue.length) + return blue; + else + return white; + } + } + return null; + } + + registerMinisHiddenHook(prop, hook) { + this._addHook("minisHidden", prop, hook); + } + + isMinisHidden(header) { + return !!this._minisHidden[header]; + } + + async pDoLoadState() { + const toLoad = await StorageUtil.pGetForPage(this._getNamespacedStorageKey()); + if (toLoad == null) + return; + this._setStateFromLoaded(toLoad, { + isUserSavedState: true + }); + } + + _setStateFromLoaded(state, {isUserSavedState=false}={}) { + state.box = state.box || {}; + this._proxyAssign("meta", "_meta", "__meta", state.box.meta || {}, true); + this._proxyAssign("minisHidden", "_minisHidden", "__minisHidden", state.box.minisHidden || {}, true); + this._proxyAssign("combineAs", "_combineAs", "__combineAs", state.box.combineAs || {}, true); + this._filters.forEach(it=>it.setStateFromLoaded(state.filters, { + isUserSavedState + })); + } + + _getSaveableState() { + const filterOut = {}; + this._filters.forEach(it=>Object.assign(filterOut, it.getSaveableState())); + return { + box: { + meta: { + ...this.__meta + }, + minisHidden: { + ...this.__minisHidden + }, + combineAs: { + ...this.__combineAs + }, + }, + filters: filterOut, + }; + } + + async _pDoSaveState() { + await StorageUtil.pSetForPage(this._getNamespacedStorageKey(), this._getSaveableState()); + } + + trimState_() { + this._filters.forEach(f=>f.trimState_()); + } + + render() { + if (this._isRendered) { + this._filters.map(f=>f.update()); + return; + } + this._isRendered = true; + + if (this._$wrpFormTop || this._$wrpMiniPills) { + if (!this._$wrpMiniPills) { + this._$wrpMiniPills = $(`
    `).insertAfter(this._$wrpFormTop); + } else { + this._$wrpMiniPills.addClass("fltr__mini-view"); + } + } + + if (this._$btnReset) { + this._$btnReset.title(FilterBox.TITLE_BTN_RESET).click((evt)=>this.reset(evt.shiftKey)); + } + + if (this._$wrpFormTop || this._$btnToggleSummaryHidden) { + if (!this._$btnToggleSummaryHidden) { + this._$btnToggleSummaryHidden = $(``).prependTo(this._$wrpFormTop); + } else if (!this._$btnToggleSummaryHidden.parent().length) { + this._$btnToggleSummaryHidden.prependTo(this._$wrpFormTop); + } + this._$btnToggleSummaryHidden.click(()=>{ + this._meta.isSummaryHidden = !this._meta.isSummaryHidden; + this._doSaveStateThrottled(); + } + ); + const summaryHiddenHook = ()=>{ + this._$btnToggleSummaryHidden.toggleClass("active", !!this._meta.isSummaryHidden); + this._$wrpMiniPills.toggleClass("ve-hidden", !!this._meta.isSummaryHidden); + } + ; + this._addHook("meta", "isSummaryHidden", summaryHiddenHook); + summaryHiddenHook(); + } + + if (this._$wrpFormTop || this._$btnOpen) { + if (!this._$btnOpen) { + this._$btnOpen = $(``).prependTo(this._$wrpFormTop); + } else if (!this._$btnOpen.parent().length) { + this._$btnOpen.prependTo(this._$wrpFormTop); + } + this._$btnOpen.click(()=>this.show()); + } + + const sourceFilter = this._filters.find(it=>it.header === FilterBox.SOURCE_HEADER); + if (sourceFilter) { + const selFnAlt = (val)=>!SourceUtil.isNonstandardSource(val) && !PrereleaseUtil.hasSourceJson(val) && !BrewUtil2.hasSourceJson(val); + const hkSelFn = ()=>{ + if (this._meta.isBrewDefaultHidden) + sourceFilter.setTempFnSel(selFnAlt); + else + sourceFilter.setTempFnSel(null); + sourceFilter.updateMiniPillClasses(); + } + ; + this._addHook("meta", "isBrewDefaultHidden", hkSelFn); + hkSelFn(); + } + + if (this._$wrpMiniPills) + this._filters.map((f,i)=>f.$renderMinis({ + filterBox: this, + isFirst: i === 0, + $wrpMini: this._$wrpMiniPills + })); + } + + async _render_pRenderModal() { + this._isModalRendered = true; + + this._modalMeta = await UiUtil.pGetShowModal({ + isHeight100: true, + isWidth100: true, + isUncappedHeight: true, + isIndestructible: true, + isClosed: true, + isEmpty: true, + title: "Filter", + cbClose: (isDataEntered)=>this._pHandleHide(!isDataEntered), + }); + + const $children = this._filters.map((f,i)=>f.$render({ + filterBox: this, + isFirst: i === 0, + $wrpMini: this._$wrpMiniPills + })); + + this._metaIptSearch = ComponentUiUtil.$getIptStr(this._compSearch, "search", { + decorationRight: "clear", + asMeta: true, + html: `` + }, ); + this._compSearch._addHookBase("search", ()=>{ + const searchTerm = this._compSearch._state.search.toLowerCase(); + this._filters.forEach(f=>f.handleSearch(searchTerm)); + } + ); + + const $btnShowAllFilters = $(``).click(()=>this.showAllFilters()); + const $btnHideAllFilters = $(``).click(()=>this.hideAllFilters()); + + const $btnReset = $(``).click(evt=>this.reset(evt.shiftKey)); + + const $btnSettings = $(``).click(()=>this._pOpenSettingsModal()); + + const $btnSaveAlt = $(``).click(()=>this._modalMeta.doClose(true)); + + const $wrpBtnCombineFilters = $(`
    `); + const $btnCombineFilterSettings = $(``).click(()=>this._pOpenCombineAsModal()); + + const btnCombineFiltersAs = e_({ + tag: "button", + clazz: `btn btn-xs btn-default`, + click: ()=>this._meta.modeCombineFilters = FilterBox._COMBINE_MODES.getNext(this._meta.modeCombineFilters), + title: `"AND" requires every filter to match. "OR" requires any filter to match. "Custom" allows you to specify a combination (every "AND" filter must match; only one "OR" filter must match) .`, + }).appendTo($wrpBtnCombineFilters[0]); + + const hook = ()=>{ + btnCombineFiltersAs.innerText = this._meta.modeCombineFilters === "custom" ? this._meta.modeCombineFilters.uppercaseFirst() : this._meta.modeCombineFilters.toUpperCase(); + if (this._meta.modeCombineFilters === "custom") + $wrpBtnCombineFilters.append($btnCombineFilterSettings); + else + $btnCombineFilterSettings.detach(); + this._doSaveStateThrottled(); + } + ; + this._addHook("meta", "modeCombineFilters", hook); + hook(); + + const $btnSave = $(``).click(()=>this._modalMeta.doClose(true)); + + const $btnCancel = $(``).click(()=>this._modalMeta.doClose(false)); + + $$(this._modalMeta.$modal)`
    +
    +

    Filters

    + ${this._metaIptSearch.$wrp.addClass("mobile__mb-2")} +
    +
    +
    +
    Combine as
    + ${$wrpBtnCombineFilters} +
    +
    +
    + ${$btnShowAllFilters} + ${$btnHideAllFilters} +
    + ${$btnReset} + ${$btnSettings} + ${$btnSaveAlt} +
    +
    +
    +
    + +
    +
    + ${$children} +
    +
    +
    ${$btnSave}${$btnCancel}
    `; + } + + async _pOpenSettingsModal() { + const {$modalInner} = await UiUtil.pGetShowModal({ + title: "Settings" + }); + + UiUtil.$getAddModalRowCb($modalInner, "Deselect Homebrew Sources by Default", this._meta, "isBrewDefaultHidden"); + + UiUtil.addModalSep($modalInner); + + UiUtil.$getAddModalRowHeader($modalInner, "Hide summary for filter...", { + helpText: "The summary is the small red and blue button panel which appear below the search bar." + }); + this._filters.forEach(f=>UiUtil.$getAddModalRowCb($modalInner, f.header, this._minisHidden, f.header)); + + UiUtil.addModalSep($modalInner); + + const $rowResetAlwaysSave = UiUtil.$getAddModalRow($modalInner, "div").addClass("pr-2"); + $rowResetAlwaysSave.append(`Always Save on Close`); + $(``).appendTo($rowResetAlwaysSave).click(async()=>{ + await StorageUtil.pRemove(FilterBox._STORAGE_KEY_ALWAYS_SAVE_UNCHANGED); + JqueryUtil.doToast("Saved!"); + } + ); + } + + async _pOpenCombineAsModal() { + const {$modalInner} = await UiUtil.pGetShowModal({ + title: "Filter Combination Logic" + }); + const $btnReset = $(``).click(()=>{ + Object.keys(this._combineAs).forEach(k=>this._combineAs[k] = "and"); + $sels.forEach($sel=>$sel.val("0")); + } + ); + UiUtil.$getAddModalRowHeader($modalInner, "Combine filters as...", { + $eleRhs: $btnReset + }); + const $sels = this._filters.map(f=>UiUtil.$getAddModalRowSel($modalInner, f.header, this._combineAs, f.header, ["and", "or"], { + fnDisplay: (it)=>it.toUpperCase() + })); + } + + getValues({nxtStateOuter=null}={}) { + const outObj = {}; + this._filters.forEach(f=>Object.assign(outObj, f.getValues({ + nxtState: nxtStateOuter?.filters + }))); + return outObj; + } + + addEventListener(type, listener) { + (this._$wrpFormTop ? this._$wrpFormTop[0] : this._$btnOpen[0]).addEventListener(type, listener); + } + + _mutNextState_reset_meta({tgt}) { + Object.assign(tgt, this._getDefaultMeta()); + } + + _mutNextState_minisHidden({tgt}) { + Object.assign(tgt, this._getDefaultMinisHidden(tgt)); + } + + _mutNextState_combineAs({tgt}) { + Object.assign(tgt, this._getDefaultCombineAs(tgt)); + } + + _reset_meta() { + const nxtBoxState = this._getNextBoxState_base(); + this._mutNextState_reset_meta({ + tgt: nxtBoxState.meta + }); + this._setBoxStateFromNextBoxState(nxtBoxState); + } + + _reset_minisHidden() { + const nxtBoxState = this._getNextBoxState_base(); + this._mutNextState_minisHidden({ + tgt: nxtBoxState.minisHidden + }); + this._setBoxStateFromNextBoxState(nxtBoxState); + } + + _reset_combineAs() { + const nxtBoxState = this._getNextBoxState_base(); + this._mutNextState_combineAs({ + tgt: nxtBoxState.combineAs + }); + this._setBoxStateFromNextBoxState(nxtBoxState); + } + + reset(isResetAll) { + this._filters.forEach(f=>f.reset({ + isResetAll + })); + if (isResetAll) { + this._reset_meta(); + this._reset_minisHidden(); + this._reset_combineAs(); + } + this.render(); + this.fireChangeEvent(); + } + + async show() { + if (!this._isModalRendered) + await this._render_pRenderModal(); + this._cachedState = this._getSaveableState(); + this._modalMeta.doOpen(); + if (this._metaIptSearch?.$ipt) + this._metaIptSearch.$ipt.focus(); + } + + async _pHandleHide(isCancel=false) { + if (this._cachedState && isCancel) { + const curState = this._getSaveableState(); + const hasChanges = !CollectionUtil.deepEquals(curState, this._cachedState); + + if (hasChanges) { + const isSave = await InputUiUtil.pGetUserBoolean({ + title: "Unsaved Changes", + textYesRemember: "Always Save", + textYes: "Save", + textNo: "Discard", + storageKey: FilterBox._STORAGE_KEY_ALWAYS_SAVE_UNCHANGED, + isGlobal: true, + }); + if (isSave) { + this._cachedState = null; + this.fireChangeEvent(); + return; + } else + this._setStateFromLoaded(this._cachedState, { + isUserSavedState: true + }); + } + } else { + this.fireChangeEvent(); + } + + this._cachedState = null; + } + + showAllFilters() { + this._filters.forEach(f=>f.show()); + } + + hideAllFilters() { + this._filters.forEach(f=>f.hide()); + } + + unpackSubHashes(subHashes, {force=false}={}) { + const unpacked = {}; + subHashes.forEach(s=>{ + const unpackedPart = UrlUtil.unpackSubHash(s, true); + if (Object.keys(unpackedPart).length > 1) + throw new Error(`Multiple keys in subhash!`); + const k = Object.keys(unpackedPart)[0]; + unpackedPart[k] = { + clean: unpackedPart[k], + raw: s + }; + Object.assign(unpacked, unpackedPart); + } + ); + + const urlHeaderToFilter = {}; + this._filters.forEach(f=>{ + const childFilters = f.getChildFilters(); + if (childFilters.length) + childFilters.forEach(f=>urlHeaderToFilter[f.header.toLowerCase()] = f); + urlHeaderToFilter[f.header.toLowerCase()] = f; + } + ); + + const urlHeadersUpdated = new Set(); + const subHashesConsumed = new Set(); + let filterInitialSearch; + + const filterBoxState = {}; + const statePerFilter = {}; + const prefixLen = this.getNamespacedHashKey().length; + Object.entries(unpacked).forEach(([hashKey,data])=>{ + const rawPrefix = hashKey.substring(0, prefixLen); + const prefix = rawPrefix.substring(0, FilterUtil.SUB_HASH_PREFIX_LENGTH); + + const urlHeader = hashKey.substring(prefixLen); + + if (FilterUtil.SUB_HASH_PREFIXES.has(prefix) && urlHeaderToFilter[urlHeader]) { + (statePerFilter[urlHeader] = statePerFilter[urlHeader] || {})[prefix] = data.clean; + urlHeadersUpdated.add(urlHeader); + subHashesConsumed.add(data.raw); + return; + } + + if (Object.values(FilterBox._SUB_HASH_PREFIXES).includes(prefix)) { + if (prefix === VeCt.FILTER_BOX_SUB_HASH_SEARCH_PREFIX) + filterInitialSearch = data.clean[0]; + else + filterBoxState[prefix] = data.clean; + subHashesConsumed.add(data.raw); + return; + } + + if (FilterUtil.SUB_HASH_PREFIXES.has(prefix)) + throw new Error(`Could not find filter with header ${urlHeader} for subhash ${data.raw}`); + } + ); + + if (!subHashesConsumed.size && !force) + return null; + + return { + urlHeaderToFilter, + filterBoxState, + statePerFilter, + urlHeadersUpdated, + unpacked, + subHashesConsumed, + filterInitialSearch, + }; + } + + setFromSubHashes(subHashes, {force=false, $iptSearch=null}={}) { + const unpackedSubhashes = this.unpackSubHashes(subHashes, { + force + }); + + if (unpackedSubhashes == null) + return subHashes; + + const {unpacked, subHashesConsumed, filterInitialSearch, } = unpackedSubhashes; + + const {box: nxtStateBox, filters: nxtStatesFilters} = this.getNextStateFromSubHashes({ + unpackedSubhashes + }); + + this._setBoxStateFromNextBoxState(nxtStateBox); + + this._filters.flatMap(f=>[f, ...f.getChildFilters(), ]).filter(filter=>nxtStatesFilters[filter.header]).forEach(filter=>filter.setStateFromNextState(nxtStatesFilters)); + + if (filterInitialSearch && ($iptSearch || this._$iptSearch)) + ($iptSearch || this._$iptSearch).val(filterInitialSearch).change().keydown().keyup().trigger("instantKeyup"); + + const [link] = Hist.getHashParts(); + + const outSub = []; + Object.values(unpacked).filter(v=>!subHashesConsumed.has(v.raw)).forEach(v=>outSub.push(v.raw)); + + Hist.setSuppressHistory(true); + Hist.replaceHistoryHash(`${link}${outSub.length ? `${HASH_PART_SEP}${outSub.join(HASH_PART_SEP)}` : ""}`); + + this.fireChangeEvent(); + Hist.hashChange({ + isBlankFilterLoad: true + }); + return outSub; + } + + getNextStateFromSubHashes({unpackedSubhashes}) { + const {urlHeaderToFilter, filterBoxState, statePerFilter, urlHeadersUpdated, } = unpackedSubhashes; + + const nxtStateBox = this._getNextBoxStateFromSubHashes(urlHeaderToFilter, filterBoxState); + + const nxtStateFilters = {}; + + Object.entries(statePerFilter).forEach(([urlHeader,state])=>{ + const filter = urlHeaderToFilter[urlHeader]; + Object.assign(nxtStateFilters, filter.getNextStateFromSubhashState(state)); + } + ); + + Object.keys(urlHeaderToFilter).filter(k=>!urlHeadersUpdated.has(k)).forEach(k=>{ + const filter = urlHeaderToFilter[k]; + Object.assign(nxtStateFilters, filter.getNextStateFromSubhashState(null)); + } + ); + + return { + box: nxtStateBox, + filters: nxtStateFilters + }; + } + + _getNextBoxState_base() { + return { + meta: MiscUtil.copyFast(this.__meta), + minisHidden: MiscUtil.copyFast(this.__minisHidden), + combineAs: MiscUtil.copyFast(this.__combineAs), + }; + } + + _getNextBoxStateFromSubHashes(urlHeaderToFilter, filterBoxState) { + const nxtBoxState = this._getNextBoxState_base(); + + let hasMeta = false; + let hasMinisHidden = false; + let hasCombineAs = false; + + Object.entries(filterBoxState).forEach(([k,vals])=>{ + const mappedK = this.getNamespacedHashKey(Parser._parse_bToA(FilterBox._SUB_HASH_PREFIXES, k)); + switch (mappedK) { + case "meta": + { + hasMeta = true; + const data = vals.map(v=>UrlUtil.mini.decompress(v)); + Object.keys(this._getDefaultMeta()).forEach((k,i)=>nxtBoxState.meta[k] = data[i]); + break; + } + case "minisHidden": + { + hasMinisHidden = true; + Object.keys(nxtBoxState.minisHidden).forEach(k=>nxtBoxState.minisHidden[k] = false); + vals.forEach(v=>{ + const [urlHeader,isHidden] = v.split("="); + const filter = urlHeaderToFilter[urlHeader]; + if (!filter) + throw new Error(`Could not find filter with name "${urlHeader}"`); + nxtBoxState.minisHidden[filter.header] = !!Number(isHidden); + } + ); + break; + } + case "combineAs": + { + hasCombineAs = true; + Object.keys(nxtBoxState.combineAs).forEach(k=>nxtBoxState.combineAs[k] = "and"); + vals.forEach(v=>{ + const [urlHeader,ixCombineMode] = v.split("="); + const filter = urlHeaderToFilter[urlHeader]; + if (!filter) + throw new Error(`Could not find filter with name "${urlHeader}"`); + nxtBoxState.combineAs[filter.header] = FilterBox._COMBINE_MODES[ixCombineMode] || FilterBox._COMBINE_MODES[0]; + } + ); + break; + } + } + } + ); + + if (!hasMeta) + this._mutNextState_reset_meta({ + tgt: nxtBoxState.meta + }); + if (!hasMinisHidden) + this._mutNextState_minisHidden({ + tgt: nxtBoxState.minisHidden + }); + if (!hasCombineAs) + this._mutNextState_combineAs({ + tgt: nxtBoxState.combineAs + }); + + return nxtBoxState; + } + + _setBoxStateFromNextBoxState(nxtBoxState) { + this._proxyAssignSimple("meta", nxtBoxState.meta, true); + this._proxyAssignSimple("minisHidden", nxtBoxState.minisHidden, true); + this._proxyAssignSimple("combineAs", nxtBoxState.combineAs, true); + } + + getSubHashes(opts) { + opts = opts || {}; + const out = []; + const boxSubHashes = this.getBoxSubHashes(); + if (boxSubHashes) + out.push(boxSubHashes); + out.push(...this._filters.map(f=>f.getSubHashes()).filter(Boolean)); + if (opts.isAddSearchTerm && this._$iptSearch) { + const searchTerm = UrlUtil.encodeForHash(this._$iptSearch.val().trim()); + if (searchTerm) + out.push(UrlUtil.packSubHash(this._getSubhashPrefix("search"), [searchTerm])); + } + return out.flat(); + } + + getBoxSubHashes() { + const out = []; + + const defaultMeta = this._getDefaultMeta(); + + const anyNotDefault = Object.keys(defaultMeta).find(k=>this._meta[k] !== defaultMeta[k]); + if (anyNotDefault) { + const serMeta = Object.keys(defaultMeta).map(k=>UrlUtil.mini.compress(this._meta[k] === undefined ? defaultMeta[k] : this._meta[k])); + out.push(UrlUtil.packSubHash(this._getSubhashPrefix("meta"), serMeta)); + } + + const setMinisHidden = Object.entries(this._minisHidden).filter(([k,v])=>!!v).map(([k])=>`${k.toUrlified()}=1`); + if (setMinisHidden.length) { + out.push(UrlUtil.packSubHash(this._getSubhashPrefix("minisHidden"), setMinisHidden)); + } + + const setCombineAs = Object.entries(this._combineAs).filter(([k,v])=>v !== FilterBox._COMBINE_MODES[0]).map(([k,v])=>`${k.toUrlified()}=${FilterBox._COMBINE_MODES.indexOf(v)}`); + if (setCombineAs.length) { + out.push(UrlUtil.packSubHash(this._getSubhashPrefix("combineAs"), setCombineAs)); + } + + return out.length ? out : null; + } + + getFilterTag({isAddSearchTerm=false}={}) { + const parts = this._filters.map(f=>f.getFilterTagPart()).filter(Boolean); + if (isAddSearchTerm && this._$iptSearch) { + const term = this._$iptSearch.val().trim(); + if (term) + parts.push(`search=${term}`); + } + return `{@filter |${UrlUtil.getCurrentPage().replace(/\.html$/, "")}|${parts.join("|")}}`; + } + + getDisplayState({nxtStateOuter=null}={}) { + return this._filters.map(filter=>filter.getDisplayStatePart({ + nxtState: nxtStateOuter?.filters + })).filter(Boolean).join("; "); + } + + /** + * Each of the objects inside 'values' will be matched with the header of any of this FilterBox's sub-filters. + * You can get the headers of the sub-filters by looping through .filters and checking their 'header' property. + * Each filter that matches their header with the object's name will have all of their filters set to 0 (false / do not let through). + * The filter will then write values from the object (like Sources in this example). + * Once complete, a change event will be fired. + * @param {any} values example: {Source: {PHB: 1, XGE: 0}} + */ + setFromValues(values) { + this._filters.forEach(it=>it.setFromValues(values)); + this.fireChangeEvent(); + } + + toDisplay(boxState, ...entryVals) { + return this._toDisplay(boxState, this._filters, entryVals); + } + + toDisplayByFilters(boxState, ...filterToValueTuples) { + return this._toDisplay(boxState, filterToValueTuples.map(it=>it.filter), filterToValueTuples.map(it=>it.value), ); + } + + _toDisplay(boxState, filters, entryVals) { + switch (this._meta.modeCombineFilters) { + case "and": + return this._toDisplay_isAndDisplay(boxState, filters, entryVals); + case "or": + return this._toDisplay_isOrDisplay(boxState, filters, entryVals); + case "custom": + { + if (entryVals.length !== filters.length) + throw new Error(`Number of filters and number of values did not match!`); + + const andFilters = []; + const andValues = []; + const orFilters = []; + const orValues = []; + + for (let i = 0; i < filters.length; ++i) { + const f = filters[i]; + if (!this._combineAs[f.header] || this._combineAs[f.header] === "and") { + andFilters.push(f); + andValues.push(entryVals[i]); + } else { + orFilters.push(f); + orValues.push(entryVals[i]); + } + } + + return this._toDisplay_isAndDisplay(boxState, andFilters, andValues) && this._toDisplay_isOrDisplay(boxState, orFilters, orValues); + } + default: + throw new Error(`Unhandled combining mode "${this._meta.modeCombineFilters}"`); + } + } + + _toDisplay_isAndDisplay(boxState, filters, vals) { + return filters.map((f,i)=>f.toDisplay(boxState, vals[i])).every(it=>it); + } + + _toDisplay_isOrDisplay(boxState, filters, vals) { + const res = filters.map((f,i)=>{ + if (!f.isActive(boxState)) + return null; + return f.toDisplay(boxState, vals[i]); + } + ).filter(it=>it != null); + return res.length === 0 || res.find(it=>it); + } + + _getSubhashPrefix(prop) { + if (FilterBox._SUB_HASH_PREFIXES[prop]) + return this.getNamespacedHashKey(FilterBox._SUB_HASH_PREFIXES[prop]); + throw new Error(`Unknown property "${prop}"`); + } + + _getDefaultMeta() { + const out = MiscUtil.copy(FilterBox._DEFAULT_META); + if (this._isCompact) + out.isSummaryHidden = true; + return out; + } + + _getDefaultMinisHidden(minisHidden) { + if (!minisHidden) + throw new Error(`Missing "minisHidden" argument!`); + return Object.keys(minisHidden).mergeMap(k=>({ + [k]: false + })); + } + + _getDefaultCombineAs(combineAs) { + if (!combineAs) + throw new Error(`Missing "combineAs" argument!`); + return Object.keys(combineAs).mergeMap(k=>({ + [k]: "and" + })); + } +}; +FilterBox.EVNT_VALCHANGE = "valchange"; +FilterBox.SOURCE_HEADER = "Source"; +FilterBox._PILL_STATES = ["ignore", "yes", "no"]; +FilterBox._COMBINE_MODES = ["and", "or", "custom"]; +FilterBox._STORAGE_KEY = "filterBoxState"; +FilterBox._DEFAULT_META = { + modeCombineFilters: "and", + isSummaryHidden: false, + isBrewDefaultHidden: false, +}; +FilterBox._STORAGE_KEY_ALWAYS_SAVE_UNCHANGED = "filterAlwaysSaveUnchanged"; + +FilterBox._SUB_HASH_BOX_META_PREFIX = "fbmt"; +FilterBox._SUB_HASH_BOX_MINIS_HIDDEN_PREFIX = "fbmh"; +FilterBox._SUB_HASH_BOX_COMBINE_AS_PREFIX = "fbca"; +FilterBox._SUB_HASH_PREFIXES = { + meta: FilterBox._SUB_HASH_BOX_META_PREFIX, + minisHidden: FilterBox._SUB_HASH_BOX_MINIS_HIDDEN_PREFIX, + combineAs: FilterBox._SUB_HASH_BOX_COMBINE_AS_PREFIX, + search: VeCt.FILTER_BOX_SUB_HASH_SEARCH_PREFIX, +}; +//#endregion + +//#region FilterItem +let FilterItem$1 = class FilterItem { + constructor(options) { + this.item = options.item; + this.pFnChange = options.pFnChange; + this.group = options.group; + this.nest = options.nest; + this.nestHidden = options.nestHidden; + this.isIgnoreRed = options.isIgnoreRed; + this.userData = options.userData; + + this.rendered = null; + this.searchText = null; + } +}; + +globalThis.FilterItem = FilterItem$1; +//#endregion + +//#region Filter & FilterBase +class FilterBase extends BaseComponent { + constructor(opts) { + super(); + this._filterBox = null; + + this.header = opts.header; + this._headerHelp = opts.headerHelp; + + this.__meta = { + ...this.getDefaultMeta() + }; + this._meta = this._getProxy("meta", this.__meta); + + this._hasUserSavedState = false; + } + + _getRenderedHeader() { + return `${this.header}`; + } + + set filterBox(it) { + this._filterBox = it; + } + + show() { + this._meta.isHidden = false; + } + + hide() { + this._meta.isHidden = true; + } + + getBaseSaveableState() { + return { + meta: { + ...this.__meta + } + }; + } + + _getNextState_base() { + return { + [this.header]: { + state: MiscUtil.copyFast(this.__state), + meta: MiscUtil.copyFast(this.__meta), + }, + }; + } + + setStateFromNextState(nxtState) { + this._proxyAssignSimple("state", nxtState[this.header].state, true); + this._proxyAssignSimple("meta", nxtState[this.header].meta, true); + } + + reset({isResetAll=false}={}) { + const nxtState = this._getNextState_base(); + this._mutNextState_reset(nxtState, { + isResetAll + }); + this.setStateFromNextState(nxtState); + } + + _mutNextState_resetBase(nxtState, {isResetAll=false}={}) { + Object.assign(nxtState[this.header].meta, MiscUtil.copy(this.getDefaultMeta())); + } + + getMetaSubHashes() { + const compressedMeta = this._getCompressedMeta(); + if (!compressedMeta) + return null; + return [UrlUtil.packSubHash(this.getSubHashPrefix("meta", this.header), compressedMeta)]; + } + + _mutNextState_meta_fromSubHashState(nxtState, subHashState) { + const hasMeta = this._mutNextState_meta_fromSubHashState_mutGetHasMeta(nxtState, subHashState, this.getDefaultMeta()); + if (!hasMeta) + this._mutNextState_resetBase(nxtState); + } + + _mutNextState_meta_fromSubHashState_mutGetHasMeta(nxtState, state, defaultMeta) { + let hasMeta = false; + + Object.entries(state).forEach(([k,vals])=>{ + const prop = FilterBase.getProp(k); + if (prop !== "meta") + return; + + hasMeta = true; + const data = vals.map(v=>UrlUtil.mini.decompress(v)); + Object.keys(defaultMeta).forEach((k,i)=>{ + if (data[i] !== undefined) + nxtState[this.header].meta[k] = data[i]; + else + nxtState[this.header].meta[k] = defaultMeta[k]; + } + ); + } + ); + + return hasMeta; + } + + setBaseStateFromLoaded(toLoad) { + Object.assign(this._meta, toLoad.meta); + } + + getSubHashPrefix(prop, header) { + if (FilterBase._SUB_HASH_PREFIXES[prop]) { + const prefix = this._filterBox.getNamespacedHashKey(FilterBase._SUB_HASH_PREFIXES[prop]); + return `${prefix}${header.toUrlified()}`; + } + throw new Error(`Unknown property "${prop}"`); + } + + static getProp(prefix) { + return Parser._parse_bToA(FilterBase._SUB_HASH_PREFIXES, prefix); + } + + _getBtnMobToggleControls(wrpControls) { + const btnMobToggleControls = e_({ + tag: "button", + clazz: `btn btn-xs btn-default mobile__visible ml-auto px-3 mr-2`, + html: ``, + click: ()=>this._meta.isMobileHeaderHidden = !this._meta.isMobileHeaderHidden, + }); + const hkMobHeaderHidden = ()=>{ + btnMobToggleControls.toggleClass("active", !this._meta.isMobileHeaderHidden); + wrpControls.toggleClass("mobile__hidden", !!this._meta.isMobileHeaderHidden); + } + ; + this._addHook("meta", "isMobileHeaderHidden", hkMobHeaderHidden); + hkMobHeaderHidden(); + + return btnMobToggleControls; + } + + getChildFilters() { + return []; + } + getDefaultMeta() { + return { + ...FilterBase._DEFAULT_META + }; + } + + isActive(vals) { + vals = vals || this.getValues(); + return vals[this.header]._isActive; + } + + _getCompressedMeta({isStripUiKeys=false}={}) { + const defaultMeta = this.getDefaultMeta(); + const isAnyNotDefault = Object.keys(defaultMeta).some(k=>this._meta[k] !== defaultMeta[k]); + if (!isAnyNotDefault) + return null; + + let keys = Object.keys(defaultMeta); + + if (isStripUiKeys) { + const popCount = Object.keys(FilterBase._DEFAULT_META).length; + if (popCount) + keys = keys.slice(0, -popCount); + } + + while (keys.length && defaultMeta[keys.last()] === this._meta[keys.last()]) + keys.pop(); + + return keys.map(k=>UrlUtil.mini.compress(this._meta[k] === undefined ? defaultMeta[k] : this._meta[k])); + } + + $render() { + throw new Error(`Unimplemented!`); + } + $renderMinis() { + throw new Error(`Unimplemented!`); + } + getValues({nxtState=null}={}) { + throw new Error(`Unimplemented!`); + } + _mutNextState_reset() { + throw new Error(`Unimplemented!`); + } + update() { + throw new Error(`Unimplemented!`); + } + toDisplay() { + throw new Error(`Unimplemented!`); + } + addItem() { + throw new Error(`Unimplemented!`); + } + getSaveableState() { + throw new Error(`Unimplemented!`); + } + setStateFromLoaded() { + throw new Error(`Unimplemented!`); + } + getSubHashes() { + throw new Error(`Unimplemented!`); + } + getNextStateFromSubhashState() { + throw new Error(`Unimplemented!`); + } + setFromValues() { + throw new Error(`Unimplemented!`); + } + handleSearch() { + throw new Error(`Unimplemented`); + } + getFilterTagPart() { + throw new Error(`Unimplemented`); + } + getDisplayStatePart({nxtState=null}={}) { + throw new Error(`Unimplemented`); + } + _doTeardown() {} + trimState_() {} +} +FilterBase._DEFAULT_META = { + isHidden: false, + isMobileHeaderHidden: true, +}; +FilterBase._SUB_HASH_STATE_PREFIX = "flst"; +FilterBase._SUB_HASH_META_PREFIX = "flmt"; +FilterBase._SUB_HASH_NESTS_HIDDEN_PREFIX = "flnh"; +FilterBase._SUB_HASH_OPTIONS_PREFIX = "flop"; +FilterBase._SUB_HASH_PREFIXES = { + state: FilterBase._SUB_HASH_STATE_PREFIX, + meta: FilterBase._SUB_HASH_META_PREFIX, + nestsHidden: FilterBase._SUB_HASH_NESTS_HIDDEN_PREFIX, + options: FilterBase._SUB_HASH_OPTIONS_PREFIX, +}; + +class Filter extends FilterBase { + + constructor(opts) { + super(opts); + this._items = Filter._getAsFilterItems(opts.items || []); + this.__itemsSet = new Set(this._items.map(it=>it.item)); + this._nests = opts.nests; + this._displayFn = opts.displayFn; + this._displayFnMini = opts.displayFnMini; + this._displayFnTitle = opts.displayFnTitle; + this._selFn = opts.selFn; + this._selFnCache = null; + this._deselFn = opts.deselFn; + this._itemSortFn = opts.itemSortFn === undefined ? SortUtil.ascSort : opts.itemSortFn; + this._itemSortFnMini = opts.itemSortFnMini; + this._groupFn = opts.groupFn; + this._minimalUi = opts.minimalUi; + this._umbrellaItems = Filter._getAsFilterItems(opts.umbrellaItems); + this._umbrellaExcludes = Filter._getAsFilterItems(opts.umbrellaExcludes); + this._isSortByDisplayItems = !!opts.isSortByDisplayItems; + this._isReprintedFilter = !!opts.isMiscFilter && this._items.some(it=>it.item === "Reprinted"); + this._isSrdFilter = !!opts.isMiscFilter && this._items.some(it=>it.item === "SRD"); + this._isBasicRulesFilter = !!opts.isMiscFilter && this._items.some(it=>it.item === "Basic Rules"); + + Filter._validateItemNests(this._items, this._nests); + + this._filterBox = null; + this._items.forEach(it=>this._defaultItemState(it, { + isForce: true + })); + this.__$wrpFilter = null; + this.__wrpPills = null; + this.__wrpMiniPills = null; + this.__$wrpNestHeadInner = null; + this._updateNestSummary = null; + this.__nestsHidden = {}; + this._nestsHidden = this._getProxy("nestsHidden", this.__nestsHidden); + this._isNestsDirty = false; + this._isItemsDirty = false; + this._pillGroupsMeta = {}; + } + + get isReprintedFilter() { + return this._isReprintedFilter; + } + get isSrdFilter() { + return this._isSrdFilter; + } + get isBasicRulesFilter() { + return this._isBasicRulesFilter; + } + + getSaveableState() { + return { + [this.header]: { + ...this.getBaseSaveableState(), + state: { + ...this.__state + }, + nestsHidden: { + ...this.__nestsHidden + }, + }, + }; + } + + setStateFromLoaded(filterState, {isUserSavedState=false}={}) { + if (!filterState?.[this.header]) + return; + + const toLoad = filterState[this.header]; + this._hasUserSavedState = this._hasUserSavedState || isUserSavedState; + this.setBaseStateFromLoaded(toLoad); + Object.assign(this._state, toLoad.state); + Object.assign(this._nestsHidden, toLoad.nestsHidden); + } + + _getStateNotDefault({nxtState=null}={}) { + const state = nxtState?.[this.header]?.state || this.__state; + + return Object.entries(state).filter(([k,v])=>{ + if (k.startsWith("_")) + return false; + const defState = this._getDefaultState(k); + return defState !== v; + } + ); + } + + getSubHashes() { + const out = []; + + const baseMeta = this.getMetaSubHashes(); + if (baseMeta) + out.push(...baseMeta); + + const areNotDefaultState = this._getStateNotDefault(); + if (areNotDefaultState.length) { + const serPillStates = areNotDefaultState.map(([k,v])=>`${k.toUrlified()}=${v}`); + out.push(UrlUtil.packSubHash(this.getSubHashPrefix("state", this.header), serPillStates)); + } + + const areNotDefaultNestsHidden = Object.entries(this._nestsHidden).filter(([k,v])=>this._nests[k] && !(this._nests[k].isHidden === v)); + if (areNotDefaultNestsHidden.length) { + const nestsHidden = areNotDefaultNestsHidden.map(([k])=>`${k.toUrlified()}=1`); + out.push(UrlUtil.packSubHash(this.getSubHashPrefix("nestsHidden", this.header), nestsHidden)); + } + + if (!out.length) + return null; + + out.push(UrlUtil.packSubHash(this.getSubHashPrefix("options", this.header), ["extend"])); + return out; + } + + getFilterTagPart() { + const areNotDefaultState = this._getStateNotDefault(); + const compressedMeta = this._getCompressedMeta({ + isStripUiKeys: true + }); + + if (!areNotDefaultState.length && !compressedMeta) + return null; + + const pt = Object.entries(this._state).filter(([k])=>!k.startsWith("_")).filter(([,v])=>v).map(([k,v])=>`${v === 2 ? "!" : ""}${k}`).join(";").toLowerCase(); + + return [this.header.toLowerCase(), pt, compressedMeta ? compressedMeta.join(HASH_SUB_LIST_SEP) : null, ].filter(it=>it != null).join("="); + } + + getDisplayStatePart({nxtState=null}={}) { + const state = nxtState?.[this.header]?.state || this.__state; + + const areNotDefaultState = this._getStateNotDefault({ + nxtState + }); + + if (!areNotDefaultState.length) + return null; + + const ptState = Object.entries(state).filter(([k])=>!k.startsWith("_")).filter(([,v])=>v).map(([k,v])=>{ + const item = this._items.find(item=>`${item.item}` === k); + if (!item) + return null; + return `${v === 2 ? "not " : ""}${this._displayFn ? this._displayFn(item.item, item) : item.item}`; + } + ).filter(Boolean).join(", "); + + if (!ptState) + return null; + + return `${this.header}: ${ptState}`; + } + + _getOptionsFromSubHashState(state) { + const opts = {}; + Object.entries(state).forEach(([k,vals])=>{ + const prop = FilterBase.getProp(k); + switch (prop) { + case "options": + { + vals.forEach(val=>{ + switch (val) { + case "extend": + { + opts.isExtendDefaultState = true; + } + } + } + ); + } + } + } + ); + return new FilterTransientOptions(opts); + } + + setStateFromNextState(nxtState) { + super.setStateFromNextState(nxtState); + this._proxyAssignSimple("nestsHidden", nxtState[this.header].nestsHidden, true); + } + + getNextStateFromSubhashState(state) { + const nxtState = this._getNextState_base(); + + if (state == null) { + this._mutNextState_reset(nxtState); + return nxtState; + } + + this._mutNextState_meta_fromSubHashState(nxtState, state); + const transientOptions = this._getOptionsFromSubHashState(state); + + let hasState = false; + let hasNestsHidden = false; + + Object.entries(state).forEach(([k,vals])=>{ + const prop = FilterBase.getProp(k); + switch (prop) { + case "state": + { + hasState = true; + if (transientOptions.isExtendDefaultState) { + Object.keys(nxtState[this.header].state).forEach(k=>nxtState[this.header].state[k] = this._getDefaultState(k)); + } else { + Object.keys(nxtState[this.header].state).forEach(k=>nxtState[this.header].state[k] = 0); + } + + vals.forEach(v=>{ + const [statePropLower,state] = v.split("="); + const stateProp = Object.keys(nxtState[this.header].state).find(k=>k.toLowerCase() === statePropLower); + if (stateProp) + nxtState[this.header].state[stateProp] = Number(state); + } + ); + break; + } + case "nestsHidden": + { + hasNestsHidden = true; + Object.keys(nxtState[this.header].nestsHidden).forEach(k=>{ + const nestKey = Object.keys(this._nests).find(it=>k.toLowerCase() === it.toLowerCase()); + nxtState[this.header].nestsHidden[k] = this._nests[nestKey] && this._nests[nestKey].isHidden; + } + ); + vals.forEach(v=>{ + const [nestNameLower,state] = v.split("="); + const nestName = Object.keys(nxtState[this.header].nestsHidden).find(k=>k.toLowerCase() === nestNameLower); + if (nestName) + nxtState[this.header].nestsHidden[nestName] = !!Number(state); + } + ); + break; + } + } + } + ); + + if (!hasState) + this._mutNextState_reset(nxtState); + if (!hasNestsHidden && this._nests) + this._mutNextState_resetNestsHidden({ + tgt: nxtState[this.header].nestsHidden + }); + + return nxtState; + } + + /** Disable all our own values and set them according to what values[this.header] says */ + setFromValues(values) { + if (values[this.header]) { + Object.keys(this._state).forEach(k=>this._state[k] = 0); + Object.assign(this._state, values[this.header]); + } + } + + setValue(k, v) { + this._state[k] = v; + } + + _mutNextState_resetNestsHidden({tgt}) { + if (!this._nests) + return; + Object.entries(this._nests).forEach(([nestName,nestMeta])=>tgt[nestName] = !!nestMeta.isHidden); + } + + _defaultItemState(item, {isForce=false}={}) { + if (!isForce && this._hasUserSavedState) + return this._state[item.item] = 0; + + this._state[item.item] = this._getDefaultState(item.item); + } + + _getDefaultState(k) { + return this._deselFn && this._deselFn(k) ? 2 : this._selFn && this._selFn(k) ? 1 : 0; + } + + _getDisplayText(item) { + return this._displayFn ? this._displayFn(item.item, item) : item.item; + } + + _getDisplayTextMini(item) { + return this._displayFnMini ? this._displayFnMini(item.item, item) : this._getDisplayText(item); + } + + _getPill(item) { + const displayText = this._getDisplayText(item); + + const btnPill = e_({ + tag: "div", + clazz: "fltr__pill", + html: displayText, + click: evt=>this._getPill_handleClick({ + evt, + item + }), + contextmenu: evt=>this._getPill_handleContextmenu({ + evt, + item + }), + }); + + this._getPill_bindHookState({ + btnPill, + item + }); + + item.searchText = displayText.toLowerCase(); + + return btnPill; + } + + _getPill_handleClick({evt, item}) { + if (evt.shiftKey) { + this._doSetPillsClear(); + } + + if (++this._state[item.item] > 2) + this._state[item.item] = 0; + } + + _getPill_handleContextmenu({evt, item}) { + evt.preventDefault(); + + if (evt.shiftKey) { + this._doSetPillsClear(); + } + + if (--this._state[item.item] < 0) + this._state[item.item] = 2; + } + + _getPill_bindHookState({btnPill, item}) { + this._addHook("state", item.item, ()=>{ + const val = FilterBox._PILL_STATES[this._state[item.item]]; + btnPill.attr("state", val); + } + )(); + } + + setTempFnSel(tempFnSel) { + this._selFnCache = this._selFnCache || this._selFn; + if (tempFnSel) + this._selFn = tempFnSel; + else + this._selFn = this._selFnCache; + } + + updateMiniPillClasses() { + this._items.filter(it=>it.btnMini).forEach(it=>{ + const isDefaultDesel = this._deselFn && this._deselFn(it.item); + const isDefaultSel = this._selFn && this._selFn(it.item); + it.btnMini.toggleClass("fltr__mini-pill--default-desel", isDefaultDesel).toggleClass("fltr__mini-pill--default-sel", isDefaultSel); + } + ); + } + + _getBtnMini(item) { + const toDisplay = this._getDisplayTextMini(item); + + const btnMini = e_({ + tag: "div", + clazz: `fltr__mini-pill ${this._filterBox.isMinisHidden(this.header) ? "ve-hidden" : ""} ${this._deselFn && this._deselFn(item.item) ? "fltr__mini-pill--default-desel" : ""} ${this._selFn && this._selFn(item.item) ? "fltr__mini-pill--default-sel" : ""}`, + html: toDisplay, + title: `${this._displayFnTitle ? `${this._displayFnTitle(item.item, item)} (` : ""}Filter: ${this.header}${this._displayFnTitle ? ")" : ""}`, + click: ()=>{ + this._state[item.item] = 0; + this._filterBox.fireChangeEvent(); + } + , + }).attr("state", FilterBox._PILL_STATES[this._state[item.item]]); + + const hook = ()=>{ + const val = FilterBox._PILL_STATES[this._state[item.item]]; + btnMini.attr("state", val); + if (item.pFnChange) + item.pFnChange(item.item, val); + } + ; + this._addHook("state", item.item, hook); + + const hideHook = ()=>btnMini.toggleClass("ve-hidden", this._filterBox.isMinisHidden(this.header)); + this._filterBox.registerMinisHiddenHook(this.header, hideHook); + + return btnMini; + } + + _doSetPillsAll() { + this._proxyAssignSimple("state", Object.keys(this._state).mergeMap(k=>({ + [k]: 1 + })), true, ); + } + + _doSetPillsClear() { + this._proxyAssignSimple("state", Object.keys(this._state).mergeMap(k=>({ + [k]: 0 + })), true, ); + } + + _doSetPillsNone() { + this._proxyAssignSimple("state", Object.keys(this._state).mergeMap(k=>({ + [k]: 2 + })), true, ); + } + + _doSetPinsDefault() { + this.reset(); + } + + _getHeaderControls(opts) { + const btnAll = e_({ + tag: "button", + clazz: `btn btn-default ${opts.isMulti ? "btn-xxs" : "btn-xs"} fltr__h-btn--all w-100`, + click: ()=>this._doSetPillsAll(), + html: "All", + }); + const btnClear = e_({ + tag: "button", + clazz: `btn btn-default ${opts.isMulti ? "btn-xxs" : "btn-xs"} fltr__h-btn--clear w-100`, + click: ()=>this._doSetPillsClear(), + html: "Clear", + }); + const btnNone = e_({ + tag: "button", + clazz: `btn btn-default ${opts.isMulti ? "btn-xxs" : "btn-xs"} fltr__h-btn--none w-100`, + click: ()=>this._doSetPillsNone(), + html: "None", + }); + const btnDefault = e_({ + tag: "button", + clazz: `btn btn-default ${opts.isMulti ? "btn-xxs" : "btn-xs"} w-100`, + click: ()=>this._doSetPinsDefault(), + html: "Default", + }); + + const wrpStateBtnsOuter = e_({ + tag: "div", + clazz: "ve-flex-v-center fltr__h-wrp-state-btns-outer", + children: [e_({ + tag: "div", + clazz: "btn-group ve-flex-v-center w-100", + children: [btnAll, btnClear, btnNone, btnDefault, ], + }), ], + }); + this._getHeaderControls_addExtraStateBtns(opts, wrpStateBtnsOuter); + + const wrpSummary = e_({ + tag: "div", + clazz: "ve-flex-vh-center ve-hidden" + }); + + const btnCombineBlue = e_({ + tag: "button", + clazz: `btn btn-default ${opts.isMulti ? "btn-xxs" : "btn-xs"} fltr__h-btn-logic--blue fltr__h-btn-logic w-100`, + click: ()=>this._meta.combineBlue = Filter._getNextCombineMode(this._meta.combineBlue), + title: `Blue match mode for this filter. "AND" requires all blues to match, "OR" requires at least one blue to match, "XOR" requires exactly one blue to match.`, + }); + const hookCombineBlue = ()=>e_({ + ele: btnCombineBlue, + text: `${this._meta.combineBlue}`.toUpperCase() + }); + this._addHook("meta", "combineBlue", hookCombineBlue); + hookCombineBlue(); + + const btnCombineRed = e_({ + tag: "button", + clazz: `btn btn-default ${opts.isMulti ? "btn-xxs" : "btn-xs"} fltr__h-btn-logic--red fltr__h-btn-logic w-100`, + click: ()=>this._meta.combineRed = Filter._getNextCombineMode(this._meta.combineRed), + title: `Red match mode for this filter. "AND" requires all reds to match, "OR" requires at least one red to match, "XOR" requires exactly one red to match.`, + }); + const hookCombineRed = ()=>e_({ + ele: btnCombineRed, + text: `${this._meta.combineRed}`.toUpperCase() + }); + this._addHook("meta", "combineRed", hookCombineRed); + hookCombineRed(); + + const btnShowHide = e_({ + tag: "button", + clazz: `btn btn-default ${opts.isMulti ? "btn-xxs" : "btn-xs"} ml-2`, + click: ()=>this._meta.isHidden = !this._meta.isHidden, + html: "Hide", + }); + const hookShowHide = ()=>{ + e_({ + ele: btnShowHide + }).toggleClass("active", this._meta.isHidden); + wrpStateBtnsOuter.toggleVe(!this._meta.isHidden); + + const cur = this.getValues()[this.header]; + + const htmlSummary = [cur._totals.yes ? `${cur._totals.yes}` : null, cur._totals.yes && cur._totals.no ? `` : null, cur._totals.no ? `${cur._totals.no}` : null, ].filter(Boolean).join(""); + e_({ + ele: wrpSummary, + html: htmlSummary + }).toggleVe(this._meta.isHidden); + } + ; + this._addHook("meta", "isHidden", hookShowHide); + hookShowHide(); + + return e_({ + tag: "div", + clazz: `ve-flex-v-center fltr__h-wrp-btns-outer`, + children: [wrpSummary, wrpStateBtnsOuter, e_({ + tag: "span", + clazz: `btn-group ml-2 ve-flex-v-center`, + children: [btnCombineBlue, btnCombineRed] + }), btnShowHide, ], + }); + } + + _getHeaderControls_addExtraStateBtns() {} + + $render(opts) { + this._filterBox = opts.filterBox; + this.__wrpMiniPills = opts.$wrpMini ? e_({ + ele: opts.$wrpMini[0] + }) : null; + + const wrpControls = this._getHeaderControls(opts); + + if (this._nests) { + const wrpNestHead = e_({ + tag: "div", + clazz: "fltr__wrp-pills--sub" + }).appendTo(this.__wrpPills); + this.__$wrpNestHeadInner = e_({ + tag: "div", + clazz: "ve-flex ve-flex-wrap fltr__container-pills" + }).appendTo(wrpNestHead); + + const wrpNestHeadSummary = e_({ + tag: "div", + clazz: "fltr__summary_nest" + }).appendTo(wrpNestHead); + + this._updateNestSummary = ()=>{ + const stats = { + high: 0, + low: 0 + }; + this._items.filter(it=>this._state[it.item] && this._nestsHidden[it.nest]).forEach(it=>{ + const key = this._state[it.item] === 1 ? "high" : "low"; + stats[key]++; + } + ); + + wrpNestHeadSummary.empty(); + + if (stats.high) { + e_({ + tag: "span", + clazz: "fltr__summary_item fltr__summary_item--include", + text: stats.high, + title: `${stats.high} hidden "required" tag${stats.high === 1 ? "" : "s"}`, + }).appendTo(wrpNestHeadSummary); + } + + if (stats.high && stats.low) + e_({ + tag: "span", + clazz: "fltr__summary_item_spacer" + }).appendTo(wrpNestHeadSummary); + + if (stats.low) { + e_({ + tag: "span", + clazz: "fltr__summary_item fltr__summary_item--exclude", + text: stats.low, + title: `${stats.low} hidden "excluded" tag${stats.low === 1 ? "" : "s"}`, + }).appendTo(wrpNestHeadSummary); + } + } + ; + + this._doRenderNests(); + } + + this._doRenderPills(); + + const btnMobToggleControls = this._getBtnMobToggleControls(wrpControls); + + this.__$wrpFilter = $$`
    + ${opts.isFirst ? "" : `
    `} +
    +
    + ${opts.isMulti ? `\u2012` : ""} + ${this._getRenderedHeader()} + ${btnMobToggleControls} +
    + ${wrpControls} +
    + ${this.__wrpPills} +
    `; + + this._doToggleDisplay(); + + return this.__$wrpFilter; + } + + $renderMinis(opts) { + if (!opts.$wrpMini) + return; + + this._filterBox = opts.filterBox; + this.__wrpMiniPills = e_({ + ele: opts.$wrpMini[0] + }); + + this._renderMinis_initWrpPills(); + + this._doRenderMiniPills(); + } + + _renderMinis_initWrpPills() { + this.__wrpPills = e_({ + tag: "div", + clazz: `fltr__wrp-pills ${this._groupFn ? "fltr__wrp-subs" : "fltr__container-pills"}` + }); + const hook = ()=>this.__wrpPills.toggleVe(!this._meta.isHidden); + this._addHook("meta", "isHidden", hook); + hook(); + } + + getValues({nxtState=null}={}) { + const state = MiscUtil.copy(nxtState?.[this.header]?.state || this.__state); + const meta = nxtState?.[this.header]?.meta || this.__meta; + + Object.keys(state).filter(k=>!this._items.some(it=>`${it.item}` === k)).forEach(k=>delete state[k]); + const out = { + ...state + }; + + out._isActive = Object.values(state).some(Boolean); + out._totals = { + yes: 0, + no: 0, + ignored: 0 + }; + Object.values(state).forEach(v=>{ + const totalKey = v === 0 ? "ignored" : v === 1 ? "yes" : "no"; + out._totals[totalKey]++; + } + ); + out._combineBlue = meta.combineBlue; + out._combineRed = meta.combineRed; + return { + [this.header]: out + }; + } + + _getNextState_base() { + return { + [this.header]: { + ...super._getNextState_base()[this.header], + nestsHidden: MiscUtil.copyFast(this.__nestsHidden), + }, + }; + } + + _mutNextState_reset(nxtState, {isResetAll=false}={}) { + if (isResetAll) { + this._mutNextState_resetBase(nxtState); + this._mutNextState_resetNestsHidden({ + tgt: nxtState[this.header].nestsHidden + }); + } else { + Object.assign(nxtState[this.header].meta, { + combineBlue: Filter._DEFAULT_META.combineBlue, + combineRed: Filter._DEFAULT_META.combineRed + }); + } + Object.keys(nxtState[this.header].state).forEach(k=>delete nxtState[this.header].state[k]); + this._items.forEach(item=>nxtState[this.header].state[item.item] = this._getDefaultState(item.item)); + } + + _doRenderPills() { + if (this._itemSortFn) + this._items.sort(this._isSortByDisplayItems && this._displayFn ? (a,b)=>this._itemSortFn(this._displayFn(a.item, a), this._displayFn(b.item, b)) : this._itemSortFn); + + this._items.forEach(it=>{ + if (!it.rendered) { + it.rendered = this._getPill(it); + if (it.nest) { + const hook = ()=>it.rendered.toggleVe(!this._nestsHidden[it.nest]); + this._addHook("nestsHidden", it.nest, hook); + hook(); + } + } + + if (this._groupFn) { + const group = this._groupFn(it); + this._doRenderPills_doRenderWrpGroup(group); + this._pillGroupsMeta[group].wrpPills.append(it.rendered); + } else + it.rendered.appendTo(this.__wrpPills); + } + ); + } + + _doRenderPills_doRenderWrpGroup(group) { + const existingMeta = this._pillGroupsMeta[group]; + if (existingMeta && !existingMeta.isAttached) { + existingMeta.hrDivider.appendTo(this.__wrpPills); + existingMeta.wrpPills.appendTo(this.__wrpPills); + existingMeta.isAttached = true; + } + if (existingMeta) + return; + + this._pillGroupsMeta[group] = { + hrDivider: this._doRenderPills_doRenderWrpGroup_getHrDivider(group).appendTo(this.__wrpPills), + wrpPills: this._doRenderPills_doRenderWrpGroup_getWrpPillsSub(group).appendTo(this.__wrpPills), + isAttached: true, + }; + + Object.entries(this._pillGroupsMeta).sort((a,b)=>SortUtil.ascSortLower(a[0], b[0])).forEach(([groupKey,groupMeta],i)=>{ + groupMeta.hrDivider.appendTo(this.__wrpPills); + groupMeta.hrDivider.toggleVe(!this._isGroupDividerHidden(groupKey, i)); + groupMeta.wrpPills.appendTo(this.__wrpPills); + } + ); + + if (this._nests) { + this._pillGroupsMeta[group].toggleDividerFromNestVisibility = ()=>{ + this._pillGroupsMeta[group].hrDivider.toggleVe(!this._isGroupDividerHidden(group)); + } + ; + + Object.keys(this._nests).forEach(nestName=>{ + const hook = ()=>this._pillGroupsMeta[group].toggleDividerFromNestVisibility(); + this._addHook("nestsHidden", nestName, hook); + hook(); + this._pillGroupsMeta[group].toggleDividerFromNestVisibility(); + } + ); + } + } + + _isGroupDividerHidden(group, ixSortedGroups) { + if (!this._nests) { + if (ixSortedGroups === undefined) + return `${group}` === `${Object.keys(this._pillGroupsMeta).sort((a,b)=>SortUtil.ascSortLower(a, b))[0]}`; + return ixSortedGroups === 0; + } + + const groupItems = this._items.filter(it=>this._groupFn(it) === group); + const hiddenGroupItems = groupItems.filter(it=>this._nestsHidden[it.nest]); + return groupItems.length === hiddenGroupItems.length; + } + + _doRenderPills_doRenderWrpGroup_getHrDivider() { + return e_({ + tag: "hr", + clazz: `fltr__dropdown-divider--sub hr-2 mx-3` + }); + } + _doRenderPills_doRenderWrpGroup_getWrpPillsSub() { + return e_({ + tag: "div", + clazz: `fltr__wrp-pills--sub fltr__container-pills` + }); + } + + _doRenderMiniPills() { + const view = this._items.slice(0); + if (this._itemSortFnMini || this._itemSortFn) { + const fnSort = this._itemSortFnMini || this._itemSortFn; + view.sort(this._isSortByDisplayItems && this._displayFn ? (a,b)=>fnSort(this._displayFn(a.item, a), this._displayFn(b.item, b)) : fnSort); + } + + if (this.__wrpMiniPills) { + view.forEach(it=>{ + (it.btnMini = it.btnMini || this._getBtnMini(it)).appendTo(this.__wrpMiniPills); + } + ); + } + } + + _doToggleDisplay() { + if (this.__$wrpFilter) + this.__$wrpFilter.toggleClass("fltr__no-items", !this._items.length); + } + + _doRenderNests() { + Object.entries(this._nests).sort((a,b)=>SortUtil.ascSort(a[0], b[0])).forEach(([nestName,nestMeta])=>{ + if (nestMeta._$btnNest == null) { + if (this._nestsHidden[nestName] == null) + this._nestsHidden[nestName] = !!nestMeta.isHidden; + + const $btnText = $(`${nestName} [${this._nestsHidden[nestName] ? "+" : "\u2212"}]`); + nestMeta._$btnNest = $$`
    ${$btnText}
    `.click(()=>this._nestsHidden[nestName] = !this._nestsHidden[nestName]); + + const hook = ()=>{ + $btnText.text(`${nestName} [${this._nestsHidden[nestName] ? "+" : "\u2212"}]`); + + const stats = { + high: 0, + low: 0, + total: 0 + }; + this._items.filter(it=>it.nest === nestName).find(it=>{ + const key = this._state[it.item] === 1 ? "high" : this._state[it.item] ? "low" : "ignored"; + stats[key]++; + stats.total++; + } + ); + const allHigh = stats.total === stats.high; + const allLow = stats.total === stats.low; + nestMeta._$btnNest.toggleClass("fltr__btn_nest--include-all", this._nestsHidden[nestName] && allHigh).toggleClass("fltr__btn_nest--exclude-all", this._nestsHidden[nestName] && allLow).toggleClass("fltr__btn_nest--include", this._nestsHidden[nestName] && !!(!allHigh && !allLow && stats.high && !stats.low)).toggleClass("fltr__btn_nest--exclude", this._nestsHidden[nestName] && !!(!allHigh && !allLow && !stats.high && stats.low)).toggleClass("fltr__btn_nest--both", this._nestsHidden[nestName] && !!(!allHigh && !allLow && stats.high && stats.low)); + + if (this._updateNestSummary) + this._updateNestSummary(); + } + ; + + this._items.filter(it=>it.nest === nestName).find(it=>{ + this._addHook("state", it.item, hook); + } + ); + + this._addHook("nestsHidden", nestName, hook); + hook(); + } + nestMeta._$btnNest.appendTo(this.__$wrpNestHeadInner); + } + ); + + if (this._updateNestSummary) + this._updateNestSummary(); + } + + update() { + if (this._isNestsDirty) { + this._isNestsDirty = false; + + this._doRenderNests(); + } + + if (this._isItemsDirty) { + this._isItemsDirty = false; + + this._doRenderPills(); + } + + this._doRenderMiniPills(); + this._doToggleDisplay(); + } + + /** + * Adds an item to the filter + * @param {any} item + */ + addItem(item) { + if (item == null) + return; + + if (item instanceof Array) { + const len = item.length; + for (let i = 0; i < len; ++i) + this.addItem(item[i]); + return; + } + + if (!this.__itemsSet.has(item.item || item)) { + item = item instanceof FilterItem$1 ? item : new FilterItem$1({ + item + }); + Filter._validateItemNest(item, this._nests); + + this._isItemsDirty = true; + this._items.push(item); + this.__itemsSet.add(item.item); + if (this._state[item.item] == null) + this._defaultItemState(item); + } + } + + static _isItemsEqual(item1, item2) { + return (item1 instanceof FilterItem$1 ? item1.item : item1) === (item2 instanceof FilterItem$1 ? item2.item : item2); + } + + removeItem(item) { + const ixItem = this._items.findIndex(it=>Filter._isItemsEqual(it, item)); + if (~ixItem) { + const item = this._items[ixItem]; + + this._isItemsDirty = true; + item.rendered.detach(); + item.btnMini.detach(); + this._items.splice(ixItem, 1); + } + } + + addNest(nestName, nestMeta) { + if (!this._nests) + throw new Error(`Filter was not nested!`); + if (!this._nests[nestName]) { + this._isNestsDirty = true; + this._nests[nestName] = nestMeta; + + if (this._groupFn) { + Object.keys(this._pillGroupsMeta).forEach(group=>{ + const hook = ()=>this._pillGroupsMeta[group].toggleDividerFromNestVisibility(); + this._addHook("nestsHidden", nestName, hook); + hook(); + this._pillGroupsMeta[group].toggleDividerFromNestVisibility(); + } + ); + } + } + } + + _toDisplay_getMappedEntryVal(entryVal) { + if (!(entryVal instanceof Array)) + entryVal = [entryVal]; + entryVal = entryVal.map(it=>it instanceof FilterItem$1 ? it : new FilterItem$1({ + item: it + })); + return entryVal; + } + + _toDisplay_getFilterState(boxState) { + return boxState[this.header]; + } + + toDisplay(boxState, entryVal) { + const filterState = this._toDisplay_getFilterState(boxState); + if (!filterState) + return true; + + const totals = filterState._totals; + + entryVal = this._toDisplay_getMappedEntryVal(entryVal); + + const isUmbrella = ()=>{ + if (this._umbrellaItems) { + if (!entryVal) + return false; + + if (this._umbrellaExcludes && this._umbrellaExcludes.some(it=>filterState[it.item])) + return false; + + return this._umbrellaItems.some(u=>entryVal.includes(u.item)) && (this._umbrellaItems.some(u=>filterState[u.item] === 0) || this._umbrellaItems.some(u=>filterState[u.item] === 1)); + } + } + ; + + let hide = false; + let display = false; + + switch (filterState._combineBlue) { + case "or": + { + if (totals.yes === 0) + display = true; + + display = display || entryVal.some(fi=>filterState[fi.item] === 1 || isUmbrella()); + + break; + } + case "xor": + { + if (totals.yes === 0) + display = true; + + display = display || entryVal.filter(fi=>filterState[fi.item] === 1 || isUmbrella()).length === 1; + + break; + } + case "and": + { + const totalYes = entryVal.filter(fi=>filterState[fi.item] === 1).length; + display = !totals.yes || totals.yes === totalYes; + + break; + } + default: + throw new Error(`Unhandled combine mode "${filterState._combineBlue}"`); + } + + switch (filterState._combineRed) { + case "or": + { + hide = hide || entryVal.filter(fi=>!fi.isIgnoreRed).some(fi=>filterState[fi.item] === 2); + + break; + } + case "xor": + { + hide = hide || entryVal.filter(fi=>!fi.isIgnoreRed).filter(fi=>filterState[fi.item] === 2).length === 1; + + break; + } + case "and": + { + const totalNo = entryVal.filter(fi=>!fi.isIgnoreRed).filter(fi=>filterState[fi.item] === 2).length; + hide = totals.no && totals.no === totalNo; + + break; + } + default: + throw new Error(`Unhandled combine mode "${filterState._combineRed}"`); + } + + return display && !hide; + } + + _doInvertPins() { + const cur = MiscUtil.copy(this._state); + Object.keys(this._state).forEach(k=>this._state[k] = cur[k] === 1 ? 0 : 1); + } + + getDefaultMeta() { + return { + ...super.getDefaultMeta(), + ...Filter._DEFAULT_META, + }; + } + + handleSearch(searchTerm) { + const isHeaderMatch = this.header.toLowerCase().includes(searchTerm); + + if (isHeaderMatch) { + this._items.forEach(it=>{ + if (!it.rendered) + return; + it.rendered.toggleClass("fltr__hidden--search", false); + } + ); + + if (this.__$wrpFilter) + this.__$wrpFilter.toggleClass("fltr__hidden--search", false); + + return true; + } + + let visibleCount = 0; + this._items.forEach(it=>{ + if (!it.rendered) + return; + const isVisible = it.searchText.includes(searchTerm); + it.rendered.toggleClass("fltr__hidden--search", !isVisible); + if (isVisible) + visibleCount++; + } + ); + + if (this.__$wrpFilter) + this.__$wrpFilter.toggleClass("fltr__hidden--search", visibleCount === 0); + + return visibleCount !== 0; + } + + static _getNextCombineMode(combineMode) { + let ix = Filter._COMBINE_MODES.indexOf(combineMode); + if (ix === -1) + ix = (Filter._COMBINE_MODES.length - 1); + if (++ix === Filter._COMBINE_MODES.length) + ix = 0; + return Filter._COMBINE_MODES[ix]; + } + + _doTeardown() { + this._items.forEach(it=>{ + if (it.rendered) + it.rendered.detach(); + if (it.btnMini) + it.btnMini.detach(); + } + ); + + Object.values(this._nests || {}).filter(nestMeta=>nestMeta._$btnNest).forEach(nestMeta=>nestMeta._$btnNest.detach()); + + Object.values(this._pillGroupsMeta || {}).forEach(it=>{ + it.hrDivider.detach(); + it.wrpPills.detach(); + it.isAttached = false; + } + ); + } + + static _getAsFilterItems(items) { + return items ? items.map(it=>it instanceof FilterItem$1 ? it : new FilterItem$1({ + item: it + })) : null; + } + + static _validateItemNests(items, nests) { + if (!nests) + return; + items = items.filter(it=>it.nest); + const noNest = items.find(it=>!nests[it.nest]); + if (noNest) + throw new Error(`Filter does not have matching nest: "${noNest.item}" (call addNest first)`); + const invalid = items.find(it=>!it.nest || !nests[it.nest]); + if (invalid) + throw new Error(`Invalid nest: "${invalid.item}"`); + } + + static _validateItemNest(item, nests) { + if (!nests || !item.nest) + return; + if (!nests[item.nest]) + throw new Error(`Filter does not have matching nest: "${item.item}" (call addNest first)`); + if (!item.nest || !nests[item.nest]) + throw new Error(`Invalid nest: "${item.item}"`); + } +}; +Filter._DEFAULT_META = { + combineBlue: "or", + combineRed: "or", +}; +Filter._COMBINE_MODES = ["or", "and", "xor"]; +globalThis.Filter = Filter; + +let FilterCommon$1 = class FilterCommon { + static getDamageVulnerableFilter() { + return this._getDamageResistVulnImmuneFilter({ + header: "Vulnerabilities", + headerShort: "Vuln.", + }); + } + + static getDamageResistFilter() { + return this._getDamageResistVulnImmuneFilter({ + header: "Resistance", + headerShort: "Res.", + }); + } + + static getDamageImmuneFilter() { + return this._getDamageResistVulnImmuneFilter({ + header: "Immunity", + headerShort: "Imm.", + }); + } + + static _getDamageResistVulnImmuneFilter({header, headerShort, }, ) { + return new Filter({ + header: header, + items: [...Parser.DMG_TYPES], + displayFnMini: str=>`${headerShort} ${str.toTitleCase()}`, + displayFnTitle: str=>`Damage ${header}: ${str.toTitleCase()}`, + displayFn: StrUtil.uppercaseFirst, + }); + } + + static _CONDS = ["blinded", "charmed", "deafened", "exhaustion", "frightened", "grappled", "incapacitated", "invisible", "paralyzed", "petrified", "poisoned", "prone", "restrained", "stunned", "unconscious", "disease", ]; + + static getConditionImmuneFilter() { + return new Filter({ + header: "Condition Immunity", + items: this._CONDS, + displayFnMini: str=>`Imm. ${str.toTitleCase()}`, + displayFnTitle: str=>`Condition Immunity: ${str.toTitleCase()}`, + displayFn: StrUtil.uppercaseFirst, + }); + } + + static mutateForFilters_damageVulnResImmune_player(ent) { + this.mutateForFilters_damageVuln_player(ent); + this.mutateForFilters_damageRes_player(ent); + this.mutateForFilters_damageImm_player(ent); + } + + static mutateForFilters_damageVuln_player(ent) { + if (!ent.vulnerable) + return; + + const out = new Set(); + ent.vulnerable.forEach(it=>this._recurseResVulnImm(out, it)); + ent._fVuln = [...out]; + } + + static mutateForFilters_damageRes_player(ent) { + if (!ent.resist) + return; + + const out = new Set(); + ent.resist.forEach(it=>this._recurseResVulnImm(out, it)); + ent._fRes = [...out]; + } + + static mutateForFilters_damageImm_player(ent) { + if (!ent.immune) + return; + + const out = new Set(); + ent.immune.forEach(iti=>this._recurseResVulnImm(out, iti)); + ent._fImm = [...out]; + } + + static mutateForFilters_conditionImmune_player(ent) { + if (!ent.conditionImmune) + return; + + const out = new Set(); + ent.conditionImmune.forEach(it=>this._recurseResVulnImm(out, it)); + ent._fCondImm = [...out]; + } + + static _recurseResVulnImm(allSet, it) { + if (typeof it === "string") + return allSet.add(it); + if (it.choose?.from) + it.choose?.from.forEach(itSub=>this._recurseResVulnImm(allSet, itSub)); + } +}; + +globalThis.FilterCommon = FilterCommon$1; + +let MultiFilter = class MultiFilter extends FilterBase { + constructor(opts) { + super(opts); + this._filters = opts.filters; + this._isAddDropdownToggle = !!opts.isAddDropdownToggle; + + Object.assign(this.__state, { + ...MultiFilter._DETAULT_STATE, + mode: opts.mode || MultiFilter._DETAULT_STATE.mode, + }, ); + this._defaultState = MiscUtil.copy(this.__state); + this._state = this._getProxy("state", this.__state); + + this.__$wrpFilter = null; + this._$wrpChildren = null; + } + + getChildFilters() { + return [...this._filters, ...this._filters.map(f=>f.getChildFilters())].flat(); + } + + getSaveableState() { + const out = { + [this.header]: { + ...this.getBaseSaveableState(), + state: { + ...this.__state + }, + }, + }; + this._filters.forEach(it=>Object.assign(out, it.getSaveableState())); + return out; + } + + setStateFromLoaded(filterState, {isUserSavedState=false}={}) { + if (!filterState?.[this.header]) + return; + + const toLoad = filterState[this.header]; + this._hasUserSavedState = this._hasUserSavedState || isUserSavedState; + this.setBaseStateFromLoaded(toLoad); + Object.assign(this._state, toLoad.state); + this._filters.forEach(it=>it.setStateFromLoaded(filterState, { + isUserSavedState + })); + } + + getSubHashes() { + const out = []; + + const baseMeta = this.getMetaSubHashes(); + if (baseMeta) + out.push(...baseMeta); + + const anyNotDefault = this._getStateNotDefault(); + if (anyNotDefault.length) { + out.push(UrlUtil.packSubHash(this.getSubHashPrefix("state", this.header), this._getCompressedState())); + } + + this._filters.map(it=>it.getSubHashes()).filter(Boolean).forEach(it=>out.push(...it)); + return out.length ? out : null; + } + + _getStateNotDefault() { + return Object.entries(this._defaultState).filter(([k,v])=>this._state[k] !== v); + } + + getFilterTagPart() { + return [this._getFilterTagPart_self(), ...this._filters.map(it=>it.getFilterTagPart()).filter(Boolean), ].filter(it=>it != null).join("|"); + } + + _getFilterTagPart_self() { + const areNotDefaultState = this._getStateNotDefault(); + if (!areNotDefaultState.length) + return null; + + return `${this.header.toLowerCase()}=${this._getCompressedState().join(HASH_SUB_LIST_SEP)}`; + } + + getDisplayStatePart({nxtState=null}={}) { + return this._filters.map(it=>it.getDisplayStatePart({ + nxtState + })).filter(Boolean).join(", "); + } + + _getCompressedState() { + return Object.keys(this._defaultState).map(k=>UrlUtil.mini.compress(this._state[k] === undefined ? this._defaultState[k] : this._state[k])); + } + + setStateFromNextState(nxtState) { + super.setStateFromNextState(nxtState); + } + + getNextStateFromSubhashState(state) { + const nxtState = this._getNextState_base(); + + if (state == null) { + this._mutNextState_reset_self(nxtState); + return nxtState; + } + + this._mutNextState_meta_fromSubHashState(nxtState, state); + + let hasState = false; + + Object.entries(state).forEach(([k,vals])=>{ + const prop = FilterBase.getProp(k); + if (prop === "state") { + hasState = true; + const data = vals.map(v=>UrlUtil.mini.decompress(v)); + Object.keys(this._defaultState).forEach((k,i)=>nxtState[this.header].state[k] = data[i]); + } + } + ); + + if (!hasState) + this._mutNextState_reset_self(nxtState); + + return nxtState; + } + + setFromValues(values) { + this._filters.forEach(it=>it.setFromValues(values)); + } + + _getHeaderControls(opts) { + const wrpSummary = e_({ + tag: "div", + clazz: "fltr__summary_item", + }).hideVe(); + + const btnForceMobile = this._isAddDropdownToggle ? ComponentUiUtil.getBtnBool(this, "isUseDropdowns", { + $ele: $(``), + stateName: "meta", + stateProp: "_meta", + }, ) : null; + const hkChildrenDropdowns = ()=>{ + this._filters.filter(it=>it instanceof RangeFilter).forEach(it=>it.isUseDropdowns = this._meta.isUseDropdowns); + } + ; + this._addHook("meta", "isUseDropdowns", hkChildrenDropdowns); + hkChildrenDropdowns(); + + const btnResetAll = e_({ + tag: "button", + clazz: "btn btn-default btn-xs ml-2", + text: "Reset All", + click: ()=>this._filters.forEach(it=>it.reset()), + }); + + const wrpBtns = e_({ + tag: "div", + clazz: "ve-flex", + children: [btnForceMobile, btnResetAll].filter(Boolean) + }); + this._getHeaderControls_addExtraStateBtns(opts, wrpBtns); + + const btnShowHide = e_({ + tag: "button", + clazz: `btn btn-default btn-xs ml-2 ${this._meta.isHidden ? "active" : ""}`, + text: "Hide", + click: ()=>this._meta.isHidden = !this._meta.isHidden, + }); + const wrpControls = e_({ + tag: "div", + clazz: "ve-flex-v-center", + children: [wrpSummary, wrpBtns, btnShowHide] + }); + + const hookShowHide = ()=>{ + wrpBtns.toggleVe(!this._meta.isHidden); + btnShowHide.toggleClass("active", this._meta.isHidden); + this._$wrpChildren.toggleVe(!this._meta.isHidden); + wrpSummary.toggleVe(this._meta.isHidden); + + const numActive = this._filters.map(it=>it.getValues()[it.header]._isActive).filter(Boolean).length; + if (numActive) { + e_({ + ele: wrpSummary, + title: `${numActive} hidden active filter${numActive === 1 ? "" : "s"}`, + text: `(${numActive})` + }); + } + } + ; + this._addHook("meta", "isHidden", hookShowHide); + hookShowHide(); + + return wrpControls; + } + + _getHeaderControls_addExtraStateBtns(opts, wrpStateBtnsOuter) {} + + $render(opts) { + const btnAndOr = e_({ + tag: "div", + clazz: `fltr__group-comb-toggle ve-muted`, + click: ()=>this._state.mode = this._state.mode === "and" ? "or" : "and", + title: `"Group AND" requires all filters in this group to match. "Group OR" required any filter in this group to match.`, + }); + + const hookAndOr = ()=>btnAndOr.innerText = `(group ${this._state.mode.toUpperCase()})`; + this._addHook("state", "mode", hookAndOr); + hookAndOr(); + + const $children = this._filters.map((it,i)=>it.$render({ + ...opts, + isMulti: true, + isFirst: i === 0 + })); + this._$wrpChildren = $$`
    ${$children}
    `; + + const wrpControls = this._getHeaderControls(opts); + + return this.__$wrpFilter = $$`
    + ${opts.isFirst ? "" : `
    `} +
    +
    +
    ${this._getRenderedHeader()}
    + ${btnAndOr} +
    + ${wrpControls} +
    + ${this._$wrpChildren} +
    `; + } + + $renderMinis(opts) { + this._filters.map((it,i)=>it.$renderMinis({ + ...opts, + isMulti: true, + isFirst: i === 0 + })); + } + + isActive(vals) { + vals = vals || this.getValues(); + return this._filters.some(it=>it.isActive(vals)); + } + + getValues({nxtState=null}={}) { + const out = {}; + this._filters.forEach(it=>Object.assign(out, it.getValues({ + nxtState + }))); + return out; + } + + _mutNextState_reset_self(nxtState) { + Object.assign(nxtState[this.header].state, MiscUtil.copy(this._defaultState)); + } + + _mutNextState_reset(nxtState, {isResetAll=false}={}) { + if (isResetAll) + this._mutNextState_resetBase(nxtState, { + isResetAll + }); + this._mutNextState_reset_self(nxtState); + } + + reset({isResetAll=false}={}) { + super.reset({ + isResetAll + }); + this._filters.forEach(it=>it.reset({ + isResetAll + })); + } + + update() { + this._filters.forEach(it=>it.update()); + } + + toDisplay(boxState, entryValArr) { + if (this._filters.length !== entryValArr.length) + throw new Error("Number of filters and number of values did not match"); + + const results = []; + for (let i = this._filters.length - 1; i >= 0; --i) { + const f = this._filters[i]; + if (f instanceof RangeFilter) { + results.push(f.toDisplay(boxState, entryValArr[i])); + } else { + const totals = boxState[f.header]._totals; + + if (totals.yes === 0 && totals.no === 0) + results.push(null); + else + results.push(f.toDisplay(boxState, entryValArr[i])); + } + } + + const resultsActive = results.filter(r=>r !== null); + if (this._state.mode === "or") { + if (!resultsActive.length) + return true; + return resultsActive.find(r=>r); + } else { + return resultsActive.filter(r=>r).length === resultsActive.length; + } + } + + addItem() { + throw new Error(`Cannot add item to MultiFilter! Add the item to a child filter instead.`); + } + + handleSearch(searchTerm) { + const isHeaderMatch = this.header.toLowerCase().includes(searchTerm); + + if (isHeaderMatch) { + if (this.__$wrpFilter) + this.__$wrpFilter.toggleClass("fltr__hidden--search", false); + this._filters.forEach(it=>it.handleSearch("")); + return true; + } + + const numVisible = this._filters.map(it=>it.handleSearch(searchTerm)).reduce((a,b)=>a + b, 0); + if (!this.__$wrpFilter) + return; + this.__$wrpFilter.toggleClass("fltr__hidden--search", numVisible === 0); + } +}; +MultiFilter._DETAULT_STATE = {mode: "and",} +globalThis.MultiFilter = MultiFilter; + +let RangeFilter = class RangeFilter extends FilterBase { + constructor(opts) { + super(opts); + + if (opts.labels && opts.min == null) + opts.min = 0; + if (opts.labels && opts.max == null) + opts.max = opts.labels.length - 1; + + this._min = Number(opts.min || 0); + this._max = Number(opts.max || 0); + this._labels = opts.isLabelled ? opts.labels : null; + this._isAllowGreater = !!opts.isAllowGreater; + this._isRequireFullRangeMatch = !!opts.isRequireFullRangeMatch; + this._sparseValues = opts.isSparse ? [] : null; + this._suffix = opts.suffix; + this._labelSortFn = opts.labelSortFn === undefined ? SortUtil.ascSort : opts.labelSortFn; + this._labelDisplayFn = opts.labelDisplayFn; + this._displayFn = opts.displayFn; + this._displayFnTooltip = opts.displayFnTooltip; + + this._filterBox = null; + Object.assign(this.__state, { + min: this._min, + max: this._max, + curMin: this._min, + curMax: this._max, + }, ); + this.__$wrpFilter = null; + this.__$wrpMini = null; + this._slider = null; + + this._labelSearchCache = null; + + this._$btnMiniGt = null; + this._$btnMiniLt = null; + this._$btnMiniEq = null; + + this._seenMin = this._min; + this._seenMax = this._max; + } + + set isUseDropdowns(val) { + this._meta.isUseDropdowns = !!val; + } + + getSaveableState() { + return { + [this.header]: { + ...this.getBaseSaveableState(), + state: { + ...this.__state + }, + }, + }; + } + + setStateFromLoaded(filterState, {isUserSavedState=false}={}) { + if (!filterState?.[this.header]) + return; + + const toLoad = filterState[this.header]; + this._hasUserSavedState = this._hasUserSavedState || isUserSavedState; + + const tgt = (toLoad.state || {}); + + if (tgt.max == null) + tgt.max = this._max; + else if (this._max > tgt.max) { + if (tgt.max === tgt.curMax) + tgt.curMax = this._max; + tgt.max = this._max; + } + + if (tgt.curMax == null) + tgt.curMax = tgt.max; + else if (tgt.curMax > tgt.max) + tgt.curMax = tgt.max; + + if (tgt.min == null) + tgt.min = this._min; + else if (this._min < tgt.min) { + if (tgt.min === tgt.curMin) + tgt.curMin = this._min; + tgt.min = this._min; + } + + if (tgt.curMin == null) + tgt.curMin = tgt.min; + else if (tgt.curMin < tgt.min) + tgt.curMin = tgt.min; + + this.setBaseStateFromLoaded(toLoad); + + Object.assign(this._state, toLoad.state); + } + + trimState_() { + if (this._seenMin <= this._state.min && this._seenMax >= this._state.max) + return; + + const nxtState = { + min: this._seenMin, + curMin: this._seenMin, + max: this._seenMax, + curMax: this._seenMax + }; + this._proxyAssignSimple("state", nxtState); + } + + getSubHashes() { + const out = []; + + const baseMeta = this.getMetaSubHashes(); + if (baseMeta) + out.push(...baseMeta); + + const serSliderState = [this._state.min !== this._state.curMin ? `min=${this._state.curMin}` : null, this._state.max !== this._state.curMax ? `max=${this._state.curMax}` : null, ].filter(Boolean); + if (serSliderState.length) { + out.push(UrlUtil.packSubHash(this.getSubHashPrefix("state", this.header), serSliderState)); + } + + return out.length ? out : null; + } + + _isAtDefaultPosition({nxtState=null}={}) { + const state = nxtState?.[this.header]?.state || this.__state; + return state.min === state.curMin && state.max === state.curMax; + } + + getFilterTagPart() { + if (this._isAtDefaultPosition()) + return null; + + if (!this._labels) { + if (this._state.curMin === this._state.curMax) + return `${this.header}=[${this._state.curMin}]`; + return `${this.header}=[${this._state.curMin};${this._state.curMax}]`; + } + + if (this._state.curMin === this._state.curMax) { + const label = this._labels[this._state.curMin]; + return `${this.header}=[&${label}]`; + } + + const labelLow = this._labels[this._state.curMin]; + const labelHigh = this._labels[this._state.curMax]; + return `${this.header}=[&${labelLow};&${labelHigh}]`; + } + + getDisplayStatePart({nxtState=null}={}) { + if (this._isAtDefaultPosition({ + nxtState + })) + return null; + + const {summary} = this._getDisplaySummary({ + nxtState + }); + + return `${this.header}: ${summary}`; + } + + getNextStateFromSubhashState(state) { + const nxtState = this._getNextState_base(); + + if (state == null) { + this._mutNextState_reset(nxtState); + return nxtState; + } + + this._mutNextState_meta_fromSubHashState(nxtState, state); + + let hasState = false; + + Object.entries(state).forEach(([k,vals])=>{ + const prop = FilterBase.getProp(k); + if (prop === "state") { + hasState = true; + vals.forEach(v=>{ + const [prop,val] = v.split("="); + if (val.startsWith("&") && !this._labels) + throw new Error(`Could not dereference label: "${val}"`); + + let num; + if (val.startsWith("&")) { + const clean = val.replace("&", "").toLowerCase(); + num = this._labels.findIndex(it=>String(it).toLowerCase() === clean); + if (!~num) + throw new Error(`Could not find index for label "${clean}"`); + } else + num = Number(val); + + switch (prop) { + case "min": + if (num < nxtState[this.header].state.min) + nxtState[this.header].state.min = num; + nxtState[this.header].state.curMin = Math.max(nxtState[this.header].state.min, num); + break; + case "max": + if (num > nxtState[this.header].state.max) + nxtState[this.header].state.max = num; + nxtState[this.header].state.curMax = Math.min(nxtState[this.header].state.max, num); + break; + default: + throw new Error(`Unknown prop "${prop}"`); + } + } + ); + } + } + ); + + if (!hasState) + this._mutNextState_reset(nxtState); + + return nxtState; + } + + setFromValues(values) { + if (!values[this.header]) + return; + + const vals = values[this.header]; + + if (vals.min != null) + this._state.curMin = Math.max(this._state.min, vals.min); + else + this._state.curMin = this._state.min; + + if (vals.max != null) + this._state.curMax = Math.max(this._state.max, vals.max); + else + this._state.curMax = this._state.max; + } + + _$getHeaderControls() { + const $btnForceMobile = ComponentUiUtil.$getBtnBool(this, "isUseDropdowns", { + $ele: $(``), + stateName: "meta", + stateProp: "_meta", + }, ); + const $btnReset = $(``).click(()=>this.reset()); + const $wrpBtns = $$`
    ${$btnForceMobile}${$btnReset}
    `; + + const $wrpSummary = $(`
    `).hideVe(); + + const $btnShowHide = $(``).click(()=>this._meta.isHidden = !this._meta.isHidden); + const hkIsHidden = ()=>{ + $btnShowHide.toggleClass("active", this._meta.isHidden); + $wrpBtns.toggleVe(!this._meta.isHidden); + $wrpSummary.toggleVe(this._meta.isHidden); + + const {summaryTitle, summary} = this._getDisplaySummary(); + $wrpSummary.title(summaryTitle).text(summary); + } + ; + this._addHook("meta", "isHidden", hkIsHidden); + hkIsHidden(); + + return $$` +
    + ${$wrpBtns} + ${$wrpSummary} + ${$btnShowHide} +
    `; + } + + _getDisplaySummary({nxtState=null}={}) { + const cur = this.getValues({ + nxtState + })[this.header]; + + const isRange = !cur.isMinVal && !cur.isMaxVal; + const isCapped = !cur.isMinVal || !cur.isMaxVal; + + return { + summaryTitle: isRange ? `Hidden range` : isCapped ? `Hidden limit` : "", + summary: isRange ? `${this._getDisplayText(cur.min)}-${this._getDisplayText(cur.max)}` : !cur.isMinVal ? `โ‰ฅ ${this._getDisplayText(cur.min)}` : !cur.isMaxVal ? `โ‰ค ${this._getDisplayText(cur.max)}` : "", + }; + } + + _getDisplayText(value, {isBeyondMax=false, isTooltip=false}={}) { + value = `${this._labels ? this._labelDisplayFn ? this._labelDisplayFn(this._labels[value]) : this._labels[value] : (isTooltip && this._displayFnTooltip) ? this._displayFnTooltip(value) : this._displayFn ? this._displayFn(value) : value}${isBeyondMax ? "+" : ""}`; + if (this._suffix) + value += this._suffix; + return value; + } + + $render(opts) { + this._filterBox = opts.filterBox; + this.__$wrpMini = opts.$wrpMini; + + const $wrpControls = opts.isMulti ? null : this._$getHeaderControls(); + + const $wrpSlider = $$`
    `; + const $wrpDropdowns = $$`
    `; + const hookHidden = ()=>{ + $wrpSlider.toggleVe(!this._meta.isHidden && !this._meta.isUseDropdowns); + $wrpDropdowns.toggleVe(!this._meta.isHidden && !!this._meta.isUseDropdowns); + } + ; + this._addHook("meta", "isHidden", hookHidden); + this._addHook("meta", "isUseDropdowns", hookHidden); + hookHidden(); + + if (this._sparseValues?.length) { + const sparseMin = this._sparseValues[0]; + if (this._state.min < sparseMin) { + this._state.curMin = Math.max(this._state.curMin, sparseMin); + this._state.min = sparseMin; + } + + const sparseMax = this._sparseValues.last(); + if (this._state.max > sparseMax) { + this._state.curMax = Math.min(this._state.curMax, sparseMax); + this._state.max = sparseMax; + } + } + + const getSliderOpts = ()=>{ + const fnDisplay = (val,{isTooltip=false}={})=>{ + return this._getDisplayText(val, { + isBeyondMax: this._isAllowGreater && val === this._state.max, + isTooltip + }); + } + ; + + return { + propMin: "min", + propMax: "max", + propCurMin: "curMin", + propCurMax: "curMax", + fnDisplay: (val)=>fnDisplay(val), + fnDisplayTooltip: (val)=>fnDisplay(val, { + isTooltip: true + }), + sparseValues: this._sparseValues, + }; + } + ; + + const hkUpdateLabelSearchCache = ()=>{ + if (this._labels) + return this._doUpdateLabelSearchCache(); + this._labelSearchCache = null; + } + ; + this._addHook("state", "curMin", hkUpdateLabelSearchCache); + this._addHook("state", "curMax", hkUpdateLabelSearchCache); + hkUpdateLabelSearchCache(); + + this._slider = new ComponentUiUtil.RangeSlider({ + comp: this, + ...getSliderOpts() + }); + $wrpSlider.append(this._slider.get()); + + const selMin = e_({ + tag: "select", + clazz: `form-control mr-2`, + change: ()=>{ + const nxtMin = Number(selMin.val()); + const [min,max] = [nxtMin, this._state.curMax].sort(SortUtil.ascSort); + this._state.curMin = min; + this._state.curMax = max; + } + , + }); + const selMax = e_({ + tag: "select", + clazz: `form-control`, + change: ()=>{ + const nxMax = Number(selMax.val()); + const [min,max] = [this._state.curMin, nxMax].sort(SortUtil.ascSort); + this._state.curMin = min; + this._state.curMax = max; + } + , + }); + $$`
    ${selMin}${selMax}
    `.appendTo($wrpDropdowns); + + const handleCurUpdate = ()=>{ + selMin.val(`${this._state.curMin}`); + selMax.val(`${this._state.curMax}`); + } + ; + + const handleLimitUpdate = ()=>{ + this._doPopulateDropdown(selMin, this._state.curMin); + this._doPopulateDropdown(selMax, this._state.curMax); + } + ; + + this._addHook("state", "min", handleLimitUpdate); + this._addHook("state", "max", handleLimitUpdate); + this._addHook("state", "curMin", handleCurUpdate); + this._addHook("state", "curMax", handleCurUpdate); + handleCurUpdate(); + handleLimitUpdate(); + + if (opts.isMulti) { + this._slider.get().classList.add("ve-grow"); + $wrpSlider.addClass("ve-grow"); + $wrpDropdowns.addClass("ve-grow"); + + return this.__$wrpFilter = $$`
    +
    ${this._getRenderedHeader()}
    + ${$wrpSlider} + ${$wrpDropdowns} +
    `; + } else { + const btnMobToggleControls = this._getBtnMobToggleControls($wrpControls); + + return this.__$wrpFilter = $$`
    + ${opts.isFirst ? "" : `
    `} +
    +
    ${this._getRenderedHeader()}${btnMobToggleControls}
    + ${$wrpControls} +
    + ${$wrpSlider} + ${$wrpDropdowns} +
    `; + } + } + + $renderMinis(opts) { + if (!opts.$wrpMini) + return; + + this._filterBox = opts.filterBox; + this.__$wrpMini = opts.$wrpMini; + + this._$btnMiniGt = this._$btnMiniGt || $(`
    `).click(()=>{ + this._state.curMin = this._state.min; + this._filterBox.fireChangeEvent(); + } + ); + this._$btnMiniGt.appendTo(this.__$wrpMini); + + this._$btnMiniLt = this._$btnMiniLt || $(`
    `).click(()=>{ + this._state.curMax = this._state.max; + this._filterBox.fireChangeEvent(); + } + ); + this._$btnMiniLt.appendTo(this.__$wrpMini); + + this._$btnMiniEq = this._$btnMiniEq || $(`
    `).click(()=>{ + this._state.curMin = this._state.min; + this._state.curMax = this._state.max; + this._filterBox.fireChangeEvent(); + } + ); + this._$btnMiniEq.appendTo(this.__$wrpMini); + + const hideHook = ()=>{ + const isHidden = this._filterBox.isMinisHidden(this.header); + this._$btnMiniGt.toggleClass("ve-hidden", isHidden); + this._$btnMiniLt.toggleClass("ve-hidden", isHidden); + this._$btnMiniEq.toggleClass("ve-hidden", isHidden); + } + ; + this._filterBox.registerMinisHiddenHook(this.header, hideHook); + hideHook(); + + const handleMiniUpdate = ()=>{ + if (this._state.curMin === this._state.curMax) { + this._$btnMiniGt.attr("state", FilterBox._PILL_STATES[0]); + this._$btnMiniLt.attr("state", FilterBox._PILL_STATES[0]); + + this._$btnMiniEq.attr("state", this._isAtDefaultPosition() ? FilterBox._PILL_STATES[0] : FilterBox._PILL_STATES[1]).text(`${this.header} = ${this._getDisplayText(this._state.curMin, { + isBeyondMax: this._isAllowGreater && this._state.curMin === this._state.max + })}`); + } else { + if (this._state.min !== this._state.curMin) { + this._$btnMiniGt.attr("state", FilterBox._PILL_STATES[1]).text(`${this.header} โ‰ฅ ${this._getDisplayText(this._state.curMin)}`); + } else + this._$btnMiniGt.attr("state", FilterBox._PILL_STATES[0]); + + if (this._state.max !== this._state.curMax) { + this._$btnMiniLt.attr("state", FilterBox._PILL_STATES[1]).text(`${this.header} โ‰ค ${this._getDisplayText(this._state.curMax)}`); + } else + this._$btnMiniLt.attr("state", FilterBox._PILL_STATES[0]); + + this._$btnMiniEq.attr("state", FilterBox._PILL_STATES[0]); + } + } + ; + + const handleCurUpdate = ()=>{ + handleMiniUpdate(); + } + ; + + const handleLimitUpdate = ()=>{ + handleMiniUpdate(); + } + ; + + this._addHook("state", "min", handleLimitUpdate); + this._addHook("state", "max", handleLimitUpdate); + this._addHook("state", "curMin", handleCurUpdate); + this._addHook("state", "curMax", handleCurUpdate); + handleCurUpdate(); + handleLimitUpdate(); + } + + _doPopulateDropdown(sel, curVal) { + let tmp = ""; + for (let i = 0, len = this._state.max - this._state.min + 1; i < len; ++i) { + const val = i + this._state.min; + const label = `${this._getDisplayText(val)}`.qq(); + tmp += ``; + } + sel.innerHTML = tmp; + return sel; + } + + getValues({nxtState=null}={}) { + const state = nxtState?.[this.header]?.state || this.__state; + + const out = { + isMaxVal: state.max === state.curMax, + isMinVal: state.min === state.curMin, + max: state.curMax, + min: state.curMin, + }; + out._isActive = !(out.isMinVal && out.isMaxVal); + return { + [this.header]: out + }; + } + + _mutNextState_reset(nxtState, {isResetAll=false}={}) { + if (isResetAll) + this._mutNextState_resetBase(nxtState, { + isResetAll + }); + nxtState[this.header].state.curMin = nxtState[this.header].state.min; + nxtState[this.header].state.curMax = nxtState[this.header].state.max; + } + + update() { + if (!this.__$wrpMini) + return; + + if (this._$btnMiniGt) + this.__$wrpMini.append(this._$btnMiniGt); + if (this._$btnMiniLt) + this.__$wrpMini.append(this._$btnMiniLt); + if (this._$btnMiniEq) + this.__$wrpMini.append(this._$btnMiniEq); + } + + toDisplay(boxState, entryVal) { + const filterState = boxState[this.header]; + if (!filterState) + return true; + if (entryVal == null) + return filterState.min === this._state.min && filterState.max === this._state.max; + + if (this._labels) { + const slice = this._labels.slice(filterState.min, filterState.max + 1); + + if (this._isAllowGreater) { + if (filterState.max === this._state.max && entryVal > this._labels[filterState.max]) + return true; + + const sliceMin = Math.min(...slice); + const sliceMax = Math.max(...slice); + + if (entryVal instanceof Array) + return entryVal.some(it=>it >= sliceMin && it <= sliceMax); + return entryVal >= sliceMin && entryVal <= sliceMax; + } + + if (entryVal instanceof Array) + return entryVal.some(it=>slice.includes(it)); + return slice.includes(entryVal); + } else { + if (entryVal instanceof Array) { + if (this._isRequireFullRangeMatch) + return filterState.min <= entryVal[0] && filterState.max >= entryVal.last(); + + return entryVal.some(ev=>this._toDisplay_isToDisplayEntry(filterState, ev)); + } + return this._toDisplay_isToDisplayEntry(filterState, entryVal); + } + } + + _toDisplay_isToDisplayEntry(filterState, ev) { + const isGtMin = filterState.min <= ev; + const isLtMax = filterState.max >= ev; + if (this._isAllowGreater) + return isGtMin && (isLtMax || filterState.max === this._state.max); + return isGtMin && isLtMax; + } + + addItem(item) { + if (item == null) + return; + + if (item instanceof Array) { + const len = item.length; + for (let i = 0; i < len; ++i) + this.addItem(item[i]); + return; + } + + if (this._labels) { + if (!this._labels.some(it=>it === item)) + this._labels.push(item); + + this._doUpdateLabelSearchCache(); + + this._addItem_addNumber(this._labels.length - 1); + } else { + this._addItem_addNumber(item); + } + } + + _doUpdateLabelSearchCache() { + this._labelSearchCache = [...new Array(Math.max(0, this._max - this._min))].map((_,i)=>i + this._min).map(val=>this._getDisplayText(val, { + isBeyondMax: this._isAllowGreater && val === this._state.max, + isTooltip: true + })).join(" -- ").toLowerCase(); + } + + _addItem_addNumber(number) { + if (number == null || isNaN(number)) + return; + + this._seenMin = Math.min(this._seenMin, number); + this._seenMax = Math.max(this._seenMax, number); + + if (this._sparseValues && !this._sparseValues.includes(number)) { + this._sparseValues.push(number); + this._sparseValues.sort(SortUtil.ascSort); + } + + if (number >= this._state.min && number <= this._state.max) + return; + if (this._state.min == null && this._state.max == null) + this._state.min = this._state.max = number; + else { + const old = { + ...this.__state + }; + + if (number < old.min) + this._state.min = number; + if (number > old.max) + this._state.max = number; + + if (old.curMin === old.min) + this._state.curMin = this._state.min; + if (old.curMax === old.max) + this._state.curMax = this._state.max; + } + } + + getDefaultMeta() { + const out = { + ...super.getDefaultMeta(), + ...RangeFilter._DEFAULT_META, + }; + if (Renderer.hover.isSmallScreen()) + out.isUseDropdowns = true; + return out; + } + + handleSearch(searchTerm) { + if (this.__$wrpFilter == null) + return; + + const isVisible = this.header.toLowerCase().includes(searchTerm) || (this._labelSearchCache != null ? this._labelSearchCache.includes(searchTerm) : [...new Array(this._state.max - this._state.min)].map((_,n)=>n + this._state.min).join(" -- ").includes(searchTerm)); + + this.__$wrpFilter.toggleClass("fltr__hidden--search", !isVisible); + + return isVisible; + } +}; +RangeFilter._DEFAULT_META = { + isUseDropdowns: false, +}; +globalThis.RangeFilter = RangeFilter; +//#endregion + +//#region SourceFilter +class SourceFilter extends Filter { + + constructor(opts) { + opts = opts || {}; + + opts.header = opts.header === undefined ? FilterBox.SOURCE_HEADER : opts.header; + opts.displayFn = opts.displayFn === undefined ? item=>Parser.sourceJsonToFullCompactPrefix(item.item || item) : opts.displayFn; + opts.displayFnMini = opts.displayFnMini === undefined ? SourceFilter._getDisplayHtmlMini.bind(SourceFilter) : opts.displayFnMini; + opts.displayFnTitle = opts.displayFnTitle === undefined ? item=>Parser.sourceJsonToFull(item.item || item) : opts.displayFnTitle; + opts.itemSortFnMini = opts.itemSortFnMini === undefined ? SourceFilter._SORT_ITEMS_MINI.bind(SourceFilter) : opts.itemSortFnMini; + opts.itemSortFn = opts.itemSortFn === undefined ? (a,b)=>SortUtil.ascSortLower(Parser.sourceJsonToFull(a.item), Parser.sourceJsonToFull(b.item)) : opts.itemSortFn; + opts.groupFn = opts.groupFn === undefined ? SourceUtil.getFilterGroup : opts.groupFn; + opts.selFn = opts.selFn === undefined ? PageFilter.defaultSourceSelFn : opts.selFn; + + super(opts); + + this.__tmpState = { ixAdded: 0 }; + this._tmpState = this._getProxy("tmpState", this.__tmpState); + } + + doSetPillsClear() { + return this._doSetPillsClear(); + } + + /** + * Add an item from the SourceFilter + * @param {any} item + */ + addItem(item) { + const out = super.addItem(item); + this._tmpState.ixAdded++; + return out; + } + + /** + * Remove an item from the SourceFilter + * @param {any} item + */ + removeItem(item) { + const out = super.removeItem(item); + this._tmpState.ixAdded--; + return out; + } + + _getHeaderControls_addExtraStateBtns(opts, wrpStateBtnsOuter) { + const btnSupplements = e_({ + tag: "button", + clazz: `btn btn-default w-100 ${opts.isMulti ? "btn-xxs" : "btn-xs"}`, + title: `SHIFT to add to existing selection; CTRL to include UA/etc.`, + html: `Core/Supplements`, + click: evt=>this._doSetPinsSupplements({ + isIncludeUnofficial: EventUtil.isCtrlMetaKey(evt), + isAdditive: evt.shiftKey + }), + }); + + const btnAdventures = e_({ + tag: "button", + clazz: `btn btn-default w-100 ${opts.isMulti ? "btn-xxs" : "btn-xs"}`, + title: `SHIFT to add to existing selection; CTRL to include UA`, + html: `Adventures`, + click: evt=>this._doSetPinsAdventures({ + isIncludeUnofficial: EventUtil.isCtrlMetaKey(evt), + isAdditive: evt.shiftKey + }), + }); + + const btnPartnered = e_({ + tag: "button", + clazz: `btn btn-default w-100 ${opts.isMulti ? "btn-xxs" : "btn-xs"}`, + title: `SHIFT to add to existing selection`, + html: `Partnered`, + click: evt=>this._doSetPinsPartnered({ + isAdditive: evt.shiftKey + }), + }); + + const btnHomebrew = e_({ + tag: "button", + clazz: `btn btn-default w-100 ${opts.isMulti ? "btn-xxs" : "btn-xs"}`, + title: `SHIFT to add to existing selection`, + html: `Homebrew`, + click: evt=>this._doSetPinsHomebrew({ + isAdditive: evt.shiftKey + }), + }); + + const hkIsButtonsActive = ()=>{ + const hasPartnered = Object.keys(this.__state).some(src=>SourceUtil.getFilterGroup(src) === SourceUtil.FILTER_GROUP_PARTNERED); + btnPartnered.toggleClass("ve-hidden", !hasPartnered); + + const hasBrew = Object.keys(this.__state).some(src=>SourceUtil.getFilterGroup(src) === SourceUtil.FILTER_GROUP_HOMEBREW); + btnHomebrew.toggleClass("ve-hidden", !hasBrew); + } + ; + this._addHook("tmpState", "ixAdded", hkIsButtonsActive); + hkIsButtonsActive(); + + const actionSelectDisplayMode = new ContextUtil.ActionSelect({ + values: Object.keys(SourceFilter._PILL_DISPLAY_MODE_LABELS).map(Number), + fnGetDisplayValue: val=>SourceFilter._PILL_DISPLAY_MODE_LABELS[val] || SourceFilter._PILL_DISPLAY_MODE_LABELS[0], + fnOnChange: val=>this._meta.pillDisplayMode = val, + }); + this._addHook("meta", "pillDisplayMode", ()=>{ + actionSelectDisplayMode.setValue(this._meta.pillDisplayMode); + } + )(); + + const menu = ContextUtil.getMenu([ + new ContextUtil.Action("Select All Standard Sources",()=>this._doSetPinsStandard(),), + new ContextUtil.Action("Select All Partnered Sources",()=>this._doSetPinsPartnered(),), + new ContextUtil.Action("Select All Non-Standard Sources",()=>this._doSetPinsNonStandard(),), + new ContextUtil.Action("Select All Homebrew Sources",()=>this._doSetPinsHomebrew(),), + null, + new ContextUtil.Action(`Select "Vanilla" Sources`,()=>this._doSetPinsVanilla(),{ + title: `Select a baseline set of sources suitable for any campaign.` + },), + new ContextUtil.Action("Select All Non-UA Sources",()=>this._doSetPinsNonUa(),), + null, + new ContextUtil.Action("Select SRD Sources",()=>this._doSetPinsSrd(),{ + title: `Select System Reference Document Sources.` + },), + new ContextUtil.Action("Select Basic Rules Sources",()=>this._doSetPinsBasicRules(),), + null, + new ContextUtil.Action("Invert Selection",()=>this._doInvertPins(),), + null, + actionSelectDisplayMode, + ]); + const btnBurger = e_({ + tag: "button", + clazz: `btn btn-default ${opts.isMulti ? "btn-xxs" : "btn-xs"}`, + html: ``, + click: evt=>ContextUtil.pOpenMenu(evt, menu), + title: "Other Options", + }); + + const btnOnlyPrimary = e_({ + tag: "button", + clazz: `btn btn-default w-100 ${opts.isMulti ? "btn-xxs" : "btn-xs"}`, + html: `Include References`, + title: `Consider entities as belonging to every source they appear in (i.e. reprints) as well as their primary source`, + click: ()=>this._meta.isIncludeOtherSources = !this._meta.isIncludeOtherSources, + }); + const hkIsIncludeOtherSources = ()=>{ + btnOnlyPrimary.toggleClass("active", !!this._meta.isIncludeOtherSources); + } + ; + hkIsIncludeOtherSources(); + this._addHook("meta", "isIncludeOtherSources", hkIsIncludeOtherSources); + + e_({ + tag: "div", + clazz: `btn-group mr-2 w-100 ve-flex-v-center mobile__m-1 mobile__mb-2`, + children: [btnSupplements, btnAdventures, btnPartnered, btnHomebrew, btnBurger, btnOnlyPrimary, ], + }).prependTo(wrpStateBtnsOuter); + } + + _doSetPinsStandard() { + Object.keys(this._state).forEach(k=>this._state[k] = SourceUtil.getFilterGroup(k) === SourceUtil.FILTER_GROUP_STANDARD ? 1 : 0); + } + + _doSetPinsPartnered({isAdditive=false}) { + this._proxyAssignSimple("state", Object.keys(this._state).mergeMap(k=>({ + [k]: SourceUtil.getFilterGroup(k) === SourceUtil.FILTER_GROUP_PARTNERED ? 1 : isAdditive ? this._state[k] : 0 + })), ); + } + + _doSetPinsNonStandard() { + Object.keys(this._state).forEach(k=>this._state[k] = SourceUtil.getFilterGroup(k) === SourceUtil.FILTER_GROUP_NON_STANDARD ? 1 : 0); + } + + _doSetPinsSupplements({isIncludeUnofficial=false, isAdditive=false}={}) { + this._proxyAssignSimple("state", Object.keys(this._state).mergeMap(k=>({ + [k]: SourceUtil.isCoreOrSupplement(k) && (isIncludeUnofficial || !SourceUtil.isNonstandardSource(k)) ? 1 : isAdditive ? this._state[k] : 0 + })), ); + } + + _doSetPinsAdventures({isIncludeUnofficial=false, isAdditive=false}) { + this._proxyAssignSimple("state", Object.keys(this._state).mergeMap(k=>({ + [k]: SourceUtil.isAdventure(k) && (isIncludeUnofficial || !SourceUtil.isNonstandardSource(k)) ? 1 : isAdditive ? this._state[k] : 0 + })), ); + } + + _doSetPinsHomebrew({isAdditive=false}) { + this._proxyAssignSimple("state", Object.keys(this._state).mergeMap(k=>({ + [k]: SourceUtil.getFilterGroup(k) === SourceUtil.FILTER_GROUP_HOMEBREW ? 1 : isAdditive ? this._state[k] : 0 + })), ); + } + + _doSetPinsVanilla() { + Object.keys(this._state).forEach(k=>this._state[k] = Parser.SOURCES_VANILLA.has(k) ? 1 : 0); + } + + _doSetPinsNonUa() { + Object.keys(this._state).forEach(k=>this._state[k] = !SourceUtil.isPrereleaseSource(k) ? 1 : 0); + } + + _doSetPinsSrd() { + SourceFilter._SRD_SOURCES = SourceFilter._SRD_SOURCES || new Set([Parser.SRC_PHB, Parser.SRC_MM, Parser.SRC_DMG]); + + Object.keys(this._state).forEach(k=>this._state[k] = SourceFilter._SRD_SOURCES.has(k) ? 1 : 0); + + const srdFilter = this._filterBox.filters.find(it=>it.isSrdFilter); + if (srdFilter) + srdFilter.setValue("SRD", 1); + + const basicRulesFilter = this._filterBox.filters.find(it=>it.isBasicRulesFilter); + if (basicRulesFilter) + basicRulesFilter.setValue("Basic Rules", 0); + + const reprintedFilter = this._filterBox.filters.find(it=>it.isReprintedFilter); + if (reprintedFilter) + reprintedFilter.setValue("Reprinted", 0); + } + + _doSetPinsBasicRules() { + SourceFilter._BASIC_RULES_SOURCES = SourceFilter._BASIC_RULES_SOURCES || new Set([Parser.SRC_PHB, Parser.SRC_MM, Parser.SRC_DMG]); + + Object.keys(this._state).forEach(k=>this._state[k] = SourceFilter._BASIC_RULES_SOURCES.has(k) ? 1 : 0); + + const basicRulesFilter = this._filterBox.filters.find(it=>it.isBasicRulesFilter); + if (basicRulesFilter) + basicRulesFilter.setValue("Basic Rules", 1); + + const srdFilter = this._filterBox.filters.find(it=>it.isSrdFilter); + if (srdFilter) + srdFilter.setValue("SRD", 0); + + const reprintedFilter = this._filterBox.filters.find(it=>it.isReprintedFilter); + if (reprintedFilter) + reprintedFilter.setValue("Reprinted", 0); + } + + static getCompleteFilterSources(ent) { + if (!ent.otherSources) + return ent.source; + + const otherSourcesFilt = ent.otherSources.filter(src=>!ExcludeUtil.isExcluded("*", "*", src.source, { + isNoCount: true + })); + if (!otherSourcesFilt.length) + return ent.source; + + return [ent.source].concat(otherSourcesFilt.map(src=>new SourceFilterItem({ + item: src.source, + isIgnoreRed: true, + isOtherSource: true + }))); + } + + _doRenderPills_doRenderWrpGroup_getHrDivider(group) { + switch (group) { + case SourceUtil.FILTER_GROUP_NON_STANDARD: + return this._doRenderPills_doRenderWrpGroup_getHrDivider_groupNonStandard(group); + case SourceUtil.FILTER_GROUP_HOMEBREW: + return this._doRenderPills_doRenderWrpGroup_getHrDivider_groupBrew(group); + default: + return super._doRenderPills_doRenderWrpGroup_getHrDivider(group); + } + } + + _doRenderPills_doRenderWrpGroup_getHrDivider_groupNonStandard(group) { + let dates = []; + const comp = BaseComponent.fromObject({ + min: 0, + max: 0, + curMin: 0, + curMax: 0, + }); + + const wrpSlider = new ComponentUiUtil.RangeSlider({ + comp, + propMin: "min", + propMax: "max", + propCurMin: "curMin", + propCurMax: "curMax", + fnDisplay: val=>dates[val]?.str, + }).get(); + + const wrpWrpSlider = e_({ + tag: "div", + clazz: `"w-100 ve-flex pt-2 pb-5 mb-2 mt-1 fltr-src__wrp-slider`, + children: [wrpSlider, ], + }).hideVe(); + + const btnCancel = e_({ + tag: "button", + clazz: `btn btn-xs btn-default px-1`, + html: "Cancel", + click: ()=>{ + grpBtnsInactive.showVe(); + wrpWrpSlider.hideVe(); + grpBtnsActive.hideVe(); + } + , + }); + + const btnConfirm = e_({ + tag: "button", + clazz: `btn btn-xs btn-default px-1`, + html: "Confirm", + click: ()=>{ + grpBtnsInactive.showVe(); + wrpWrpSlider.hideVe(); + grpBtnsActive.hideVe(); + + const min = comp._state.curMin; + const max = comp._state.curMax; + + const allowedDateSet = new Set(dates.slice(min, max + 1).map(it=>it.str)); + const nxtState = {}; + Object.keys(this._state).filter(k=>SourceUtil.isNonstandardSource(k)).forEach(k=>{ + const sourceDate = Parser.sourceJsonToDate(k); + nxtState[k] = allowedDateSet.has(sourceDate) ? 1 : 0; + } + ); + this._proxyAssign("state", "_state", "__state", nxtState); + } + , + }); + + const btnShowSlider = e_({ + tag: "button", + clazz: `btn btn-xxs btn-default px-1`, + html: "Select by Date", + click: ()=>{ + grpBtnsInactive.hideVe(); + wrpWrpSlider.showVe(); + grpBtnsActive.showVe(); + + dates = Object.keys(this._state).filter(it=>SourceUtil.isNonstandardSource(it)).map(it=>Parser.sourceJsonToDate(it)).filter(Boolean).unique().map(it=>({ + str: it, + date: new Date(it) + })).sort((a,b)=>SortUtil.ascSortDate(a.date, b.date)).reverse(); + + comp._proxyAssignSimple("state", { + min: 0, + max: dates.length - 1, + curMin: 0, + curMax: dates.length - 1, + }, ); + } + , + }); + + const btnClear = e_({ + tag: "button", + clazz: `btn btn-xxs btn-default px-1`, + html: "Clear", + click: ()=>{ + const nxtState = {}; + Object.keys(this._state).filter(k=>SourceUtil.isNonstandardSource(k)).forEach(k=>nxtState[k] = 0); + this._proxyAssign("state", "_state", "__state", nxtState); + } + , + }); + + const grpBtnsActive = e_({ + tag: "div", + clazz: `ve-flex-v-center btn-group`, + children: [btnCancel, btnConfirm, ], + }).hideVe(); + + const grpBtnsInactive = e_({ + tag: "div", + clazz: `ve-flex-v-center btn-group`, + children: [btnClear, btnShowSlider, ], + }); + + return e_({ + tag: "div", + clazz: `ve-flex-col w-100`, + children: [super._doRenderPills_doRenderWrpGroup_getHrDivider(), e_({ + tag: "div", + clazz: `mb-1 ve-flex-h-right`, + children: [grpBtnsActive, grpBtnsInactive, ], + }), wrpWrpSlider, ], + }); + } + + _doRenderPills_doRenderWrpGroup_getHrDivider_groupBrew(group) { + const btnClear = e_({ + tag: "button", + clazz: `btn btn-xxs btn-default px-1`, + html: "Clear", + click: ()=>{ + const nxtState = {}; + Object.keys(this._state).filter(k=>BrewUtil2.hasSourceJson(k)).forEach(k=>nxtState[k] = 0); + this._proxyAssign("state", "_state", "__state", nxtState); + } + , + }); + + return e_({ + tag: "div", + clazz: `ve-flex-col w-100`, + children: [super._doRenderPills_doRenderWrpGroup_getHrDivider(), e_({ + tag: "div", + clazz: `mb-1 ve-flex-h-right`, + children: [e_({ + tag: "div", + clazz: `ve-flex-v-center btn-group`, + children: [btnClear, ], + }), ], + }), ], + }); + } + + _toDisplay_getMappedEntryVal(entryVal) { + entryVal = super._toDisplay_getMappedEntryVal(entryVal); + if (!this._meta.isIncludeOtherSources) + entryVal = entryVal.filter(it=>!it.isOtherSource); + return entryVal; + } + + _getPill(item) { + const displayText = this._getDisplayText(item); + const displayTextMini = this._getDisplayTextMini(item); + + const dispName = e_({ + tag: "span", + html: displayText, + }); + + const spc = e_({ + tag: "span", + clazz: "px-2 fltr-src__spc-pill", + text: "|", + }); + + const dispAbbreviation = e_({ + tag: "span", + html: displayTextMini, + }); + + const btnPill = e_({ + tag: "div", + clazz: "fltr__pill", + children: [dispAbbreviation, spc, dispName, ], + click: evt=>this._getPill_handleClick({ + evt, + item + }), + contextmenu: evt=>this._getPill_handleContextmenu({ + evt, + item + }), + }); + + this._getPill_bindHookState({ + btnPill, + item + }); + + this._addHook("meta", "pillDisplayMode", ()=>{ + dispAbbreviation.toggleVe(this._meta.pillDisplayMode !== 0); + spc.toggleVe(this._meta.pillDisplayMode === 2); + dispName.toggleVe(this._meta.pillDisplayMode !== 1); + } + )(); + + item.searchText = `${Parser.sourceJsonToAbv(item.item || item).toLowerCase()} -- ${displayText.toLowerCase()}`; + + return btnPill; + } + + getSources() { + const out = { all: [], }; + this._items.forEach(it=>{ + out.all.push(it.item); + const group = this._groupFn(it); + (out[group] ||= []).push(it.item); + }); + return out; + } + + getDefaultMeta() { + return { + ...super.getDefaultMeta(), + ...SourceFilter._DEFAULT_META, + }; + } + + static _SORT_ITEMS_MINI(a, b) { + a = a.item ?? a; + b = b.item ?? b; + const valA = BrewUtil2.hasSourceJson(a) ? 2 : (SourceUtil.isNonstandardSource(a) || PrereleaseUtil.hasSourceJson(a)) ? 1 : 0; + const valB = BrewUtil2.hasSourceJson(b) ? 2 : (SourceUtil.isNonstandardSource(b) || PrereleaseUtil.hasSourceJson(b)) ? 1 : 0; + return SortUtil.ascSort(valA, valB) || SortUtil.ascSortLower(Parser.sourceJsonToFull(a), Parser.sourceJsonToFull(b)); + } + + static _getDisplayHtmlMini(item) { + item = item.item || item; + const isBrewSource = BrewUtil2.hasSourceJson(item); + const isNonStandardSource = !isBrewSource && (SourceUtil.isNonstandardSource(item) || PrereleaseUtil.hasSourceJson(item)); + return ` ${Parser.sourceJsonToAbv(item)}`; + } +}; +SourceFilter._DEFAULT_META = { + isIncludeOtherSources: false, + pillDisplayMode: 0, +}; +SourceFilter._PILL_DISPLAY_MODE_LABELS = { + "0": "As Names", + "1": "As Abbreviations", + "2": "As Names Plus Abbreviations", +}; +SourceFilter._SRD_SOURCES = null; +SourceFilter._BASIC_RULES_SOURCES = null; + +class SourceFilterItem extends FilterItem { + constructor(options) { + super(options); + this.isOtherSource = options.isOtherSource; + } +} +//#endregion +//#region OptionsFilter +let OptionsFilter = class OptionsFilter extends FilterBase { + constructor(opts) { + super(opts); + this._defaultState = opts.defaultState; + this._displayFn = opts.displayFn; + this._displayFnMini = opts.displayFnMini; + + Object.assign(this.__state, MiscUtil.copy(opts.defaultState), ); + + this._filterBox = null; + this.__$wrpMini = null; + } + + getSaveableState() { + return { + [this.header]: { + ...this.getBaseSaveableState(), + state: { + ...this.__state + }, + }, + }; + } + + setStateFromLoaded(filterState, {isUserSavedState=false}={}) { + if (!filterState?.[this.header]) + return; + + const toLoad = filterState[this.header]; + this._hasUserSavedState = this._hasUserSavedState || isUserSavedState; + + this.setBaseStateFromLoaded(toLoad); + + const toAssign = {}; + Object.keys(this._defaultState).forEach(k=>{ + if (toLoad.state[k] == null) + return; + if (typeof toLoad.state[k] !== typeof this._defaultState[k]) + return; + toAssign[k] = toLoad.state[k]; + } + ); + + Object.assign(this._state, toAssign); + } + + _getStateNotDefault() { + return Object.entries(this._state).filter(([k,v])=>this._defaultState[k] !== v); + } + + getSubHashes() { + const out = []; + + const baseMeta = this.getMetaSubHashes(); + if (baseMeta) + out.push(...baseMeta); + + const serOptionState = []; + Object.entries(this._defaultState).forEach(([k,vDefault])=>{ + if (this._state[k] !== vDefault) + serOptionState.push(`${k.toLowerCase()}=${UrlUtil.mini.compress(this._state[k])}`); + } + ); + if (serOptionState.length) { + out.push(UrlUtil.packSubHash(this.getSubHashPrefix("state", this.header), serOptionState)); + } + + return out.length ? out : null; + } + + getFilterTagPart() { + const areNotDefaultState = this._getStateNotDefault(); + if (!areNotDefaultState.length) + return null; + + const pt = areNotDefaultState.map(([k,v])=>`${v ? "" : "!"}${k}`).join(";").toLowerCase(); + + return `${this.header.toLowerCase()}=::${pt}::`; + } + + getDisplayStatePart({nxtState=null}={}) { + return null; + } + + getNextStateFromSubhashState(state) { + const nxtState = this._getNextState_base(); + + if (state == null) { + this._mutNextState_reset(nxtState); + return nxtState; + } + + this._mutNextState_meta_fromSubHashState(nxtState, state); + + let hasState = false; + + Object.entries(state).forEach(([k,vals])=>{ + const prop = FilterBase.getProp(k); + if (prop !== "state") + return; + + hasState = true; + vals.forEach(v=>{ + const [prop,valCompressed] = v.split("="); + const val = UrlUtil.mini.decompress(valCompressed); + + const casedProp = Object.keys(this._defaultState).find(k=>k.toLowerCase() === prop); + if (!casedProp) + return; + + if (this._defaultState[casedProp] != null && typeof val === typeof this._defaultState[casedProp]) + nxtState[this.header].state[casedProp] = val; + } + ); + } + ); + + if (!hasState) + this._mutNextState_reset(nxtState); + + return nxtState; + } + + setFromValues(values) { + if (!values[this.header]) + return; + const vals = values[this.header]; + Object.entries(vals).forEach(([k,v])=>{ + if (this._defaultState[k] && typeof this._defaultState[k] === typeof v) + this._state[k] = v; + } + ); + } + + setValue(k, v) { + this._state[k] = v; + } + + $render(opts) { + this._filterBox = opts.filterBox; + this.__$wrpMini = opts.$wrpMini; + + const $wrpControls = opts.isMulti ? null : this._$getHeaderControls(); + + const $btns = Object.keys(this._defaultState).map(k=>this._$render_$getPill(k)); + const $wrpButtons = $$`
    ${$btns}
    `; + + if (opts.isMulti) { + return this.__$wrpFilter = $$`
    +
    ${this._getRenderedHeader()}
    + ${$wrpButtons} +
    `; + } else { + return this.__$wrpFilter = $$`
    + ${opts.isFirst ? "" : `
    `} +
    +
    ${this._getRenderedHeader()}
    + ${$wrpControls} +
    + ${$wrpButtons} +
    `; + } + } + + $renderMinis(opts) { + if (!opts.$wrpMini) + return; + + this._filterBox = opts.filterBox; + this.__$wrpMini = opts.$wrpMini; + + const $btnsMini = Object.keys(this._defaultState).map(k=>this._$render_$getMiniPill(k)); + $btnsMini.forEach($btn=>$btn.appendTo(this.__$wrpMini)); + } + + _$render_$getPill(key) { + const displayText = this._displayFn(key); + + const $btnPill = $(`
    ${displayText}
    `).click(()=>{ + this._state[key] = !this._state[key]; + } + ).contextmenu((evt)=>{ + evt.preventDefault(); + this._state[key] = !this._state[key]; + } + ); + const hook = ()=>{ + const val = FilterBox._PILL_STATES[this._state[key] ? 1 : 2]; + $btnPill.attr("state", val); + } + ; + this._addHook("state", key, hook); + hook(); + + return $btnPill; + } + + _$render_$getMiniPill(key) { + const displayTextFull = this._displayFnMini ? this._displayFn(key) : null; + const displayText = this._displayFnMini ? this._displayFnMini(key) : this._displayFn(key); + + const $btnMini = $(`
    ${displayText}
    `).title(`${displayTextFull ? `${displayTextFull} (` : ""}Filter: ${this.header}${displayTextFull ? ")" : ""}`).click(()=>{ + this._state[key] = this._defaultState[key]; + this._filterBox.fireChangeEvent(); + } + ); + + const hook = ()=>$btnMini.attr("state", FilterBox._PILL_STATES[this._defaultState[key] === this._state[key] ? 0 : this._state[key] ? 1 : 2]); + this._addHook("state", key, hook); + + const hideHook = ()=>$btnMini.toggleClass("ve-hidden", this._filterBox.isMinisHidden(this.header)); + this._filterBox.registerMinisHiddenHook(this.header, hideHook); + + return $btnMini; + } + + _$getHeaderControls() { + const $btnReset = $(``).click(()=>this.reset()); + const $wrpBtns = $$`
    ${$btnReset}
    `; + + const $wrpSummary = $(`
    `).hideVe(); + + const $btnShowHide = $(``).click(()=>this._meta.isHidden = !this._meta.isHidden); + const hkIsHidden = ()=>{ + $btnShowHide.toggleClass("active", this._meta.isHidden); + $wrpBtns.toggleVe(!this._meta.isHidden); + $wrpSummary.toggleVe(this._meta.isHidden); + + const cntNonDefault = Object.entries(this._defaultState).filter(([k,v])=>this._state[k] != null && this._state[k] !== v).length; + + $wrpSummary.title(`${cntNonDefault} non-default option${cntNonDefault === 1 ? "" : "s"} selected`).text(cntNonDefault); + } + ; + this._addHook("meta", "isHidden", hkIsHidden); + hkIsHidden(); + + return $$` +
    + ${$wrpBtns} + ${$wrpSummary} + ${$btnShowHide} +
    `; + } + + getValues({nxtState=null}={}) { + const state = nxtState?.[this.header]?.state || this.__state; + + const out = Object.entries(this._defaultState).mergeMap(([k,v])=>({ + [k]: state[k] == null ? v : state[k] + })); + out._isActive = Object.entries(this._defaultState).some(([k,v])=>state[k] != null && state[k] !== v); + return { + [this.header]: out, + }; + } + + _mutNextState_reset(nxtState, {isResetAll=false}={}) { + if (isResetAll) + this._mutNextState_resetBase(nxtState, { + isResetAll + }); + Object.assign(nxtState[this.header].state, MiscUtil.copy(this._defaultState)); + } + + update() {} + + toDisplay(boxState, entryVal) { + const filterState = boxState[this.header]; + if (!filterState) + return true; + if (entryVal == null) + return true; + return Object.entries(entryVal).every(([k,v])=>this._state[k] === v); + } + + getDefaultMeta() { + return { + ...super.getDefaultMeta(), + ...OptionsFilter._DEFAULT_META, + }; + } + + handleSearch(searchTerm) { + if (this.__$wrpFilter == null) + return; + + const isVisible = this.header.toLowerCase().includes(searchTerm) || Object.keys(this._defaultState).map(it=>this._displayFn(it).toLowerCase()).some(it=>it.includes(searchTerm)); + + this.__$wrpFilter.toggleClass("fltr__hidden--search", !isVisible); + + return isVisible; + } +} +; +OptionsFilter._DEFAULT_META = {}; +//#endregion +//#region AbilityScoreFilter +let AbilityScoreFilter = class AbilityScoreFilter extends FilterBase { + static _MODIFIER_SORT_OFFSET = 10000; + constructor(opts) { + super(opts); + + this._items = []; + this._isItemsDirty = false; + this._itemsLookup = {}; + this._seenUids = {}; + + this.__$wrpFilter = null; + this.__wrpPills = null; + this.__wrpPillsRows = {}; + this.__wrpMiniPills = null; + + this._maxMod = 2; + this._minMod = 0; + + Parser.ABIL_ABVS.forEach(ab=>{ + const itemAnyIncrease = new AbilityScoreFilter.FilterItem({ + isAnyIncrease: true, + ability: ab + }); + const itemAnyDecrease = new AbilityScoreFilter.FilterItem({ + isAnyDecrease: true, + ability: ab + }); + this._items.push(itemAnyIncrease, itemAnyDecrease); + this._itemsLookup[itemAnyIncrease.uid] = itemAnyIncrease; + this._itemsLookup[itemAnyDecrease.uid] = itemAnyDecrease; + if (this.__state[itemAnyIncrease.uid] == null) + this.__state[itemAnyIncrease.uid] = 0; + if (this.__state[itemAnyDecrease.uid] == null) + this.__state[itemAnyDecrease.uid] = 0; + } + ); + + for (let i = this._minMod; i <= this._maxMod; ++i) { + if (i === 0) + continue; + Parser.ABIL_ABVS.forEach(ab=>{ + const item = new AbilityScoreFilter.FilterItem({ + modifier: i, + ability: ab + }); + this._items.push(item); + this._itemsLookup[item.uid] = item; + if (this.__state[item.uid] == null) + this.__state[item.uid] = 0; + } + ); + } + } + + $render(opts) { + this._filterBox = opts.filterBox; + this.__wrpMiniPills = e_({ + ele: opts.$wrpMini[0] + }); + + const wrpControls = this._getHeaderControls(opts); + + this.__wrpPills = e_({ + tag: "div", + clazz: `fltr__wrp-pills overflow-x-auto ve-flex-col w-100` + }); + const hook = ()=>this.__wrpPills.toggleVe(!this._meta.isHidden); + this._addHook("meta", "isHidden", hook); + hook(); + + this._doRenderPills(); + + const btnMobToggleControls = Filter.prototype._getBtnMobToggleControls.bind(this)(wrpControls); + + this.__$wrpFilter = $$`
    + ${opts.isFirst ? "" : `
    `} +
    +
    ${opts.isMulti ? `\u2012` : ""}${this._getRenderedHeader()}${btnMobToggleControls}
    + ${wrpControls} +
    + ${this.__wrpPills} +
    `; + + this.update(); + return this.__$wrpFilter; + } + + _getHeaderControls(opts) { + const btnClear = e_({ + tag: "button", + clazz: `btn btn-default ${opts.isMulti ? "btn-xxs" : "btn-xs"} fltr__h-btn--clear w-100`, + click: ()=>this._doSetPillsClear(), + html: "Clear", + }); + + const wrpStateBtnsOuter = e_({ + tag: "div", + clazz: "ve-flex-v-center fltr__h-wrp-state-btns-outer", + children: [e_({ + tag: "div", + clazz: "btn-group ve-flex-v-center w-100", + children: [btnClear, ], + }), ], + }); + + const wrpSummary = e_({ + tag: "div", + clazz: "ve-flex-vh-center ve-hidden" + }); + + const btnShowHide = e_({ + tag: "button", + clazz: `btn btn-default ${opts.isMulti ? "btn-xxs" : "btn-xs"} ml-2`, + click: ()=>this._meta.isHidden = !this._meta.isHidden, + html: "Hide", + }); + const hookShowHide = ()=>{ + e_({ + ele: btnShowHide + }).toggleClass("active", this._meta.isHidden); + wrpStateBtnsOuter.toggleVe(!this._meta.isHidden); + + const cur = this.getValues()[this.header]; + + const htmlSummary = [cur._totals?.yes ? `${cur._totals.yes}` : null, ].filter(Boolean).join(""); + e_({ + ele: wrpSummary, + html: htmlSummary + }).toggleVe(this._meta.isHidden); + } + ; + this._addHook("meta", "isHidden", hookShowHide); + hookShowHide(); + + return e_({ + tag: "div", + clazz: `ve-flex-v-center fltr__h-wrp-btns-outer`, + children: [wrpSummary, wrpStateBtnsOuter, btnShowHide, ], + }); + } + + _doRenderPills() { + this._items.sort(this.constructor._ascSortItems.bind(this.constructor)); + + if (!this.__wrpPills) + return; + this._items.forEach(it=>{ + if (!it.rendered) + it.rendered = this._getPill(it); + if (!it.isAnyIncrease && !it.isAnyDecrease) + it.rendered.toggleClass("fltr__pill--muted", !this._seenUids[it.uid]); + + if (!this.__wrpPillsRows[it.ability]) { + this.__wrpPillsRows[it.ability] = { + row: e_({ + tag: "div", + clazz: "ve-flex-v-center w-100 my-1", + children: [e_({ + tag: "div", + clazz: "mr-3 text-right fltr__label-ability-score no-shrink no-grow", + text: Parser.attAbvToFull(it.ability), + }), ], + }).appendTo(this.__wrpPills), + searchText: Parser.attAbvToFull(it.ability).toLowerCase(), + }; + } + + it.rendered.appendTo(this.__wrpPillsRows[it.ability].row); + } + ); + } + + _getPill(item) { + const unsetRow = ()=>{ + const nxtState = {}; + for (let i = this._minMod; i <= this._maxMod; ++i) { + if (!i || i === item.modifier) + continue; + const siblingUid = AbilityScoreFilter.FilterItem.getUid_({ + ability: item.ability, + modifier: i + }); + nxtState[siblingUid] = 0; + } + + if (!item.isAnyIncrease) + nxtState[AbilityScoreFilter.FilterItem.getUid_({ + ability: item.ability, + isAnyIncrease: true + })] = 0; + if (!item.isAnyDecrease) + nxtState[AbilityScoreFilter.FilterItem.getUid_({ + ability: item.ability, + isAnyDecrease: true + })] = 0; + + this._proxyAssignSimple("state", nxtState); + } + ; + + const btnPill = e_({ + tag: "div", + clazz: `fltr__pill fltr__pill--ability-bonus px-2`, + html: item.getPillDisplayHtml(), + click: evt=>{ + if (evt.shiftKey) { + const nxtState = {}; + Object.keys(this._state).forEach(k=>nxtState[k] = 0); + this._proxyAssign("state", "_state", "__state", nxtState, true); + } + + this._state[item.uid] = this._state[item.uid] ? 0 : 1; + if (this._state[item.uid]) + unsetRow(); + } + , + contextmenu: (evt)=>{ + evt.preventDefault(); + + this._state[item.uid] = this._state[item.uid] ? 0 : 1; + if (this._state[item.uid]) + unsetRow(); + } + , + }); + + const hook = ()=>{ + const val = FilterBox._PILL_STATES[this._state[item.uid] || 0]; + btnPill.attr("state", val); + } + ; + this._addHook("state", item.uid, hook); + hook(); + + return btnPill; + } + + _doRenderMiniPills() { + this._items.slice(0).sort(this.constructor._ascSortMiniPills.bind(this.constructor)).forEach(it=>{ + (it.btnMini = it.btnMini || this._getBtnMini(it)).appendTo(this.__wrpMiniPills); + } + ); + } + + _getBtnMini(item) { + const btnMini = e_({ + tag: "div", + clazz: `fltr__mini-pill ${this._filterBox.isMinisHidden(this.header) ? "ve-hidden" : ""}`, + text: item.getMiniPillDisplayText(), + title: `Filter: ${this.header}`, + click: ()=>{ + this._state[item.uid] = 0; + this._filterBox.fireChangeEvent(); + } + , + }).attr("state", FilterBox._PILL_STATES[this._state[item.uid] || 0]); + + const hook = ()=>btnMini.attr("state", FilterBox._PILL_STATES[this._state[item.uid] || 0]); + this._addHook("state", item.uid, hook); + + const hideHook = ()=>btnMini.toggleClass("ve-hidden", this._filterBox.isMinisHidden(this.header)); + this._filterBox.registerMinisHiddenHook(this.header, hideHook); + + return btnMini; + } + + static _ascSortItems(a, b) { + return SortUtil.ascSort(Number(b.isAnyIncrease), Number(a.isAnyIncrease)) || SortUtil.ascSortAtts(a.ability, b.ability) || SortUtil.ascSort(b.modifier ? b.modifier + AbilityScoreFilter._MODIFIER_SORT_OFFSET : b.modifier, a.modifier ? a.modifier + AbilityScoreFilter._MODIFIER_SORT_OFFSET : a.modifier) || SortUtil.ascSort(Number(b.isAnyDecrease), Number(a.isAnyDecrease)); + } + + static _ascSortMiniPills(a, b) { + return SortUtil.ascSort(Number(b.isAnyIncrease), Number(a.isAnyIncrease)) || SortUtil.ascSort(Number(b.isAnyDecrease), Number(a.isAnyDecrease)) || SortUtil.ascSort(b.modifier ? b.modifier + AbilityScoreFilter._MODIFIER_SORT_OFFSET : b.modifier, a.modifier ? a.modifier + AbilityScoreFilter._MODIFIER_SORT_OFFSET : a.modifier) || SortUtil.ascSortAtts(a.ability, b.ability); + } + + $renderMinis(opts) { + this._filterBox = opts.filterBox; + this.__wrpMiniPills = e_({ + ele: opts.$wrpMini[0] + }); + + this._doRenderMiniPills(); + } + + getValues({nxtState=null}={}) { + const out = { + _totals: { + yes: 0 + }, + }; + + const state = nxtState?.[this.header]?.state || this.__state; + + Object.entries(state).filter(([,value])=>value).forEach(([uid])=>{ + out._totals.yes++; + out[uid] = true; + } + ); + + return { + [this.header]: out + }; + } + + _mutNextState_reset(nxtState, {isResetAll=false}={}) { + Object.keys(nxtState[this.header].state).forEach(k=>delete nxtState[this.header].state[k]); + } + + update() { + if (this._isItemsDirty) { + this._isItemsDirty = false; + + this._doRenderPills(); + } + + this._doRenderMiniPills(); + } + + _doSetPillsClear() { + Object.keys(this._state).forEach(k=>{ + if (this._state[k] !== 0) + this._state[k] = 0; + } + ); + } + + toDisplay(boxState, entryVal) { + const filterState = boxState[this.header]; + if (!filterState) + return true; + + const activeItems = Object.keys(filterState).filter(it=>!it.startsWith("_")).map(it=>this._itemsLookup[it]).filter(Boolean); + + if (!activeItems.length) + return true; + if ((!entryVal || !entryVal.length) && activeItems.length) + return false; + + return entryVal.some(abilObject=>{ + const cpyAbilObject = MiscUtil.copy(abilObject); + const vewActiveItems = [...activeItems]; + + Parser.ABIL_ABVS.forEach(ab=>{ + if (!cpyAbilObject[ab] || !vewActiveItems.length) + return; + + const ixExact = vewActiveItems.findIndex(it=>it.ability === ab && it.modifier === cpyAbilObject[ab]); + if (~ixExact) + return vewActiveItems.splice(ixExact, 1); + } + ); + if (!vewActiveItems.length) + return true; + + if (cpyAbilObject.choose?.from) { + const amount = cpyAbilObject.choose.amount || 1; + const count = cpyAbilObject.choose.count || 1; + + for (let i = 0; i < count; ++i) { + if (!vewActiveItems.length) + break; + + const ix = vewActiveItems.findIndex(it=>cpyAbilObject.choose.from.includes(it.ability) && amount === it.modifier); + if (~ix) { + const [cpyActiveItem] = vewActiveItems.splice(ix, 1); + cpyAbilObject.choose.from = cpyAbilObject.choose.from.filter(it=>it !== cpyActiveItem.ability); + } + } + } else if (cpyAbilObject.choose?.weighted?.weights && cpyAbilObject.choose?.weighted?.from) { + cpyAbilObject.choose.weighted.weights.forEach(weight=>{ + const ix = vewActiveItems.findIndex(it=>cpyAbilObject.choose.weighted.from.includes(it.ability) && weight === it.modifier); + if (~ix) { + const [cpyActiveItem] = vewActiveItems.splice(ix, 1); + cpyAbilObject.choose.weighted.from = cpyAbilObject.choose.weighted.from.filter(it=>it !== cpyActiveItem.ability); + } + } + ); + } + if (!vewActiveItems.length) + return true; + + Parser.ABIL_ABVS.forEach(ab=>{ + if (!cpyAbilObject[ab] || !vewActiveItems.length) + return; + + const ix = vewActiveItems.findIndex(it=>it.ability === ab && ((cpyAbilObject[ab] > 0 && it.isAnyIncrease) || (cpyAbilObject[ab] < 0 && it.isAnyDecrease))); + if (~ix) + return vewActiveItems.splice(ix, 1); + } + ); + if (!vewActiveItems.length) + return true; + + if (cpyAbilObject.choose?.from) { + const amount = cpyAbilObject.choose.amount || 1; + const count = cpyAbilObject.choose.count || 1; + + for (let i = 0; i < count; ++i) { + if (!vewActiveItems.length) + return true; + + const ix = vewActiveItems.findIndex(it=>cpyAbilObject.choose.from.includes(it.ability) && ((amount > 0 && it.isAnyIncrease) || (amount < 0 && it.isAnyDecrease))); + if (~ix) { + const [cpyActiveItem] = vewActiveItems.splice(ix, 1); + cpyAbilObject.choose.from = cpyAbilObject.choose.from.filter(it=>it !== cpyActiveItem.ability); + } + } + } else if (cpyAbilObject.choose?.weighted?.weights && cpyAbilObject.choose?.weighted?.from) { + cpyAbilObject.choose.weighted.weights.forEach(weight=>{ + if (!vewActiveItems.length) + return; + + const ix = vewActiveItems.findIndex(it=>cpyAbilObject.choose.weighted.from.includes(it.ability) && ((weight > 0 && it.isAnyIncrease) || (weight < 0 && it.isAnyDecrease))); + if (~ix) { + const [cpyActiveItem] = vewActiveItems.splice(ix, 1); + cpyAbilObject.choose.weighted.from = cpyAbilObject.choose.weighted.from.filter(it=>it !== cpyActiveItem.ability); + } + } + ); + } + return !vewActiveItems.length; + } + ); + } + + addItem(abilArr) { + if (!abilArr?.length) + return; + + let nxtMaxMod = this._maxMod; + let nxtMinMod = this._minMod; + + abilArr.forEach(abilObject=>{ + Parser.ABIL_ABVS.forEach(ab=>{ + if (abilObject[ab] != null) { + nxtMaxMod = Math.max(nxtMaxMod, abilObject[ab]); + nxtMinMod = Math.min(nxtMinMod, abilObject[ab]); + + const uid = AbilityScoreFilter.FilterItem.getUid_({ + ability: ab, + modifier: abilObject[ab] + }); + if (!this._seenUids[uid]) + this._isItemsDirty = true; + this._seenUids[uid] = true; + } + } + ); + + if (abilObject.choose?.from) { + const amount = abilObject.choose.amount || 1; + nxtMaxMod = Math.max(nxtMaxMod, amount); + nxtMinMod = Math.min(nxtMinMod, amount); + + abilObject.choose.from.forEach(ab=>{ + const uid = AbilityScoreFilter.FilterItem.getUid_({ + ability: ab, + modifier: amount + }); + if (!this._seenUids[uid]) + this._isItemsDirty = true; + this._seenUids[uid] = true; + } + ); + } + + if (abilObject.choose?.weighted?.weights) { + nxtMaxMod = Math.max(nxtMaxMod, ...abilObject.choose.weighted.weights); + nxtMinMod = Math.min(nxtMinMod, ...abilObject.choose.weighted.weights); + + abilObject.choose.weighted.from.forEach(ab=>{ + abilObject.choose.weighted.weights.forEach(weight=>{ + const uid = AbilityScoreFilter.FilterItem.getUid_({ + ability: ab, + modifier: weight + }); + if (!this._seenUids[uid]) + this._isItemsDirty = true; + this._seenUids[uid] = true; + } + ); + } + ); + } + } + ); + + if (nxtMaxMod > this._maxMod) { + for (let i = this._maxMod + 1; i <= nxtMaxMod; ++i) { + if (i === 0) + continue; + Parser.ABIL_ABVS.forEach(ab=>{ + const item = new AbilityScoreFilter.FilterItem({ + modifier: i, + ability: ab + }); + this._items.push(item); + this._itemsLookup[item.uid] = item; + if (this.__state[item.uid] == null) + this.__state[item.uid] = 0; + } + ); + } + + this._isItemsDirty = true; + this._maxMod = nxtMaxMod; + } + + if (nxtMinMod < this._minMod) { + for (let i = nxtMinMod; i < this._minMod; ++i) { + if (i === 0) + continue; + Parser.ABIL_ABVS.forEach(ab=>{ + const item = new AbilityScoreFilter.FilterItem({ + modifier: i, + ability: ab + }); + this._items.push(item); + this._itemsLookup[item.uid] = item; + if (this.__state[item.uid] == null) + this.__state[item.uid] = 0; + } + ); + } + + this._isItemsDirty = true; + this._minMod = nxtMinMod; + } + } + + getSaveableState() { + return { + [this.header]: { + ...this.getBaseSaveableState(), + state: { + ...this.__state + }, + }, + }; + } + + setStateFromLoaded(filterState, {isUserSavedState=false}={}) { + if (!filterState?.[this.header]) + return; + + const toLoad = filterState[this.header]; + this._hasUserSavedState = this._hasUserSavedState || isUserSavedState; + this.setBaseStateFromLoaded(toLoad); + Object.assign(this._state, toLoad.state); + } + + getSubHashes() { + const out = []; + + const baseMeta = this.getMetaSubHashes(); + if (baseMeta) + out.push(...baseMeta); + + const areNotDefaultState = Object.entries(this._state).filter(([k,v])=>{ + if (k.startsWith("_")) + return false; + return !!v; + } + ); + if (areNotDefaultState.length) { + const serPillStates = areNotDefaultState.map(([k,v])=>`${k.toUrlified()}=${v}`); + out.push(UrlUtil.packSubHash(this.getSubHashPrefix("state", this.header), serPillStates)); + } + + if (!out.length) + return null; + + return out; + } + + getNextStateFromSubhashState(state) { + const nxtState = this._getNextState_base(); + + if (state == null) { + this._mutNextState_reset(nxtState); + return nxtState; + } + + let hasState = false; + + Object.entries(state).forEach(([k,vals])=>{ + const prop = FilterBase.getProp(k); + switch (prop) { + case "state": + { + hasState = true; + Object.keys(nxtState[this.header].state).forEach(k=>nxtState[this.header].state[k] = 0); + + vals.forEach(v=>{ + const [statePropLower,state] = v.split("="); + const stateProp = Object.keys(nxtState[this.header].state).find(k=>k.toLowerCase() === statePropLower); + if (stateProp) + nxtState[this.header].state[stateProp] = Number(state) ? 1 : 0; + } + ); + break; + } + } + } + ); + + if (!hasState) + this._mutNextState_reset(nxtState); + + return nxtState; + } + + setFromValues(values) { + if (!values[this.header]) + return; + const nxtState = {}; + Object.keys(this._state).forEach(k=>nxtState[k] = 0); + Object.assign(nxtState, values[this.header]); + } + + handleSearch(searchTerm) { + const isHeaderMatch = this.header.toLowerCase().includes(searchTerm); + + if (isHeaderMatch) { + Object.values(this.__wrpPillsRows).forEach(meta=>meta.row.removeClass("fltr__hidden--search")); + + if (this.__$wrpFilter) + this.__$wrpFilter.toggleClass("fltr__hidden--search", false); + + return true; + } + + const isModNumber = /^[-+]\d*$/.test(searchTerm); + + let visibleCount = 0; + Object.values(this.__wrpPillsRows).forEach(({row, searchText})=>{ + const isVisible = isModNumber || searchText.includes(searchTerm); + row.toggleClass("fltr__hidden--search", !isVisible); + if (isVisible) + visibleCount++; + } + ); + + if (this.__$wrpFilter) + this.__$wrpFilter.toggleClass("fltr__hidden--search", visibleCount === 0); + + return visibleCount !== 0; + } + + _doTeardown() { + this._items.forEach(it=>{ + if (it.rendered) + it.rendered.detach(); + if (it.btnMini) + it.btnMini.detach(); + } + ); + + Object.values(this.__wrpPillsRows).forEach(meta=>meta.row.detach()); + } + + _getStateNotDefault() { + return Object.entries(this._state).filter(([,v])=>!!v); + } + + getFilterTagPart() { + const areNotDefaultState = this._getStateNotDefault(); + const compressedMeta = this._getCompressedMeta({ + isStripUiKeys: true + }); + + if (!areNotDefaultState.length && !compressedMeta) + return null; + + const pt = Object.entries(this._state).filter(([,v])=>!!v).map(([k,v])=>`${v === 2 ? "!" : ""}${k}`).join(";").toLowerCase(); + + return [this.header.toLowerCase(), pt, compressedMeta ? compressedMeta.join(HASH_SUB_LIST_SEP) : null, ].filter(it=>it != null).join("="); + } + + getDisplayStatePart({nxtState=null}={}) { + const state = nxtState?.[this.header]?.state || this.__state; + + const areNotDefaultState = this._getStateNotDefault({ + nxtState + }); + + if (!areNotDefaultState.length) + return null; + + const ptState = Object.entries(state).filter(([,v])=>!!v).map(([k,v])=>{ + const item = this._items.find(item=>item.uid === k); + if (!item) + return null; + return `${v === 2 ? "not " : ""}${item.getMiniPillDisplayText()}`; + } + ).join(", "); + + return `${this.header}: ${ptState}`; + } +}; +globalThis.AbilityScoreFilter = AbilityScoreFilter; +AbilityScoreFilter.FilterItem = class { + static getUid_({ability=null, isAnyIncrease=false, isAnyDecrease=false, modifier=null}) { + return `${Parser.attAbvToFull(ability)} ${modifier != null ? UiUtil.intToBonus(modifier) : (isAnyIncrease ? `+any` : isAnyDecrease ? `-any` : "?")}`; + } + + constructor({isAnyIncrease=false, isAnyDecrease=false, modifier=null, ability=null}) { + if (isAnyIncrease && isAnyDecrease) + throw new Error(`Invalid arguments!`); + if ((isAnyIncrease || isAnyDecrease) && modifier != null) + throw new Error(`Invalid arguments!`); + + this._ability = ability; + this._modifier = modifier; + this._isAnyIncrease = isAnyIncrease; + this._isAnyDecrease = isAnyDecrease; + this._uid = AbilityScoreFilter.FilterItem.getUid_({ + isAnyIncrease: this._isAnyIncrease, + isAnyDecrease: this._isAnyDecrease, + modifier: this._modifier, + ability: this._ability, + }); + } + + get ability() { + return this._ability; + } + get modifier() { + return this._modifier; + } + get isAnyIncrease() { + return this._isAnyIncrease; + } + get isAnyDecrease() { + return this._isAnyDecrease; + } + get uid() { + return this._uid; + } + + getMiniPillDisplayText() { + if (this._isAnyIncrease) + return `+Any ${Parser.attAbvToFull(this._ability)}`; + if (this._isAnyDecrease) + return `\u2012Any ${Parser.attAbvToFull(this._ability)}`; + return `${UiUtil.intToBonus(this._modifier, { + isPretty: true + })} ${Parser.attAbvToFull(this._ability)}`; + } + + getPillDisplayHtml() { + if (this._isAnyIncrease) + return `+Any`; + if (this._isAnyDecrease) + return `\u2012Any`; + return UiUtil.intToBonus(this._modifier, { + isPretty: true + }); + } +}; +//#endregion + +//#region PageFilters +class PageFilter { + + constructor(opts) { + opts = opts || {}; + this._sourceFilter = new SourceFilter(opts.sourceFilterOpts); + this._filterBox = null; + } + + get filterBox() { return this._filterBox; } + get sourceFilter() { return this._sourceFilter; } + + mutateAndAddToFilters(entity, isExcluded, opts) { + this.constructor.mutateForFilters(entity, opts); + this.addToFilters(entity, isExcluded, opts); + } + + static mutateForFilters(entity, opts) { + throw new Error("Unimplemented!"); + } + addToFilters(entity, isExcluded, opts) { + throw new Error("Unimplemented!"); + } + toDisplay(values, entity) { + throw new Error("Unimplemented!"); + } + async _pPopulateBoxOptions() { + throw new Error("Unimplemented!"); + } + + async pInitFilterBox(opts) { + opts = opts || {}; + await this._pPopulateBoxOptions(opts); + this._filterBox = new FilterBox(opts); + await this._filterBox.pDoLoadState(); + return this._filterBox; + } + + trimState() { + return this._filterBox.trimState_(); + } + + static _getClassFilterItem({className, classSource, isVariantClass, definedInSource}) { + const nm = className.split("(")[0].trim(); + const variantSuffix = isVariantClass ? ` [${definedInSource ? Parser.sourceJsonToAbv(definedInSource) : "Unknown"}]` : ""; + const sourceSuffix = (SourceUtil.isNonstandardSource(classSource || Parser.SRC_PHB) || (typeof PrereleaseUtil !== "undefined" && PrereleaseUtil.hasSourceJson(classSource || Parser.SRC_PHB)) || (typeof BrewUtil2 !== "undefined" && BrewUtil2.hasSourceJson(classSource || Parser.SRC_PHB))) ? ` (${Parser.sourceJsonToAbv(classSource)})` : ""; + const name = `${nm}${variantSuffix}${sourceSuffix}`; + + const opts = { + item: name, + userData: { + group: SourceUtil.getFilterGroup(classSource || Parser.SRC_PHB), + }, + }; + + if (isVariantClass) { + opts.nest = definedInSource ? Parser.sourceJsonToFull(definedInSource) : "Unknown"; + opts.userData.equivalentClassName = `${nm}${sourceSuffix}`; + opts.userData.definedInSource = definedInSource; + } + + return new FilterItem$1(opts); + } + + static _getSubclassFilterItem({className, classSource, subclassShortName, subclassName, subclassSource, subSubclassName, isVariantClass, definedInSource}) { + const group = SourceUtil.isSubclassReprinted(className, classSource, subclassShortName, subclassSource) || Parser.sourceJsonToFull(subclassSource).startsWith(Parser.UA_PREFIX) || Parser.sourceJsonToFull(subclassSource).startsWith(Parser.PS_PREFIX); + + const classFilterItem = this._getClassFilterItem({ + className: subclassShortName || subclassName, + classSource: subclassSource, + }); + + return new FilterItem$1({ + item: `${className}: ${classFilterItem.item}${subSubclassName ? `, ${subSubclassName}` : ""}`, + nest: className, + userData: { + group, + }, + }); + } + + static _isReprinted({reprintedAs, tag, page, prop}) { + return reprintedAs?.length && reprintedAs.some(it=>{ + const {name, source} = DataUtil.generic.unpackUid(it?.uid ?? it, tag); + const hash = UrlUtil.URL_TO_HASH_BUILDER[page]({ + name, + source + }); + return !ExcludeUtil.isExcluded(hash, prop, source, { + isNoCount: true + }); + } + ); + } + + static getListAliases(ent) { + return (ent.alias || []).map(it=>`"${it}"`).join(","); + } + + static defaultSourceSelFn(val) { + return SourceUtil.getFilterGroup(val) === SourceUtil.FILTER_GROUP_STANDARD; + } +}; + +//#region PageFilterClasses +class PageFilterClassesBase extends PageFilter { + constructor() { + super(); + + this._miscFilter = new Filter({ + header: "Miscellaneous", + items: ["Reprinted", "Sidekick", "SRD", "Basic Rules"], + deselFn: (it)=>{ + return it === "Reprinted" || it === "Sidekick"; + } + , + displayFnMini: it=>it === "Reprinted" ? "Repr." : it, + displayFnTitle: it=>it === "Reprinted" ? it : "", + isMiscFilter: true, + }); + + this._optionsFilter = new OptionsFilter({ + header: "Other/Text Options", + defaultState: { + isDisplayClassIfSubclassActive: false, + isClassFeatureVariant: true, + }, + displayFn: k=>{ + switch (k) { + case "isClassFeatureVariant": + return "Class Feature Options/Variants"; + case "isDisplayClassIfSubclassActive": + return "Display Class if Any Subclass is Visible"; + default: + throw new Error(`Unhandled key "${k}"`); + } + } + , + displayFnMini: k=>{ + switch (k) { + case "isClassFeatureVariant": + return "C.F.O/V."; + case "isDisplayClassIfSubclassActive": + return "Sc>C"; + default: + throw new Error(`Unhandled key "${k}"`); + } + } + , + }); + } + + get optionsFilter() { + return this._optionsFilter; + } + + static mutateForFilters(cls) { + cls.source = cls.source || Parser.SRC_PHB; + cls.subclasses = cls.subclasses || []; + + cls._fSources = SourceFilter.getCompleteFilterSources(cls); + + cls._fSourceSubclass = [...new Set([cls.source, ...cls.subclasses.map(it=>[it.source, ...(it.otherSources || []).map(it=>it.source)]).flat(), ]), ]; + + cls._fMisc = []; + if (cls.isReprinted) + cls._fMisc.push("Reprinted"); + if (cls.srd) + cls._fMisc.push("SRD"); + if (cls.basicRules) + cls._fMisc.push("Basic Rules"); + if (cls.isSidekick) + cls._fMisc.push("Sidekick"); + + cls.subclasses.forEach(sc=>{ + sc.source = sc.source || cls.source; + sc.shortName = sc.shortName || sc.name; + sc._fMisc = []; + if (sc.srd) + sc._fMisc.push("SRD"); + if (sc.basicRules) + sc._fMisc.push("Basic Rules"); + if (sc.isReprinted) + sc._fMisc.push("Reprinted"); + } + ); + } + + _addEntrySourcesToFilter(entry) { + this._addEntrySourcesToFilter_walk(entry); + } + + _addEntrySourcesToFilter_walk = (obj)=>{ + if ((typeof obj !== "object") || obj == null) + return; + + if (obj instanceof Array) + return obj.forEach(this._addEntrySourcesToFilter_walk.bind(this)); + + if (obj.source) + this._sourceFilter.addItem(obj.source); + if (obj.entries) + this._addEntrySourcesToFilter_walk(obj.entries); + } + ; + + addToFilters(cls, isExcluded, opts) { + if (isExcluded) + return; + opts = opts || {}; + const subclassExclusions = opts.subclassExclusions || {}; + + this._sourceFilter.addItem(cls.source); + + if (cls.fluff) + cls.fluff.forEach(it=>this._addEntrySourcesToFilter(it)); + cls.classFeatures.forEach(lvlFeatures=>lvlFeatures.forEach(feature=>this._addEntrySourcesToFilter(feature))); + + cls.subclasses.forEach(sc=>{ + const isScExcluded = (subclassExclusions[sc.source] || {})[sc.name] || false; + if (!isScExcluded) { + this._sourceFilter.addItem(sc.source); + sc.subclassFeatures.forEach(lvlFeatures=>lvlFeatures.forEach(feature=>this._addEntrySourcesToFilter(feature))); + } + } + ); + } + + async _pPopulateBoxOptions(opts) { + opts.filters = [this._sourceFilter, this._miscFilter, this._optionsFilter, ]; + opts.isCompact = true; + } + + isClassNaturallyDisplayed(values, cls) { + return this._filterBox.toDisplay(values, ...this.constructor._getIsClassNaturallyDisplayedToDisplayParams(cls), ); + } + + static _getIsClassNaturallyDisplayedToDisplayParams(cls) { + return [cls._fSources, cls._fMisc]; + } + + isAnySubclassDisplayed(values, cls) { + return values[this._optionsFilter.header].isDisplayClassIfSubclassActive && (cls.subclasses || []).some(sc=>{ + if (this._filterBox.toDisplay(values, ...this.constructor._getIsSubclassDisplayedToDisplayParams(cls, sc), )) + return true; + + return sc.otherSources?.length && sc.otherSources.some(src=>this._filterBox.toDisplay(values, ...this.constructor._getIsSubclassDisplayedToDisplayParams(cls, sc, src), )); + } + ); + } + + static _getIsSubclassDisplayedToDisplayParams(cls, sc, otherSourcesSource) { + return [otherSourcesSource || sc.source, sc._fMisc, null, ]; + } + + isSubclassVisible(f, cls, sc) { + if (this.filterBox.toDisplay(f, ...this.constructor._getIsSubclassVisibleToDisplayParams(cls, sc), )) + return true; + + if (!sc.otherSources?.length) + return false; + + return sc.otherSources.some(src=>this.filterBox.toDisplay(f, ...this.constructor._getIsSubclassVisibleToDisplayParams(cls, sc, src.source), )); + } + + static _getIsSubclassVisibleToDisplayParams(cls, sc, otherSourcesSource) { + return [otherSourcesSource || sc.source, sc._fMisc, null, ]; + } + + getActiveSource(values) { + const sourceFilterValues = values[this._sourceFilter.header]; + if (!sourceFilterValues) + return null; + return Object.keys(sourceFilterValues).find(it=>this._sourceFilter.toDisplay(values, it)); + } + + toDisplay(values, it) { + return this._filterBox.toDisplay(values, ...this._getToDisplayParams(values, it), ); + } + + _getToDisplayParams(values, cls) { + return [this.isAnySubclassDisplayed(values, cls) ? cls._fSourceSubclass : (cls._fSources ?? cls.source), cls._fMisc, null, ]; + } +}; + +class PageFilterClasses extends PageFilterClassesBase { + static _getClassSubclassLevelArray(it) { + return it.classFeatures.map((_,i)=>i + 1); + } + + constructor() { + super(); + + this._levelFilter = new RangeFilter({ + header: "Feature Level", + min: 1, + max: 20, + }); + } + + get levelFilter() { + return this._levelFilter; + } + + static mutateForFilters(cls) { + super.mutateForFilters(cls); + + cls._fLevelRange = this._getClassSubclassLevelArray(cls); + } + + addToFilters(cls, isExcluded, opts) { + super.addToFilters(cls, isExcluded, opts); + + if (isExcluded) + return; + + this._levelFilter.addItem(cls._fLevelRange); + } + + async _pPopulateBoxOptions(opts) { + await super._pPopulateBoxOptions(opts); + + opts.filters = [this._sourceFilter, this._miscFilter, this._levelFilter, this._optionsFilter, ]; + } + + static _getIsClassNaturallyDisplayedToDisplayParams(cls) { + return [cls._fSources, cls._fMisc, cls._fLevelRange]; + } + + static _getIsSubclassDisplayedToDisplayParams(cls, sc, otherSourcesSource) { + return [otherSourcesSource || sc.source, sc._fMisc, cls._fLevelRange]; + } + + static _getIsSubclassVisibleToDisplayParams(cls, sc, otherSourcesSource) { + return [otherSourcesSource || sc.source, sc._fMisc, cls._fLevelRange, null]; + } + + _getToDisplayParams(values, cls) { + return [this.isAnySubclassDisplayed(values, cls) ? cls._fSourceSubclass : (cls._fSources ?? cls.source), cls._fMisc, cls._fLevelRange, ]; + } +}; + +class PageFilterClassesRaw extends PageFilterClassesBase { + static _WALKER = null; + static _IMPLS_SIDE_DATA = {}; + + async _pPopulateBoxOptions(opts) { + await super._pPopulateBoxOptions(opts); + opts.isCompact = false; + } + + + /** + * Add a class (and any attached class features and subclasses) to the filter + * @param {Object} cls - the class object + * @param {any} isExcluded - will return if true + * @param {any} opts - only opts.subclassExclusions matters + */ + addToFilters(cls, isExcluded, opts) { + if (isExcluded) + return; + opts = opts || {}; + const subclassExclusions = opts.subclassExclusions || {}; + + this._sourceFilter.addItem(cls.source); + + if (cls.fluff) + cls.fluff.forEach(it=>this._addEntrySourcesToFilter(it)); + + cls.classFeatures.forEach(feature=>feature.loadeds.forEach(ent=>this._addEntrySourcesToFilter(ent.entity))); + + cls.subclasses.forEach(sc=>{ + const isScExcluded = (subclassExclusions[sc.source] || {})[sc.name] || false; + if (!isScExcluded) { + this._sourceFilter.addItem(sc.source); + sc.subclassFeatures.forEach(feature=> { + if(!feature.loadeds){console.error("Subclassfeature is lacking loadeds", feature);} + feature.loadeds.forEach(ent=>this._addEntrySourcesToFilter(ent.entity)) + }); + } + } + ); + } + + static async _pGetParentClass(sc) { + let baseClass = (await DataUtil.class.loadRawJSON()).class.find(bc=>bc.name.toLowerCase() === sc.className.toLowerCase() && (bc.source.toLowerCase() || Parser.SRC_PHB) === sc.classSource.toLowerCase()); + + baseClass = baseClass || await this._pGetParentClass_pPrerelease({sc}); + baseClass = baseClass || await this._pGetParentClass_pBrew({sc}); + + return baseClass; + } + + static async _pGetParentClass_pPrerelease({sc}) { + await this._pGetParentClass_pPrereleaseBrew({ + sc, + brewUtil: PrereleaseUtil + }); + } + + static async _pGetParentClass_pBrew({sc}) { + await this._pGetParentClass_pPrereleaseBrew({ + sc, + brewUtil: BrewUtil2 + }); + } + + static async _pGetParentClass_pPrereleaseBrew({sc, brewUtil}) { + const brew = await brewUtil.pGetBrewProcessed(); + return (brew.class || []).find(bc=>bc.name.toLowerCase() === sc.className.toLowerCase() && (bc.source.toLowerCase() || Parser.SRC_PHB) === sc.classSource.toLowerCase()); + } + + + /** + * Postload classes and subclasses. Matches subclasses to classes (and nests them inside, instead of letting them live in data.subclass) + * Also sanity checks classes to make sure their properties are in order and that they are sorted by name. + * Also adds 'loadeds' to class features and subclass features + * @param {{class:any[], subclass:any[]}} data + * @param {any} {...opts}={} + * @returns {any} returns the data + */ + static async pPostLoad(data, {...opts}={}) { + data = MiscUtil.copy(data); + + await PrereleaseUtil.pGetBrewProcessed(); + await BrewUtil2.pGetBrewProcessed(); + + if (!data.class) { data.class = []; } //Make sure this property is initalized + + //If data has subclasses listed, go through them and match each subclass to the corresponding class + //We only want subclasses to exist nested within their parent classes + if (data.subclass) { + for (const sc of data.subclass) { + if (!sc.className) { continue; } //The subclass must have a className listed + sc.classSource = sc.classSource || Parser.SRC_PHB; //Default to PHB source if none is provided + + //Lets try to find a class that matches this subclass + let cls = data.class.find(it=> + (it.name || "").toLowerCase() === sc.className.toLowerCase() //class name must match + && (it.source || Parser.SRC_PHB).toLowerCase() === sc.classSource.toLowerCase()); //class source must match + + if (!cls) { //If we failed to get a match + cls = await this._pGetParentClass(sc); //Try to get a match another way + if (cls) { + cls = MiscUtil.copy(cls); + cls.subclasses = []; + data.class.push(cls); + } + else { + //Just create a stub class for now + cls = { name: sc.className, source: sc.classSource }; + data.class.push(cls); //And add it to the data, why not + } + } + + //Then push the subclass to the class's 'subclasses array', which we initialize here if it doesnt already exist + (cls.subclasses = cls.subclasses || []).push(sc); + } + + delete data.subclass; //When done, we also want to sipe data.subclass so that subclasses only exist nested inside their parent classes + } + + //Make sure each class their properties sanity checked and in order + data.class.forEach(cls=>{ + cls.source = cls.source || Parser.SRC_PHB; + + cls.subclasses = cls.subclasses || []; + + cls.subclasses.forEach(sc=>{ + sc.name = sc.name || "(Unnamed subclass)"; + sc.source = sc.source || cls.source; + sc.className = sc.className || cls.name; + sc.classSource = sc.classSource || cls.source || Parser.SRC_PHB; + } + ); + + cls.subclasses.sort((a,b)=>SortUtil.ascSortLower(a.name, b.name) || SortUtil.ascSortLower(a.source || cls.source, b.source || cls.source)); + + cls._cntStartingSkillChoices = (MiscUtil.get(cls, "startingProficiencies", "skills") || []).map(it=>it.choose ? (it.choose.count || 1) : 0).reduce((a,b)=>a + b, 0); + + cls._cntStartingSkillChoicesMutliclass = (MiscUtil.get(cls, "multiclassing", "proficienciesGained", "skills") || []).map(it=>it.choose ? (it.choose.count || 1) : 0).reduce((a,b)=>a + b, 0); + } + ); + //Then sort all the classes by name + data.class.sort((a,b)=>SortUtil.ascSortLower(a.name, b.name) || SortUtil.ascSortLower(a.source, b.source)); + + data.class.forEach(cls=>{ + cls.classFeatures = (cls.classFeatures || []).map(cf=>typeof cf === "string" ? { classFeature: cf } : cf); + + (cls.subclasses || []).forEach(sc=>{ + sc.subclassFeatures = (sc.subclassFeatures || []).map(cf=>typeof cf === "string" ? { + subclassFeature: cf + } : cf); + }); + }); + + await this._pPreloadSideData(); + + for (const cls of data.class) { + //Load the 'loadeds' of all the class features + await (cls.classFeatures || []).pSerialAwaitMap(cf=>this.pInitClassFeatureLoadeds({ + ...opts, + classFeature: cf, + className: cls.name + })); + + //Then filter away all ignored class features + if (cls.classFeatures) {cls.classFeatures = cls.classFeatures.filter(it=>!it.isIgnored);} + + //Moving on to subclasses, we will repeat the procedure for each subclass feature + for (const sc of cls.subclasses || []) { + await (sc.subclassFeatures || []).pSerialAwaitMap(scf=>this.pInitSubclassFeatureLoadeds({ + ...opts, + subclassFeature: scf, + className: cls.name, + subclassName: sc.name + })); + + if (sc.subclassFeatures) + sc.subclassFeatures = sc.subclassFeatures.filter(it=>!it.isIgnored); + } + } + + return data; + } + + /** + * Sets the 'loadeds' property of a classFeature, which contains information about choices that the class feature can make (expertise, etc) + * Normally this information is not stored in the same .json as the classfeature itself, but is stored in some external .json file + * @param {{classFeature:string}} classFeature + * @param {string} className + * @param {any} opts + */ + static async pInitClassFeatureLoadeds({classFeature, className, ...opts}) { + if (typeof classFeature !== "object") + throw new Error(`Expected an object of the form {classFeature: ""}`); + + //Unpack the UID to get some strings + const unpacked = DataUtil.class.unpackUidClassFeature(classFeature.classFeature); + + classFeature.hash = UrlUtil.URL_TO_HASH_BUILDER["classFeature"](unpacked); + + const {name, level, source} = unpacked; + classFeature.name = name; + classFeature.level = level; + classFeature.source = source; + + //Now, the information about the class feature choices is usually stored in some other place, not within the classFeature itself + //So we have to get that information from somewhere + + //Ask the cache to get us a raw classfeature from our source, and with our hash + const entityRoot = await DataLoader.pCacheAndGet("raw_classFeature", classFeature.source, classFeature.hash, { + isCopy: true //And make it a copy + }); + + const loadedRoot = { + type: "classFeature", + entity: entityRoot, + page: "classFeature", + source: classFeature.source, + hash: classFeature.hash, + className, + }; + + //Check if this class feature is on the ignore list + const isIgnored = await this._pGetIgnoredAndApplySideData(entityRoot, "classFeature"); + if (isIgnored) { + classFeature.isIgnored = true; + return; + } + + const {entityRoot: entityRootNxt, subLoadeds} = await this._pLoadSubEntries(this._getPostLoadWalker(), entityRoot, { + ...opts, + ancestorType: "classFeature", + ancestorMeta: { _ancestorClassName: className, }, + }, ); + loadedRoot.entity = entityRootNxt; + + //Finally, set the loadeds + classFeature.loadeds = [loadedRoot, ...subLoadeds]; + } + + static async pInitSubclassFeatureLoadeds({subclassFeature, className, subclassName, ...opts}) { + if (typeof subclassFeature !== "object") + throw new Error(`Expected an object of the form {subclassFeature: ""}`); + + const unpacked = DataUtil.class.unpackUidSubclassFeature(subclassFeature.subclassFeature); + + subclassFeature.hash = UrlUtil.URL_TO_HASH_BUILDER["subclassFeature"](unpacked); + + const {name, level, source} = unpacked; + subclassFeature.name = name; + subclassFeature.level = level; + subclassFeature.source = source; + + const entityRoot = await DataLoader.pCacheAndGet("raw_subclassFeature", subclassFeature.source, subclassFeature.hash, { + isCopy: true + }); + const loadedRoot = { + type: "subclassFeature", + entity: entityRoot, + page: "subclassFeature", + source: subclassFeature.source, + hash: subclassFeature.hash, + className, + subclassName, + }; + + const isIgnored = await this._pGetIgnoredAndApplySideData(entityRoot, "subclassFeature"); + if (isIgnored) { + subclassFeature.isIgnored = true; + return; + } + + if (entityRoot.isGainAtNextFeatureLevel) { + subclassFeature.isGainAtNextFeatureLevel = true; + } + + const {entityRoot: entityRootNxt, subLoadeds} = await this._pLoadSubEntries(this._getPostLoadWalker(), entityRoot, { + ...opts, + ancestorType: "subclassFeature", + ancestorMeta: { + _ancestorClassName: className, + _ancestorSubclassName: subclassName, + }, + }, ); + loadedRoot.entity = entityRootNxt; + + subclassFeature.loadeds = [loadedRoot, ...subLoadeds]; + } + + static async pInitFeatLoadeds({feat, raw, ...opts}) { + return this._pInitGenericLoadeds({ + ...opts, + ent: feat, + prop: "feat", + page: UrlUtil.PG_FEATS, + propAncestorName: "_ancestorFeatName", + raw, + }); + } + + static async pInitOptionalFeatureLoadeds({optionalfeature, raw, ...opts}) { + return this._pInitGenericLoadeds({ + ...opts, + ent: optionalfeature, + prop: "optionalfeature", + page: UrlUtil.PG_OPT_FEATURES, + propAncestorName: "_ancestorOptionalfeatureName", + raw, + }); + } + + static async pInitRewardLoadeds({reward, raw, ...opts}) { + return this._pInitGenericLoadeds({ + ...opts, + ent: reward, + prop: "reward", + page: UrlUtil.PG_REWARDS, + propAncestorName: "_ancestorRewardName", + raw, + }); + } + + static async pInitCharCreationOptionLoadeds({charoption, raw, ...opts}) { + return this._pInitGenericLoadeds({ + ...opts, + ent: charoption, + prop: "charoption", + page: UrlUtil.PG_CHAR_CREATION_OPTIONS, + propAncestorName: "_ancestorCharoptionName", + raw, + }); + } + + static async pInitVehicleUpgradeLoadeds({vehicleUpgrade, raw, ...opts}) { + return this._pInitGenericLoadeds({ + ...opts, + ent: vehicleUpgrade, + prop: "vehicleUpgrade", + page: UrlUtil.PG_VEHICLES, + propAncestorName: "_ancestorVehicleUpgradeName", + raw, + }); + } + + static async _pInitGenericLoadeds({ent, prop, page, propAncestorName, raw, ...opts}) { + if (typeof ent !== "object") + throw new Error(`Expected an object of the form {${prop}: ""}`); + + const unpacked = DataUtil.generic.unpackUid(ent[prop]); + + ent.hash = UrlUtil.URL_TO_HASH_BUILDER[page](unpacked); + + const {name, source} = unpacked; + ent.name = name; + ent.source = source; + + const entityRoot = raw != null ? MiscUtil.copy(raw) : await DataLoader.pCacheAndGet(`raw_${prop}`, ent.source, ent.hash, { + isCopy: true + }); + const loadedRoot = { + type: prop, + entity: entityRoot, + page, + source: ent.source, + hash: ent.hash, + }; + + const isIgnored = await this._pGetIgnoredAndApplySideData(entityRoot, prop); + if (isIgnored) { + ent.isIgnored = true; + return; + } + + const {entityRoot: entityRootNxt, subLoadeds} = await this._pLoadSubEntries(this._getPostLoadWalker(), entityRoot, { + ...opts, + ancestorType: prop, + ancestorMeta: { + [propAncestorName]: entityRoot.name, + }, + }, ); + loadedRoot.entity = entityRootNxt; + + ent.loadeds = [loadedRoot, ...subLoadeds]; + } + + static async _pPreloadSideData() { + await Promise.all(Object.values(PageFilterClassesRaw._IMPLS_SIDE_DATA).map(Impl=>Impl.pPreloadSideData())); + } + + /** + * @param {any} entity + * @param {string} type + * @returns {boolean} + */ + static async _pGetIgnoredAndApplySideData(entity, type) { + if (!PageFilterClassesRaw._IMPLS_SIDE_DATA[type]) + throw new Error(`Unhandled type "${type}"`); + + const sideData = await PageFilterClassesRaw._IMPLS_SIDE_DATA[type].pGetSideLoaded(entity, { isSilent: true }); + + if (!sideData) + return false; + if (sideData.isIgnored) + return true; + + if (sideData.entries) + entity.entries = MiscUtil.copy(sideData.entries); + if (sideData.entryData) + entity.entryData = MiscUtil.copy(sideData.entryData); + + return false; + } + + static async _pLoadSubEntries(walker, entityRoot, {ancestorType, ancestorMeta, ...opts}) { + const out = []; + + const pRecurse = async(parent,toWalk)=>{ + const references = []; + const path = []; + + toWalk = walker.walk(toWalk, { + array: (arr)=>{ + arr = arr.map(it=>this._pLoadSubEntries_getMappedWalkerArrayEntry({ + ...opts, + it, + path, + references + })).filter(Boolean); + return arr; + } + , + preObject: (obj)=>{ + if (obj.type === "options") { + const parentName = (path.last() || {}).name ?? parent.name; + + if (obj.count != null) { + const optionSetId = CryptUtil.uid(); + obj.entries.forEach(ent=>{ + ent._optionsMeta = { + setId: optionSetId, + count: obj.count, + name: parentName, + }; + } + ); + } + + if (parentName) { + obj.entries.forEach(ent=>{ + if (typeof ent !== "object") + return; + ent._displayNamePrefix = `${parentName}: `; + } + ); + } + } + + if (obj.name) + path.push(obj); + } + , + postObject: (obj)=>{ + if (obj.name) + path.pop(); + } + , + }, ); + + for (const ent of references) { + const isRequiredOption = !!MiscUtil.get(ent, "data", "isRequiredOption"); + switch (ent.type) { + case "refClassFeature": + { + const unpacked = DataUtil.class.unpackUidClassFeature(ent.classFeature); + const {source} = unpacked; + const hash = UrlUtil.URL_TO_HASH_BUILDER["classFeature"](unpacked); + + let entity = await DataLoader.pCacheAndGet("raw_classFeature", source, hash, { + isCopy: true + }); + + if (!entity) { + this._handleReferenceError(`Failed to load "classFeature" reference "${ent.classFeature}" (not found)`); + continue; + } + + if (toWalk.__prop === entity.__prop && UrlUtil.URL_TO_HASH_BUILDER["classFeature"](toWalk) === hash) { + this._handleReferenceError(`Failed to load "classFeature" reference "${ent.classFeature}" (circular reference)`); + continue; + } + + const isIgnored = await this._pGetIgnoredAndApplySideData(entity, "classFeature"); + if (isIgnored) + continue; + + this.populateEntityTempData({ + entity, + displayName: ent._displayNamePrefix ? `${ent._displayNamePrefix}${entity.name}` : null, + ...ancestorMeta, + }); + + out.push({ + type: "classFeature", + entry: `{@classFeature ${ent.classFeature}}`, + entity, + optionsMeta: ent._optionsMeta, + page: "classFeature", + source, + hash, + isRequiredOption, + }); + + entity = await pRecurse(entity, entity.entries); + + break; + } + case "refSubclassFeature": + { + const unpacked = DataUtil.class.unpackUidSubclassFeature(ent.subclassFeature); + const {source} = unpacked; + const hash = UrlUtil.URL_TO_HASH_BUILDER["subclassFeature"](unpacked); + + let entity = await DataLoader.pCacheAndGet("raw_subclassFeature", source, hash, { + isCopy: true + }); + + if (!entity) { + this._handleReferenceError(`Failed to load "subclassFeature" reference "${ent.subclassFeature}" (not found)`); + continue; + } + + if (toWalk.__prop === entity.__prop && UrlUtil.URL_TO_HASH_BUILDER["subclassFeature"](toWalk) === hash) { + this._handleReferenceError(`Failed to load "subclassFeature" reference "${ent.subclassFeature}" (circular reference)`); + continue; + } + + const isIgnored = await this._pGetIgnoredAndApplySideData(entity, "subclassFeature"); + if (isIgnored) + continue; + + this.populateEntityTempData({ + entity, + displayName: ent._displayNamePrefix ? `${ent._displayNamePrefix}${entity.name}` : null, + ...ancestorMeta, + }); + + out.push({ + type: "subclassFeature", + entry: `{@subclassFeature ${ent.subclassFeature}}`, + entity, + optionsMeta: ent._optionsMeta, + page: "subclassFeature", + source, + hash, + isRequiredOption, + }); + + entity = await pRecurse(entity, entity.entries); + + break; + } + case "refOptionalfeature": + { + const unpacked = DataUtil.generic.unpackUid(ent.optionalfeature, "optfeature"); + const page = UrlUtil.PG_OPT_FEATURES; + const {source} = unpacked; + const hash = UrlUtil.URL_TO_HASH_BUILDER[page](unpacked); + + const entity = await DataLoader.pCacheAndGet(page, source, hash, { + isCopy: true + }); + + if (!entity) { + this._handleReferenceError(`Failed to load "optfeature" reference "${ent.optionalfeature}" (not found)`); + continue; + } + + if (toWalk.__prop === entity.__prop && UrlUtil.URL_TO_HASH_BUILDER[page](toWalk) === hash) { + this._handleReferenceError(`Failed to load "optfeature" reference "${ent.optionalfeature}" (circular reference)`); + continue; + } + + const isIgnored = await this._pGetIgnoredAndApplySideData(entity, "optionalfeature"); + if (isIgnored) + continue; + + this.populateEntityTempData({ + entity, + ancestorType, + displayName: ent._displayNamePrefix ? `${ent._displayNamePrefix}${entity.name}` : null, + ...ancestorMeta, + foundrySystem: { + requirements: entityRoot.className ? `${entityRoot.className} ${entityRoot.level}${entityRoot.subclassShortName ? ` (${entityRoot.subclassShortName})` : ""}` : null, + }, + }); + + out.push({ + type: "optionalfeature", + entry: `{@optfeature ${ent.optionalfeature}}`, + entity, + optionsMeta: ent._optionsMeta, + page, + source, + hash, + isRequiredOption, + }); + + break; + } + default: + throw new Error(`Unhandled type "${ent.type}"`); + } + } + + return toWalk; + } + ; + + if (entityRoot.entries) //entityRoot.entryData is already set by this point + entityRoot.entries = await pRecurse(entityRoot, entityRoot.entries); + + return { + entityRoot, + subLoadeds: out + }; + } + + static _pLoadSubEntries_getMappedWalkerArrayEntry({it, path, references, ...opts}) { + if (it.type !== "refClassFeature" && it.type !== "refSubclassFeature" && it.type !== "refOptionalfeature") + return it; + + it.parentName = (path.last() || {}).name; + references.push(it); + + return null; + } + + static populateEntityTempData({entity, ancestorType, displayName, foundrySystem, ...others}, ) { + if (ancestorType) + entity._ancestorType = ancestorType; + if (displayName) + entity._displayName = displayName; + if (foundrySystem) + entity._foundrySystem = foundrySystem; + Object.assign(entity, { + ...others + }); + } + + static _handleReferenceError(msg) { + JqueryUtil.doToast({ + type: "danger", + content: msg + }); + } + + static _getPostLoadWalker() { + PageFilterClassesRaw._WALKER = PageFilterClassesRaw._WALKER || MiscUtil.getWalker({ + keyBlocklist: MiscUtil.GENERIC_WALKER_ENTRIES_KEY_BLOCKLIST, + isDepthFirst: true, + }); + return PageFilterClassesRaw._WALKER; + } + + static setImplSideData(prop, Impl) { + PageFilterClassesRaw._IMPLS_SIDE_DATA[prop] = Impl; + } +}; + +class PageFilterClassesFoundry extends PageFilterClassesRaw { + static _handleReferenceError(msg) { + console.error(...LGT, msg); + ui.notifications.error(msg); + } + + static _pLoadSubEntries_getMappedWalkerArrayEntry({it, path, references, actor, isIgnoredLookup, ...opts}) { + const out = super._pLoadSubEntries_getMappedWalkerArrayEntry({ + it, + path, + references, + actor, + ...opts + }); + if (out != null) + return out; + + const isIgnored = this._pLoadSubEntries_getMappedWalkerArrayEntry_isIgnored({ + it, + isIgnoredLookup + }); + if (isIgnored) + return null; + + const meta = this._pLoadSubEntries_getMappedWalkerArrayEntry_getMeta({ + it + }); + const {name, source} = meta; + + const ident = this._pLoadSubEntries_getMappedWalkerArrayEntry_getPageSourceHash({ + it + }); + const b64Ident = btoa(encodeURIComponent(JSON.stringify(ident))); + + return { + type: "wrapper", + wrapped: actor ? `@UUID[Actor.${actor.id}.Item.temp-${SharedConsts.MODULE_ID_FAKE}-${b64Ident}]{${name}}` : `@UUID[Item.temp-${SharedConsts.MODULE_ID_FAKE}-${b64Ident}]{${name}}`, + source, + data: { + isFvttSyntheticFeatureLink: true, + }, + }; + } + + static _pLoadSubEntries_getMappedWalkerArrayEntry_getMeta({it}) { + switch (it.type) { + case "refClassFeature": + return DataUtil.class.unpackUidClassFeature(it.classFeature); + case "refSubclassFeature": + return DataUtil.class.unpackUidSubclassFeature(it.subclassFeature); + case "refOptionalfeature": + return DataUtil.proxy.unpackUid("optionalfeature", it.optionalfeature, "optfeature"); + default: + throw new Error(`Unhandled reference type "${it.type}"`); + } + } + + static _pLoadSubEntries_getMappedWalkerArrayEntry_getPageSourceHash({it}) { + let page; + let source; + let hash; + switch (it.type) { + case "refClassFeature": + { + const meta = DataUtil.class.unpackUidClassFeature(it.classFeature); + page = "classFeature"; + hash = UrlUtil.URL_TO_HASH_BUILDER[page](meta); + source = meta.source; + break; + } + + case "refSubclassFeature": + { + const meta = DataUtil.class.unpackUidSubclassFeature(it.subclassFeature); + page = "subclassFeature"; + hash = UrlUtil.URL_TO_HASH_BUILDER[page](meta); + source = meta.source; + break; + } + + case "refOptionalfeature": + { + const meta = DataUtil.proxy.unpackUid("optionalfeature", it.optionalfeature, "optfeature"); + page = UrlUtil.PG_OPT_FEATURES; + hash = UrlUtil.URL_TO_HASH_BUILDER[page](meta); + source = meta.source; + break; + } + + default: + throw new Error(`Unhandled reference type "${it.type}"`); + } + + return { + page, + source, + hash + }; + } + + static _pLoadSubEntries_getMappedWalkerArrayEntry_isIgnored({it, isIgnoredLookup}) { + if (!isIgnoredLookup) + return false; + + switch (it.type) { + case "refClassFeature": + return isIgnoredLookup[(it.classFeature || "").toLowerCase()]; + case "refSubclassFeature": + return isIgnoredLookup[(it.subclassFeature || "").toLowerCase()]; + default: + return false; + } + } +} +//#endregion + +//#region PageFilterRaces +class PageFilterRaces extends PageFilter { + + + constructor() { + super(); + + //Create sub-filters + //sourcefilter is created by super + + this._sizeFilter = new Filter({ + header: "Size", + displayFn: Parser.sizeAbvToFull, + itemSortFn: PageFilterRaces.filterAscSortSize + }); + this._asiFilter = new AbilityScoreFilter({ + header: "Ability Scores (Including Subrace)" + }); + this._baseRaceFilter = new Filter({ + header: "Base Race" + }); + this._speedFilter = new Filter({ + header: "Speed", + items: ["Climb", "Fly", "Swim", "Walk (Fast)", "Walk", "Walk (Slow)"] + }); + this._traitFilter = new Filter({ + header: "Traits", + items: ["Amphibious", "Armor Proficiency", "Blindsight", "Darkvision", "Superior Darkvision", "Dragonmark", "Feat", "Improved Resting", "Monstrous Race", "Natural Armor", "Natural Weapon", "NPC Race", "Powerful Build", "Skill Proficiency", "Spellcasting", "Sunlight Sensitivity", "Tool Proficiency", "Uncommon Race", "Weapon Proficiency", ], + deselFn: (it)=>{ + return it === "NPC Race"; + } + , + }); + this._vulnerableFilter = FilterCommon.getDamageVulnerableFilter(); + this._resistFilter = FilterCommon.getDamageResistFilter(); + this._immuneFilter = FilterCommon.getDamageImmuneFilter(); + this._defenceFilter = new MultiFilter({ + header: "Damage", + filters: [this._vulnerableFilter, this._resistFilter, this._immuneFilter] + }); + this._conditionImmuneFilter = FilterCommon.getConditionImmuneFilter(); + this._languageFilter = new Filter({ + header: "Languages", + items: ["Abyssal", "Celestial", "Choose", "Common", "Draconic", "Dwarvish", "Elvish", "Giant", "Gnomish", "Goblin", "Halfling", "Infernal", "Orc", "Other", "Primordial", "Sylvan", "Undercommon", ], + umbrellaItems: ["Choose"], + }); + this._creatureTypeFilter = new Filter({ + header: "Creature Type", + items: Parser.MON_TYPES, + displayFn: StrUtil.toTitleCase, + itemSortFn: SortUtil.ascSortLower, + }); + this._ageFilter = new RangeFilter({ + header: "Adult Age", + isRequireFullRangeMatch: true, + isSparse: true, + displayFn: it=>`${it} y.o.`, + displayFnTooltip: it=>`${it} year${it === 1 ? "" : "s"} old`, + }); + this._miscFilter = new Filter({ + header: "Miscellaneous", + items: ["Base Race", "Key Race", "Lineage", "Modified Copy", "Reprinted", "SRD", "Basic Rules", "Has Images", "Has Info"], + isMiscFilter: true, + }); + } + + static mutateForFilters(r) { + r._fSize = r.size ? [...r.size] : []; + if (r._fSize.length > 1) + r._fSize.push("V"); + r._fSpeed = r.speed ? r.speed.walk ? [r.speed.climb ? "Climb" : null, r.speed.fly ? "Fly" : null, r.speed.swim ? "Swim" : null, PageFilterRaces.getSpeedRating(r.speed.walk)].filter(it=>it) : [PageFilterRaces.getSpeedRating(r.speed)] : []; + r._fTraits = [r.darkvision === 120 ? "Superior Darkvision" : r.darkvision ? "Darkvision" : null, r.blindsight ? "Blindsight" : null, r.skillProficiencies ? "Skill Proficiency" : null, r.toolProficiencies ? "Tool Proficiency" : null, r.feats ? "Feat" : null, r.additionalSpells ? "Spellcasting" : null, r.armorProficiencies ? "Armor Proficiency" : null, r.weaponProficiencies ? "Weapon Proficiency" : null, ].filter(it=>it); + r._fTraits.push(...(r.traitTags || [])); + r._fSources = SourceFilter.getCompleteFilterSources(r); + r._fLangs = PageFilterRaces.getLanguageProficiencyTags(r.languageProficiencies); + r._fCreatureTypes = r.creatureTypes ? r.creatureTypes.map(it=>it.choose || it).flat() : ["humanoid"]; + r._fMisc = []; + if (r._isBaseRace) + r._fMisc.push("Base Race"); + if (r._isBaseRace || !r._isSubRace) + r._fMisc.push("Key Race"); + if (r._isCopy) + r._fMisc.push("Modified Copy"); + if (r.srd) + r._fMisc.push("SRD"); + if (r.basicRules) + r._fMisc.push("Basic Rules"); + if (r.hasFluff || r.fluff?.entries) + r._fMisc.push("Has Info"); + if (r.hasFluffImages || r.fluff?.images) + r._fMisc.push("Has Images"); + if (r.lineage) + r._fMisc.push("Lineage"); + if (this._isReprinted({ + reprintedAs: r.reprintedAs, + tag: "race", + prop: "race", + page: UrlUtil.PG_RACES + })) + r._fMisc.push("Reprinted"); + + const ability = r.ability ? Renderer.getAbilityData(r.ability, { + isOnlyShort: true, + isCurrentLineage: r.lineage === "VRGR" + }) : { + asTextShort: "None" + }; + r._slAbility = ability.asTextShort; + + if (r.age?.mature != null && r.age?.max != null) + r._fAge = [r.age.mature, r.age.max]; + else if (r.age?.mature != null) + r._fAge = r.age.mature; + else if (r.age?.max != null) + r._fAge = r.age.max; + + FilterCommon.mutateForFilters_damageVulnResImmune_player(r); + FilterCommon.mutateForFilters_conditionImmune_player(r); + } + + /** + * Add an race's filterable tags to the sub-filters (source filter, size filter, language filter, etc) + * @param {any} r A race object + * @param {Boolean} isExcluded if this is true, we return immediately + */ + addToFilters(r, isExcluded) { + if (isExcluded) + return; + + this._sourceFilter.addItem(r._fSources); + this._sizeFilter.addItem(r._fSize); + this._asiFilter.addItem(r.ability); + this._baseRaceFilter.addItem(r._baseName); + this._creatureTypeFilter.addItem(r._fCreatureTypes); + this._traitFilter.addItem(r._fTraits); + this._vulnerableFilter.addItem(r._fVuln); + this._resistFilter.addItem(r._fRes); + this._immuneFilter.addItem(r._fImm); + this._conditionImmuneFilter.addItem(r._fCondImm); + this._ageFilter.addItem(r._fAge); + this._languageFilter.addItem(r._fLangs); + } + + async _pPopulateBoxOptions(opts) { + opts.filters = [this._sourceFilter, this._asiFilter, this._sizeFilter, this._speedFilter, this._traitFilter, this._defenceFilter, this._conditionImmuneFilter, this._languageFilter, this._baseRaceFilter, this._creatureTypeFilter, this._miscFilter, this._ageFilter, ]; + } + + toDisplay(values, r) { + return this._filterBox.toDisplay(values, r._fSources, r.ability, r._fSize, r._fSpeed, r._fTraits, [r._fVuln, r._fRes, r._fImm, ], r._fCondImm, r._fLangs, r._baseName, r._fCreatureTypes, r._fMisc, r._fAge, ); + } + + static getListAliases(race) { + return (race.alias || []).map(it=>{ + const invertedName = PageFilterRaces.getInvertedName(it); + return [`"${it}"`, invertedName ? `"${invertedName}"` : false].filter(Boolean); + } + ).flat().join(","); + } + + static getInvertedName(name) { + const bracketMatch = /^(.*?) \((.*?)\)$/.exec(name); + return bracketMatch ? `${bracketMatch[2]} ${bracketMatch[1]}` : null; + } + + static getLanguageProficiencyTags(lProfs) { + if (!lProfs) + return []; + + const outSet = new Set(); + lProfs.forEach(lProfGroup=>{ + Object.keys(lProfGroup).forEach(k=>{ + if (!["choose", "any", "anyStandard", "anyExotic"].includes(k)) + outSet.add(k.toTitleCase()); + else + outSet.add("Choose"); + } + ); + } + ); + + return [...outSet]; + } + + static getSpeedRating(speed) { + return speed > 30 ? "Walk (Fast)" : speed < 30 ? "Walk (Slow)" : "Walk"; + } + + static filterAscSortSize(a, b) { + a = a.item; + b = b.item; + + return SortUtil.ascSort(toNum(a), toNum(b)); + + function toNum(size) { + switch (size) { + case "M": + return 0; + case "S": + return -1; + case "V": + return 1; + } + } + } +}; +//#endregion + +//#region PageFilterBackgrounds +let PageFilterBackgrounds$1 = class PageFilterBackgrounds extends PageFilter { + static _getToolDisplayText(tool) { + if (tool === "anyTool") + return "Any Tool"; + if (tool === "anyArtisansTool") + return "Any Artisan's Tool"; + if (tool === "anyMusicalInstrument") + return "Any Musical Instrument"; + return tool.toTitleCase(); + } + + constructor() { + super(); + + this._skillFilter = new Filter({ + header: "Skill Proficiencies", + displayFn: StrUtil.toTitleCase + }); + this._toolFilter = new Filter({ + header: "Tool Proficiencies", + displayFn: PageFilterBackgrounds$1._getToolDisplayText.bind(PageFilterBackgrounds$1) + }); + this._languageFilter = new Filter({ + header: "Language Proficiencies", + displayFn: it=>it === "anyStandard" ? "Any Standard" : it === "anyExotic" ? "Any Exotic" : StrUtil.toTitleCase(it), + }); + this._asiFilter = new AbilityScoreFilter({ + header: "Ability Scores" + }); + this._otherBenefitsFilter = new Filter({ + header: "Other Benefits" + }); + this._miscFilter = new Filter({ + header: "Miscellaneous", + items: ["Has Info", "Has Images", "SRD", "Basic Rules"], + isMiscFilter: true + }); + } + + static mutateForFilters(bg) { + bg._fSources = SourceFilter.getCompleteFilterSources(bg); + + const {summary: skillDisplay, collection: skills} = Renderer.generic.getSkillSummary({ + skillProfs: bg.skillProficiencies, + skillToolLanguageProfs: bg.skillToolLanguageProficiencies, + isShort: true, + }); + bg._fSkills = skills; + + const {collection: tools} = Renderer.generic.getToolSummary({ + toolProfs: bg.toolProficiencies, + skillToolLanguageProfs: bg.skillToolLanguageProficiencies, + isShort: true, + }); + bg._fTools = tools; + + const {collection: languages} = Renderer.generic.getLanguageSummary({ + languageProfs: bg.languageProficiencies, + skillToolLanguageProfs: bg.skillToolLanguageProficiencies, + isShort: true, + }); + bg._fLangs = languages; + + bg._fMisc = []; + if (bg.srd) + bg._fMisc.push("SRD"); + if (bg.basicRules) + bg._fMisc.push("Basic Rules"); + if (bg.hasFluff || bg.fluff?.entries) + bg._fMisc.push("Has Info"); + if (bg.hasFluffImages || bg.fluff?.images) + bg._fMisc.push("Has Images"); + bg._fOtherBenifits = []; + if (bg.feats) + bg._fOtherBenifits.push("Feat"); + if (bg.additionalSpells) + bg._fOtherBenifits.push("Additional Spells"); + if (bg.armorProficiencies) + bg._fOtherBenifits.push("Armor Proficiencies"); + if (bg.weaponProficiencies) + bg._fOtherBenifits.push("Weapon Proficiencies"); + bg._skillDisplay = skillDisplay; + } + + addToFilters(bg, isExcluded) { + if (isExcluded) + return; + + this._sourceFilter.addItem(bg._fSources); + this._skillFilter.addItem(bg._fSkills); + this._toolFilter.addItem(bg._fTools); + this._languageFilter.addItem(bg._fLangs); + this._asiFilter.addItem(bg.ability); + this._otherBenefitsFilter.addItem(bg._fOtherBenifits); + } + + async _pPopulateBoxOptions(opts) { + opts.filters = [this._sourceFilter, this._skillFilter, this._toolFilter, this._languageFilter, this._asiFilter, this._otherBenefitsFilter, this._miscFilter, ]; + } + + toDisplay(values, bg) { + return this._filterBox.toDisplay(values, bg._fSources, bg._fSkills, bg._fTools, bg._fLangs, bg.ability, bg._fOtherBenifits, bg._fMisc, ); + } +} +; +//#endregion + +//#region PageFilterSpells +class PageFilterSpells extends PageFilter { + static _META_ADD_CONC = "Concentration"; + static _META_ADD_V = "Verbal"; + static _META_ADD_S = "Somatic"; + static _META_ADD_M = "Material"; + static _META_ADD_R = "Royalty"; + static _META_ADD_M_COST = "Material with Cost"; + static _META_ADD_M_CONSUMED = "Material is Consumed"; + static _META_ADD_M_CONSUMED_OPTIONAL = "Material is Optionally Consumed"; + + static F_RNG_POINT = "Point"; + static F_RNG_SELF_AREA = "Self (Area)"; + static F_RNG_SELF = "Self"; + static F_RNG_TOUCH = "Touch"; + static F_RNG_SPECIAL = "Special"; + + static _META_FILTER_BASE_ITEMS = [this._META_ADD_CONC, this._META_ADD_V, this._META_ADD_S, this._META_ADD_M, this._META_ADD_R, this._META_ADD_M_COST, this._META_ADD_M_CONSUMED, this._META_ADD_M_CONSUMED_OPTIONAL, ...Object.keys(Parser.SP_MISC_TAG_TO_FULL), ]; + + static INCHES_PER_FOOT = 12; + static FEET_PER_YARD = 3; + static FEET_PER_MILE = 5280; + + static sortSpells(a, b, o) { + switch (o.sortBy) { + case "name": + return SortUtil.compareListNames(a, b); + case "source": + case "level": + case "school": + case "concentration": + case "ritual": + return SortUtil.ascSort(a.values[o.sortBy], b.values[o.sortBy]) || SortUtil.compareListNames(a, b); + case "time": + return SortUtil.ascSort(a.values.normalisedTime, b.values.normalisedTime) || SortUtil.compareListNames(a, b); + case "range": + return SortUtil.ascSort(a.values.normalisedRange, b.values.normalisedRange) || SortUtil.compareListNames(a, b); + } + } + + static sortMetaFilter(a, b) { + const ixA = PageFilterSpells._META_FILTER_BASE_ITEMS.indexOf(a.item); + const ixB = PageFilterSpells._META_FILTER_BASE_ITEMS.indexOf(b.item); + + if (~ixA && ~ixB) + return ixA - ixB; + if (~ixA) + return -1; + if (~ixB) + return 1; + return SortUtil.ascSortLower(a, b); + } + + static getFilterAbilitySave(ability) { + return `${ability.uppercaseFirst()} Save`; + } + static getFilterAbilityCheck(ability) { + return `${ability.uppercaseFirst()} Check`; + } + + static getMetaFilterObj(s) { + const out = []; + if (s.meta) { + Object.entries(s.meta).filter(([_,v])=>v).sort(SortUtil.ascSort).forEach(([k])=>out.push(k.toTitleCase())); + } + if (s.duration.filter(d=>d.concentration).length) { + out.push(PageFilterSpells._META_ADD_CONC); + s._isConc = true; + } else + s._isConc = false; + if (s.components && s.components.v) + out.push(PageFilterSpells._META_ADD_V); + if (s.components && s.components.s) + out.push(PageFilterSpells._META_ADD_S); + if (s.components && s.components.m) + out.push(PageFilterSpells._META_ADD_M); + if (s.components && s.components.r) + out.push(PageFilterSpells._META_ADD_R); + if (s.components && s.components.m && s.components.m.cost) + out.push(PageFilterSpells._META_ADD_M_COST); + if (s.components && s.components.m && s.components.m.consume) { + if (s.components.m.consume === "optional") + out.push(PageFilterSpells._META_ADD_M_CONSUMED_OPTIONAL); + else + out.push(PageFilterSpells._META_ADD_M_CONSUMED); + } + if (s.miscTags) + out.push(...s.miscTags); + if ((!s.miscTags || (s.miscTags && !s.miscTags.includes("PRM"))) && s.duration.filter(it=>it.type === "permanent").length) + out.push("PRM"); + if ((!s.miscTags || (s.miscTags && !s.miscTags.includes("SCL"))) && s.entriesHigherLevel) + out.push("SCL"); + if (s.srd) + out.push("SRD"); + if (s.basicRules) + out.push("Basic Rules"); + if (s.hasFluff || s.fluff?.entries) + out.push("Has Info"); + if (s.hasFluffImages || s.fluff?.images) + out.push("Has Images"); + return out; + } + + static getFilterDuration(spell) { + const fDur = spell.duration[0] || { + type: "special" + }; + switch (fDur.type) { + case "instant": + return "Instant"; + case "timed": + { + if (!fDur.duration) + return "Special"; + switch (fDur.duration.type) { + case "turn": + case "round": + return "1 Round"; + + case "minute": + { + const amt = fDur.duration.amount || 0; + if (amt <= 1) + return "1 Minute"; + if (amt <= 10) + return "10 Minutes"; + if (amt <= 60) + return "1 Hour"; + if (amt <= 8 * 60) + return "8 Hours"; + return "24+ Hours"; + } + + case "hour": + { + const amt = fDur.duration.amount || 0; + if (amt <= 1) + return "1 Hour"; + if (amt <= 8) + return "8 Hours"; + return "24+ Hours"; + } + + case "week": + case "day": + case "year": + return "24+ Hours"; + default: + return "Special"; + } + } + case "permanent": + return "Permanent"; + case "special": + default: + return "Special"; + } + } + + static getNormalisedTime(time) { + const firstTime = time[0]; + let multiplier = 1; + let offset = 0; + switch (firstTime.unit) { + case Parser.SP_TM_B_ACTION: + offset = 1; + break; + case Parser.SP_TM_REACTION: + offset = 2; + break; + case Parser.SP_TM_ROUND: + multiplier = 6; + break; + case Parser.SP_TM_MINS: + multiplier = 60; + break; + case Parser.SP_TM_HRS: + multiplier = 3600; + break; + } + if (time.length > 1) + offset += 0.5; + return (multiplier * firstTime.number) + offset; + } + + static getNormalisedRange(range) { + const state = { + multiplier: 1, + distance: 0, + offset: 0, + }; + + switch (range.type) { + case Parser.RNG_SPECIAL: + return 1000000000; + case Parser.RNG_POINT: + this._getNormalisedRange_getAdjustedForDistance({ + range, + state + }); + break; + case Parser.RNG_LINE: + state.offset = 1; + this._getNormalisedRange_getAdjustedForDistance({ + range, + state + }); + break; + case Parser.RNG_CONE: + state.offset = 2; + this._getNormalisedRange_getAdjustedForDistance({ + range, + state + }); + break; + case Parser.RNG_RADIUS: + state.offset = 3; + this._getNormalisedRange_getAdjustedForDistance({ + range, + state + }); + break; + case Parser.RNG_HEMISPHERE: + state.offset = 4; + this._getNormalisedRange_getAdjustedForDistance({ + range, + state + }); + break; + case Parser.RNG_SPHERE: + state.offset = 5; + this._getNormalisedRange_getAdjustedForDistance({ + range, + state + }); + break; + case Parser.RNG_CYLINDER: + state.offset = 6; + this._getNormalisedRange_getAdjustedForDistance({ + range, + state + }); + break; + case Parser.RNG_CUBE: + state.offset = 7; + this._getNormalisedRange_getAdjustedForDistance({ + range, + state + }); + break; + } + + return (state.multiplier * state.distance) + state.offset; + } + + static _getNormalisedRange_getAdjustedForDistance({range, state}) { + const dist = range.distance; + switch (dist.type) { + case Parser.UNT_FEET: + state.multiplier = PageFilterSpells.INCHES_PER_FOOT; + state.distance = dist.amount; + break; + case Parser.UNT_YARDS: + state.multiplier = PageFilterSpells.INCHES_PER_FOOT * PageFilterSpells.FEET_PER_YARD; + state.distance = dist.amount; + break; + case Parser.UNT_MILES: + state.multiplier = PageFilterSpells.INCHES_PER_FOOT * PageFilterSpells.FEET_PER_MILE; + state.distance = dist.amount; + break; + case Parser.RNG_SELF: + state.distance = 0; + break; + case Parser.RNG_TOUCH: + state.distance = 1; + break; + case Parser.RNG_SIGHT: + state.multiplier = PageFilterSpells.INCHES_PER_FOOT * PageFilterSpells.FEET_PER_MILE; + state.distance = 12; + break; + case Parser.RNG_UNLIMITED_SAME_PLANE: + state.distance = 900000000; + break; + case Parser.RNG_UNLIMITED: + state.distance = 900000001; + break; + default: + { + this._getNormalisedRange_getAdjustedForDistance_prereleaseBrew({ + range, + state, + brewUtil: PrereleaseUtil + }) || this._getNormalisedRange_getAdjustedForDistance_prereleaseBrew({ + range, + state, + brewUtil: BrewUtil2 + }); + } + } + } + + static _getNormalisedRange_getAdjustedForDistance_prereleaseBrew({range, state, brewUtil}) { + const dist = range.distance; + const fromBrew = brewUtil.getMetaLookup("spellDistanceUnits")?.[dist.type]; + if (!fromBrew) + return false; + + const ftPerUnit = fromBrew.feetPerUnit; + if (ftPerUnit != null) { + state.multiplier = PageFilterSpells.INCHES_PER_FOOT * ftPerUnit; + state.distance = dist.amount; + } else { + state.distance = 910000000; + } + + return true; + } + + static getRangeType(range) { + switch (range.type) { + case Parser.RNG_SPECIAL: + return PageFilterSpells.F_RNG_SPECIAL; + case Parser.RNG_POINT: + switch (range.distance.type) { + case Parser.RNG_SELF: + return PageFilterSpells.F_RNG_SELF; + case Parser.RNG_TOUCH: + return PageFilterSpells.F_RNG_TOUCH; + default: + return PageFilterSpells.F_RNG_POINT; + } + case Parser.RNG_LINE: + case Parser.RNG_CONE: + case Parser.RNG_RADIUS: + case Parser.RNG_HEMISPHERE: + case Parser.RNG_SPHERE: + case Parser.RNG_CYLINDER: + case Parser.RNG_CUBE: + return PageFilterSpells.F_RNG_SELF_AREA; + } + } + + static getTblTimeStr(time) { + return (time.number === 1 && Parser.SP_TIME_SINGLETONS.includes(time.unit)) ? `${time.unit.uppercaseFirst()}` : `${time.number ? `${time.number} ` : ""}${Parser.spTimeUnitToShort(time.unit).uppercaseFirst()}`; + } + + static getTblLevelStr(spell) { + return `${Parser.spLevelToFull(spell.level)}${spell.meta && spell.meta.ritual ? " (rit.)" : ""}${spell.meta && spell.meta.technomagic ? " (tec.)" : ""}`; + } + + static getRaceFilterItem(r) { + const addSuffix = (r.source === Parser.SRC_DMG || SourceUtil.isNonstandardSource(r.source || Parser.SRC_PHB) || (typeof PrereleaseUtil !== "undefined" && PrereleaseUtil.hasSourceJson(r.source || Parser.SRC_PHB)) || (typeof BrewUtil2 !== "undefined" && BrewUtil2.hasSourceJson(r.source || Parser.SRC_PHB))) && !r.name.includes(Parser.sourceJsonToAbv(r.source)); + const name = `${r.name}${addSuffix ? ` (${Parser.sourceJsonToAbv(r.source)})` : ""}`; + const opts = { + item: name, + userData: { + group: SourceUtil.getFilterGroup(r.source || Parser.SRC_PHB), + }, + }; + if (r.baseName) + opts.nest = r.baseName; + else + opts.nest = "(No Subraces)"; + return new FilterItem(opts); + } + + constructor() { + super(); + + this._classFilter = new Filter({ + header: "Class", + groupFn: it=>it.userData.group, + }); + this._subclassFilter = new Filter({ + header: "Subclass", + nests: {}, + groupFn: it=>it.userData.group, + }); + this._levelFilter = new Filter({ + header: "Level", + items: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ], + displayFn: (lvl)=>Parser.spLevelToFullLevelText(lvl, { + isPluralCantrips: false + }), + }); + this._variantClassFilter = new VariantClassFilter(); + this._classAndSubclassFilter = new MultiFilterClasses({ + classFilter: this._classFilter, + subclassFilter: this._subclassFilter, + variantClassFilter: this._variantClassFilter, + }); + this._raceFilter = new Filter({ + header: "Race", + nests: {}, + groupFn: it=>it.userData.group, + }); + this._backgroundFilter = new SearchableFilter({ + header: "Background" + }); + this._featFilter = new SearchableFilter({ + header: "Feat" + }); + this._optionalfeaturesFilter = new SearchableFilter({ + header: "Other Option/Feature" + }); + this._metaFilter = new Filter({ + header: "Components & Miscellaneous", + items: [...PageFilterSpells._META_FILTER_BASE_ITEMS, "Ritual", "SRD", "Basic Rules", "Has Images", "Has Token"], + itemSortFn: PageFilterSpells.sortMetaFilter, + isMiscFilter: true, + displayFn: it=>Parser.spMiscTagToFull(it), + }); + this._groupFilter = new Filter({ + header: "Group" + }); + this._schoolFilter = new Filter({ + header: "School", + items: [...Parser.SKL_ABVS], + displayFn: Parser.spSchoolAbvToFull, + itemSortFn: (a,b)=>SortUtil.ascSortLower(Parser.spSchoolAbvToFull(a.item), Parser.spSchoolAbvToFull(b.item)), + }); + this._subSchoolFilter = new Filter({ + header: "Subschool", + items: [], + displayFn: it=>Parser.spSchoolAbvToFull(it).toTitleCase(), + itemSortFn: (a,b)=>SortUtil.ascSortLower(Parser.spSchoolAbvToFull(a.item), Parser.spSchoolAbvToFull(b.item)), + }); + this._damageFilter = new Filter({ + header: "Damage Type", + items: MiscUtil.copy(Parser.DMG_TYPES), + displayFn: StrUtil.uppercaseFirst, + }); + this._conditionFilter = new Filter({ + header: "Conditions Inflicted", + items: [...Parser.CONDITIONS], + displayFn: uid=>uid.split("|")[0].toTitleCase(), + }); + this._spellAttackFilter = new Filter({ + header: "Spell Attack", + items: ["M", "R", "O"], + displayFn: Parser.spAttackTypeToFull, + itemSortFn: null, + }); + this._saveFilter = new Filter({ + header: "Saving Throw", + items: ["strength", "dexterity", "constitution", "intelligence", "wisdom", "charisma"], + displayFn: PageFilterSpells.getFilterAbilitySave, + itemSortFn: null, + }); + this._checkFilter = new Filter({ + header: "Ability Check", + items: ["strength", "dexterity", "constitution", "intelligence", "wisdom", "charisma"], + displayFn: PageFilterSpells.getFilterAbilityCheck, + itemSortFn: null, + }); + this._timeFilter = new Filter({ + header: "Cast Time", + items: [Parser.SP_TM_ACTION, Parser.SP_TM_B_ACTION, Parser.SP_TM_REACTION, Parser.SP_TM_ROUND, Parser.SP_TM_MINS, Parser.SP_TM_HRS, ], + displayFn: Parser.spTimeUnitToFull, + itemSortFn: null, + }); + this._durationFilter = new RangeFilter({ + header: "Duration", + isLabelled: true, + labelSortFn: null, + labels: ["Instant", "1 Round", "1 Minute", "10 Minutes", "1 Hour", "8 Hours", "24+ Hours", "Permanent", "Special"], + }); + this._rangeFilter = new Filter({ + header: "Range", + items: [PageFilterSpells.F_RNG_SELF, PageFilterSpells.F_RNG_TOUCH, PageFilterSpells.F_RNG_POINT, PageFilterSpells.F_RNG_SELF_AREA, PageFilterSpells.F_RNG_SPECIAL, ], + itemSortFn: null, + }); + this._areaTypeFilter = new Filter({ + header: "Area Style", + items: ["ST", "MT", "R", "N", "C", "Y", "H", "L", "S", "Q", "W"], + displayFn: Parser.spAreaTypeToFull, + itemSortFn: null, + }); + this._affectsCreatureTypeFilter = new Filter({ + header: "Affects Creature Types", + items: [...Parser.MON_TYPES], + displayFn: StrUtil.toTitleCase, + }); + } + + static mutateForFilters(s) { + Renderer.spell.initBrewSources(s); + + s._normalisedTime = PageFilterSpells.getNormalisedTime(s.time); + s._normalisedRange = PageFilterSpells.getNormalisedRange(s.range); + + s._fSources = SourceFilter.getCompleteFilterSources(s); + s._fMeta = PageFilterSpells.getMetaFilterObj(s); + s._fClasses = Renderer.spell.getCombinedClasses(s, "fromClassList").map(c=>{ + return this._getClassFilterItem({ + className: c.name, + definedInSource: c.definedInSource, + classSource: c.source, + isVariantClass: false, + }); + } + ); + s._fSubclasses = Renderer.spell.getCombinedClasses(s, "fromSubclass").map(c=>{ + return this._getSubclassFilterItem({ + className: c.class.name, + classSource: c.class.source, + subclassName: c.subclass.name, + subclassShortName: c.subclass.shortName, + subclassSource: c.subclass.source, + subSubclassName: c.subclass.subSubclass, + }); + } + ); + s._fVariantClasses = Renderer.spell.getCombinedClasses(s, "fromClassListVariant").map(c=>{ + return this._getClassFilterItem({ + className: c.name, + definedInSource: c.definedInSource, + classSource: c.source, + isVariantClass: true, + }); + } + ); + s._fClassesAndVariantClasses = [...s._fClasses, ...s._fVariantClasses.map(it=>(it.userData.definedInSource && !SourceUtil.isNonstandardSource(it.userData.definedInSource)) ? new FilterItem({ + item: it.userData.equivalentClassName + }) : null).filter(Boolean).filter(it=>!s._fClasses.some(itCls=>itCls.item === it.item)), ]; + s._fRaces = Renderer.spell.getCombinedGeneric(s, { + propSpell: "races", + prop: "race" + }).map(PageFilterSpells.getRaceFilterItem); + s._fBackgrounds = Renderer.spell.getCombinedGeneric(s, { + propSpell: "backgrounds", + prop: "background" + }).map(it=>it.name); + s._fFeats = Renderer.spell.getCombinedGeneric(s, { + propSpell: "feats", + prop: "feat" + }).map(it=>it.name); + s._fOptionalfeatures = Renderer.spell.getCombinedGeneric(s, { + propSpell: "optionalfeatures", + prop: "optionalfeature" + }).map(it=>it.name); + s._fGroups = Renderer.spell.getCombinedGeneric(s, { + propSpell: "groups" + }).map(it=>it.name); + s._fTimeType = s.time.map(t=>t.unit); + s._fDurationType = PageFilterSpells.getFilterDuration(s); + s._fRangeType = PageFilterSpells.getRangeType(s.range); + + s._fAreaTags = [...(s.areaTags || [])]; + if (s.range.type === "line" && !s._fAreaTags.includes("L")) + s._fAreaTags.push("L"); + + s._fAffectsCreatureType = s.affectsCreatureType || [...Parser.MON_TYPES]; + } + + static unmutateForFilters(s) { + Renderer.spell.uninitBrewSources(s); + + delete s._normalisedTime; + delete s._normalisedRange; + + Object.keys(s).filter(it=>it.startsWith("_f")).forEach(it=>delete s[it]); + } + + addToFilters(s, isExcluded) { + if (isExcluded) + return; + + if (s.level > 9) + this._levelFilter.addItem(s.level); + this._groupFilter.addItem(s._fGroups); + this._schoolFilter.addItem(s.school); + this._sourceFilter.addItem(s._fSources); + this._metaFilter.addItem(s._fMeta); + this._backgroundFilter.addItem(s._fBackgrounds); + this._featFilter.addItem(s._fFeats); + this._optionalfeaturesFilter.addItem(s._fOptionalfeatures); + s._fClasses.forEach(c=>this._classFilter.addItem(c)); + s._fSubclasses.forEach(sc=>{ + this._subclassFilter.addNest(sc.nest, { + isHidden: true + }); + this._subclassFilter.addItem(sc); + } + ); + s._fRaces.forEach(r=>{ + if (r.nest) + this._raceFilter.addNest(r.nest, { + isHidden: true + }); + this._raceFilter.addItem(r); + } + ); + s._fVariantClasses.forEach(c=>{ + this._variantClassFilter.addNest(c.nest, { + isHidden: true + }); + this._variantClassFilter.addItem(c); + } + ); + this._subSchoolFilter.addItem(s.subschools); + this._conditionFilter.addItem(s.conditionInflict); + this._affectsCreatureTypeFilter.addItem(s.affectsCreatureType); + } + + async _pPopulateBoxOptions(opts) { + await SourceUtil.pInitSubclassReprintLookup(); + + opts.filters = [this._sourceFilter, this._levelFilter, this._classAndSubclassFilter, this._raceFilter, this._backgroundFilter, this._featFilter, this._optionalfeaturesFilter, this._metaFilter, this._groupFilter, this._schoolFilter, this._subSchoolFilter, this._damageFilter, this._conditionFilter, this._spellAttackFilter, this._saveFilter, this._checkFilter, this._timeFilter, this._durationFilter, this._rangeFilter, this._areaTypeFilter, this._affectsCreatureTypeFilter, ]; + } + + toDisplay(values, s) { + return this._filterBox.toDisplay(values, s._fSources, s.level, [this._classAndSubclassFilter.isVariantSplit ? s._fClasses : s._fClassesAndVariantClasses, s._fSubclasses, this._classAndSubclassFilter.isVariantSplit ? s._fVariantClasses : null, ], s._fRaces, s._fBackgrounds, s._fFeats, s._fOptionalfeatures, s._fMeta, s._fGroups, s.school, s.subschools, s.damageInflict, s.conditionInflict, s.spellAttack, s.savingThrow, s.abilityCheck, s._fTimeType, s._fDurationType, s._fRangeType, s._fAreaTags, s._fAffectsCreatureType, ); + } +} +//#endregion + +//#region PageFilterFeats +let PageFilterFeats$1 = class PageFilterFeats extends PageFilter { + static _PREREQ_KEY_TO_FULL = { + "other": "Special", + "spellcasting2020": "Spellcasting", + "spellcastingFeature": "Spellcasting", + "spellcastingPrepared": "Spellcasting", + }; + + constructor() { + super(); + + this._asiFilter = new Filter({ + header: "Ability Bonus", + items: ["str", "dex", "con", "int", "wis", "cha", ], + displayFn: Parser.attAbvToFull, + itemSortFn: null, + }); + this._categoryFilter = new Filter({ + header: "Category", + displayFn: StrUtil.toTitleCase, + }); + this._otherPrereqFilter = new Filter({ + header: "Other", + items: ["Ability", "Race", "Psionics", "Proficiency", "Special", "Spellcasting"], + }); + this._levelFilter = new Filter({ + header: "Level", + itemSortFn: SortUtil.ascSortNumericalSuffix, + }); + this._prerequisiteFilter = new MultiFilter({ + header: "Prerequisite", + filters: [this._otherPrereqFilter, this._levelFilter] + }); + this._benefitsFilter = new Filter({ + header: "Benefits", + items: ["Armor Proficiency", "Language Proficiency", "Skill Proficiency", "Spellcasting", "Tool Proficiency", "Weapon Proficiency", ], + }); + this._vulnerableFilter = FilterCommon.getDamageVulnerableFilter(); + this._resistFilter = FilterCommon.getDamageResistFilter(); + this._immuneFilter = FilterCommon.getDamageImmuneFilter(); + this._defenceFilter = new MultiFilter({ + header: "Damage", + filters: [this._vulnerableFilter, this._resistFilter, this._immuneFilter] + }); + this._conditionImmuneFilter = FilterCommon.getConditionImmuneFilter(); + this._miscFilter = new Filter({ + header: "Miscellaneous", + items: ["Has Info", "Has Images", "SRD", "Basic Rules"], + isMiscFilter: true + }); + } + + static mutateForFilters(feat) { + const ability = Renderer.getAbilityData(feat.ability); + feat._fAbility = ability.asCollection.filter(a=>!ability.areNegative.includes(a)); + const prereqText = Renderer.utils.prerequisite.getHtml(feat.prerequisite, { + isListMode: true + }) || VeCt.STR_NONE; + + feat._fPrereqOther = [...new Set((feat.prerequisite || []).flatMap(it=>Object.keys(it)))].map(it=>(this._PREREQ_KEY_TO_FULL[it] || it).uppercaseFirst()); + if (feat.prerequisite) + feat._fPrereqLevel = feat.prerequisite.filter(it=>it.level != null).map(it=>`Level ${it.level.level ?? it.level}`); + feat._fBenifits = [feat.resist ? "Damage Resistance" : null, feat.immune ? "Damage Immunity" : null, feat.conditionImmune ? "Condition Immunity" : null, feat.skillProficiencies ? "Skill Proficiency" : null, feat.additionalSpells ? "Spellcasting" : null, feat.armorProficiencies ? "Armor Proficiency" : null, feat.weaponProficiencies ? "Weapon Proficiency" : null, feat.toolProficiencies ? "Tool Proficiency" : null, feat.languageProficiencies ? "Language Proficiency" : null, ].filter(it=>it); + if (feat.skillToolLanguageProficiencies?.length) { + if (feat.skillToolLanguageProficiencies.some(it=>(it.choose || []).some(x=>x.from || [].includes("anySkill")))) + feat._fBenifits.push("Skill Proficiency"); + if (feat.skillToolLanguageProficiencies.some(it=>(it.choose || []).some(x=>x.from || [].includes("anyTool")))) + feat._fBenifits.push("Tool Proficiency"); + if (feat.skillToolLanguageProficiencies.some(it=>(it.choose || []).some(x=>x.from || [].includes("anyLanguage")))) + feat._fBenifits.push("Language Proficiency"); + } + feat._fMisc = feat.srd ? ["SRD"] : []; + if (feat.basicRules) + feat._fMisc.push("Basic Rules"); + if (feat.hasFluff || feat.fluff?.entries) + feat._fMisc.push("Has Info"); + if (feat.hasFluffImages || feat.fluff?.images) + feat._fMisc.push("Has Images"); + if (feat.repeatable != null) + feat._fMisc.push(feat.repeatable ? "Repeatable" : "Not Repeatable"); + + feat._slAbility = ability.asText || VeCt.STR_NONE; + feat._slPrereq = prereqText; + + FilterCommon.mutateForFilters_damageVulnResImmune_player(feat); + FilterCommon.mutateForFilters_conditionImmune_player(feat); + } + + addToFilters(feat, isExcluded) { + if (isExcluded) + return; + + this._sourceFilter.addItem(feat.source); + this._categoryFilter.addItem(feat.category); + if (feat.prerequisite) + this._levelFilter.addItem(feat._fPrereqLevel); + this._vulnerableFilter.addItem(feat._fVuln); + this._resistFilter.addItem(feat._fRes); + this._immuneFilter.addItem(feat._fImm); + this._conditionImmuneFilter.addItem(feat._fCondImm); + this._benefitsFilter.addItem(feat._fBenifits); + this._miscFilter.addItem(feat._fMisc); + } + + async _pPopulateBoxOptions(opts) { + opts.filters = [this._sourceFilter, this._asiFilter, this._categoryFilter, this._prerequisiteFilter, this._benefitsFilter, this._defenceFilter, this._conditionImmuneFilter, this._miscFilter, ]; + } + + toDisplay(values, ft) { + return this._filterBox.toDisplay(values, ft.source, ft._fAbility, ft.category, [ft._fPrereqOther, ft._fPrereqLevel, ], ft._fBenifits, [ft._fVuln, ft._fRes, ft._fImm, ], ft._fCondImm, ft._fMisc, ); + } +} +; +//#endregion + +//#region PageFilterEquipment +class PageFilterEquipment extends PageFilter { + static _MISC_FILTER_ITEMS = ["Item Group", "Bundle", "SRD", "Basic Rules", "Has Images", "Has Info", "Reprinted", ]; + + static _RE_FOUNDRY_ATTR = /(?:[-+*/]\s*)?@[a-z0-9.]+/gi; + static _RE_DAMAGE_DICE_JUNK = /[^-+*/0-9d]/gi; + static _RE_DAMAGE_DICE_D = /d/gi; + + static _getSortableDamageTerm(t) { + try { + return eval(`${t}`.replace(this._RE_FOUNDRY_ATTR, "").replace(this._RE_DAMAGE_DICE_JUNK, "").replace(this._RE_DAMAGE_DICE_D, "*"), ); + } catch (ignored) { + return Number.MAX_SAFE_INTEGER; + } + } + + static _sortDamageDice(a, b) { + return this._getSortableDamageTerm(a.item) - this._getSortableDamageTerm(b.item); + } + + static _getMasteryDisplay(mastery) { + const {name, source} = DataUtil.proxy.unpackUid("itemMastery", mastery, "itemMastery"); + if (SourceUtil.isSiteSource(source)) + return name.toTitleCase(); + return `${name.toTitleCase()} (${Parser.sourceJsonToAbv(source)})`; + } + + constructor({filterOpts=null}={}) { + super(); + + this._typeFilter = new Filter({ + header: "Type", + deselFn: (it)=>PageFilterItems$1._DEFAULT_HIDDEN_TYPES.has(it), + displayFn: StrUtil.toTitleCase, + }); + this._propertyFilter = new Filter({ + header: "Property", + displayFn: StrUtil.toTitleCase + }); + this._categoryFilter = new Filter({ + header: "Category", + items: ["Basic", "Generic Variant", "Specific Variant", "Other"], + deselFn: (it)=>it === "Specific Variant", + itemSortFn: null, + ...(filterOpts?.["Category"] || {}), + }); + this._costFilter = new RangeFilter({ + header: "Cost", + isLabelled: true, + isAllowGreater: true, + labelSortFn: null, + labels: [0, ...[...new Array(9)].map((_,i)=>i + 1), ...[...new Array(9)].map((_,i)=>10 * (i + 1)), ...[...new Array(100)].map((_,i)=>100 * (i + 1)), ], + labelDisplayFn: it=>!it ? "None" : Parser.getDisplayCurrency(CurrencyUtil.doSimplifyCoins({ + cp: it + })), + }); + this._weightFilter = new RangeFilter({ + header: "Weight", + min: 0, + max: 100, + isAllowGreater: true, + suffix: " lb." + }); + this._focusFilter = new Filter({ + header: "Spellcasting Focus", + items: [...Parser.ITEM_SPELLCASTING_FOCUS_CLASSES] + }); + this._damageTypeFilter = new Filter({ + header: "Weapon Damage Type", + displayFn: it=>Parser.dmgTypeToFull(it).uppercaseFirst(), + itemSortFn: (a,b)=>SortUtil.ascSortLower(Parser.dmgTypeToFull(a), Parser.dmgTypeToFull(b)) + }); + this._damageDiceFilter = new Filter({ + header: "Weapon Damage Dice", + items: ["1", "1d4", "1d6", "1d8", "1d10", "1d12", "2d6"], + itemSortFn: (a,b)=>PageFilterEquipment._sortDamageDice(a, b) + }); + this._miscFilter = new Filter({ + header: "Miscellaneous", + items: [...PageFilterEquipment._MISC_FILTER_ITEMS, ...Object.values(Parser.ITEM_MISC_TAG_TO_FULL)], + isMiscFilter: true, + }); + this._poisonTypeFilter = new Filter({ + header: "Poison Type", + items: ["ingested", "injury", "inhaled", "contact"], + displayFn: StrUtil.toTitleCase + }); + this._masteryFilter = new Filter({ + header: "Mastery", + displayFn: this.constructor._getMasteryDisplay.bind(this) + }); + } + + static mutateForFilters(item) { + item._fSources = SourceFilter.getCompleteFilterSources(item); + + item._fProperties = item.property ? item.property.map(p=>Renderer.item.getProperty(p).name).filter(n=>n) : []; + + item._fMisc = []; + if (item._isItemGroup) + item._fMisc.push("Item Group"); + if (item.packContents) + item._fMisc.push("Bundle"); + if (item.srd) + item._fMisc.push("SRD"); + if (item.basicRules) + item._fMisc.push("Basic Rules"); + if (item.hasFluff || item.fluff?.entries) + item._fMisc.push("Has Info"); + if (item.hasFluffImages || item.fluff?.images) + item._fMisc.push("Has Images"); + if (item.miscTags) + item._fMisc.push(...item.miscTags.map(Parser.itemMiscTagToFull)); + if (this._isReprinted({ + reprintedAs: item.reprintedAs, + tag: "item", + prop: "item", + page: UrlUtil.PG_ITEMS + })) + item._fMisc.push("Reprinted"); + + if (item.focus || item.name === "Thieves' Tools" || item.type === "INS" || item.type === "SCF" || item.type === "AT") { + item._fFocus = item.focus ? item.focus === true ? [...Parser.ITEM_SPELLCASTING_FOCUS_CLASSES] : [...item.focus] : []; + if ((item.name === "Thieves' Tools" || item.type === "AT") && !item._fFocus.includes("Artificer")) + item._fFocus.push("Artificer"); + if (item.type === "INS" && !item._fFocus.includes("Bard")) + item._fFocus.push("Bard"); + if (item.type === "SCF") { + switch (item.scfType) { + case "arcane": + { + if (!item._fFocus.includes("Sorcerer")) + item._fFocus.push("Sorcerer"); + if (!item._fFocus.includes("Warlock")) + item._fFocus.push("Warlock"); + if (!item._fFocus.includes("Wizard")) + item._fFocus.push("Wizard"); + break; + } + case "druid": + { + if (!item._fFocus.includes("Druid")) + item._fFocus.push("Druid"); + break; + } + case "holy": + if (!item._fFocus.includes("Cleric")) + item._fFocus.push("Cleric"); + if (!item._fFocus.includes("Paladin")) + item._fFocus.push("Paladin"); + break; + } + } + } + + item._fValue = Math.round(item.value || 0); + + item._fDamageDice = []; + if (item.dmg1) + item._fDamageDice.push(item.dmg1); + if (item.dmg2) + item._fDamageDice.push(item.dmg2); + + item._fMastery = item.mastery ? item.mastery.map(it=>{ + const {name, source} = DataUtil.proxy.unpackUid("itemMastery", it, "itemMastery", { + isLower: true + }); + return [name, source].join("|"); + } + ) : null; + } + + addToFilters(item, isExcluded) { + if (isExcluded) + return; + + this._sourceFilter.addItem(item._fSources); + this._typeFilter.addItem(item._typeListText); + this._propertyFilter.addItem(item._fProperties); + this._damageTypeFilter.addItem(item.dmgType); + this._damageDiceFilter.addItem(item._fDamageDice); + this._poisonTypeFilter.addItem(item.poisonTypes); + this._miscFilter.addItem(item._fMisc); + this._masteryFilter.addItem(item._fMastery); + } + + async _pPopulateBoxOptions(opts) { + opts.filters = [this._sourceFilter, this._typeFilter, this._propertyFilter, this._categoryFilter, this._costFilter, this._weightFilter, this._focusFilter, this._damageTypeFilter, this._damageDiceFilter, this._miscFilter, this._poisonTypeFilter, this._masteryFilter, ]; + } + + toDisplay(values, it) { + return this._filterBox.toDisplay(values, it._fSources, it._typeListText, it._fProperties, it._category, it._fValue, it.weight, it._fFocus, it.dmgType, it._fDamageDice, it._fMisc, it.poisonTypes, it._fMastery, ); + } +}; +//#endregion + +//#region PageFilterItems +let PageFilterItems$1 = class PageFilterItems extends PageFilterEquipment { + static _DEFAULT_HIDDEN_TYPES = new Set(["treasure", "futuristic", "modern", "renaissance"]); + static _FILTER_BASE_ITEMS_ATTUNEMENT = ["Requires Attunement", "Requires Attunement By...", "Attunement Optional", VeCt.STR_NO_ATTUNEMENT]; + + static sortItems(a, b, o) { + if (o.sortBy === "name") + return SortUtil.compareListNames(a, b); + else if (o.sortBy === "type") + return SortUtil.ascSortLower(a.values.type, b.values.type) || SortUtil.compareListNames(a, b); + else if (o.sortBy === "source") + return SortUtil.ascSortLower(a.values.source, b.values.source) || SortUtil.compareListNames(a, b); + else if (o.sortBy === "rarity") + return SortUtil.ascSortItemRarity(a.values.rarity, b.values.rarity) || SortUtil.compareListNames(a, b); + else if (o.sortBy === "attunement") + return SortUtil.ascSort(a.values.attunement, b.values.attunement) || SortUtil.compareListNames(a, b); + else if (o.sortBy === "count") + return SortUtil.ascSort(a.data.count, b.data.count) || SortUtil.compareListNames(a, b); + else if (o.sortBy === "weight") + return SortUtil.ascSort(a.values.weight, b.values.weight) || SortUtil.compareListNames(a, b); + else if (o.sortBy === "cost") + return SortUtil.ascSort(a.values.cost, b.values.cost) || SortUtil.compareListNames(a, b); + else + return 0; + } + + static _getBaseItemDisplay(baseItem) { + if (!baseItem) + return null; + let[name,source] = baseItem.split("__"); + name = name.toTitleCase(); + source = source || Parser.SRC_DMG; + if (source.toLowerCase() === Parser.SRC_PHB.toLowerCase()) + return name; + return `${name} (${Parser.sourceJsonToAbv(source)})`; + } + + static _sortAttunementFilter(a, b) { + const ixA = PageFilterItems$1._FILTER_BASE_ITEMS_ATTUNEMENT.indexOf(a.item); + const ixB = PageFilterItems$1._FILTER_BASE_ITEMS_ATTUNEMENT.indexOf(b.item); + + if (~ixA && ~ixB) + return ixA - ixB; + if (~ixA) + return -1; + if (~ixB) + return 1; + return SortUtil.ascSortLower(a, b); + } + + static _getAttunementFilterItems(item) { + const out = item._attunementCategory ? [item._attunementCategory] : []; + + if (!item.reqAttuneTags && !item.reqAttuneAltTags) + return out; + + [...item.reqAttuneTags || [], ...item.reqAttuneAltTags || []].forEach(tagSet=>{ + Object.entries(tagSet).forEach(([prop,val])=>{ + switch (prop) { + case "background": + out.push(`Background: ${val.split("|")[0].toTitleCase()}`); + break; + case "languageProficiency": + out.push(`Language Proficiency: ${val.toTitleCase()}`); + break; + case "skillProficiency": + out.push(`Skill Proficiency: ${val.toTitleCase()}`); + break; + case "race": + out.push(`Race: ${val.split("|")[0].toTitleCase()}`); + break; + case "creatureType": + out.push(`Creature Type: ${val.toTitleCase()}`); + break; + case "size": + out.push(`Size: ${Parser.sizeAbvToFull(val)}`.toTitleCase()); + break; + case "class": + out.push(`Class: ${val.split("|")[0].toTitleCase()}`); + break; + case "alignment": + out.push(`Alignment: ${Parser.alignmentListToFull(val).toTitleCase()}`); + break; + + case "str": + case "dex": + case "con": + case "int": + case "wis": + case "cha": + out.push(`${Parser.attAbvToFull(prop)}: ${val} or Higher`); + break; + + case "spellcasting": + out.push("Spellcaster"); + break; + case "psionics": + out.push("Psionics"); + break; + } + } + ); + } + ); + + return out; + } + + constructor(opts) { + super(opts); + + this._tierFilter = new Filter({ + header: "Tier", + items: ["none", "minor", "major"], + itemSortFn: null, + displayFn: StrUtil.toTitleCase + }); + this._attachedSpellsFilter = new SearchableFilter({ + header: "Attached Spells", + displayFn: (it)=>it.split("|")[0].toTitleCase(), + itemSortFn: SortUtil.ascSortLower + }); + this._lootTableFilter = new Filter({ + header: "Found On", + items: ["Magic Item Table A", "Magic Item Table B", "Magic Item Table C", "Magic Item Table D", "Magic Item Table E", "Magic Item Table F", "Magic Item Table G", "Magic Item Table H", "Magic Item Table I"], + displayFn: it=>{ + const [name,sourceJson] = it.split("|"); + return `${name}${sourceJson ? ` (${Parser.sourceJsonToAbv(sourceJson)})` : ""}`; + } + , + }); + this._rarityFilter = new Filter({ + header: "Rarity", + items: [...Parser.ITEM_RARITIES], + itemSortFn: null, + displayFn: StrUtil.toTitleCase, + }); + this._attunementFilter = new Filter({ + header: "Attunement", + items: [...PageFilterItems$1._FILTER_BASE_ITEMS_ATTUNEMENT], + itemSortFn: PageFilterItems$1._sortAttunementFilter + }); + this._bonusFilter = new Filter({ + header: "Bonus", + items: ["Armor Class", "Proficiency Bonus", "Spell Attacks", "Spell Save DC", "Saving Throws", ...([...new Array(4)]).map((_,i)=>`Weapon Attack and Damage Rolls${i ? ` (+${i})` : ""}`), ...([...new Array(4)]).map((_,i)=>`Weapon Attack Rolls${i ? ` (+${i})` : ""}`), ...([...new Array(4)]).map((_,i)=>`Weapon Damage Rolls${i ? ` (+${i})` : ""}`), ], + itemSortFn: null, + }); + this._rechargeTypeFilter = new Filter({ + header: "Recharge Type", + displayFn: Parser.itemRechargeToFull + }); + this._miscFilter = new Filter({ + header: "Miscellaneous", + items: ["Ability Score Adjustment", "Charges", "Cursed", "Grants Language", "Grants Proficiency", "Magic", "Mundane", "Sentient", "Speed Adjustment", ...PageFilterEquipment._MISC_FILTER_ITEMS], + isMiscFilter: true + }); + this._baseSourceFilter = new SourceFilter({ + header: "Base Source", + selFn: null + }); + this._baseItemFilter = new Filter({ + header: "Base Item", + displayFn: this.constructor._getBaseItemDisplay.bind(this.constructor) + }); + this._optionalfeaturesFilter = new Filter({ + header: "Feature", + displayFn: (it)=>{ + const [name,source] = it.split("|"); + if (!source) + return name.toTitleCase(); + const sourceJson = Parser.sourceJsonToJson(source); + if (!SourceUtil.isNonstandardSourceWotc(sourceJson)) + return name.toTitleCase(); + return `${name.toTitleCase()} (${Parser.sourceJsonToAbv(sourceJson)})`; + } + , + itemSortFn: SortUtil.ascSortLower, + }); + } + + static mutateForFilters(item) { + super.mutateForFilters(item); + + item._fTier = [item.tier ? item.tier : "none"]; + + if (item.curse) + item._fMisc.push("Cursed"); + const isMundane = Renderer.item.isMundane(item); + item._fMisc.push(isMundane ? "Mundane" : "Magic"); + item._fIsMundane = isMundane; + if (item.ability) + item._fMisc.push("Ability Score Adjustment"); + if (item.modifySpeed) + item._fMisc.push("Speed Adjustment"); + if (item.charges) + item._fMisc.push("Charges"); + if (item.sentient) + item._fMisc.push("Sentient"); + if (item.grantsProficiency) + item._fMisc.push("Grants Proficiency"); + if (item.grantsLanguage) + item._fMisc.push("Grants Language"); + if (item.critThreshold) + item._fMisc.push("Expanded Critical Range"); + + const fBaseItemSelf = item._isBaseItem ? `${item.name}__${item.source}`.toLowerCase() : null; + item._fBaseItem = [item.baseItem ? (item.baseItem.includes("|") ? item.baseItem.replace("|", "__") : `${item.baseItem}__${Parser.SRC_DMG}`).toLowerCase() : null, item._baseName ? `${item._baseName}__${item._baseSource || item.source}`.toLowerCase() : null, ].filter(Boolean); + item._fBaseItemAll = fBaseItemSelf ? [fBaseItemSelf, ...item._fBaseItem] : item._fBaseItem; + + item._fBonus = []; + if (item.bonusAc) + item._fBonus.push("Armor Class"); + this._mutateForFilters_bonusWeapon({ + prop: "bonusWeapon", + item, + text: "Weapon Attack and Damage Rolls" + }); + this._mutateForFilters_bonusWeapon({ + prop: "bonusWeaponAttack", + item, + text: "Weapon Attack Rolls" + }); + this._mutateForFilters_bonusWeapon({ + prop: "bonusWeaponDamage", + item, + text: "Weapon Damage Rolls" + }); + if (item.bonusWeaponCritDamage) + item._fBonus.push("Weapon Critical Damage"); + if (item.bonusSpellAttack) + item._fBonus.push("Spell Attacks"); + if (item.bonusSpellSaveDc) + item._fBonus.push("Spell Save DC"); + if (item.bonusSavingThrow) + item._fBonus.push("Saving Throws"); + if (item.bonusProficiencyBonus) + item._fBonus.push("Proficiency Bonus"); + + item._fAttunement = this._getAttunementFilterItems(item); + } + + static _mutateForFilters_bonusWeapon({prop, item, text}) { + if (!item[prop]) + return; + item._fBonus.push(text); + switch (item[prop]) { + case "+1": + case "+2": + case "+3": + item._fBonus.push(`${text} (${item[prop]})`); + break; + } + } + + addToFilters(item, isExcluded) { + if (isExcluded) + return; + + super.addToFilters(item, isExcluded); + + this._sourceFilter.addItem(item.source); + this._tierFilter.addItem(item._fTier); + this._attachedSpellsFilter.addItem(item.attachedSpells); + this._lootTableFilter.addItem(item.lootTables); + this._baseItemFilter.addItem(item._fBaseItem); + this._baseSourceFilter.addItem(item._baseSource); + this._attunementFilter.addItem(item._fAttunement); + this._rechargeTypeFilter.addItem(item.recharge); + this._optionalfeaturesFilter.addItem(item.optionalfeatures); + } + + async _pPopulateBoxOptions(opts) { + await super._pPopulateBoxOptions(opts); + + opts.filters = [this._sourceFilter, this._typeFilter, this._tierFilter, this._rarityFilter, this._propertyFilter, this._attunementFilter, this._categoryFilter, this._costFilter, this._weightFilter, this._focusFilter, this._damageTypeFilter, this._damageDiceFilter, this._bonusFilter, this._miscFilter, this._rechargeTypeFilter, this._poisonTypeFilter, this._masteryFilter, this._lootTableFilter, this._baseItemFilter, this._baseSourceFilter, this._optionalfeaturesFilter, this._attachedSpellsFilter, ]; + } + + toDisplay(values, it) { + return this._filterBox.toDisplay(values, it._fSources, it._typeListText, it._fTier, it.rarity, it._fProperties, it._fAttunement, it._category, it._fValue, it.weight, it._fFocus, it.dmgType, it._fDamageDice, it._fBonus, it._fMisc, it.recharge, it.poisonTypes, it._fMastery, it.lootTables, it._fBaseItemAll, it._baseSource, it.optionalfeatures, it.attachedSpells, ); + } +} +; +//#endregion + +class VariantClassFilter extends Filter { + constructor(opts) { + super({ + header: "Optional/Variant Class", + nests: {}, + groupFn: it=>it.userData.group, + ...opts, + }); + + this._parent = null; + } + + set parent(multiFilterClasses) { + this._parent = multiFilterClasses; + } + + handleVariantSplit(isVariantSplit) { + this.__$wrpFilter.toggleVe(isVariantSplit); + } +} +class MultiFilterClasses extends MultiFilter { + constructor(opts) { + super({ + header: "Classes", + mode: "or", + filters: [opts.classFilter, opts.subclassFilter, opts.variantClassFilter], + ...opts + }); + + this._classFilter = opts.classFilter; + this._subclassFilter = opts.subclassFilter; + this._variantClassFilter = opts.variantClassFilter; + + this._variantClassFilter.parent = this; + } + + get classFilter_() { + return this._classFilter; + } + get isVariantSplit() { + return this._meta.isVariantSplit; + } + + $render(opts) { + const $out = super.$render(opts); + + const hkVariantSplit = ()=>this._variantClassFilter.handleVariantSplit(this._meta.isVariantSplit); + this._addHook("meta", "isVariantSplit", hkVariantSplit); + hkVariantSplit(); + + return $out; + } + + _getHeaderControls_addExtraStateBtns(opts, wrpStateBtnsOuter) { + const btnToggleVariantSplit = ComponentUiUtil.getBtnBool(this, "isVariantSplit", { + ele: e_({ + tag: "button", + clazz: "btn btn-default btn-xs", + text: "Include Variants" + }), + isInverted: true, + stateName: "meta", + stateProp: "_meta", + title: `If "Optional/Variant Class" spell lists should be treated as part of the "Class" filter.`, + }, ); + + e_({ + tag: "div", + clazz: `btn-group w-100 ve-flex-v-center mobile__m-1 mobile__mb-2`, + children: [btnToggleVariantSplit, ], + }).prependTo(wrpStateBtnsOuter); + } + + getDefaultMeta() { + return { + ...MultiFilterClasses._DEFAULT_META, + ...super.getDefaultMeta() + }; + } +} +MultiFilterClasses._DEFAULT_META = { + isVariantSplit: false, +}; +class SearchableFilter extends Filter { + constructor(opts) { + super(opts); + + this._compSearch = BaseComponent.fromObject({ + search: "", + searchTermParent: "", + }); + } + + handleSearch(searchTerm) { + const out = super.handleSearch(searchTerm); + + this._compSearch._state.searchTermParent = searchTerm; + + return out; + } + + _getPill(item) { + const btnPill = super._getPill(item); + + const hkIsVisible = ()=>{ + if (this._compSearch._state.searchTermParent) + return btnPill.toggleClass("fltr__hidden--inactive", false); + + btnPill.toggleClass("fltr__hidden--inactive", this._state[item.item] === 0); + } + ; + this._addHook("state", item.item, hkIsVisible); + this._compSearch._addHookBase("searchTermParent", hkIsVisible); + hkIsVisible(); + + return btnPill; + } + + _getPill_handleClick({evt, item}) { + if (this._compSearch._state.searchTermParent) + return super._getPill_handleClick({ + evt, + item + }); + + this._state[item.item] = 0; + } + + _getPill_handleContextmenu({evt, item}) { + if (this._compSearch._state.searchTermParent) + return super._getPill_handleContextmenu({ + evt, + item + }); + + evt.preventDefault(); + this._state[item.item] = 0; + } + + _$render_getRowBtn({fnsCleanup, $iptSearch, item, subtype, state}) { + const handleClick = evt=>{ + evt.stopPropagation(); + evt.preventDefault(); + + $iptSearch.focus(); + + if (evt.shiftKey) { + this._doSetPillsClear(); + } + + if (this._state[item.item] === state) + this._state[item.item] = 0; + else + this._state[item.item] = state; + } + ; + + const btn = e_({ + tag: "div", + clazz: `no-shrink clickable fltr-search__btn-activate fltr-search__btn-activate--${subtype} ve-flex-vh-center`, + click: evt=>handleClick(evt), + contextmenu: evt=>handleClick(evt), + mousedown: evt=>{ + evt.stopPropagation(); + evt.preventDefault(); + } + , + }); + + const hkIsActive = ()=>{ + btn.innerText = this._state[item.item] === state ? "ร—" : ""; + } + ; + this._addHookBase(item.item, hkIsActive); + hkIsActive(); + fnsCleanup.push(()=>this._removeHookBase(item.item, hkIsActive)); + + return btn; + } + + $render(opts) { + const $out = super.$render(opts); + + const $iptSearch = ComponentUiUtil.$getIptStr(this._compSearch, "search", { + html: ``, + }, ); + + const wrpValues = e_({ + tag: "div", + clazz: "overflow-y-auto bt-0 absolute fltr-search__wrp-values", + }); + + const fnsCleanup = []; + const rowMetas = []; + + this._$render_bindSearchHandler_keydown({ + $iptSearch, + fnsCleanup, + rowMetas + }); + this._$render_bindSearchHandler_focus({ + $iptSearch, + fnsCleanup, + rowMetas, + wrpValues + }); + this._$render_bindSearchHandler_blur({ + $iptSearch + }); + + const $wrp = $$``.prependTo(this.__wrpPills); + + const hkParentSearch = ()=>{ + $wrp.toggleVe(!this._compSearch._state.searchTermParent); + } + ; + this._compSearch._addHookBase("searchTermParent", hkParentSearch); + hkParentSearch(); + + return $out; + } + + _$render_bindSearchHandler_keydown({$iptSearch, rowMetas}) { + $iptSearch.on("keydown", evt=>{ + switch (evt.key) { + case "Escape": + evt.stopPropagation(); + return $iptSearch.blur(); + + case "ArrowDown": + { + evt.preventDefault(); + const visibleRowMetas = rowMetas.filter(it=>it.isVisible); + if (!visibleRowMetas.length) + return; + visibleRowMetas[0].row.focus(); + break; + } + + case "Enter": + { + const visibleRowMetas = rowMetas.filter(it=>it.isVisible); + if (!visibleRowMetas.length) + return; + if (evt.shiftKey) + this._doSetPillsClear(); + this._state[visibleRowMetas[0].item.item] = (EventUtil.isCtrlMetaKey(evt)) ? 2 : 1; + $iptSearch.blur(); + break; + } + } + } + ); + } + + _$render_bindSearchHandler_focus({$iptSearch, fnsCleanup, rowMetas, wrpValues}) { + $iptSearch.on("focus", ()=>{ + fnsCleanup.splice(0, fnsCleanup.length).forEach(fn=>fn()); + + rowMetas.splice(0, rowMetas.length); + + wrpValues.innerHTML = ""; + + rowMetas.push(...this._items.map(item=>this._$render_bindSearchHandler_focus_getRowMeta({ + $iptSearch, + fnsCleanup, + rowMetas, + wrpValues, + item + })), ); + + this._$render_bindSearchHandler_focus_addHookSearch({ + rowMetas, + fnsCleanup + }); + + wrpValues.scrollIntoView({ + block: "nearest", + inline: "nearest" + }); + } + ); + } + + _$render_bindSearchHandler_focus_getRowMeta({$iptSearch, fnsCleanup, rowMetas, wrpValues, item}) { + const dispName = this._getDisplayText(item); + + const eleName = e_({ + tag: "div", + clazz: "fltr-search__disp-name ml-2", + }); + + const btnBlue = this._$render_getRowBtn({ + fnsCleanup, + $iptSearch, + item, + subtype: "yes", + state: 1, + }); + btnBlue.addClass("br-0"); + btnBlue.addClass("btr-0"); + btnBlue.addClass("bbr-0"); + + const btnRed = this._$render_getRowBtn({ + fnsCleanup, + $iptSearch, + item, + subtype: "no", + state: 2, + }); + btnRed.addClass("bl-0"); + btnRed.addClass("btl-0"); + btnRed.addClass("bbl-0"); + + const row = e_({ + tag: "div", + clazz: "py-1p px-2 ve-flex-v-center fltr-search__wrp-row", + children: [btnBlue, btnRed, eleName, ], + attrs: { + tabindex: "0", + }, + keydown: evt=>{ + switch (evt.key) { + case "Escape": + evt.stopPropagation(); + return row.blur(); + + case "ArrowDown": + { + evt.preventDefault(); + const visibleRowMetas = rowMetas.filter(it=>it.isVisible); + if (!visibleRowMetas.length) + return; + const ixCur = visibleRowMetas.indexOf(out); + const nxt = visibleRowMetas[ixCur + 1]; + if (nxt) + nxt.row.focus(); + break; + } + + case "ArrowUp": + { + evt.preventDefault(); + const visibleRowMetas = rowMetas.filter(it=>it.isVisible); + if (!visibleRowMetas.length) + return; + const ixCur = visibleRowMetas.indexOf(out); + const prev = visibleRowMetas[ixCur - 1]; + if (prev) + return prev.row.focus(); + $iptSearch.focus(); + break; + } + + case "Enter": + { + if (evt.shiftKey) + this._doSetPillsClear(); + this._state[item.item] = (EventUtil.isCtrlMetaKey(evt)) ? 2 : 1; + row.blur(); + break; + } + } + } + , + }); + + wrpValues.appendChild(row); + + const out = { + isVisible: true, + item, + row, + dispName, + eleName, + }; + + return out; + } + + _$render_bindSearchHandler_focus_addHookSearch({rowMetas, fnsCleanup}) { + const hkSearch = ()=>{ + const searchTerm = this._compSearch._state.search.toLowerCase(); + + rowMetas.forEach(({item, row})=>{ + row.isVisible = item.searchText.includes(searchTerm); + row.toggleVe(row.isVisible); + } + ); + + if (!this._compSearch._state.search) { + rowMetas.forEach(({dispName, eleName})=>eleName.textContent = dispName); + return; + } + + const re = new RegExp(this._compSearch._state.search.qq().escapeRegexp(),"gi"); + + rowMetas.forEach(({dispName, eleName})=>{ + eleName.innerHTML = dispName.qq().replace(re, (...m)=>`${m[0]}`); + } + ); + } + ; + this._compSearch._addHookBase("search", hkSearch); + hkSearch(); + fnsCleanup.push(()=>this._compSearch._removeHookBase("search", hkSearch)); + } + + _$render_bindSearchHandler_blur({$iptSearch}) { + $iptSearch.on("blur", ()=>{ + this._compSearch._state.search = ""; + } + ); + } +} + +//#endregion + +//#region Modals + +//#region ModalFilter +class ModalFilter { + static _$getFilterColumnHeaders(btnMeta) { + return btnMeta.map((it,i)=>$(``)); + } + + /** + * Description + * @param {{modalTitle:string, fnSort:Function, pageFilter:PageFilter, namespace:string, isRadio:boolean, allData:any}} opts + * @returns {any} + */ + constructor(opts) { + this._modalTitle = opts.modalTitle; + this._fnSort = opts.fnSort; + this._pageFilter = opts.pageFilter; + this._namespace = opts.namespace; + this._allData = opts.allData || null; //This is all data (classes, races, etc) that we are provided + this._isRadio = !!opts.isRadio; + + this._list = null; + this._filterCache = null; + } + + /** + * @returns {PageFilter} + */ + get pageFilter() {return this._pageFilter;} + get allData() { return this._allData;} + + _$getWrpList() { + return $(`
    `); + } + + _$getColumnHeaderPreviewAll(opts) { + return $(``); + } + + async pPopulateWrapper($wrp, opts) { + opts = opts || {}; + + await this._pInit(); + + const $ovlLoading = $(`
    Loading...
    `).appendTo($wrp); + + const $iptSearch = (opts.$iptSearch || $(``)).disableSpellcheck(); + const $btnReset = opts.$btnReset || $(``); + const $dispNumVisible = $(`
    `); + + const $wrpIptSearch = $$`
    + ${$iptSearch} +
    + ${$dispNumVisible} +
    `; + + const $wrpFormTop = $$`
    ${$wrpIptSearch}${$btnReset}
    `; + + const $wrpFormBottom = opts.$wrpMiniPills || $(`
    `); + + const $wrpFormHeaders = $(`
    `); + const $cbSelAll = opts.isBuildUi || this._isRadio ? null : $(``); + const $btnSendAllToRight = opts.isBuildUi ? $(``) : null; + + if (!opts.isBuildUi) { + if (this._isRadio) + $wrpFormHeaders.append(``); + else + $$``.appendTo($wrpFormHeaders); + } + + const $btnTogglePreviewAll = this._$getColumnHeaderPreviewAll(opts).appendTo($wrpFormHeaders); + + this._$getColumnHeaders().forEach($ele=>$wrpFormHeaders.append($ele)); + if (opts.isBuildUi) + $btnSendAllToRight.appendTo($wrpFormHeaders); + + const $wrpForm = $$`
    ${$wrpFormTop}${$wrpFormBottom}${$wrpFormHeaders}
    `; + const $wrpList = this._$getWrpList(); + + const $btnConfirm = opts.isBuildUi ? null : $(``); + + this._list = new List({ + $iptSearch, + $wrpList, + fnSort: this._fnSort, + }); + const listSelectClickHandler = new ListSelectClickHandler({ + list: this._list + }); + + if (!opts.isBuildUi && !this._isRadio) + listSelectClickHandler.bindSelectAllCheckbox($cbSelAll); + ListUiUtil.bindPreviewAllButton($btnTogglePreviewAll, this._list); + SortUtil.initBtnSortHandlers($wrpFormHeaders, this._list); + this._list.on("updated", ()=>$dispNumVisible.html(`${this._list.visibleItems.length}/${this._list.items.length}`)); + + this._allData = this._allData || await this._pLoadAllData(); + + await this._pageFilter.pInitFilterBox({ + $wrpFormTop, + $btnReset, + $wrpMiniPills: $wrpFormBottom, + namespace: this._namespace, + $btnOpen: opts.$btnOpen, + $btnToggleSummaryHidden: opts.$btnToggleSummaryHidden, + }); + + this._allData.forEach((it,i)=>{ + this._pageFilter.mutateAndAddToFilters(it); + const filterListItem = this._getListItem(this._pageFilter, it, i); + this._list.addItem(filterListItem); + if (!opts.isBuildUi) { + if (this._isRadio) + filterListItem.ele.addEventListener("click", evt=>listSelectClickHandler.handleSelectClickRadio(filterListItem, evt)); + else + filterListItem.ele.addEventListener("click", evt=>listSelectClickHandler.handleSelectClick(filterListItem, evt)); + } + } + ); + + this._list.init(); + this._list.update(); + + const handleFilterChange = ()=>{ + const f = this._pageFilter.filterBox.getValues(); + this._list.filter(li=>this._isListItemMatchingFilter(f, li)); + } + ; + + this._pageFilter.trimState(); + + this._pageFilter.filterBox.on(FilterBox.EVNT_VALCHANGE, handleFilterChange); + this._pageFilter.filterBox.render(); + handleFilterChange(); + + $ovlLoading.remove(); + + const $wrpInner = $$`
    + ${$wrpForm} + ${$wrpList} + ${opts.isBuildUi ? null : $$`
    ${$btnConfirm}
    `} +
    `.appendTo($wrp.empty()); + + return { + $wrpIptSearch, + $iptSearch, + $wrpInner, + $btnConfirm, + pageFilter: this._pageFilter, + list: this._list, + $cbSelAll, + $btnSendAllToRight, + }; + } + + _isListItemMatchingFilter(f, li) { + return this._isEntityItemMatchingFilter(f, this._allData[li.ix]); + } + _isEntityItemMatchingFilter(f, it) { + return this._pageFilter.toDisplay(f, it); + } + + async pPopulateHiddenWrapper() { + await this._pInit(); + + this._allData = this._allData || await this._pLoadAllData(); + + await this._pageFilter.pInitFilterBox({ + namespace: this._namespace + }); + + this._allData.forEach(it=>{ + this._pageFilter.mutateAndAddToFilters(it); + } + ); + + this._pageFilter.trimState(); + } + + handleHiddenOpenButtonClick() { + this._pageFilter.filterBox.show(); + } + + handleHiddenResetButtonClick(evt) { + this._pageFilter.filterBox.reset(evt.shiftKey); + } + + _getStateFromFilterExpression(filterExpression) { + const filterSubhashMeta = Renderer.getFilterSubhashes(Renderer.splitTagByPipe(filterExpression), this._namespace); + const subhashes = filterSubhashMeta.subhashes.map(it=>`${it.key}${HASH_SUB_KV_SEP}${it.value}`); + const unpackedSubhashes = this.pageFilter.filterBox.unpackSubHashes(subhashes, { + force: true + }); + return this.pageFilter.filterBox.getNextStateFromSubHashes({ + unpackedSubhashes + }); + } + + getItemsMatchingFilterExpression({filterExpression}) { + const nxtStateOuter = this._getStateFromFilterExpression(filterExpression); + + const f = this._pageFilter.filterBox.getValues({ + nxtStateOuter + }); + const filteredItems = this._filterCache.list.getFilteredItems({ + items: this._filterCache.list.items, + fnFilter: li=>this._isListItemMatchingFilter(f, li), + }); + + return this._filterCache.list.getSortedItems({ + items: filteredItems + }); + } + + getEntitiesMatchingFilterExpression({filterExpression}) { + const nxtStateOuter = this._getStateFromFilterExpression(filterExpression); + + const f = this._pageFilter.filterBox.getValues({ + nxtStateOuter + }); + return this._allData.filter(this._isEntityItemMatchingFilter.bind(this, f)); + } + + getRenderedFilterExpression({filterExpression}) { + const nxtStateOuter = this._getStateFromFilterExpression(filterExpression); + return this.pageFilter.filterBox.getDisplayState({ + nxtStateOuter + }); + } + + async pGetUserSelection({filterExpression=null}={}) { + return new Promise(async resolve=>{ + const {$modalInner, doClose} = await this._pGetShowModal(resolve); + + await this.pPreloadHidden($modalInner); + + this._doApplyFilterExpression(filterExpression); + + this._filterCache.$btnConfirm.off("click").click(async()=>{ + const checked = this._filterCache.list.visibleItems.filter(it=>it.data.cbSel.checked); + resolve(checked); + + doClose(true); + + if (this._filterCache.$cbSelAll) + this._filterCache.$cbSelAll.prop("checked", false); + this._filterCache.list.items.forEach(it=>{ + if (it.data.cbSel) + it.data.cbSel.checked = false; + it.ele.classList.remove("list-multi-selected"); + } + ); + } + ); + + await UiUtil.pDoForceFocus(this._filterCache.$iptSearch[0]); + } + ); + } + + async _pGetShowModal(resolve) { + const {$modalInner, doClose} = await UiUtil.pGetShowModal({ + isHeight100: true, + isWidth100: true, + title: `Filter/Search for ${this._modalTitle}`, + cbClose: (isDataEntered)=>{ //isDataEntered is a boolean which tells us if the user entered any data or not + if(this._filterCache){this._filterCache.$wrpModalInner.detach();} + if (!isDataEntered){resolve([]);} + }, + isUncappedHeight: true, + }); + + return { $modalInner, doClose }; + } + + _doApplyFilterExpression(filterExpression) { + if (!filterExpression) + return; + + const filterSubhashMeta = Renderer.getFilterSubhashes(Renderer.splitTagByPipe(filterExpression), this._namespace); + const subhashes = filterSubhashMeta.subhashes.map(it=>`${it.key}${HASH_SUB_KV_SEP}${it.value}`); + this.pageFilter.filterBox.setFromSubHashes(subhashes, { + force: true, $iptSearch: this._filterCache.$iptSearch + }); + } + + _getNameStyle() {return `bold`;} + + async pPreloadHidden($modalInner) { + $modalInner = $modalInner || $(`
    `); + + if (this._filterCache) { + this._filterCache.$wrpModalInner.appendTo($modalInner); + } + else { + const meta = await this.pPopulateWrapper($modalInner); + const {$iptSearch, $btnConfirm, pageFilter, list, $cbSelAll} = meta; + const $wrpModalInner = meta.$wrpInner; + + this._filterCache = { + $iptSearch, + $wrpModalInner, + $btnConfirm, + pageFilter, + list, + $cbSelAll + }; + } + } + + _$getColumnHeaders() { + throw new Error(`Unimplemented!`); + } + /**Blank init function that can be overridden */ + async _pInit() {} + async _pLoadAllData() { + throw new Error(`Unimplemented!`); + } + async _getListItem() { + throw new Error(`Unimplemented!`); + } +} +class ModalFilterClasses extends ModalFilter { + + constructor(opts) { + opts = opts || {}; + + super({ + ...opts, + modalTitle: "Class and Subclass", + pageFilter: new PageFilterClassesRaw(), + fnSort: ModalFilterClasses.fnSort, + }); + + this._pLoadingAllData = null; + + this._ixPrevSelectedClass = null; + this._isClassDisabled = false; + this._isSubclassDisabled = false; + } + + /** + * The PageFilter + * @returns {PageFilterClassesRaw} + */ + get pageFilter() {return this._pageFilter;} + + static fnSort(a, b, opts) { + const out = SortUtil.listSort(a, b, opts); + + if (opts.sortDir === "desc" && a.data.ixClass === b.data.ixClass && (a.data.ixSubclass != null || b.data.ixSubclass != null)) { + return a.data.ixSubclass != null ? -1 : 1; + } + + return out; + } + + + /** + * Description + * @param {{className: String, classSource:String, subclassName:String, subclassSource:string}} classSubclassMeta + * @returns {{class:Object, subclass:Object}} + */ + async pGetSelection(classSubclassMeta) { + const {className, classSource, subclassName, subclassSource} = classSubclassMeta; + + const allData = this._allData || await this._pLoadAllData(); + + const cls = allData.find(it=>it.name === className && it.source === classSource); + if (!cls) + throw new Error(`Could not find class with name "${className}" and source "${classSource}"`); + + const out = {class: cls, }; + + if (subclassName && subclassSource) { + const sc = cls.subclasses.find(it=>it.name === subclassName && it.source === subclassSource); + if (!sc) + throw new Error(`Could not find subclass with name "${subclassName}" and source "${subclassSource}" on class with name "${className}" and source "${classSource}"`); + + out.subclass = sc; + } + + return out; + } + + async pGetUserSelection({filterExpression=null, selectedClass=null, selectedSubclass=null, isClassDisabled=false, isSubclassDisabled=false}={}) { + return new Promise(async resolve=>{ + const {$modalInner, doClose} = await this._pGetShowModal(resolve); + + await this.pPreloadHidden($modalInner); + + this._doApplyFilterExpression(filterExpression); + + this._filterCache.$btnConfirm.off("click").click(async()=>{ + const checked = this._filterCache.list.items.filter(it=>it.data.tglSel.classList.contains("active")); + const out = {}; + checked.forEach(it=>{ + if (it.data.ixSubclass == null) + out.class = this._filterCache.allData[it.data.ixClass]; + else + out.subclass = this._filterCache.allData[it.data.ixClass].subclasses[it.data.ixSubclass]; + } + ); + resolve(MiscUtil.copy(out)); + + doClose(true); + + ModalFilterClasses._doListDeselectAll(this._filterCache.list); + } + ); + + this._ixPrevSelectedClass = selectedClass != null ? this._filterCache.allData.findIndex(it=>it.name === selectedClass.name && it.source === selectedClass.source) : null; + this._isClassDisabled = isClassDisabled; + this._isSubclassDisabled = isSubclassDisabled; + this._filterCache.list.items.forEach(li=>{ + const isScLi = li.data.ixSubclass != null; + if (isScLi) { + li.data.tglSel.classList.toggle("disabled", this._isSubclassDisabled || (this._isClassDisabled && li.data.ixClass !== this._ixPrevSelectedClass)); + } else { + li.data.tglSel.classList.toggle("disabled", this._isClassDisabled); + } + } + ); + + if (selectedClass != null) { + const ixSubclass = ~this._ixPrevSelectedClass && selectedSubclass != null ? this._filterCache.allData[this._ixPrevSelectedClass].subclasses.findIndex(it=>it.name === selectedSubclass.name && it.source === selectedSubclass.source) : -1; + + if (~this._ixPrevSelectedClass) { + ModalFilterClasses._doListDeselectAll(this._filterCache.list); + + const clsItem = this._filterCache.list.items.find(it=>it.data.ixClass === this._ixPrevSelectedClass && it.data.ixSubclass == null); + if (clsItem) { + clsItem.data.tglSel.classList.add("active"); + clsItem.ele.classList.add("list-multi-selected"); + } + + if (~ixSubclass && clsItem) { + const scItem = this._filterCache.list.items.find(it=>it.data.ixClass === this._ixPrevSelectedClass && it.data.ixSubclass === ixSubclass); + scItem.data.tglSel.classList.add("active"); + scItem.ele.classList.add("list-multi-selected"); + } + } + + this._filterCache.list.setFnSearch((li,searchTerm)=>{ + if (li.data.ixClass !== this._ixPrevSelectedClass) + return false; + return List.isVisibleDefaultSearch(li, searchTerm); + } + ); + } else { + this._filterCache.list.setFnSearch(null); + } + + this._filterCache.list.update(); + + await UiUtil.pDoForceFocus(this._filterCache.$iptSearch[0]); + } + ); + } + + /**Called by ActorCharactermancerClass before the first render*/ + async pPreloadHidden($modalInner) { + $modalInner = $modalInner || $(`
    `); + + if (this._filterCache) { + this._filterCache.$wrpModalInner.appendTo($modalInner); + } + else { + await this._pInit(); + + //Loading text + const $ovlLoading = $(`
    Loading...
    `).appendTo($modalInner); + + const $iptSearch = $(``); + const $btnReset = $(``); + const $wrpFormTop = $$`
    ${$iptSearch}${$btnReset}
    `; + + const $wrpFormBottom = $(`
    `); + + const $wrpFormHeaders = $(`
    +
    + + +
    `); + + const $wrpForm = $$`
    ${$wrpFormTop}${$wrpFormBottom}${$wrpFormHeaders}
    `; + const $wrpList = this._$getWrpList(); + + const $btnConfirm = $(``); + + const list = new List({ $iptSearch, $wrpList, fnSort: this._fnSort, }); + + SortUtil.initBtnSortHandlers($wrpFormHeaders, list); + + //allData is probably already set + const allData = this._allData || await this._pLoadAllData(); + const pageFilter = this._pageFilter; + + await pageFilter.pInitFilterBox({ $wrpFormTop, $btnReset, $wrpMiniPills: $wrpFormBottom, namespace: this._namespace, }); + + allData.forEach((it,i)=>{ + pageFilter.mutateAndAddToFilters(it); + const filterListItems = this._getListItems(pageFilter, it, i); + filterListItems.forEach(li=>{ + list.addItem(li); + li.ele.addEventListener("click", evt=>{ + const isScLi = li.data.ixSubclass != null; + + if (isScLi) { + if (this._isSubclassDisabled) + return; + if (this._isClassDisabled && li.data.ixClass !== this._ixPrevSelectedClass) + return; + } else { + if (this._isClassDisabled) + return; + } + + this._handleSelectClick({ + list, + filterListItems, + filterListItem: li, + evt, + }); + } + ); + } + ); + } + ); + + list.init(); + list.update(); + + //Wrapper to a function to be called when the filter changes + const handleFilterChange = ()=>{ + return this.constructor.handleFilterChange({ pageFilter, list, allData }); + }; + + pageFilter.trimState(); + + pageFilter.filterBox.on(FilterBox.EVNT_VALCHANGE, handleFilterChange); + pageFilter.filterBox.render(); //Render the filterbox already + handleFilterChange(); //Lets call that filter function right away + + $ovlLoading.remove(); + + const $wrpModalInner = $$`
    + ${$wrpForm} + ${$wrpList} +
    ${$btnConfirm}
    +
    `.appendTo($modalInner); + + this._filterCache = { + $wrpModalInner, + $btnConfirm, + pageFilter, + list, + allData, + $iptSearch + }; + } + } + + /**Called when the filter changes */ + static handleFilterChange({pageFilter, list, allData}) { + //Get the values from the filterbox + const f = pageFilter.filterBox.getValues(); + if(!f.Source?._combineBlue){console.error("Combine blue is not set!"); + if(!pageFilter.filterBox._filters[0].__meta.combineBlue){ + console.error("Source filter does not have combineBlue!"); + } + } + + list.filter(li=>{ + const cls = allData[li.data.ixClass]; + + if (li.data.ixSubclass != null) { + const sc = cls.subclasses[li.data.ixSubclass]; + if (!pageFilter.toDisplay(f, cls, [], null, )){return false;} + + return pageFilter.filterBox.toDisplay(f, sc.source, sc._fMisc, null, ); + } + + return pageFilter.toDisplay(f, cls, [], null); + }); + } + + static _doListDeselectAll(list, {isSubclassItemsOnly=false}={}) { + list.items.forEach(it=>{ + if (isSubclassItemsOnly && it.data.ixSubclass == null) + return; + + if (it.data.tglSel) + it.data.tglSel.classList.remove("active"); + it.ele.classList.remove("list-multi-selected"); + } + ); + } + + _handleSelectClick({list, filterListItems, filterListItem, evt}) { + evt.preventDefault(); + evt.stopPropagation(); + + const isScLi = filterListItem.data.ixSubclass != null; + + if (this._isClassDisabled && this._ixPrevSelectedClass != null && isScLi) { + if (!filterListItem.data.tglSel.classList.contains("active")) + this.constructor._doListDeselectAll(list, { + isSubclassItemsOnly: true + }); + filterListItem.data.tglSel.classList.toggle("active"); + filterListItem.ele.classList.toggle("list-multi-selected"); + return; + } + + if (filterListItem.data.tglSel.classList.contains("active")) { + this.constructor._doListDeselectAll(list); + return; + } + + this.constructor._doListDeselectAll(list); + + if (isScLi) { + const classItem = filterListItems[0]; + classItem.data.tglSel.classList.add("active"); + classItem.ele.classList.add("list-multi-selected"); + } + + filterListItem.data.tglSel.classList.add("active"); + filterListItem.ele.classList.add("list-multi-selected"); + } + + async _pLoadAllData() { + this._pLoadingAllData = this._pLoadingAllData || (async()=>{ + const [data,prerelease,brew] = await Promise.all([MiscUtil.copy(await DataUtil.class.loadRawJSON()), + PrereleaseUtil.pGetBrewProcessed(), BrewUtil2.pGetBrewProcessed(), ]); + + this._pLoadAllData_mutAddPrereleaseBrew({ + data, brew: prerelease, brewUtil: PrereleaseUtil + }); + this._pLoadAllData_mutAddPrereleaseBrew({ + data, brew: brew, brewUtil: BrewUtil2 + }); + + this._allData = (await PageFilterClassesRaw.pPostLoad(data)).class; + })(); + + await this._pLoadingAllData; + return this._allData; + } + + _pLoadAllData_mutAddPrereleaseBrew({data, brew, brewUtil}) { + const clsProps = brewUtil.getPageProps({ + page: UrlUtil.PG_CLASSES + }); + + if (!clsProps.includes("*")) { + clsProps.forEach(prop=>data[prop] = [...(data[prop] || []), ...MiscUtil.copy(brew[prop] || [])]); + return; + } + + Object.entries(brew).filter(([,brewVal])=>brewVal instanceof Array).forEach(([prop,brewArr])=>data[prop] = [...(data[prop] || []), ...MiscUtil.copy(brewArr)]); + } + + _getListItems(pageFilter, cls, clsI) { + return [this._getListItems_getClassItem(pageFilter, cls, clsI), ...cls.subclasses.map((sc,scI)=>this._getListItems_getSubclassItem(pageFilter, cls, clsI, sc, scI)), ]; + } + + _getListItems_getClassItem(pageFilter, cls, clsI) { + const eleLabel = document.createElement("label"); + eleLabel.className = `w-100 ve-flex lst--border veapp__list-row no-select lst__wrp-cells`; + + const source = Parser.sourceJsonToAbv(cls.source); + + eleLabel.innerHTML = `
    +
    ${cls._versionBase_isVersion ? `` : ""}${cls.name}
    +
    ${source}
    `; + + return new ListItem(clsI,eleLabel,`${cls.name} -- ${cls.source}`,{ + source: `${source} -- ${cls.name}`, + },{ + ixClass: clsI, + tglSel: eleLabel.firstElementChild.firstElementChild, + },); + } + + _getListItems_getSubclassItem(pageFilter, cls, clsI, sc, scI) { + const eleLabel = document.createElement("label"); + eleLabel.className = `w-100 ve-flex lst--border veapp__list-row no-select lst__wrp-cells`; + + const source = Parser.sourceJsonToAbv(sc.source); + + eleLabel.innerHTML = `
    +
    ${sc._versionBase_isVersion ? `` : ""}\u2014 ${sc.name}
    +
    ${source}
    `; + + return new ListItem(`${clsI}--${scI}`,eleLabel,`${cls.name} -- ${cls.source} -- ${sc.name} -- ${sc.source}`,{ + source: `${cls.source} -- ${cls.name} -- ${source} -- ${sc.name}`, + },{ + ixClass: clsI, + ixSubclass: scI, + tglSel: eleLabel.firstElementChild.firstElementChild, + },); + } +} + +class ModalFilterRaces extends ModalFilter { + constructor(opts) { + opts = opts || {}; + super({ + ...opts, //pass on allData to super + modalTitle: `Race${opts.isRadio ? "" : "s"}`, + pageFilter: new PageFilterRaces(), //Create a pass filter that handles only races + }); + } + + /** + * The PageFilter + * @returns {PageFilterRaces} + */ + get pageFilter() {return this._pageFilter;} + + _$getColumnHeaders() { + const btnMeta = [{ + sort: "name", + text: "Name", + width: "4" + }, { + sort: "ability", + text: "Ability", + width: "4" + }, { + sort: "size", + text: "Size", + width: "2" + }, { + sort: "source", + text: "Source", + width: "1" + }, ]; + return ModalFilter._$getFilterColumnHeaders(btnMeta); + } + + async _pLoadAllData() { + return [...await DataUtil.race.loadJSON(), ...((await DataUtil.race.loadPrerelease({ + isAddBaseRaces: false + })).race || []), ...((await DataUtil.race.loadBrew({ + isAddBaseRaces: false + })).race || []), ]; + } + + _getListItem(pageFilter, race, rI) { + const eleRow = document.createElement("div"); + eleRow.className = "px-0 w-100 ve-flex-col no-shrink"; + + const hash = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_RACES](race); + const ability = race.ability ? Renderer.getAbilityData(race.ability) : { + asTextShort: "None" + }; + const size = (race.size || [Parser.SZ_VARIES]).map(sz=>Parser.sizeAbvToFull(sz)).join("/"); + const source = Parser.sourceJsonToAbv(race.source); + + eleRow.innerHTML = `
    +
    ${this._isRadio ? `` : ``}
    + +
    +
    [+]
    +
    + +
    ${race._versionBase_isVersion ? `` : ""}${race.name}
    +
    ${ability.asTextShort}
    +
    ${size}
    +
    ${source}
    +
    `; + + const btnShowHidePreview = eleRow.firstElementChild.children[1].firstElementChild; + + const listItem = new ListItem(rI,eleRow,race.name,{ + hash, + source, + sourceJson: race.source, + ability: ability.asTextShort, + size, + cleanName: PageFilterRaces.getInvertedName(race.name) || "", + alias: PageFilterRaces.getListAliases(race), + },{ + cbSel: eleRow.firstElementChild.firstElementChild.firstElementChild, + btnShowHidePreview, + },); + + ListUiUtil.bindPreviewButton(UrlUtil.PG_RACES, this._allData, listItem, btnShowHidePreview); + + return listItem; + } +} +class ModalFilterBackgrounds extends ModalFilter { + constructor(opts) { + opts = opts || {}; + super({ + ...opts, + modalTitle: `Background${opts.isRadio ? "" : "s"}`, + pageFilter: new PageFilterBackgrounds$1(), + }); + } + + _$getColumnHeaders() { + const btnMeta = [{ + sort: "name", + text: "Name", + width: "4" + }, { + sort: "skills", + text: "Skills", + width: "6" + }, { + sort: "source", + text: "Source", + width: "1" + }, ]; + return ModalFilter._$getFilterColumnHeaders(btnMeta); + } + + async _pLoadAllData() { + return [...(await DataUtil.loadJSON(`${Renderer.get().baseUrl}data/backgrounds.json`)).background, ...((await PrereleaseUtil.pGetBrewProcessed()).background || []), ...((await BrewUtil2.pGetBrewProcessed()).background || []), ]; + } + + _getListItem(pageFilter, bg, bgI) { + const eleRow = document.createElement("div"); + eleRow.className = "px-0 w-100 ve-flex-col no-shrink"; + + const hash = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_BACKGROUNDS](bg); + const source = Parser.sourceJsonToAbv(bg.source); + + eleRow.innerHTML = `
    +
    ${this._isRadio ? `` : ``}
    + +
    +
    [+]
    +
    + +
    ${bg._versionBase_isVersion ? `` : ""}${bg.name}
    +
    ${bg._skillDisplay}
    +
    ${source}
    +
    `; + + const btnShowHidePreview = eleRow.firstElementChild.children[1].firstElementChild; + + const listItem = new ListItem(bgI,eleRow,bg.name,{ + hash, + source, + sourceJson: bg.source, + skills: bg._skillDisplay, + },{ + cbSel: eleRow.firstElementChild.firstElementChild.firstElementChild, + btnShowHidePreview, + },); + + ListUiUtil.bindPreviewButton(UrlUtil.PG_BACKGROUNDS, this._allData, listItem, btnShowHidePreview); + + return listItem; + } +} +class ModalFilterFeats extends ModalFilter { + constructor(opts) { + opts = opts || {}; + super({ + ...opts, + modalTitle: `Feat${opts.isRadio ? "" : "s"}`, + pageFilter: new PageFilterFeats$1(), + }); + } + + _$getColumnHeaders() { + const btnMeta = [{ + sort: "name", + text: "Name", + width: "4" + }, { + sort: "ability", + text: "Ability", + width: "3" + }, { + sort: "prerequisite", + text: "Prerequisite", + width: "3" + }, { + sort: "source", + text: "Source", + width: "1" + }, ]; + return ModalFilter._$getFilterColumnHeaders(btnMeta); + } + + async _pLoadAllData() { + return [...(await DataUtil.loadJSON(`${Renderer.get().baseUrl}data/feats.json`)).feat, ...((await PrereleaseUtil.pGetBrewProcessed()).feat || []), ...((await BrewUtil2.pGetBrewProcessed()).feat || []), ]; + } + + _getListItem(pageFilter, feat, ftI) { + const eleRow = document.createElement("div"); + eleRow.className = "px-0 w-100 ve-flex-col no-shrink"; + + const hash = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_FEATS](feat); + const source = Parser.sourceJsonToAbv(feat.source); + + eleRow.innerHTML = `
    +
    ${this._isRadio ? `` : ``}
    + +
    +
    [+]
    +
    + +
    ${feat._versionBase_isVersion ? `` : ""}${feat.name}
    + ${feat._slAbility} + ${feat._slPrereq} +
    ${source}
    +
    `; + + const btnShowHidePreview = eleRow.firstElementChild.children[1].firstElementChild; + + const listItem = new ListItem(ftI,eleRow,feat.name,{ + hash, + source, + sourceJson: feat.source, + ability: feat._slAbility, + prerequisite: feat._slPrereq, + },{ + cbSel: eleRow.firstElementChild.firstElementChild.firstElementChild, + btnShowHidePreview, + },); + + ListUiUtil.bindPreviewButton(UrlUtil.PG_FEATS, this._allData, listItem, btnShowHidePreview); + + return listItem; + } +} +class ModalFilterItems extends ModalFilter { + constructor(opts) { + opts = opts || {}; + super({ + ...opts, + modalTitle: `Item${opts.isRadio ? "" : "s"}`, + pageFilter: new PageFilterItems$1(opts?.pageFilterOpts), + }); + } + + _$getColumnHeaders() { + const btnMeta = [{ + sort: "name", + text: "Name", + width: "4" + }, { + sort: "type", + text: "Type", + width: "6" + }, { + sort: "source", + text: "Source", + width: "1" + }, ]; + return ModalFilter._$getFilterColumnHeaders(btnMeta); + } + + async _pInit() { + await Renderer.item.pPopulatePropertyAndTypeReference(); + } + + async _pLoadAllData() { + return [...(await Renderer.item.pBuildList()), ...(await Renderer.item.pGetItemsFromPrerelease()), ...(await Renderer.item.pGetItemsFromBrew()), ]; + } + + _getListItem(pageFilter, item, itI) { + if (item.noDisplay) + return null; + + Renderer.item.enhanceItem(item); + pageFilter.mutateAndAddToFilters(item); + + const eleRow = document.createElement("div"); + eleRow.className = "px-0 w-100 ve-flex-col no-shrink"; + + const hash = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_ITEMS](item); + const source = Parser.sourceJsonToAbv(item.source); + const type = item._typeListText.join(", "); + + eleRow.innerHTML = `
    +
    ${this._isRadio ? `` : ``}
    + +
    +
    [+]
    +
    + +
    ${item._versionBase_isVersion ? `` : ""}${item.name}
    +
    ${type.uppercaseFirst()}
    +
    ${source}
    +
    `; + + const btnShowHidePreview = eleRow.firstElementChild.children[1].firstElementChild; + + const listItem = new ListItem(itI,eleRow,item.name,{ + hash, + source, + sourceJson: item.source, + type, + },{ + cbSel: eleRow.firstElementChild.firstElementChild.firstElementChild, + btnShowHidePreview, + },); + + ListUiUtil.bindPreviewButton(UrlUtil.PG_ITEMS, this._allData, listItem, btnShowHidePreview); + + return listItem; + } +} +class ModalFilterSpells extends ModalFilter { + constructor(opts) { + opts = opts || {}; + super({ + ...opts, + modalTitle: `Spell${opts.isRadio ? "" : "s"}`, + pageFilter: new PageFilterSpells(), + fnSort: PageFilterSpells.sortSpells, + }); + } + + _$getColumnHeaders() { + const btnMeta = [{ + sort: "name", + text: "Name", + width: "3" + }, { + sort: "level", + text: "Level", + width: "1-5" + }, { + sort: "time", + text: "Time", + width: "2" + }, { + sort: "school", + text: "School", + width: "1" + }, { + sort: "concentration", + text: "C.", + title: "Concentration", + width: "0-5" + }, { + sort: "range", + text: "Range", + width: "2" + }, { + sort: "source", + text: "Source", + width: "1" + }, ]; + return ModalFilter._$getFilterColumnHeaders(btnMeta); + } + + async _pInit() { + if (typeof PrereleaseUtil !== "undefined") + Renderer.spell.populatePrereleaseLookup(await PrereleaseUtil.pGetBrewProcessed()); + if (typeof BrewUtil2 !== "undefined") + Renderer.spell.populateBrewLookup(await BrewUtil2.pGetBrewProcessed()); + } + + async _pLoadAllData() { + return [...(await DataUtil.spell.pLoadAll()), ...((await PrereleaseUtil.pGetBrewProcessed()).spell || []), ...((await BrewUtil2.pGetBrewProcessed()).spell || []), ]; + } + + _getListItem(pageFilter, spell, spI) { + const eleRow = document.createElement("div"); + eleRow.className = "px-0 w-100 ve-flex-col no-shrink"; + + const hash = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_SPELLS](spell); + const source = Parser.sourceJsonToAbv(spell.source); + const levelText = PageFilterSpells.getTblLevelStr(spell); + const time = PageFilterSpells.getTblTimeStr(spell.time[0]); + const school = Parser.spSchoolAndSubschoolsAbvsShort(spell.school, spell.subschools); + const concentration = spell._isConc ? "ร—" : ""; + const range = Parser.spRangeToFull(spell.range); + + eleRow.innerHTML = `
    +
    ${this._isRadio ? `` : ``}
    + +
    +
    [+]
    +
    + +
    ${spell._versionBase_isVersion ? `` : ""}${spell.name}
    +
    ${levelText}
    +
    ${time}
    +
    ${school}
    +
    ${concentration}
    +
    ${range}
    +
    ${source}
    +
    `; + + const btnShowHidePreview = eleRow.firstElementChild.children[1].firstElementChild; + + const listItem = new ListItem(spI,eleRow,spell.name,{ + hash, + source, + sourceJson: spell.source, + level: spell.level, + time, + school: Parser.spSchoolAbvToFull(spell.school), + classes: Parser.spClassesToFull(spell, {isTextOnly: true}), + concentration, + normalisedTime: spell._normalisedTime, + normalisedRange: spell._normalisedRange, + },{ + cbSel: eleRow.firstElementChild.firstElementChild.firstElementChild, + btnShowHidePreview, + },); + + ListUiUtil.bindPreviewButton(UrlUtil.PG_SPELLS, this._allData, listItem, btnShowHidePreview); + + return listItem; + } +} + +class ModalFilterEquipment extends ModalFilter { + static _$getFilterColumnHeaders(btnMeta) { + return super._$getFilterColumnHeaders(btnMeta).map($btn=>$btn.addClass(`btn-5et`)); + } + + /** + * @param {Charactermancer_StartingEquipment.ComponentGold} compStartingEquipment + */ + constructor(compStartingEquipment) { + super({ + pageFilter: new PageFilterEquipment(), + namespace: "ImportListCharacter_modalFilterEquipment", + }); + this._compParent = compStartingEquipment; + } + + _$getColumnHeaders() { + const btnMeta = [{ + sort: "name", + text: "Name", + width: "3-2" + }, { + sort: "type", + text: "Type", + width: "3-2" + }, { + sort: "cost", + text: "Cost", + width: "1-8" + }, { + sort: "source", + text: "Source", + width: "1-8" + }, ]; + return this.constructor._$getFilterColumnHeaders(btnMeta); + } + + async _pLoadAllData() { + return []; + } + + _$getWrpList() { + return $(`
    `); + } + + _$getColumnHeaderPreviewAll(opts) { + return super._$getColumnHeaderPreviewAll(opts).addClass(["btn-5et", "ve-muted"]); + } + + _getListItem(pageFilter, item, itI) { + if (item.noDisplay) + return null; + + Renderer.item.enhanceItem(item); + pageFilter.mutateAndAddToFilters(item); + + const eleRow = document.createElement("div"); + eleRow.className = "px-0 w-100 veapp__list-row ve-flex-col no-shrink"; + + const hash = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_ITEMS](item); + const source = Parser.sourceJsonToAbv(item.source); + const type = item._typeListText.join(", "); + + + + const priceMult = Config.get("equipmentShop", "priceMultiplier") || 1.0; + + eleRow.innerHTML = `
    +
    +
    ${ListUiUtil.HTML_GLYPHICON_EXPAND}
    +
    + + ${item.name} + ${item._typeListText.join(", ").toTitleCase()} + ${Parser.itemValueToFullMultiCurrency(item, { + isShortForm: true, + multiplier: priceMult + }).replace(/ +/g, "\u00A0")} + ${source} +
    +
    `; + + const btnShowHidePreview = eleRow.firstElementChild.firstElementChild.firstElementChild; + btnShowHidePreview.addEventListener("click", evt=>{ + evt.stopPropagation(); + evt.preventDefault(); + + const elePreviewWrp = ListUiUtil.getOrAddListItemPreviewLazy(listItem); + + ListUiUtil.handleClickBtnShowHideListPreview(evt, UrlUtil.PG_ITEMS, item, btnShowHidePreview, elePreviewWrp, ); + } + ); + + const listItem = new ListItem(itI, eleRow, item.name,{ + hash, + source, + sourceJson: item.source, + cost: (item.value || 0) * priceMult, + type, + },{ + btnSendToRight: eleRow.firstElementChild.lastElementChild.lastElementChild, + btnShowHidePreview, + },); + + return listItem; + } + + /** + * Add items from allData to the graphical list of purchaseble items + * @param {any} allData + */ + setDataList(allData) { + //Remove all items from the list first + this._list.removeAllItems(); + + this._allData = (allData?.item || []).filter(it=>it.value != null && it.type !== "$"); + + //Then add items back to the list + this._allData.forEach((it,i)=>{ + this._pageFilter.mutateAndAddToFilters(it); + const filterListItem = this._getListItem(this._pageFilter, it, i); + this._list.addItem(filterListItem); + const itemUid = `${it.name}|${it.source}`; + //Add an event listener to the button for sending the item right to cart + filterListItem.data.btnSendToRight.addEventListener("click", evt=>{ + const isIgnoreCost = evt.ctrlKey; + //Set quantity to 5 if pressing shift + if (evt.shiftKey) { this._compParent.addBoughtItem(itemUid, { quantity: 5, isIgnoreCost });} + else { this._compParent.addBoughtItem(itemUid, { isIgnoreCost });} + }); + }); + + this._pageFilter.sourceFilter.setFromValues({ "Source": {} }); + + this._pageFilter.filterBox.render(); + this._list.update(); + } +}; + +class MixedModalFilterFvtt //extends Cls +{ + constructor(...args) { + //super(...args); + + this._prevApp = null; + } + + _getNameStyle() { + return ""; + } + + _getShowModal(resolve) { + if (this._prevApp) + this._prevApp.close(); + + const self = this; + + const app = new class TempApplication extends Application { + constructor() { + super({ + title: `Filter/Search for ${self._modalTitle}`, + template: `${SharedConsts.MODULE_LOCATION}/template/_Generic.hbs`, + width: Util.getMaxWindowWidth(900), + height: Util.getMaxWindowHeight(), + resizable: true, + }); + + this._$wrpHtmlInner = $(`
    `); + } + + get $modalInner() { + return this._$wrpHtmlInner; + } + + async close(...args) { + self._filterCache.$wrpModalInner.detach(); + await super.close(...args); + resolve([]); + } + + activateListeners($html) { + this._$wrpHtmlInner.appendTo($html); + } + } + (); + + app.render(true); + this._prevApp = app; + + return { + $modalInner: app.$modalInner, + doClose: app.close.bind(app) + }; + } + + getDataFromSelected(selected) { + return this._allData[selected.ix]; + } +} +//Cls is an input class we choose to extend +function MixinModalFilterFvtt(Cls) { + class MixedModalFilterFvtt extends Cls { + constructor(...args) { + super(...args); + + this._prevApp = null; + } + + _getNameStyle() {return "";} + + _getShowModal(resolve) { + if (this._prevApp) + this._prevApp.close(); + + const self = this; + + const app = new class TempApplication extends Application { + constructor() { + super({ + title: `Filter/Search for ${self._modalTitle}`, + template: `${SharedConsts.MODULE_LOCATION}/template/_Generic.hbs`, + width: Util.getMaxWindowWidth(900), + height: Util.getMaxWindowHeight(), + resizable: true, + }); + + this._$wrpHtmlInner = $(`
    `); + } + + get $modalInner() { return this._$wrpHtmlInner; } + + async close(...args) { + self._filterCache.$wrpModalInner.detach(); + await super.close(...args); + resolve([]); + } + + activateListeners($html) { + this._$wrpHtmlInner.appendTo($html); + } + } + (); + + app.render(true); + this._prevApp = app; + + return { + $modalInner: app.$modalInner, + doClose: app.close.bind(app) + }; + } + + getDataFromSelected(selected) { return this._allData[selected.ix]; } + } + return MixedModalFilterFvtt; +} +class ModalFilterBackgroundsFvtt extends MixinModalFilterFvtt(ModalFilterBackgrounds) { +} + +class ModalFilterClassesFvtt extends MixinModalFilterFvtt(ModalFilterClasses) { +} + +class ModalFilterFeatsFvtt extends MixinModalFilterFvtt(ModalFilterFeats) { +} + +class ModalFilterRacesFvtt extends MixinModalFilterFvtt(ModalFilterRaces) { +} + +class ModalFilterSpellsFvtt extends MixinModalFilterFvtt(ModalFilterSpells) { +} + +class ModalFilterItemsFvtt extends MixinModalFilterFvtt(ModalFilterItems) { +} +//#endregion +//#endregion + +class AppFilter { + constructor() { + this._filterBox = null; + } + + get filterBox() { + return this._filterBox; + } + + mutateAndAddToFilters(entity, isExcluded, opts) { + this.constructor.mutateForFilters(entity, opts); + this.addToFilters(entity, isExcluded, opts); + } + + static mutateForFilters(entity, opts) { + throw new Error("Unimplemented!"); + } + addToFilters(entity, isExcluded, opts) { + throw new Error("Unimplemented!"); + } + toDisplay(values, entity) { + throw new Error("Unimplemented!"); + } + async _pPopulateBoxOptions() { + throw new Error("Unimplemented!"); + } + + async pInitFilterBox(opts) { + opts = opts || {}; + await this._pPopulateBoxOptions(opts); + this._filterBox = new FilterBox(opts); + await this._filterBox.pDoLoadState(); + return this._filterBox; + } + + trimState() { + return this._filterBox.trimState_(); + } + + teardown() { + this._filterBox.teardown(); + } +} +class AppSourceSelectorAppFilter extends AppFilter { + static _sortTypeFilterItems(a, b) { + a = a.item; + b = b.item; + + const ixA = UtilDataSource.SOURCE_TYPE_ORDER__FILTER.indexOf(a); + const ixB = UtilDataSource.SOURCE_TYPE_ORDER__FILTER.indexOf(b); + + return SortUtil.ascSort(ixA, ixB); + } + + constructor() { + super(); + + this._typeFilter = new Filter({ + header: "Type", + itemSortFn: AppSourceSelectorAppFilter._sortTypeFilterItems, + }); + } + + static mutateForFilters() {} + + addToFilters(entity, isExcluded) { + if (isExcluded){return;} + + this._typeFilter.addItem(entity.filterTypes); + } + + async _pPopulateBoxOptions(opts) { + opts.filters = [this._typeFilter, ]; + } + + toDisplay(values, ent) { + return this._filterBox.toDisplay(values, ent.filterTypes, ); + } +} + +class FilterUtil { + static SUB_HASH_PREFIX_LENGTH = 4; + static SUB_HASH_PREFIXES = new Set([...Object.values(FilterBox._SUB_HASH_PREFIXES), ...Object.values(FilterBase._SUB_HASH_PREFIXES)]); +} +class FilterTransientOptions { + constructor(opts) { + this.isExtendDefaultState = opts.isExtendDefaultState; + } +} \ No newline at end of file diff --git a/charbuilder/js/plutonium/importlist.js b/charbuilder/js/plutonium/importlist.js new file mode 100644 index 0000000..250ab39 --- /dev/null +++ b/charbuilder/js/plutonium/importlist.js @@ -0,0 +1,82 @@ +class ImportListClass{ + //THIS IS A STUB +} + +ImportListClass.Utils = class { + static getDedupedData({ allContentMerged: allContentMerged }) { + allContentMerged = MiscUtil.copy(allContentMerged); + Object.entries(allContentMerged).forEach(([propName, value]) => { + if (propName !== "class") { + return; + } + if (!(value instanceof Array)) { + return; + } + const contentHolder = []; + const hashSet = new Set(); + value.forEach(obj => { + const classHash = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_CLASSES](obj); + if (hashSet.has(classHash)) { + if (obj.subclasses?.length) { + const existingClass = contentHolder.find(cls => UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_CLASSES](cls) === classHash); + (existingClass.subclasses = existingClass.subclasses || []).push(...obj.subclasses); + } + return; + } + hashSet.add(classHash); + contentHolder.push(obj); + }); + allContentMerged[propName] = contentHolder; + }); + return allContentMerged; + } + static getBlocklistFilteredData({ dedupedAllContentMerged: dedupedAllContentMerged }) { + dedupedAllContentMerged = { ...dedupedAllContentMerged}; + Object.entries(dedupedAllContentMerged).forEach(([propName, value]) => { + if (propName !== 'class') { return; } + if (!(value instanceof Array)) { + return; + } + const filteredClasses = value.filter(obj => { + if (obj.source === VeCt.STR_GENERIC) { + return false; + } + return !ExcludeUtil.isExcluded(UrlUtil.URL_TO_HASH_BUILDER['class'](obj), 'class', obj.source, { + 'isNoCount': true + }); + }); + filteredClasses.forEach(cls => { + if (!cls.classFeatures) { + return; + } + cls.classFeatures = cls.classFeatures.filter(f => !ExcludeUtil.isExcluded(f.hash, "classFeature", f.source, { + 'isNoCount': true + })); + }); + filteredClasses.forEach(cls => { + if (!cls.subclasses) { + return; + } + cls.subclasses = cls.subclasses.filter(sc => { + if (sc.source === VeCt.STR_GENERIC) { + return false; + } + return !ExcludeUtil.isExcluded(UrlUtil.URL_TO_HASH_BUILDER.subclass(sc), 'subclass', sc.source, { + 'isNoCount': true + }); + }); + cls.subclasses.forEach(sc => { + if (!sc.subclassFeatures) { + return; + } + sc.subclassFeatures = sc.subclassFeatures.filter(f => !ExcludeUtil.isExcluded(f.hash, "subclassFeature", f.source, { + 'isNoCount': true + })); + }); + }); + dedupedAllContentMerged[propName] = filteredClasses; + }); + return dedupedAllContentMerged; + } +}; + diff --git a/charbuilder/js/plutonium/jqueryutil.js b/charbuilder/js/plutonium/jqueryutil.js new file mode 100644 index 0000000..c7c2478 --- /dev/null +++ b/charbuilder/js/plutonium/jqueryutil.js @@ -0,0 +1,296 @@ +globalThis.JqueryUtil = { + _isEnhancementsInit: false, + initEnhancements() { + if (JqueryUtil._isEnhancementsInit) + return; + JqueryUtil._isEnhancementsInit = true; + + JqueryUtil.addSelectors(); + + window.$$ = function(parts, ...args) { + if (parts instanceof jQuery || parts instanceof HTMLElement) { + return (...passed)=>{ + const parts2 = [...passed[0]]; + const args2 = passed.slice(1); + parts2[0] = `
    ${parts2[0]}`; + parts2.last(`${parts2.last()}
    `); + + const $temp = $$(parts2, ...args2); + $temp.children().each((i,e)=>$(e).appendTo(parts)); + return parts; + } + ; + } else { + const $eles = []; + let ixArg = 0; + + const handleArg = (arg)=>{ + if (arg instanceof $) { + $eles.push(arg); + return `<${arg.tag()} data-r="true">`; + } else if (arg instanceof HTMLElement) { + return handleArg($(arg)); + } else + return arg; + } + ; + const raw = parts.reduce((html,p)=>{ + const myIxArg = ixArg++; + if (args[myIxArg] == null) + return `${html}${p}`; + if (args[myIxArg]instanceof Array) + return `${html}${args[myIxArg].map(arg=>handleArg(arg)).join("")}${p}`; + else + return `${html}${handleArg(args[myIxArg])}${p}`; + } + ); + const $res = $(raw); + + if ($res.length === 1) { + if ($res.attr("data-r") === "true") + return $eles[0]; + else + $res.find(`[data-r=true]`).replaceWith(i=>$eles[i]); + } else { + const $tmp = $(`
    `); + $tmp.append($res); + $tmp.find(`[data-r=true]`).replaceWith(i=>$eles[i]); + return $tmp.children(); + } + + return $res; + } + } + ; + + $.fn.extend({ + disableSpellcheck: function() { + return this.attr("autocomplete", "new-password").attr("autocapitalize", "off").attr("spellcheck", "false"); + }, + tag: function() { + return this.prop("tagName").toLowerCase(); + }, + title: function(...args) { + return this.attr("title", ...args); + }, + placeholder: function(...args) { + return this.attr("placeholder", ...args); + }, + disable: function() { + return this.attr("disabled", true); + }, + + fastSetHtml: function(html) { + if (!this.length) + return this; + let tgt = this[0]; + while (tgt.children.length) { + tgt = tgt.children[0]; + } + tgt.innerHTML = html; + return this; + }, + + blurOnEsc: function() { + return this.keydown(evt=>{ + if (evt.which === 27) + this.blur(); + } + ); + }, + + hideVe: function() { + return this.addClass("ve-hidden"); + }, + showVe: function() { + return this.removeClass("ve-hidden"); + }, + toggleVe: function(val) { + if (val === undefined) + return this.toggleClass("ve-hidden", !this.hasClass("ve-hidden")); + else + return this.toggleClass("ve-hidden", !val); + }, + }); + + $.event.special.destroyed = { + remove: function(o) { + if (o.handler) + o.handler(); + }, + }; + }, + + addSelectors() { + $.expr[":"].textEquals = (el,i,m)=>$(el).text().toLowerCase().trim() === m[3].unescapeQuotes(); + + $.expr[":"].containsInsensitive = (el,i,m)=>{ + const searchText = m[3]; + const textNode = $(el).contents().filter((i,e)=>e.nodeType === 3)[0]; + if (!textNode) + return false; + const match = textNode.nodeValue.toLowerCase().trim().match(`${searchText.toLowerCase().trim().escapeRegexp()}`); + return match && match.length > 0; + } + ; + }, + + showCopiedEffect(eleOr$Ele, text="Copied!", bubble) { + const $ele = eleOr$Ele instanceof $ ? eleOr$Ele : $(eleOr$Ele); + + const top = $(window).scrollTop(); + const pos = $ele.offset(); + + const animationOptions = { + top: "-=8", + opacity: 0, + }; + if (bubble) { + animationOptions.left = `${Math.random() > 0.5 ? "-" : "+"}=${~~(Math.random() * 17)}`; + } + const seed = Math.random(); + const duration = bubble ? 250 + seed * 200 : 250; + const offsetY = bubble ? 16 : 0; + + const $dispCopied = $(`
    `); + $dispCopied.html(text).css({ + top: (pos.top - 24) + offsetY - top, + left: pos.left + ($ele.width() / 2), + }).appendTo(document.body).animate(animationOptions, { + duration, + complete: ()=>$dispCopied.remove(), + progress: (_,progress)=>{ + if (bubble) { + const diffProgress = 0.5 - progress; + animationOptions.top = `${diffProgress > 0 ? "-" : "+"}=40`; + $dispCopied.css("transform", `rotate(${seed > 0.5 ? "-" : ""}${seed * 500 * progress}deg)`); + } + } + , + }, ); + }, + + _dropdownInit: false, + bindDropdownButton($ele) { + if (!JqueryUtil._dropdownInit) { + JqueryUtil._dropdownInit = true; + document.addEventListener("click", ()=>[...document.querySelectorAll(`.open`)].filter(ele=>!(ele.className || "").split(" ").includes(`dropdown--navbar`)).forEach(ele=>ele.classList.remove("open"))); + } + $ele.click(()=>setTimeout(()=>$ele.parent().addClass("open"), 1)); + }, + + _WRP_TOAST: null, + _ACTIVE_TOAST: [], + doToast(options) { + if (typeof window === "undefined") + return; + + if (JqueryUtil._WRP_TOAST == null) { + JqueryUtil._WRP_TOAST = e_({ + tag: "div", + clazz: "toast__container no-events w-100 overflow-y-hidden ve-flex-col", + }); + document.body.appendChild(JqueryUtil._WRP_TOAST); + } + + if (typeof options === "string") { + options = { + content: options, + type: "info", + }; + } + options.type = options.type || "info"; + + options.isAutoHide = options.isAutoHide ?? true; + options.autoHideTime = options.autoHideTime ?? 5000; + + const eleToast = e_({ + tag: "div", + clazz: `toast toast--type-${options.type} events-initial relative my-2 mx-auto`, + children: [e_({ + tag: "div", + clazz: "toast__wrp-content", + children: [options.content instanceof $ ? options.content[0] : options.content, ], + }), e_({ + tag: "div", + clazz: "toast__wrp-control", + children: [e_({ + tag: "button", + clazz: "btn toast__btn-close", + children: [e_({ + tag: "span", + clazz: "glyphicon glyphicon-remove", + }), ], + }), ], + }), ], + mousedown: evt=>{ + evt.preventDefault(); + } + , + click: evt=>{ + evt.preventDefault(); + JqueryUtil._doToastCleanup(toastMeta); + + if (!evt.shiftKey) + return; + [...JqueryUtil._ACTIVE_TOAST].forEach(toastMeta=>JqueryUtil._doToastCleanup(toastMeta)); + } + , + }); + + eleToast.prependTo(JqueryUtil._WRP_TOAST); + + const toastMeta = { + isAutoHide: !!options.isAutoHide, + eleToast + }; + JqueryUtil._ACTIVE_TOAST.push(toastMeta); + + AnimationUtil.pRecomputeStyles().then(()=>{ + eleToast.addClass(`toast--animate`); + + if (options.isAutoHide) { + setTimeout(()=>{ + JqueryUtil._doToastCleanup(toastMeta); + } + , options.autoHideTime); + } + + if (JqueryUtil._ACTIVE_TOAST.length >= 3) { + JqueryUtil._ACTIVE_TOAST.filter(({isAutoHide})=>!isAutoHide).forEach(toastMeta=>{ + JqueryUtil._doToastCleanup(toastMeta); + } + ); + } + } + ); + }, + + _doToastCleanup(toastMeta) { + toastMeta.eleToast.removeClass("toast--animate"); + JqueryUtil._ACTIVE_TOAST.splice(JqueryUtil._ACTIVE_TOAST.indexOf(toastMeta), 1); + setTimeout(()=>toastMeta.eleToast.parentElement && toastMeta.eleToast.remove(), 85); + }, + + isMobile() { + if (navigator?.userAgentData?.mobile) + return true; + return window.matchMedia("(max-width: 768px)").matches; + }, +}; + +class JqueryExtension { + static init() { + $.fn.extend({ + + swap: function($eleMap) { + Object.entries($eleMap).forEach(([k,$v])=>{ + this.find(`[data-r="${k}"]`).replaceWith($v); + } + ); + + return this; + }, + }); + } +} \ No newline at end of file diff --git a/charbuilder/js/plutonium/proxy.js b/charbuilder/js/plutonium/proxy.js new file mode 100644 index 0000000..fd2dc8f --- /dev/null +++ b/charbuilder/js/plutonium/proxy.js @@ -0,0 +1,558 @@ +class MixedProxyBase //extends Cls +{ + constructor(...args) { + //super(...args); + this.__hooks = {}; + this.__hooksAll = {}; + this.__hooksTmp = null; + this.__hooksAllTmp = null; + } + + _getProxy(hookProp, toProxy) { + return new Proxy(toProxy,{ + set: (object,prop,value)=>{ + return this._doProxySet(hookProp, object, prop, value); + } + , + deleteProperty: (object,prop)=>{ + if (!(prop in object)) + return true; + const prevValue = object[prop]; + Reflect.deleteProperty(object, prop); + this._doFireHooksAll(hookProp, prop, undefined, prevValue); + if (this.__hooks[hookProp] && this.__hooks[hookProp][prop]) + this.__hooks[hookProp][prop].forEach(hook=>hook(prop, undefined, prevValue)); + return true; + } + , + }); + } + + _doProxySet(hookProp, object, prop, value) { + if (object[prop] === value){return true;} + const prevValue = object[prop]; + Reflect.set(object, prop, value); + this._doFireHooksAll(hookProp, prop, value, prevValue); + this._doFireHooks(hookProp, prop, value, prevValue); + return true; + } + + async _pDoProxySet(hookProp, object, prop, value) { + if (object[prop] === value) + return true; + const prevValue = object[prop]; + Reflect.set(object, prop, value); + if (this.__hooksAll[hookProp]) + for (const hook of this.__hooksAll[hookProp]) + await hook(prop, value, prevValue); + if (this.__hooks[hookProp] && this.__hooks[hookProp][prop]) + for (const hook of this.__hooks[hookProp][prop]) + await hook(prop, value, prevValue); + return true; + } + + _doFireHooks(hookProp, prop, value, prevValue) { + if (this.__hooks[hookProp] && this.__hooks[hookProp][prop]){ + console.warn("Fire hook", hookProp, prop, value, prevValue, this.constructor.name); + this.__hooks[hookProp][prop].forEach(hook=>hook(prop, value, prevValue)); + } + } + + _doFireHooksAll(hookProp, prop, value, prevValue) { + if (this.__hooksAll[hookProp]) + this.__hooksAll[hookProp].forEach(hook=>hook(prop, undefined, prevValue)); + } + + _doFireAllHooks(hookProp) { + if (this.__hooks[hookProp]) + Object.entries(this.__hooks[hookProp]).forEach(([prop,hk])=>hk(prop)); + } + + _addHook(hookProp, prop, hook) { + ProxyBase._addHook_to(this.__hooks, hookProp, prop, hook); + if (this.__hooksTmp) + ProxyBase._addHook_to(this.__hooksTmp, hookProp, prop, hook); + return hook; + } + + static _addHook_to(obj, hookProp, prop, hook) { + ((obj[hookProp] = obj[hookProp] || {})[prop] = (obj[hookProp][prop] || [])).push(hook); + } + + _addHookAll(hookProp, hook) { + ProxyBase._addHookAll_to(this.__hooksAll, hookProp, hook); + if (this.__hooksAllTmp) + ProxyBase._addHookAll_to(this.__hooksAllTmp, hookProp, hook); + return hook; + } + + static _addHookAll_to(obj, hookProp, hook) { + (obj[hookProp] = obj[hookProp] || []).push(hook); + } + + _removeHook(hookProp, prop, hook) { + ProxyBase._removeHook_from(this.__hooks, hookProp, prop, hook); + if (this.__hooksTmp) + ProxyBase._removeHook_from(this.__hooksTmp, hookProp, prop, hook); + } + + static _removeHook_from(obj, hookProp, prop, hook) { + if (obj[hookProp] && obj[hookProp][prop]) { + const ix = obj[hookProp][prop].findIndex(hk=>hk === hook); + if (~ix) + obj[hookProp][prop].splice(ix, 1); + } + } + + _removeHooks(hookProp, prop) { + if (this.__hooks[hookProp]) + delete this.__hooks[hookProp][prop]; + if (this.__hooksTmp && this.__hooksTmp[hookProp]) + delete this.__hooksTmp[hookProp][prop]; + } + + _removeHookAll(hookProp, hook) { + ProxyBase._removeHookAll_from(this.__hooksAll, hookProp, hook); + if (this.__hooksAllTmp) + ProxyBase._removeHook_from(this.__hooksAllTmp, hookProp, hook); + } + + static _removeHookAll_from(obj, hookProp, hook) { + if (obj[hookProp]) { + const ix = obj[hookProp].findIndex(hk=>hk === hook); + if (~ix) + obj[hookProp].splice(ix, 1); + } + } + + _resetHooks(hookProp) { + if (hookProp !== undefined) + delete this.__hooks[hookProp]; + else + Object.keys(this.__hooks).forEach(prop=>delete this.__hooks[prop]); + } + + _resetHooksAll(hookProp) { + if (hookProp !== undefined) + delete this.__hooksAll[hookProp]; + else + Object.keys(this.__hooksAll).forEach(prop=>delete this.__hooksAll[prop]); + } + + _saveHookCopiesTo(obj) { + this.__hooksTmp = obj; + } + _saveHookAllCopiesTo(obj) { + this.__hooksAllTmp = obj; + } + + _proxyAssign(hookProp, proxyProp, underProp, toObj, isOverwrite) { + const oldKeys = Object.keys(this[proxyProp]); + const nuKeys = new Set(Object.keys(toObj)); + const dirtyKeyValues = {}; + + if (isOverwrite) { + oldKeys.forEach(k=>{ + if (!nuKeys.has(k) && this[underProp] !== undefined) { + const prevValue = this[proxyProp][k]; + delete this[underProp][k]; + dirtyKeyValues[k] = prevValue; + } + } + ); + } + + nuKeys.forEach(k=>{ + if (!CollectionUtil.deepEquals(this[underProp][k], toObj[k])) { + const prevValue = this[proxyProp][k]; + this[underProp][k] = toObj[k]; + dirtyKeyValues[k] = prevValue; + } + } + ); + + Object.entries(dirtyKeyValues).forEach(([k,prevValue])=>{ + this._doFireHooksAll(hookProp, k, this[underProp][k], prevValue); + if (this.__hooks[hookProp] && this.__hooks[hookProp][k]) + this.__hooks[hookProp][k].forEach(hk=>hk(k, this[underProp][k], prevValue)); + } + ); + } + + _proxyAssignSimple(hookProp, toObj, isOverwrite) { + return this._proxyAssign(hookProp, `_${hookProp}`, `__${hookProp}`, toObj, isOverwrite); + } +} +class ProxyBase extends MixedProxyBase{ + +} +class BaseComponent /*extends Cls*/ extends MixedProxyBase +{ + _state; + constructor(...args) { + super(...args); + + this.__locks = {}; + this.__rendered = {}; + + this.__state = {...this._getDefaultState()}; + this._state = this._getProxy("state", this.__state); + } + + _addHookBase(prop, hook) { + return this._addHook("state", prop, hook); + } + + _removeHookBase(prop, hook) { + return this._removeHook("state", prop, hook); + } + + _removeHooksBase(prop) { + return this._removeHooks("state", prop); + } + + _addHookAllBase(hook) { + return this._addHookAll("state", hook); + } + + _removeHookAllBase(hook) { + return this._removeHookAll("state", hook); + } + + _setState(toState) { + this._proxyAssign("state", "_state", "__state", toState, true); + } + + _setStateValue(prop, value, {isForceTriggerHooks=true}={}) { + if (this._state[prop] === value && !isForceTriggerHooks) + return value; + if (this._state[prop] !== value) + return this._state[prop] = value; + + this._doFireHooksAll("state", prop, value, value); + this._doFireHooks("state", prop, value, value); + return value; + } + + _getState() { + return MiscUtil.copyFast(this.__state); + } + + getPod() { + this.__pod = this.__pod || { + get: (prop)=>this._state[prop], + set: (prop,val)=>this._state[prop] = val, + delete: (prop)=>delete this._state[prop], + addHook: (prop,hook)=>this._addHookBase(prop, hook), + addHookAll: (hook)=>this._addHookAllBase(hook), + removeHook: (prop,hook)=>this._removeHookBase(prop, hook), + removeHookAll: (hook)=>this._removeHookAllBase(hook), + triggerCollectionUpdate: (prop)=>this._triggerCollectionUpdate(prop), + setState: (state)=>this._setState(state), + getState: ()=>this._getState(), + assign: (toObj,isOverwrite)=>this._proxyAssign("state", "_state", "__state", toObj, isOverwrite), + pLock: lockName=>this._pLock(lockName), + unlock: lockName=>this._unlock(lockName), + component: this, + }; + return this.__pod; + } + + _getDefaultState() { + return {}; + } + + getBaseSaveableState() { + return { + state: MiscUtil.copyFast(this.__state), + }; + } + + setBaseSaveableStateFrom(toLoad, isOverwrite=false) { + toLoad?.state && this._proxyAssignSimple("state", toLoad.state, isOverwrite); + } + + _getRenderedCollection(opts) { + opts = opts || {}; + const renderedLookupProp = opts.namespace ? `${opts.namespace}.${opts.prop}` : opts.prop; + return (this.__rendered[renderedLookupProp] = this.__rendered[renderedLookupProp] || {}); + } + + _renderCollection(opts) { + opts = opts || {}; + + const rendered = this._getRenderedCollection(opts); + const entities = this._state[opts.prop] || []; + return this._renderCollection_doRender(rendered, entities, opts); + } + + _renderCollection_doRender(rendered, entities, opts) { + opts = opts || {}; + + const toDelete = new Set(Object.keys(rendered)); + + for (let i = 0; i < entities.length; ++i) { + const it = entities[i]; + + if (it.id == null) + throw new Error(`Collection item did not have an ID!`); + const meta = rendered[it.id]; + + toDelete.delete(it.id); + if (meta) { + if (opts.isDiffMode) { + const nxtHash = this._getCollectionEntityHash(it); + if (nxtHash !== meta.__hash) + meta.__hash = nxtHash; + else + continue; + } + + meta.data = it; + opts.fnUpdateExisting(meta, it, i); + } else { + const meta = opts.fnGetNew(it, i); + + if (meta == null) + continue; + + meta.data = it; + if (!meta.$wrpRow && !meta.fnRemoveEles) + throw new Error(`A "$wrpRow" or a "fnRemoveEles" property is required for deletes!`); + + if (opts.isDiffMode) + meta.__hash = this._getCollectionEntityHash(it); + + rendered[it.id] = meta; + } + } + + const doRemoveElements = meta=>{ + if (meta.$wrpRow) + meta.$wrpRow.remove(); + if (meta.fnRemoveEles) + meta.fnRemoveEles(); + } + ; + + toDelete.forEach(id=>{ + const meta = rendered[id]; + doRemoveElements(meta); + delete rendered[id]; + if (opts.fnDeleteExisting) + opts.fnDeleteExisting(meta); + } + ); + + if (opts.fnReorderExisting) { + entities.forEach((it,i)=>{ + const meta = rendered[it.id]; + opts.fnReorderExisting(meta, it, i); + } + ); + } + } + + async _pRenderCollection(opts) { + opts = opts || {}; + + const rendered = this._getRenderedCollection(opts); + const entities = this._state[opts.prop] || []; + return this._pRenderCollection_doRender(rendered, entities, opts); + } + + async _pRenderCollection_doRender(rendered, entities, opts) { + opts = opts || {}; + + const toDelete = new Set(Object.keys(rendered)); + + for (let i = 0; i < entities.length; ++i) { + const it = entities[i]; + + if (!it.id) + throw new Error(`Collection item did not have an ID!`); + const meta = rendered[it.id]; + + toDelete.delete(it.id); + if (meta) { + if (opts.isDiffMode) { + const nxtHash = this._getCollectionEntityHash(it); + if (nxtHash !== meta.__hash) + meta.__hash = nxtHash; + else + continue; + } + + const nxtMeta = await opts.pFnUpdateExisting(meta, it); + if (opts.isMultiRender) + rendered[it.id] = nxtMeta; + } else { + const meta = await opts.pFnGetNew(it); + + if (meta == null) + continue; + + if (!opts.isMultiRender && !meta.$wrpRow && !meta.fnRemoveEles) + throw new Error(`A "$wrpRow" or a "fnRemoveEles" property is required for deletes!`); + if (opts.isMultiRender && meta.some(it=>!it.$wrpRow && !it.fnRemoveEles)) + throw new Error(`A "$wrpRow" or a "fnRemoveEles" property is required for deletes!`); + + if (opts.isDiffMode) + meta.__hash = this._getCollectionEntityHash(it); + + rendered[it.id] = meta; + } + } + + const doRemoveElements = meta=>{ + if (meta.$wrpRow) + meta.$wrpRow.remove(); + if (meta.fnRemoveEles) + meta.fnRemoveEles(); + } + ; + + for (const id of toDelete) { + const meta = rendered[id]; + if (opts.isMultiRender) + meta.forEach(it=>doRemoveElements(it)); + else + doRemoveElements(meta); + if (opts.additionalCaches) + opts.additionalCaches.forEach(it=>delete it[id]); + delete rendered[id]; + if (opts.pFnDeleteExisting) + await opts.pFnDeleteExisting(meta); + } + + if (opts.pFnReorderExisting) { + await entities.pSerialAwaitMap(async(it,i)=>{ + const meta = rendered[it.id]; + await opts.pFnReorderExisting(meta, it, i); + } + ); + } + } + + _detachCollection(prop, namespace=null) { + const renderedLookupProp = namespace ? `${namespace}.${prop}` : prop; + const rendered = (this.__rendered[renderedLookupProp] = this.__rendered[renderedLookupProp] || {}); + Object.values(rendered).forEach(it=>it.$wrpRow.detach()); + } + + _resetCollectionRenders(prop, namespace=null) { + const renderedLookupProp = namespace ? `${namespace}.${prop}` : prop; + const rendered = (this.__rendered[renderedLookupProp] = this.__rendered[renderedLookupProp] || {}); + Object.values(rendered).forEach(it=>it.$wrpRow.remove()); + delete this.__rendered[renderedLookupProp]; + } + + _getCollectionEntityHash(ent) { + return CryptUtil.md5(JSON.stringify(ent)); + } + + render() { + throw new Error("Unimplemented!"); + } + + getSaveableState() { + return { + ...this.getBaseSaveableState() + }; + } + setStateFrom(toLoad, isOverwrite=false) { + this.setBaseSaveableStateFrom(toLoad, isOverwrite); + } + + async _pLock(lockName) { + while (this.__locks[lockName]) + await this.__locks[lockName].lock; + let unlock = null; + const lock = new Promise(resolve=>unlock = resolve); + this.__locks[lockName] = { + lock, + unlock, + }; + } + + async _pGate(lockName) { + while (this.__locks[lockName]) + await this.__locks[lockName].lock; + } + + _unlock(lockName) { + const lockMeta = this.__locks[lockName]; + if (lockMeta) { + delete this.__locks[lockName]; + lockMeta.unlock(); + } + } + + async _pDoProxySetBase(prop, value) { + return this._pDoProxySet("state", this.__state, prop, value); + } + + _triggerCollectionUpdate(prop) { + if (!this._state[prop]) + return; + this._state[prop] = [...this._state[prop]]; + } + + static _toCollection(array) { + if (array) + return array.map(it=>({ + id: CryptUtil.uid(), + entity: it + })); + } + + static _fromCollection(array) { + if (array) + return array.map(it=>it.entity); + } + + /** + * Create a new BaseComponent and paste every entry in 'obj' to the new component's __state + * @param {any} obj + * @param {string[]} noModCollections + * @returns {BaseComponent} + */ + static fromObject(obj, ...noModCollections) { + const comp = new this(); + Object.entries(MiscUtil.copyFast(obj)).forEach(([k,v])=>{ + if (v == null) + comp.__state[k] = v; + else if (noModCollections.includes(k) || noModCollections.includes("*")) + comp.__state[k] = v; + else if (typeof v === "object" && v instanceof Array) + comp.__state[k] = BaseComponent._toCollection(v); + else + comp.__state[k] = v; + } + ); + return comp; + } + + static fromObjectNoMod(obj) { + return this.fromObject(obj, "*"); + } + + toObject(...noModCollections) { + const cpy = MiscUtil.copyFast(this.__state); + Object.entries(cpy).forEach(([k,v])=>{ + if (v == null) + return; + + if (noModCollections.includes(k) || noModCollections.includes("*")) + cpy[k] = v; + else if (v instanceof Array && v.every(it=>it && it.id)) + cpy[k] = BaseComponent._fromCollection(v); + } + ); + return cpy; + } + + toObjectNoMod() { + return this.toObject("*"); + } +} \ No newline at end of file diff --git a/charbuilder/js/plutonium/render_addons.js b/charbuilder/js/plutonium/render_addons.js new file mode 100644 index 0000000..e367cae --- /dev/null +++ b/charbuilder/js/plutonium/render_addons.js @@ -0,0 +1,178 @@ +class RenderableCollectionBase { + constructor(comp, prop, opts) { + opts = opts || {}; + this._comp = comp; + this._prop = prop; + this._namespace = opts.namespace; + this._isDiffMode = opts.isDiffMode; + } + + getNewRender(entity, i) { + throw new Error(`Unimplemented!`); + } + + doUpdateExistingRender(renderedMeta, entity, i) { + throw new Error(`Unimplemented!`); + } + + doDeleteExistingRender(renderedMeta) {} + + doReorderExistingComponent(renderedMeta, entity, i) {} + + _getCollectionItem(id) { + return this._comp._state[this._prop].find(it=>it.id === id); + } + + render(opts) { + opts = opts || {}; + this._comp._renderCollection({ + prop: this._prop, + fnUpdateExisting: (rendered,ent,i)=>this.doUpdateExistingRender(rendered, ent, i), + fnGetNew: (entity,i)=>this.getNewRender(entity, i), + fnDeleteExisting: (rendered)=>this.doDeleteExistingRender(rendered), + fnReorderExisting: (rendered,ent,i)=>this.doReorderExistingComponent(rendered, ent, i), + namespace: this._namespace, + isDiffMode: opts.isDiffMode != null ? opts.isDiffMode : this._isDiffMode, + }); + } +}; + +class _RenderableCollectionGenericRowsSyncAsyncUtils { + constructor({comp, prop, $wrpRows, namespace}) { + this._comp = comp; + this._prop = prop; + this._$wrpRows = $wrpRows; + this._namespace = namespace; + } + + _getCollectionItem(id) { + return this._comp._state[this._prop].find(it=>it.id === id); + } + + getNewRenderComp(entity, i) { + const comp = BaseComponent.fromObject(entity.entity, "*"); + comp._addHookAll("state", ()=>{ + this._getCollectionItem(entity.id).entity = comp.toObject("*"); + this._comp._triggerCollectionUpdate(this._prop); + } + ); + return comp; + } + + doUpdateExistingRender(renderedMeta, entity, i) { + renderedMeta.comp._proxyAssignSimple("state", entity.entity, true); + if (!renderedMeta.$wrpRow.parent().is(this._$wrpRows)) + renderedMeta.$wrpRow.appendTo(this._$wrpRows); + } + + static _doSwapJqueryElements($eles, ixA, ixB) { + if (ixA > ixB) + [ixA,ixB] = [ixB, ixA]; + + const eleA = $eles.get(ixA); + const eleB = $eles.get(ixB); + + const eleActive = document.activeElement; + + $(eleA).insertAfter(eleB); + $(eleB).insertBefore($eles.get(ixA + 1)); + + if (eleActive) + eleActive.focus(); + } + + doReorderExistingComponent(renderedMeta, entity, i) { + const ix = this._comp._state[this._prop].map(it=>it.id).indexOf(entity.id); + const $rows = this._$wrpRows.find(`> *`); + const curIx = $rows.index(renderedMeta.$wrpRow); + + const isMove = !this._$wrpRows.length || curIx !== ix; + if (!isMove) + return; + + this.constructor._doSwapJqueryElements($rows, curIx, ix); + } + + $getBtnDelete({entity, title="Delete"}) { + return $(``).click(()=>this.doDelete({ + entity + })); + } + + doDelete({entity}) { + this._comp._state[this._prop] = this._comp._state[this._prop].filter(it=>it?.id !== entity.id); + } + + doDeleteMultiple({entities}) { + const ids = new Set(entities.map(it=>it.id)); + this._comp._state[this._prop] = this._comp._state[this._prop].filter(it=>!ids.has(it?.id)); + } + + $getPadDrag({$wrpRow}) { + return DragReorderUiUtil$1.$getDragPadOpts(()=>$wrpRow, { + swapRowPositions: (ixA,ixB)=>{ + [this._comp._state[this._prop][ixA],this._comp._state[this._prop][ixB]] = [this._comp._state[this._prop][ixB], this._comp._state[this._prop][ixA]]; + this._comp._triggerCollectionUpdate(this._prop); + } + , + $getChildren: ()=>{ + const rendered = this._comp._getRenderedCollection({ + prop: this._prop, + namespace: this._namespace + }); + return this._comp._state[this._prop].map(it=>rendered[it.id].$wrpRow); + } + , + $parent: this._$wrpRows, + }, ); + } +} + +class RenderableCollectionGenericRows extends RenderableCollectionBase { + constructor(comp, prop, $wrpRows, opts) { + super(comp, prop, opts); + this._$wrpRows = $wrpRows; + + this._utils = new _RenderableCollectionGenericRowsSyncAsyncUtils({ + comp, + prop, + $wrpRows, + namespace: opts?.namespace, + }); + } + + doUpdateExistingRender(renderedMeta, entity, i) { + return this._utils.doUpdateExistingRender(renderedMeta, entity, i); + } + + doReorderExistingComponent(renderedMeta, entity, i) { + return this._utils.doReorderExistingComponent(renderedMeta, entity, i); + } + + getNewRender(entity, i) { + const comp = this._utils.getNewRenderComp(entity, i); + + const $wrpRow = this._$getWrpRow().appendTo(this._$wrpRows); + + const renderAdditional = this._populateRow({ + comp, + $wrpRow, + entity + }); + + return { + ...(renderAdditional || {}), + id: entity.id, + comp, + $wrpRow, + }; + } + + _$getWrpRow() { + return $(`
    `); + } + + _populateRow({comp, $wrpRow, entity}) { + throw new Error(`Unimplemented!`); + } +}; \ No newline at end of file diff --git a/charbuilder/js/plutonium/sidedata.js b/charbuilder/js/plutonium/sidedata.js new file mode 100644 index 0000000..a5bd506 --- /dev/null +++ b/charbuilder/js/plutonium/sidedata.js @@ -0,0 +1,530 @@ +class SideDataInterfaceBase { + static _SIDE_DATA = null; + + static async pPreloadSideData() { + this._SIDE_DATA = await this._pGetPreloadSideData(); + return this._SIDE_DATA; + } + + static async _pGetPreloadSideData() { + throw new Error("Unimplemented!"); + } + + static init() {} + + /** + * @param {{name: string, className: string, classSource: string, level: number, source: string, displayText: string}} ent + * @returns {any} + */ + static _getSideLoadOpts(ent) { + return null; + } + + static _SIDE_LOAD_OPTS = null; + + /** + * @param {{ent: {name: string, className: string, classSource: string, level: number, source: string, displayText: string}, propOpts:string}} + * @returns {any} + */ + static _getResolvedOpts({ent, propOpts="_SIDE_LOAD_OPTS"}={}) { + const out = this._getSideLoadOpts(ent) || this[propOpts]; + if (out.propsMatch) + return out; + return { + ...out, + propsMatch: ["source", "name"], + }; + } + + static async pGetRoot(ent, {propOpts="_SIDE_LOAD_OPTS", actorType=undefined}={}) { + const opts = this._getResolvedOpts({ + ent, + propOpts + }); + if (!opts) + return null; + + const {propBrew, fnLoadJson, propJson, propsMatch} = opts; + return this._pGetStarSideLoaded(ent, { + propBrew, + fnLoadJson, + propJson, + propsMatch, + propFromEntity: "foundryRoot", + propFromSideLoaded: "root", + actorType + }); + } + + static async pGetDataSideLoaded(ent, {propOpts="_SIDE_LOAD_OPTS", systemBase=undefined, actorType=undefined}={}) { + const opts = this._getResolvedOpts({ + ent, + propOpts + }); + if (!opts) + return null; + + const {propBrew, fnLoadJson, propJson, propsMatch} = opts; + return this._pGetStarSideLoaded(ent, { + propBrew, + fnLoadJson, + propJson, + propsMatch, + propFromEntity: "foundrySystem", + propFromSideLoaded: "system", + base: systemBase, + actorType + }); + } + + static async pGetFlagsSideLoaded(ent, {propOpts="_SIDE_LOAD_OPTS", actorType=undefined}={}) { + const opts = this._getResolvedOpts({ + ent, + propOpts + }); + if (!opts) + return null; + + const {propBrew, fnLoadJson, propJson, propsMatch} = opts; + return this._pGetStarSideLoaded(ent, { + propBrew, + fnLoadJson, + propJson, + propsMatch, + propFromEntity: "foundryFlags", + propFromSideLoaded: "flags", + actorType + }); + } + + static async _pGetAdvancementSideLoaded(ent, {propOpts="_SIDE_LOAD_OPTS", actorType=undefined}={}) { + const opts = this._getResolvedOpts({ + ent, + propOpts + }); + if (!opts) + return null; + + const {propBrew, fnLoadJson, propJson, propsMatch} = opts; + return this._pGetStarSideLoaded(ent, { + propBrew, + fnLoadJson, + propJson, + propsMatch, + propFromEntity: "foundryAdvancement", + propFromSideLoaded: "advancement", + actorType + }); + } + + static async pGetImgSideLoaded(ent, {propOpts="_SIDE_LOAD_OPTS", actorType=undefined}={}) { + const opts = this._getResolvedOpts({ + ent, + propOpts + }); + if (!opts) + return null; + + const {propBrew, fnLoadJson, propJson, propsMatch} = opts; + return this._pGetStarSideLoaded(ent, { + propBrew, + fnLoadJson, + propJson, + propsMatch, + propFromEntity: "foundryImg", + propFromSideLoaded: "img", + actorType + }); + } + + /** + * @param {{name: string, className: string, classSource: string, level: number, source: string, displayText: string}} ent + * @param {any} propOpts + * @param {any} actorType + * @returns {any} + */ + static async pGetIsIgnoredSideLoaded(ent, {propOpts="_SIDE_LOAD_OPTS", actorType=undefined}={}) { + const opts = this._getResolvedOpts({ ent, propOpts }); + if (!opts) + return null; + + const {propBrew, fnLoadJson, propJson, propsMatch} = opts; + return this._pGetStarSideLoaded(ent, { + propBrew, + fnLoadJson, + propJson, + propsMatch, + propFromEntity: "foundryIsIgnored", + propFromSideLoaded: "isIgnored", + actorType + }); + } + + static async pIsIgnoreSrdEffectsSideLoaded(ent, {propOpts="_SIDE_LOAD_OPTS", actorType=undefined}={}) { + const opts = this._getResolvedOpts({ + ent, + propOpts + }); + if (!opts) + return null; + return this._pGetStarSideLoaded(ent, { + ...opts, + propFromEntity: "foundryIgnoreSrdEffects", + propFromSideLoaded: "ignoreSrdEffects", + actorType + }); + } + + static async pGetEffectsRawSideLoaded(ent, {propOpts="_SIDE_LOAD_OPTS", actorType=undefined}={}) { + const opts = this._getResolvedOpts({ + ent, + propOpts + }); + if (!opts) + return null; + + const {propBrew, fnLoadJson, propJson, propsMatch} = opts; + const out = await this._pGetStarSideLoaded(ent, { + propBrew, + fnLoadJson, + propJson, + propsMatch, + propFromEntity: "foundryEffects", + propFromSideLoaded: "effects", + actorType + }); + + if (!out?.length) + return out; + + return out.filter(it=>{ + if (!it) + return false; + if (!it.requires) + return true; + + return Object.keys(it.requires).every(k=>UtilCompat.isModuleActive(k)); + } + ); + } + + static async pGetEffectsSideLoadedTuples({ent, actor=null, sheetItem=null, additionalData=null, img=null}, {propOpts="_SIDE_LOAD_OPTS"}={}) { + const outRaw = await this.pGetEffectsRawSideLoaded(ent, { + propOpts + }); + if (!outRaw?.length) + return []; + + return UtilActiveEffects.getExpandedEffects(outRaw, { + actor, + sheetItem, + parentName: UtilEntityGeneric.getName(ent), + img, + }, { + isTuples: true, + }, ); + } + + static async pGetSideLoaded(ent, {propOpts="_SIDE_LOAD_OPTS", actorType=undefined, isSilent=false}={}) { + const opts = this._getResolvedOpts({ + ent, + propOpts + }); + if (!opts) + return null; + return this._pGetSideLoadedMatch(ent, { + ...opts, + actorType, + isSilent + }); + } + + static async pGetSideLoadedType(ent, {propOpts="_SIDE_LOAD_OPTS", validTypes, actorType=undefined}={}) { + const opts = this._getResolvedOpts({ + ent, + propOpts + }); + if (!opts) + return null; + + const {propBrew, fnLoadJson, propJson, propsMatch} = opts; + + let out = await this._pGetStarSideLoaded(ent, { + propBrew, + fnLoadJson, + propJson, + propsMatch, + propFromEntity: "foundryType", + propFromSideLoaded: "type", + actorType + }); + if (!out) + return out; + out = out.toLowerCase().trim(); + if (validTypes && !validTypes.has(out)) + return null; + return out; + } + + static async _pGetStarSideLoaded(ent, {propBrew, fnLoadJson, propJson, propsMatch, propFromEntity, propFromSideLoaded, base=undefined, actorType=undefined, }, ) { + const found = await this._pGetSideLoadedMatch(ent, { + propBrew, + fnLoadJson, + propJson, + propsMatch, + propBase: propFromSideLoaded, + base, + actorType + }); + return this._pGetStarSideLoaded_found(ent, { + propFromEntity, + propFromSideLoaded, + found + }); + } + + static async _pGetStarSideLoaded_found(ent, {propFromEntity, propFromSideLoaded, found}) { + const fromEntity = ent[propFromEntity]; + + if ((!found || !found[propFromSideLoaded]) && !fromEntity) + return null; + + const out = MiscUtil.copy(found?.[propFromSideLoaded] ? found[propFromSideLoaded] : fromEntity); + if (found?.[propFromSideLoaded] && fromEntity) { + if (out instanceof Array) + out.push(...MiscUtil.copy(fromEntity)); + else + Object.assign(out, MiscUtil.copy(fromEntity)); + } + + return out; + } + + static async _pGetSideLoadedMatch(ent, {propBrew, fnLoadJson, propJson, propsMatch, propBase, base=undefined, actorType=undefined, isSilent=false}={}) { + const founds = []; + + //TEMPFIX + /* if (UtilCompat.isPlutoniumAddonAutomationActive()) { + const valsLookup = propsMatch.map(prop=>ent[prop]).filter(Boolean); + const found = await UtilCompat.getApi(UtilCompat.MODULE_PLUTONIUM_ADDON_AUTOMATION).pGetExpandedAddonData({ + propJson, + path: valsLookup, + fnMatch: this._pGetAdditional_fnMatch.bind(this, propsMatch, ent), + ent, + propBase, + base, + actorType, + isSilent, + }); + if (found) + founds.push(found); + } */ + + if (propBrew) { + const prerelease = await PrereleaseUtil.pGetBrewProcessed(); + const foundPrerelease = (MiscUtil.get(prerelease, propBrew) || []).find(it=>this._pGetAdditional_fnMatch(propsMatch, ent, it)); + if (foundPrerelease) + founds.push(foundPrerelease); + + const brew = await BrewUtil2.pGetBrewProcessed(); + const foundBrew = (MiscUtil.get(brew, propBrew) || []).find(it=>this._pGetAdditional_fnMatch(propsMatch, ent, it)); + if (foundBrew) + founds.push(foundBrew); + } + + if (fnLoadJson && propJson) { + const sideJson = await fnLoadJson(); + const found = (sideJson[propJson] || []).find(it=>this._pGetAdditional_fnMatch(propsMatch, ent, it)); + if (found) + founds.push(found); + } + + if (!founds.length) + return null; + if (founds.length === 1) + return founds[0]; + + const out = MiscUtil.copy(founds[0]); + this._pGetSideLoaded_match_mutMigrateData(out); + delete out._merge; + + founds.slice(1).forEach((found,i)=>{ + this._pGetSideLoaded_match_mutMigrateData(found); + + Object.entries(found).filter(([prop])=>prop !== "_merge").forEach(([prop,v])=>{ + if (out[prop] === undefined) + return out[prop] = v; + + const prevFounds = founds.slice(0, i + 1); + if (!prevFounds.every(foundPrev=>foundPrev[prop] === undefined || foundPrev._merge?.[prop])) + return; + + if (out[prop] == null) + return out[prop] = v; + if (typeof out[prop] !== "object") + return out[prop] = v; + + if (out[prop]instanceof Array) { + if (!(v instanceof Array)) + throw new Error(`Could not _merge array and non-array`); + return out[prop] = [...out[prop], ...v]; + } + + out[prop] = foundry.utils.mergeObject(v, out[prop]); + } + ); + } + ); + + return out; + } + + static _pGetSideLoaded_match_mutMigrateData(found) { + if (!found) + return; + return found; + } + + static _pGetAdditional_fnMatch(propsMatch, entity, additionalDataEntity) { + return propsMatch.every(prop=>{ + if (typeof entity[prop] === "number" || typeof additionalDataEntity[prop] === "number") + return Number(entity[prop]) === Number(additionalDataEntity[prop]); + return `${(entity[prop] || "")}`.toLowerCase() === `${(additionalDataEntity[prop] || "").toLowerCase()}`; + } + ); + } +} + +class SideDataInterfaceClass extends SideDataInterfaceBase { + static _SIDE_LOAD_OPTS = { + propBrew: "foundryClass", + fnLoadJson: async()=>this.pPreloadSideData(), + propJson: "class", + }; + + static _SIDE_LOAD_OPTS_SUBCLASS = { + propBrew: "foundrySubclass", + fnLoadJson: async()=>this.pPreloadSideData(), + propJson: "subclass", + propsMatch: ["classSource", "className", "source", "name"], + }; + + static async pPreloadSideData() { + return Vetools.pGetClassSubclassSideData(); + } + + static init() { + PageFilterClassesFoundry.setImplSideData("class", this); + PageFilterClassesFoundry.setImplSideData("subclass", this); + } +} +class SideDataInterfaceClassSubclassFeature extends SideDataInterfaceBase { + /** + * @param {{name: string, className: string, classSource: string, level: number, source: string, displayText: string}} feature + * @returns {any} + */ + static _getSideLoadOpts(feature) { + return { + propBrew: UtilEntityClassSubclassFeature.getBrewProp(feature), + fnLoadJson: async()=>this.pPreloadSideData(), + propJson: UtilEntityClassSubclassFeature.getEntityType(feature), + propsMatch: ["classSource", "className", "subclassSource", "subclassShortName", "level", "source", "name"], + }; + } + + static async _pGetPreloadSideData() { + return Vetools.pGetClassSubclassSideData(); + } + + static init() { + PageFilterClassesFoundry.setImplSideData("classFeature", this); + PageFilterClassesFoundry.setImplSideData("subclassFeature", this); + } +} +class SideDataInterfaceFeat extends SideDataInterfaceBase { + static _SIDE_LOAD_OPTS = { + propBrew: "foundryFeat", + fnLoadJson: async()=>this.pPreloadSideData(), + propJson: "feat", + }; + + static async _pGetPreloadSideData() { + return Vetools.pGetFeatSideData(); + } + + static init() { + PageFilterClassesFoundry.setImplSideData("feat", this); + } +} +class SideDataInterfaceOptionalfeature extends SideDataInterfaceBase { + static _SIDE_LOAD_OPTS = { + propBrew: "foundryOptionalfeature", + fnLoadJson: async()=>this.pPreloadSideData(), + propJson: "optionalfeature", + }; + + static async _pGetPreloadSideData() { + return Vetools.pGetOptionalFeatureSideData(); + } + + static init() { + PageFilterClassesFoundry.setImplSideData("optionalfeature", this); + } +} +class SideDataInterfaceReward extends SideDataInterfaceBase { + static _SIDE_LOAD_OPTS = { + propBrew: "foundryReward", + fnLoadJson: async()=>this.pPreloadSideData(), + propJson: "reward", + }; + + static async _pGetPreloadSideData() { + return Vetools.pGetRewardSideData(); + } + + static init() { + PageFilterClassesFoundry.setImplSideData("reward", this); + } +} +class SideDataInterfaceCharCreationOption extends SideDataInterfaceBase { + static _SIDE_LOAD_OPTS = { + propBrew: "foundryCharoption", + fnLoadJson: async()=>this.pPreloadSideData(), + propJson: "charoption", + }; + + static async _pGetPreloadSideData() { + return Vetools.pGetCharCreationOptionSideData(); + } + + static init() { + PageFilterClassesFoundry.setImplSideData("charoption", this); + } +} +class SideDataInterfaceVehicleUpgrade extends SideDataInterfaceBase { + static _SIDE_LOAD_OPTS = { + propBrew: "foundryVehicleUpgrade", + fnLoadJson: async()=>this.pPreloadSideData(), + propJson: "vehicleUpgrade", + }; + + static async _pGetPreloadSideData() { + return Vetools.pGetVehicleUpgradeSideData(); + } + + static init() { + PageFilterClassesFoundry.setImplSideData("vehicleUpgrade", this); + } +} + +class SideDataInterfaces { + static init() { + SideDataInterfaceClass.init(); + SideDataInterfaceClassSubclassFeature.init(); + SideDataInterfaceOptionalfeature.init(); + SideDataInterfaceFeat.init(); + SideDataInterfaceReward.init(); + SideDataInterfaceCharCreationOption.init(); + SideDataInterfaceVehicleUpgrade.init(); + } +} \ No newline at end of file diff --git a/charbuilder/js/plutonium/sourcemodal.js b/charbuilder/js/plutonium/sourcemodal.js new file mode 100644 index 0000000..6ddf82f --- /dev/null +++ b/charbuilder/js/plutonium/sourcemodal.js @@ -0,0 +1,1376 @@ +class PageFilterSourcesRaw extends AppSourceSelectorAppFilter { + _getToDisplayParams(values, cls) { + return cls.name;//this.isAnySubclassDisplayed(values, cls) ? cls._fSourceSubclass : (cls._fSources ?? cls.source), cls._fMisc, null, ]; + } +} +class AppSourceSelectorMulti extends ModalFilter { + /** + * @param {{title:string, sourcesToDisplay:any[], savedSelectionKey:string, filterNamespace:string, isRadio:boolean}} opts + * @returns {any} + */ + constructor(opts) { + super({ + modalTitle: opts.title || "Select Sources", + }); + + this._sourcesToDisplay = opts.sourcesToDisplay; + this._savedSelectionKey = opts.savedSelectionKey; + this._preEnabledSources = opts.preEnabledSources; + this._filterNamespace = opts.filterNamespace; + this._props = opts.props; + this._isRadio = !!opts.isRadio; + + this._list = null; + this._pageFilter = null; + + this._$stgNone = null; + this._$stgUpload = null; + this._$stgUrl = null; + this._$stgSpecial = null; + + //Create a new component, and set the __state with these entries + this._comp = BaseComponent.fromObject({ + uploadedFileMetas: [], + isShowCustomUrlForm: false, + urlMetas: [], + specialMetas: [], + }); + + this._isDedupable = !!opts.isDedupable; + this._page = opts.page; + this._fnGetDedupedData = opts.fnGetDedupedData; + this._fnGetBlocklistFilteredData = opts.fnGetBlocklistFilteredData; + + this._resolve = null; + this._reject = null; + this._pUserInput = null; + } + + get pageFilter() { return this._pageFilter; } + get uploadedFileMetas() { return this._comp._state.uploadedFileMetas.map(it=>it.data); } + pGetSelectedSources() { + if (!this._list) {return this._pGetInitialSources();} + return this._list.items.filter(it=>it.data.cbSel.checked).map(li=>this._sourcesToDisplay[li.ix]); + } + + async pGetElements($wrpList, cbSourceChange) { return this._pGetElements_pGetListElements($wrpList, cbSourceChange); } + + _$getStageNone() { + return $(`
    +
    Select a source to view import options
    +
    +
    `); + } + + _$getStageUpload() { + const $btnAddUpload = $(``).click(()=>{ + const nxt = { + id: CryptUtil.uid(), + data: { + name: null, + contents: null, + }, + }; + this._comp._state.uploadedFileMetas = [...this._comp._state.uploadedFileMetas, nxt]; + + const renderedCollection = this._comp._getRenderedCollection({ + prop: "uploadedFileMetas" + }); + const renderedMeta = renderedCollection[nxt.id]; + renderedMeta.$btnUpload.click(); + } + ); + + const $wrpUploadRows = $(`
    `); + + const $stgUpload = $$`
    +
    +
    File Sources:
    + ${$btnAddUpload} +
    + ${$wrpUploadRows} +
    +
    `; + + const hkUploadFileMetas = ()=>{ + this._comp._renderCollection({ + prop: "uploadedFileMetas", + fnUpdateExisting: (renderedMeta,uploadFileMeta)=>{ + renderedMeta.comp._proxyAssignSimple("state", uploadFileMeta.data, true); + if (!renderedMeta.$wrpRow.parent().is($wrpUploadRows)) + renderedMeta.$wrpRow.appendTo($wrpUploadRows); + } + , + fnGetNew: uploadFileMeta=>{ + const comp = BaseComponent.fromObject(uploadFileMeta.data, "*"); + comp._addHookAll("state", ()=>{ + uploadFileMeta.data = comp.toObject("*"); + this._comp._triggerCollectionUpdate("uploadedFileMetas"); + } + ); + + const $dispName = $(`
    `).click(()=>$btnUpload.click()); + const hkName = ()=>$dispName.text(comp._state.name || "Select file...").title(comp._state.name || ""); + comp._addHookBase("name", hkName); + hkName(); + + const $btnUpload = $(``).click(()=>{ + const $ipt = $(``).change(evt=>{ + const input = evt.target; + const files = $ipt[0].files; + + const reader = new FileReader(); + reader.onload = async()=>{ + const file = (files[0] || {}); + + try { + const json = JSON.parse(reader.result); + comp._proxyAssignSimple("state", { + name: file.name, + contents: json + }); + } catch (e) { + ui.notifications.error(`Failed to read file! ${VeCt.STR_SEE_CONSOLE}`); + throw e; + } + } + ; + reader.readAsText(input.files[0]); + } + ).click(); + } + ); + + const $btnDelete = $(``).click(()=>this._comp._state.uploadedFileMetas = this._comp._state.uploadedFileMetas.filter(it=>it !== uploadFileMeta)); + + const $wrpRow = $$`
    + ${$btnUpload}${$dispName}${$btnDelete} +
    `.appendTo($wrpUploadRows); + + return { + comp, + $wrpRow, + $btnUpload, + }; + } + , + }); + } + ; + hkUploadFileMetas(); + this._comp._addHookBase("uploadedFileMetas", hkUploadFileMetas); + + return $stgUpload; + } + + _$getStageUrl() { + const $btnAddUrl = $(``).click(()=>{ + const nxt = { + id: CryptUtil.uid(), + data: { + displayName: "Custom Url", + isCustom: true, + url: null, + }, + }; + + this._comp._state.urlMetas = [...this._comp._state.urlMetas, nxt, ]; + + const renderedCollection = this._comp._getRenderedCollection({ + prop: "urlMetas" + }); + const renderedMeta = renderedCollection[nxt.id]; + renderedMeta.$iptUrl.focus(); + } + ); + const hkIsCustomUrls = ()=>$btnAddUrl.toggleVe(this._comp._state.isShowCustomUrlForm); + this._comp._addHookBase("isShowCustomUrlForm", hkIsCustomUrls); + hkIsCustomUrls(); + + const $wrpUrlRows = $(`
    `); + + const $stgUrl = $$`
    +
    +
    URL Sources:
    + ${$btnAddUrl} +
    + ${$wrpUrlRows} +
    +
    `; + + const hkUrlMetas = ()=>{ + this._comp._renderCollection({ + prop: "urlMetas", + fnUpdateExisting: (renderedMeta,urlMeta)=>{ + renderedMeta.comp._proxyAssignSimple("state", urlMeta.data, true); + if (!renderedMeta.$wrpRow.parent().is($wrpUrlRows)) + renderedMeta.$wrpRow.appendTo($wrpUrlRows); + } + , + fnGetNew: urlMeta=>{ + const comp = BaseComponent.fromObject(urlMeta.data, "*"); + comp._addHookAll("state", ()=>{ + urlMeta.data = comp.toObject("*"); + this._comp._triggerCollectionUpdate("urlMetas"); + } + ); + + const $iptUrl = ComponentUiUtil.$getIptStr(comp, "url"); + if (Config.get("ui", "isStreamerMode")) + $iptUrl.addClass("text-sneaky"); + const hkDisplayNameUrl = ()=>{ + if (comp._state.isCustom) { + $iptUrl.title("Enter JSON URL").placeholder("Enter JSON URL").val(comp._state.url); + } else { + $iptUrl.title("JSON URL").val(comp._state.displayName).disable(); + } + } + ; + this._comp._addHookBase("url", hkDisplayNameUrl); + this._comp._addHookBase("displayName", hkDisplayNameUrl); + hkDisplayNameUrl(); + + const $btnDelete = !comp._state.isCustom ? null : $(``).click(()=>this._comp._state.urlMetas = this._comp._state.urlMetas.filter(it=>it !== urlMeta)); + + const $wrpRow = $$`
    + ${$iptUrl}${$btnDelete} +
    `.appendTo($wrpUrlRows); + + return { + comp, + $wrpRow, + $iptUrl, + }; + } + , + }); + } + ; + hkUrlMetas(); + this._comp._addHookBase("urlMetas", hkUrlMetas); + + return $stgUrl; + } + + _$getStageSpecial() { + const $wrpSpecialRows = $(`
    `); + + const $stgSpecial = $$`
    +
    +
    Pre-Compiled Sources:
    +
    + ${$wrpSpecialRows} +
    +
    `; + + const hkSpecialMetas = ()=>{ + this._comp._renderCollection({ + prop: "specialMetas", + fnUpdateExisting: (renderedMeta,specialMeta)=>{ + renderedMeta.comp._proxyAssignSimple("state", specialMeta.data, true); + if (!renderedMeta.$wrpRow.parent().is($wrpSpecialRows)) + renderedMeta.$wrpRow.appendTo($wrpSpecialRows); + } + , + fnGetNew: specialMetas=>{ + const comp = BaseComponent.fromObject(specialMetas.data, "*"); + comp._addHookAll("state", ()=>{ + specialMetas.data = comp.toObject("*"); + this._comp._triggerCollectionUpdate("urlMetas"); + } + ); + + const $dispName = $(`
    `); + const hkDisplayName = ()=>$dispName.text(comp._state.displayName); + this._comp._addHookBase("displayName", hkDisplayName); + hkDisplayName(); + + const $wrpRow = $$`
    ${$dispName}
    `.appendTo($wrpSpecialRows); + + return { + comp, + $wrpRow, + }; + } + , + }); + } + ; + hkSpecialMetas(); + this._comp._addHookBase("specialMetas", hkSpecialMetas); + + return $stgSpecial; + } + + async _pGetInitialSources() { + + const initialSourceIds = await this._pGetInitialSourceIds(); + const initialSources = this._sourcesToDisplay.filter(it=>initialSourceIds.has(it.identifier)); + if (!initialSources.length) + initialSources.push(...this._sourcesToDisplay.filter(it=>it.isDefault)); + return initialSources; + } + + async _pGetInitialSourceIds() { + if (this.isForceSelectAllSources()) { + return new Set(this._sourcesToDisplay.map(it=>it.identifier)); + } + + //return new Set((await StorageUtil.pGet(this._savedSelectionKey)) || []); + + let s = new Set(); + for(let srcId of this._preEnabledSources){ + s.add(srcId.identifier); + } + return s; + } + + isForceSelectAllSources() { + if (this._isRadio){return false;} + if(!SETTINGS.USE_FVTT){return false;} + if (game.user.isGM){return Config.get("dataSources", "isGmForceSelectAllowedSources");} + return Config.get("dataSources", "isPlayerForceSelectAllowedSources"); + } + + static $getFilterListElements({isRadio=false, isForceSelectAll=false}={}) { + const $btnOpenFilter = $(``); + const $btnToggleSummaryHidden = $(``); + const $iptSearch = $(``); + const $btnReset = $(``).click(()=>$iptSearch.val("")); + const $wrpMiniPills = $(`
    `); + + const isHideCbAll = isRadio || isForceSelectAll; + + const $cbAll = isHideCbAll ? null : $(``); + const $lblCbAll = $$``; + + const $wrpBtnsSort = $$`
    + ${$lblCbAll} + +
    `; + const $list = $(`
    `); + + const $wrpFilterControls = $$`
    + ${$btnOpenFilter} + ${$btnToggleSummaryHidden} + ${$iptSearch} + ${$btnReset} +
    `; + + return { + $cbAll, + $wrpFilterControls, + $wrpMiniPills, + $wrpBtnsSort, + $list, + $btnOpenFilter, + $iptSearch, + $btnReset, + $btnToggleSummaryHidden, + }; + } + + static getListItem({pageFilter, isRadio, list, listSelectClickHandler, src, srcI, fnOnClick, initialSources, isSelected, isForceSelectAll, }, ) { + isSelected = isSelected === undefined && initialSources ? initialSources.includes(src) : !!isSelected; + + pageFilter.mutateAndAddToFilters(src); + + const eleCb = e_({ + outer: isRadio ? `` : ``, + }); + if (isSelected) + eleCb.checked = true; + if (isForceSelectAll) + eleCb.setAttribute("disabled", true); + + const eleWrpCb = e_({ + tag: "span", + clazz: "col-0-5 ve-flex-vh-center", + children: [eleCb, ], + }); + + const eleLi = e_({ + tag: "label", + clazz: `row imp-wiz__row veapp__list-row-hoverable ve-flex-v-center ${isSelected ? "list-multi-selected" : ""}`, + children: [eleWrpCb, e_({ + tag: "span", + clazz: "col-11-5", + html: `${this._getFilterTypesIcon(src.filterTypes)}${src.name}`, + }), ], + }); + + const listItem = new ListItem(srcI,eleLi,src.name,{ + filterTypes: src.filterTypes, + abbreviations: src.abbreviations || [], + },{ + identifierWorld: src.identifierWorld, + cbSel: eleCb, + },); + + if (!isForceSelectAll) { + eleLi.addEventListener("click", evt=>{ + if (isRadio) + listSelectClickHandler.handleSelectClickRadio(listItem, evt); + else + listSelectClickHandler.handleSelectClick(listItem, evt); + if (fnOnClick) + fnOnClick({ + list, + listItem + }); + } + ); + } + + return listItem; + } + + async _pGetElements_pGetListElements($wrpList, cbSourceChange=null) { + this._$stgNone = this._$getStageNone(); + this._$stgUpload = this._$getStageUpload(); + this._$stgUrl = this._$getStageUrl(); + this._$stgSpecial = this._$getStageSpecial(); + + const initialSources = await this._pGetInitialSources(); + + const setSources = ({isSkipSave}={})=>{ + const selSources = this._list.items.filter(it=>it.data.cbSel.checked).map(li=>this._sourcesToDisplay[li.ix]); + + const selSourceIdentifiers = selSources.map(source=>source.identifier); + if (!isSkipSave) + StorageUtil.pSet(this._savedSelectionKey, selSourceIdentifiers); + + const isShowStageUpload = selSources.some(it=>it.isFile); + const isShowStageUrl = selSources.some(it=>it.url != null); + const isShowStageSpecial = selSources.some(it=>it.url == null && !it.isFile); + + this._$stgNone.toggleVe(!isShowStageUpload && !isShowStageUrl && !isShowStageSpecial); + this._$stgUpload.toggleVe(isShowStageUpload); + this._$stgUrl.toggleVe(isShowStageUrl); + this._$stgSpecial.toggleVe(isShowStageSpecial); + + if (isShowStageUrl) { + this._comp._state.isShowCustomUrlForm = selSources.some(it=>it.url === ""); + + const customUrlMetas = this._comp._state.isShowCustomUrlForm ? this._comp._state.urlMetas.filter(it=>it.data.isCustom) : []; + + this._comp._state.urlMetas = [...selSources.filter(it=>it.url).map(it=>({ + id: it.url, + data: { + isCustom: false, + displayName: it.url + } + })), ...customUrlMetas, ]; + } else { + this._comp._state.urlMetas = []; + } + + if (isShowStageSpecial) { + this._comp._state.specialMetas = [...selSources.filter(it=>it.url == null && !it.isFile).map(it=>({ + id: it.cacheKey, + data: { + displayName: it.name + } + })), ]; + } else { + this._comp._state.specialMetas = []; + } + + if (cbSourceChange) + cbSourceChange(selSources); + } + ; + + if (this._pageFilter){this._pageFilter.teardown();} + this._pageFilter = new AppSourceSelectorAppFilter(); + + const isForceSelectAll = this.isForceSelectAllSources(); + + const {$cbAll, $wrpFilterControls, $wrpMiniPills, $wrpBtnsSort, $list, $btnOpenFilter, $iptSearch, $btnReset, $btnToggleSummaryHidden, } = this.constructor.$getFilterListElements({ + isRadio: this._isRadio, + isForceSelectAll + }); + + $$($wrpList)` + ${$wrpFilterControls} + ${$wrpMiniPills} + ${$wrpBtnsSort} + ${$list} + `; + + this._list = new List({ + $iptSearch, + $wrpList: $list, + fnSort: UtilDataSource.sortListItems.bind(UtilDataSource), + }); + const listSelectClickHandler = new ListSelectClickHandler({ + list: this._list + }); + SortUtil.initBtnSortHandlers($wrpBtnsSort, this._list); + if ($cbAll) { + listSelectClickHandler.bindSelectAllCheckbox($cbAll); + $cbAll.change(()=>setSources()); + } + + await this._pageFilter.pInitFilterBox({ + $iptSearch, + $btnReset, + $btnOpen: $btnOpenFilter, + $btnToggleSummaryHidden, + $wrpMiniPills, + namespace: this._filterNamespace, + }); + + this._sourcesToDisplay.forEach((src,srcI)=>{ + const listItem = this.constructor.getListItem({ + pageFilter: this._pageFilter, + list: this._list, + listSelectClickHandler, + isRadio: this._isRadio, + src, + srcI, + fnOnClick: setSources, + initialSources, + isForceSelectAll, + }); + this._list.addItem(listItem); + } + ); + + setSources({ + isSkipSave: true + }); + + this._list.init(); + + this._pageFilter.trimState(); + this._pageFilter.filterBox.render(); + + this._pageFilter.filterBox.on(FilterBox.EVNT_VALCHANGE, this._handleFilterChange.bind(this), ); + + this._handleFilterChange(); + + return { + $stgNone: this._$stgNone, + $stgUpload: this._$stgUpload, + $stgUrl: this._$stgUrl, + $stgSpecial: this._$stgSpecial, + + $iptSearch, + }; + } + + _handleFilterChange() { + const f = this._pageFilter.filterBox.getValues(); + this._list.filter(li=>this._pageFilter.toDisplay(f, this._sourcesToDisplay[li.ix])); + } + + static _getFilterTypesIcon(filterTypes) { + if (filterTypes.includes(UtilDataSource.SOURCE_TYP_OFFICIAL_ALL) || filterTypes.includes(UtilDataSource.SOURCE_TYP_OFFICIAL_SINGLE)) { + return ``; + } + + if (filterTypes.includes(UtilDataSource.SOURCE_TYP_EXTRAS)) { + return ``; + } + + if (filterTypes.includes(UtilDataSource.SOURCE_TYP_PRERELEASE_LOCAL)) { + return ``; + } + + if (filterTypes.includes(UtilDataSource.SOURCE_TYP_PRERELEASE)) { + return ``; + } + + if (filterTypes.includes(UtilDataSource.SOURCE_TYP_BREW_LOCAL)) { + return ``; + } + + if (filterTypes.includes(UtilDataSource.SOURCE_TYP_BREW)) { + return ``; + } + + if (filterTypes.includes(UtilDataSource.SOURCE_TYP_CUSTOM)) { + return ``; + } + + if (filterTypes.includes(UtilDataSource.SOURCE_TYP_UNKNOWN)) { + return ``; + } + + return ""; + } + + activateListeners($html) { + (async()=>{ + const $ovrLoading = $(`
    Loading...
    `).appendTo($html.empty()); + + const $wrpList = $(`
    `); + + const {$iptSearch} = await this.pGetElements($wrpList); + + $iptSearch.keydown(evt=>{ + if (evt.key === "Enter") + $btnAccept.click(); + } + ); + + const $btnAccept = $(``).click(()=>this._pAcceptAndResolveSelection({ + $ovrLoading + })); + + $$($html)` + ${$wrpList} +
    +
    +

    Source

    + ${this._$stgNone} + ${this._$stgUpload} + ${this._$stgUrl} + ${this._$stgSpecial} +
    + ${$btnAccept} + ${$ovrLoading.hideVe()}`; + + $iptSearch.focus(); + } + )(); + } + + async _pAcceptAndResolveSelection({$ovrLoading, fnClose, fnResolve, isSilent=false, isBackground=false, isAutoSelectAll=false}={}) { + + try { + if ($ovrLoading){$ovrLoading.showVe();} + + const sources = await this.pGetSelectedSources(); + if (!isSilent && !sources.length) { + if ($ovrLoading){$ovrLoading.hideVe();} + //return ui.notifications.error(`No sources selected!`); + console.error(`No sources selected!`); return; + } + + if (!isSilent && sources.length > 10) { + const isContinue = await InputUiUtil.pGetUserBoolean({ + title: `You have many sources selected, which may negatively impact performance. Do you want to continue?`, + storageKey: "AppSourceSelectorMulti__massSelectionWarning", + textYesRemember: "Continue and Remember", + textYes: "Continue", + textNo: "Cancel", + }); + + if (!isContinue) { + if ($ovrLoading){$ovrLoading.hideVe();} + return; + } + } + + const isSure = await InputUiUtil.pGetUserBoolean({ + title: `Are you sure you wish to change sources?`, + htmlDescription: `This will reset your character!`, + }); + //Perhaps show some info here that characters using content from non-enabled sources will break badly + if (!isSure){ + if ($ovrLoading){$ovrLoading.hideVe();} + return; + } + + /* const out = await this._pGetOutputEntities(sources, { isBackground, isAutoSelectAll }); + if (!out){return;} + if (!isSilent && !Object.values(out).some(it=>it?.length)) { + if ($ovrLoading){$ovrLoading.hideVe();} + return ui.notifications.warn(`No sources to be loaded! Please finish entering source details first.`); + } */ + + + //We don't want to return entities, we just want to return metadata about sources + const out = {sourceIds: sources, uploadedFileMetas: this.uploadedFileMetas, customUrls: this.getCustomUrls()}; + + fnResolve(out); //Calls for this window to return a solution to whoever has been waiting + this.close(fnClose); + } catch (e) { + if ($ovrLoading){$ovrLoading.hideVe();} + //ui.notifications.error(`Failed to load sources! ${VeCt.STR_SEE_CONSOLE}`); + console.error("Failed to load sources!"); + throw e; + } + } + + async _pGetOutputEntities(sources, {isBackground=false, isAutoSelectAll=false}={}) { + const allContentMeta = await UtilDataSource.pGetAllContent({ + sources, + uploadedFileMetas: this.uploadedFileMetas, + customUrls: this.getCustomUrls(), + isBackground, + + page: this._page, + + isDedupable: this._isDedupable, + fnGetDedupedData: this._fnGetDedupedData, + + fnGetBlocklistFilteredData: this._fnGetBlocklistFilteredData, + + isAutoSelectAll, + }); + if (!allContentMeta) + return null; + + const out = allContentMeta.dedupedAllContentMerged; + + Renderer.spell.populatePrereleaseLookup(await PrereleaseUtil.pGetBrewProcessed(), { + isForce: true + }); + Renderer.spell.populateBrewLookup(await BrewUtil2.pGetBrewProcessed(), { + isForce: true + }); + + (out.spell || []).forEach(sp=>{ + Renderer.spell.uninitBrewSources(sp); + Renderer.spell.initBrewSources(sp); + } + ); + + return out; + } + + getCustomUrls() { + return this._comp._state.urlMetas.filter(it=>it.data.isCustom && it.data.url && it.data.url.trim()).map(it=>it.data.url.trim()); + } + + handlePreClose() { + this._comp._detachCollection("urlMetas"); + this._comp._detachCollection("uploadedFileMetas"); + this._comp._detachCollection("specialMetas"); + } + + handlePostClose() { + if (this._pageFilter) + this._pageFilter.teardown(); + } + + async close(fnClose) { + this.handlePreClose(); + fnClose(); + this.handlePostClose(); + } + + async _render(){ + + return new Promise(async resolve => { + const {$modalInner, doClose} = await this._pGetShowModal(resolve); + const $ovrLoading = $(`
    Loading...
    `).appendTo($modalInner.empty()); + + const $wrpList = $(`
    `); + + const {$iptSearch} = await this.pGetElements($wrpList); + + $iptSearch.keydown(evt=>{ + if (evt.key === "Enter"){$btnAccept.click();}} + ); + + const $btnAccept = $(``).click(()=>this._pAcceptAndResolveSelection({ + $ovrLoading, fnClose: doClose, fnResolve: resolve + })); + + $$($modalInner)` + ${$wrpList} +
    +
    +

    Source

    + ${this._$stgNone} + ${this._$stgUpload} + ${this._$stgUrl} + ${this._$stgSpecial} +
    + ${$btnAccept} + ${$ovrLoading.hideVe()}`; + + $iptSearch.focus(); + }); + } + + pWaitForUserInput({isRenderApp=true}={}) { + const isSelectAll = this.isForceSelectAllSources(); + + if (!isSelectAll && isRenderApp){ + return this._render(); //Return this promise instead + } + + this._pUserInput = new Promise((resolve, reject)=>{ + this._resolve = resolve; //Call this later when this modal has resolved the choices + this._reject = reject; //Call this later if this modal has rejected the choices + }); + + //TODO: make this call resolve in a better way + if (isSelectAll) { + this._pAcceptAndResolveSelection({ + isSilent: true, + isBackground: true, + isAutoSelectAll: isSelectAll + }).then(null); + } + + + return this._pUserInput; + } + + async pLoadInitialSelection() { + const initialSources = await this._pGetInitialSources(); + return this._pGetOutputEntities(initialSources); + } +} + +class ModalFilterSources extends ModalFilter { + + constructor(opts) { + opts = opts || {}; + + super({ + ...opts, + modalTitle: "Sources", + pageFilter: new PageFilterSourcesRaw(), + fnSort: ModalFilterClasses.fnSort, + }); + + this._filterNamespace = opts.filterNamespace; + this._savedSelectionKey = opts.savedSelectionKey; + this._sourcesToDisplay= opts.sourcesToDisplay; + this._preEnabledSources = opts.preEnabledSources; + + this._pLoadingAllData = null; + + this._ixPrevSelectedClass = null; + this._isClassDisabled = false; + this._isSubclassDisabled = false; + } + + /** + * The PageFilter + * @returns {PageFilterClassesRaw} + */ + get pageFilter() {return this._pageFilter;} + + static fnSort(a, b, opts) { + const out = SortUtil.listSort(a, b, opts); + + if (opts.sortDir === "desc" && a.data.ixClass === b.data.ixClass && (a.data.ixSubclass != null || b.data.ixSubclass != null)) { + return a.data.ixSubclass != null ? -1 : 1; + } + + return out; + } + + + /** + * @param {{className: String, classSource:String, subclassName:String, subclassSource:string}} classSubclassMeta + * @returns {{class:Object, subclass:Object}} + */ + async pGetSelection(classSubclassMeta) { + const {className, classSource, subclassName, subclassSource} = classSubclassMeta; + + const allData = this._allData || await this._pLoadAllData(); + + const cls = allData.find(it=>it.name === className && it.source === classSource); + if (!cls) + throw new Error(`Could not find class with name "${className}" and source "${classSource}"`); + + const out = {class: cls, }; + + if (subclassName && subclassSource) { + const sc = cls.subclasses.find(it=>it.name === subclassName && it.source === subclassSource); + if (!sc) + throw new Error(`Could not find subclass with name "${subclassName}" and source "${subclassSource}" on class with name "${className}" and source "${classSource}"`); + + out.subclass = sc; + } + + return out; + } + + async pGetUserSelection({filterExpression=null, selectedClass=null, selectedSubclass=null, isClassDisabled=false, isSubclassDisabled=false}={}) { + //Return a promise that only resolves once the modal is exited + return new Promise(async resolve=>{ + const {$modalInner, doClose} = await this._pGetShowModal(resolve); + + //Build the list + await this.pPreloadHidden($modalInner); + + //Apply the filter expression + this._doApplyFilterExpression(filterExpression); + + //If the confirm button is clicked + this._filterCache.$btnConfirm.off("click").click(async()=>{ + //Get all the checked list items + const checked = this._filterCache.list.items.filter(it=>it.data.cbSel.checked); + let out = checked.map(li=>this._sourcesToDisplay[li.ix]); + //Call resolve + resolve(MiscUtil.copy(out)); + + doClose(true); + + ModalFilterClasses._doListDeselectAll(this._filterCache.list); + }); + + this._ixPrevSelectedClass = selectedClass != null ? this._filterCache.allData.findIndex(it=>it.name === selectedClass.name && it.source === selectedClass.source) : null; + this._isClassDisabled = isClassDisabled; + this._isSubclassDisabled = isSubclassDisabled; + this._filterCache.list.items.forEach(li=>{ + let isClassDisabled = false; + //li.data.cbSel.classList.toggle("disabled", this._isClassDisabled); + li.data.cbSel.classList.toggle("disabled", isClassDisabled); + }); + + if (selectedClass != null) { + const ixSubclass = ~this._ixPrevSelectedClass && selectedSubclass != null ? this._filterCache.allData[this._ixPrevSelectedClass].subclasses.findIndex(it=>it.name === selectedSubclass.name && it.source === selectedSubclass.source) : -1; + + if (~this._ixPrevSelectedClass) { + ModalFilterClasses._doListDeselectAll(this._filterCache.list); + + const clsItem = this._filterCache.list.items.find(it=>it.data.ixClass === this._ixPrevSelectedClass && it.data.ixSubclass == null); + if (clsItem) { + clsItem.data.tglSel.classList.add("active"); + clsItem.ele.classList.add("list-multi-selected"); + } + + if (~ixSubclass && clsItem) { + const scItem = this._filterCache.list.items.find(it=>it.data.ixClass === this._ixPrevSelectedClass && it.data.ixSubclass === ixSubclass); + scItem.data.tglSel.classList.add("active"); + scItem.ele.classList.add("list-multi-selected"); + } + } + + this._filterCache.list.setFnSearch((li,searchTerm)=>{ + if (li.data.ixClass !== this._ixPrevSelectedClass) + return false; + return List.isVisibleDefaultSearch(li, searchTerm); + } + ); + } + else { this._filterCache.list.setFnSearch(null); } + + this._filterCache.list.update(); + + await UiUtil.pDoForceFocus(this._filterCache.$iptSearch[0]); + }); + } + + /**Called by ActorCharactermancerClass before the first render*/ + async pPreloadHidden($modalInner) { + $modalInner = $modalInner || $(`
    `); + + if (this._filterCache) { + this._filterCache.$wrpModalInner.appendTo($modalInner); + } + else { + await this._pInit(); + + //Loading text + const $ovlLoading = $(`
    Loading...
    `).appendTo($modalInner); + + const $iptSearch = $(``); + const $btnReset = $(``); + const $wrpFormTop = $$`
    ${$iptSearch}${$btnReset}
    `; + + const $wrpFormBottom = $(`
    `); + + const $wrpFormHeaders = $(`
    +
    + + +
    `); + + const $wrpForm = $$`
    ${$wrpFormTop}${$wrpFormBottom}${$wrpFormHeaders}
    `; + const $wrpList = this._$getWrpList(); + + const $btnConfirm = $(``); + + const list = new List({ $iptSearch, $wrpList, fnSort: this._fnSort, }); + //create a click handler + const listSelectClickHandler = new ListSelectClickHandler({list}); + + SortUtil.initBtnSortHandlers($wrpFormHeaders, list); + + //allData is probably already set + const allData = this._allData || await this._pLoadAllData(); + const pageFilter = this._pageFilter; + + await pageFilter.pInitFilterBox({ $wrpFormTop, $btnReset, $wrpMiniPills: $wrpFormBottom, namespace: this._namespace, }); + + //test - source data + //get some sources + const allSources = await CharacterBuilder._pGetSources({actor: null}); + const existingSources = this._preEnabledSources; + allSources.forEach((it,i)=>{ + let isDefaultContent = it.isDefault; + let isCustomUserContent = it.filterTypes?.includes("Custom/User"); + let isExistingEnabledContent = existingSources.filter(src => src.name == it.name).length > 0; + let isSelected = isDefaultContent || isExistingEnabledContent; + if(true){ + const listSelectClickHandler = new ListSelectClickHandler({list}); + const listItem = ModalFilterSources.getListItem({listSelectClickHandler: listSelectClickHandler + , pageFilter: pageFilter, src:it, srcI:i, isSelected:isSelected, pageFilter: pageFilter}); + + list.addItem(listItem); + } + }); + + const filterListItems = this._getListItems(pageFilter, allData[0], 0); + + //Class data + /* allData.forEach((it,i)=>{ + pageFilter.mutateAndAddToFilters(it); + const filterListItems = this._getListItems(pageFilter, it, i); + filterListItems.forEach(li=>{ + list.addItem(li); + li.ele.addEventListener("click", evt=>{ + + this._handleSelectClick({ + list, + filterListItems, + filterListItem: li, + evt, + }); + }); + }); + }); */ + + list.init(); + list.update(); + + //Wrapper to a function to be called when the filter changes + const handleFilterChange = ()=>{ + return this.constructor.handleFilterChange({ pageFilter, list, allData }); + }; + + pageFilter.trimState(); + + pageFilter.filterBox.on(FilterBox.EVNT_VALCHANGE, handleFilterChange); + pageFilter.filterBox.render(); //Render the filterbox already + handleFilterChange(); //Lets call that filter function right away + + $ovlLoading.remove(); + + const $wrpModalInner = $$`
    + ${$wrpForm} + ${$wrpList} +
    ${$btnConfirm}
    +
    `.appendTo($modalInner); + + this._filterCache = { + $wrpModalInner, + $btnConfirm, + pageFilter, + list, + allData, + $iptSearch + }; + } + } + + /**Called when the filter changes */ + static handleFilterChange({pageFilter, list, allData}) { + //Get the values from the filterbox + const f = pageFilter.filterBox.getValues(); + if(!f.Source?._combineBlue){console.error("Combine blue is not set!"); + if(!pageFilter.filterBox._filters[0].__meta.combineBlue){ + console.error("Source filter does not have combineBlue!"); + } + } + + list.filter(li=>{ + const cls = li; + + //Ask pageFilter if this list item should be displayed + const toDisplay = true;//pageFilter.toDisplay(f, cls, [], null); + return toDisplay; + }); + } + + static _doListDeselectAll(list, {isSubclassItemsOnly=false}={}) { + list.items.forEach(it=>{ + if (isSubclassItemsOnly && it.data.ixSubclass == null){return;} + + if (it.data.tglSel){it.data.tglSel.classList.remove("active");} + it.ele.classList.remove("list-multi-selected"); + } + ); + } + + _handleSelectClick({list, filterListItems, filterListItem, evt}) { + evt.preventDefault(); + evt.stopPropagation(); + + const isScLi = filterListItem.data.ixSubclass != null; + + if (this._isClassDisabled && this._ixPrevSelectedClass != null && isScLi) { + if (!filterListItem.data.tglSel.classList.contains("active")) + this.constructor._doListDeselectAll(list, { + isSubclassItemsOnly: true + }); + filterListItem.data.tglSel.classList.toggle("active"); + filterListItem.ele.classList.toggle("list-multi-selected"); + return; + } + + if (filterListItem.data.tglSel.classList.contains("active")) { + this.constructor._doListDeselectAll(list); + return; + } + + this.constructor._doListDeselectAll(list); + + if (isScLi) { + const classItem = filterListItems[0]; + classItem.data.tglSel.classList.add("active"); + classItem.ele.classList.add("list-multi-selected"); + } + + filterListItem.data.tglSel.classList.add("active"); + filterListItem.ele.classList.add("list-multi-selected"); + } + + async _pLoadAllData() { + this._pLoadingAllData = this._pLoadingAllData || (async()=>{ + const [data,prerelease,brew] = await Promise.all([MiscUtil.copy(await DataUtil.class.loadRawJSON()), + PrereleaseUtil.pGetBrewProcessed(), BrewUtil2.pGetBrewProcessed(), ]); + + this._pLoadAllData_mutAddPrereleaseBrew({ + data, brew: prerelease, brewUtil: PrereleaseUtil + }); + this._pLoadAllData_mutAddPrereleaseBrew({ + data, brew: brew, brewUtil: BrewUtil2 + }); + + this._allData = (await PageFilterClassesRaw.pPostLoad(data)).class; + })(); + + await this._pLoadingAllData; + return this._allData; + } + + _pLoadAllData_mutAddPrereleaseBrew({data, brew, brewUtil}) { + const clsProps = brewUtil.getPageProps({ + page: UrlUtil.PG_CLASSES + }); + + if (!clsProps.includes("*")) { + clsProps.forEach(prop=>data[prop] = [...(data[prop] || []), ...MiscUtil.copy(brew[prop] || [])]); + return; + } + + Object.entries(brew).filter(([,brewVal])=>brewVal instanceof Array).forEach(([prop,brewArr])=>data[prop] = [...(data[prop] || []), ...MiscUtil.copy(brewArr)]); + } + + _getListItems(pageFilter, cls, clsI) { + return [this._getListItems_getClassItem(pageFilter, cls, clsI), ...cls.subclasses.map((sc,scI)=>this._getListItems_getSubclassItem(pageFilter, cls, clsI, sc, scI)), ]; + } + + _getListItems_getClassItem(pageFilter, cls, clsI) { + const eleLabel = document.createElement("label"); + eleLabel.className = `w-100 ve-flex lst--border veapp__list-row no-select lst__wrp-cells`; + + const source = Parser.sourceJsonToAbv(cls.source); + + eleLabel.innerHTML = `
    +
    ${cls._versionBase_isVersion ? `` : ""}${cls.name}
    +
    ${source}
    `; + + return new ListItem(clsI,eleLabel,`${cls.name} -- ${cls.source}`,{ + source: `${source} -- ${cls.name}`, + },{ + ixClass: clsI, + tglSel: eleLabel.firstElementChild.firstElementChild, + },); + } + + _getListItems_getSubclassItem(pageFilter, cls, clsI, sc, scI) { + const eleLabel = document.createElement("label"); + eleLabel.className = `w-100 ve-flex lst--border veapp__list-row no-select lst__wrp-cells`; + + const source = Parser.sourceJsonToAbv(sc.source); + + eleLabel.innerHTML = `
    +
    ${sc._versionBase_isVersion ? `` : ""}\u2014 ${sc.name}
    +
    ${source}
    `; + + return new ListItem(`${clsI}--${scI}`,eleLabel,`${cls.name} -- ${cls.source} -- ${sc.name} -- ${sc.source}`,{ + source: `${cls.source} -- ${cls.name} -- ${source} -- ${sc.name}`, + },{ + ixClass: clsI, + ixSubclass: scI, + tglSel: eleLabel.firstElementChild.firstElementChild, + },); + } + + static getListItem({pageFilter, isRadio, list, listSelectClickHandler, src, srcI, fnOnClick, initialSources, isSelected, isForceSelectAll, }, ) { + isSelected = isSelected === undefined && initialSources ? initialSources.includes(src) : !!isSelected; + + pageFilter.mutateAndAddToFilters(src); + + const eleCb = e_({ + outer: isRadio ? `` : ``, + }); + if (isSelected){eleCb.checked = true;} + if (isForceSelectAll){eleCb.setAttribute("disabled", true);} + + const eleWrpCb = e_({ tag: "span", clazz: "col-0-5 ve-flex-vh-center", children: [eleCb, ], }); + + const eleLi = e_({ + tag: "label", + clazz: `row imp-wiz__row veapp__list-row-hoverable ve-flex-v-center ${isSelected ? "list-multi-selected" : ""}`, + children: [eleWrpCb, e_({ + tag: "span", + clazz: "col-11-5", + //html: `${this._getFilterTypesIcon(src.filterTypes)}${src.name}`, + html: `${src.name}`, + }), ], + }); + + const listItem = new ListItem(srcI,eleLi,src.name,{ filterTypes: src.filterTypes, abbreviations: src.abbreviations || [], }, + { identifierWorld: src.identifierWorld, cbSel: eleCb, },); + + if (!isForceSelectAll) { + eleLi.addEventListener("click", evt=>{ + if (isRadio){listSelectClickHandler.handleSelectClickRadio(listItem, evt);} + else{listSelectClickHandler.handleSelectClick(listItem, evt);} + if (fnOnClick){fnOnClick({ list, listItem });} + }); + } + + return listItem; + } +} +class ActorCharactermancerSourceSelector extends AppSourceSelectorMulti { + static _BREW_DIRS = ["class", 'subclass', "race", "subrace", + "background", "item", 'baseitem', "magicvariant", "spell", "feat", "optionalfeature"]; + static async ["api_pOpen"]({ actor: actor }) { + if (game.user.role < Config.get('charactermancer', 'minimumRole')) { + throw new Error("You do not have sufficient permissions!"); + } + if (!actor) { + throw new Error("\"actor\" option must be provided!"); + } + return this._pOpen({ actor: actor }); + } + static prePreInit() { this._preInit_registerKeybinds(); } + static _preInit_registerKeybinds() { + const fnOpenPlayer = () => { + const playerActor = UtilKeybinding.getPlayerActor({ minRole: Config.get("charactermancer", "minimumRole") }); + if (!playerActor) { return true; } + this._pOpen({ actor: playerActor }); + return true; + }; + const fnOpenSheet = () => { + const meta = UtilKeybinding.getCurrentImportableSheetDocumentMeta({ + isRequireActor: true, + isRequireOwnership: true, + minRole: Config.get("charactermancer", "minimumRole") + }); + if (!meta?.["actor"]) { return true; } + this._pOpen({ ...meta }); + return true; + }; + game.keybindings.register(SharedConsts.MODULE_ID, "ActorCharactermancerSourceSelector__openForCharacter", { + name: "Open Charactermancer Targeting Player Character", + editable: [], + onDown: () => fnOpenPlayer() + }); + game.keybindings.register(SharedConsts.MODULE_ID, "ActorCharactermancerSourceSelector__openForCurrentSheet", { + name: "Open Charactermancer Targeting Current Sheet", + editable: [], + onDown: () => fnOpenSheet() + }); + } + static async pHandleButtonClick(e, opts) { + return this._pOpen({actor: opts.actor}); + } + static async _pOpen({ actor: actor }) { + const sources = await this._pGetSources({ actor: actor }); + const selector = new ActorCharactermancerSourceSelector({ + title: "Charactermancer (Actor \"" + actor.name + "\"): Select Sources", + filterNamespace: 'ActorCharactermancerSourceSelector_filter', + savedSelectionKey: "ActorCharactermancerSourceSelector_savedSelection", + sourcesToDisplay: sources + }); + const hasConfirmed = await selector.pWaitForUserInput(); + if (hasConfirmed == null) { return; } + const data = this._postProcessAllSelectedData(hasConfirmed); + const mancer = new ActorCharactermancer({ actor: actor, data: data }); + mancer.render(true); + } + static _postProcessAllSelectedData(results) { + results = ImportListClass.Utils.getDedupedData({ allContentMerged: results }); + results = ImportListClass.Utils.getBlocklistFilteredData({ dedupedAllContentMerged: results }); + delete results.subclass; + Charactermancer_Feature_Util.addFauxOptionalFeatureEntries(results, results.optionalfeature); + Charactermancer_Class_Util.addFauxOptionalFeatureFeatures(results.class, results.optionalfeature); + return results; + } + static async _pGetSources({ actor: actor }) { + return [new UtilDataSource.DataSourceSpecial(Config.get('ui', 'isStreamerMode') ? "SRD" : "5etools", this._pLoadVetoolsSource.bind(this), { + 'cacheKey': '5etools-charactermancer', + 'filterTypes': [UtilDataSource.SOURCE_TYP_OFFICIAL_ALL], + 'isDefault': true, + 'pPostLoad': this._pPostLoad.bind(this, { + 'actor': actor + }) + }), ...UtilDataSource.getSourcesCustomUrl({ + 'pPostLoad': this._pPostLoad.bind(this, { + 'isBrewOrPrerelease': true, + 'actor': actor + }) + }), ...UtilDataSource.getSourcesUploadFile({ + 'pPostLoad': this._pPostLoad.bind(this, { + 'isBrewOrPrerelease': true, + 'actor': actor + }) + }), ...(await UtilDataSource.pGetSourcesPrerelease(ActorCharactermancerSourceSelector._BREW_DIRS, { + 'pPostLoad': this._pPostLoad.bind(this, { + 'isPrerelease': true, + 'actor': actor + }) + })), ...(await UtilDataSource.pGetSourcesBrew(ActorCharactermancerSourceSelector._BREW_DIRS, { + 'pPostLoad': this._pPostLoad.bind(this, { + 'isBrew': true, + 'actor': actor + }) + }))].filter(src => !UtilWorldDataSourceSelector.isFiltered(src)); + } + static async _pLoadVetoolsSource() { + const out = {}; + const [replyClass, replyRace, replyBk, replyItem, replySpell, replyFeat, replyOptionalFeature] = + await Promise.all([Vetools.pGetClasses(), Vetools.pGetRaces(), DataUtil.loadJSON(Vetools.DATA_URL_BACKGROUNDS), Vetools.pGetItems(), Vetools.pGetAllSpells(), DataUtil.loadJSON(Vetools.DATA_URL_FEATS), DataUtil.loadJSON(Vetools.DATA_URL_OPTIONALFEATURES)]); + Object.assign(out, replyClass); + out.race = replyRace.race; + out.background = replyBk.background; + out.item = replyItem.item; + out.spell = replySpell.spell; + out.feat = replyFeat.feat; + out.optionalfeature = replyOptionalFeature.optionalfeature; + return out; + } + static async _pPostLoad(opts) { + if (isBrewOrPrerelease) { + const { isPrerelease: isPrerelease, isBrew: isBrew } = UtilDataSource.getSourceType(opts, { 'isErrorOnMultiple': true }); + isPrerelease; + isBrew = isBrew; + } + opts = await UtilDataSource.pPostLoadGeneric({ isBrew: isBrew, isPrerelease: isPrerelease }, opts); + if (opts.class || opts.subclass) { + const isIgnoredLookup = await DataConverterClassSubclassFeature.pGetClassSubclassFeatureIgnoredLookup({ data: opts }); + const postLoaded = await PageFilterClassesFoundry.pPostLoad({ + 'class': opts.class, + 'subclass': opts.subclass, + 'classFeature': opts.classFeature, + 'subclassFeature': opts.subclassFeature + }, { + 'actor': actor, + 'isIgnoredLookup': isIgnoredLookup + }); + Object.assign(opts, postLoaded); + if (opts.class) { opts.class.forEach(cls => PageFilterClasses.mutateForFilters(cls)); } + } + if (opts.feat) { opts.feat = MiscUtil.copy(opts.feat); } + if (opts.optionalfeature) { opts.optionalfeature = MiscUtil.copy(opts.optionalfeature); } + return opts; + } +} \ No newline at end of file diff --git a/charbuilder/js/plutonium/utils-entity.js b/charbuilder/js/plutonium/utils-entity.js new file mode 100644 index 0000000..6872c5e --- /dev/null +++ b/charbuilder/js/plutonium/utils-entity.js @@ -0,0 +1,1005 @@ +//#region UtilEntity +class UtilEntityBase { + static _PropNamespacer = null; + + static getCompendiumDeepKeys() { + return null; + } + + static getCompendiumAliases(ent, {isStrict=false}={}) { + return []; + } + + static getEntityAliases(ent, {isStrict=false}={}) { + return []; + } + + static getNamespacedProp(prop) { + if (!this._PropNamespacer) + throw new Error("Unimplemented!"); + return this._PropNamespacer.getNamespacedProp(prop); + } + + static getUnNamespacedProp(propNamespaced) { + if (!this._PropNamespacer) + throw new Error("Unimplemented!"); + return this._PropNamespacer.getUnNamespacedProp(propNamespaced); + } +} +class UtilEntityClassSubclassFeature extends UtilEntityBase { + static getBrewProp(feature) { + const type = this.getEntityType(feature); + switch (type) { + case "classFeature": + return "foundryClassFeature"; + case "subclassFeature": + return "foundrySubclassFeature"; + default: + throw new Error(`Unhandled feature type "${type}"`); + } + } + + static getEntityType(feature) { + if (feature.subclassShortName) + return "subclassFeature"; + if (feature.className) + return "classFeature"; + return null; + } + + static getCompendiumAliases(ent, {isStrict=false}={}) { + return this.getEntityAliases(ent).map(it=>it.name); + } + + static _FEATURE_SRD_ALIAS_WITH_CLASSNAME = new Set(["unarmored defense", "channel divinity", "expertise", "land's stride", "timeless body", "spellcasting", ]); + + static getEntityAliases(ent, {isStrict=false}={}) { + if (!ent.name) + return []; + + const out = []; + + const lowName = ent.name.toLowerCase().trim(); + + const noBrackets = ent.name.replace(/\([^)]+\)/g, "").replace(/\s+/g, " ").trim(); + if (noBrackets !== ent.name) + out.push({ + ...ent, + name: noBrackets + }); + + const splitColon = ent.name.split(":")[0].trim(); + const isSplitColonName = splitColon !== ent.name; + if (isSplitColonName) { + out.push({ + ...ent, + name: splitColon + }); + + if (this._FEATURE_SRD_ALIAS_WITH_CLASSNAME.has(splitColon.toLowerCase())) { + out.push({ + ...ent, + name: `${splitColon} (${ent.className})` + }); + } + } + + if (this._FEATURE_SRD_ALIAS_WITH_CLASSNAME.has(lowName)) { + out.push({ + ...ent, + name: `${ent.name} (${ent.className})` + }); + } + + if (lowName.startsWith("mystic arcanum")) { + out.push({ + ...ent, + name: `${ent.name} (${ent.className})` + }); + } + + if (!isSplitColonName) { + out.push({ + ...ent, + name: `Channel Divinity: ${ent.name}` + }); + out.push({ + ...ent, + name: `Ki: ${ent.name}` + }); + } + + return out; + } +} + +class UtilEntityBackground extends UtilEntityBase { + static getCompendiumAliases(ent, {isStrict=false}={}) { + return this.getEntityAliases(ent).map(it=>it.name); + } + + static getEntityAliases(ent, {isStrict=false}={}) { + if (!ent.name) + return []; + return []; + } + + static isCustomBackground(ent) { + return ent?.name === "Custom Background" && ent?.source === Parser.SRC_PHB; + } +} + +class UtilEntityGeneric extends UtilEntityBase { + static getName(ent) { + if (ent._fvttCustomizerState) { + const rename = CustomizerStateBase.fromJson(ent._fvttCustomizerState)?.rename?.rename; + if (rename) + return rename; + } + + return ent._displayName || ent.name || ""; + } + + static _RE_RECHARGE_TAG = /{@recharge(?: (?\d+))?}/gi; + static _RE_RECHARGE_TEXT = /\(Recharge (?\d+)(?:[-\u2011-\u2014]\d+)?\)/gi; + + static isRecharge(ent) { + if (!ent.name) + return false; + + this._RE_RECHARGE_TAG.lastIndex = 0; + this._RE_RECHARGE_TEXT.lastIndex = 0; + + return this._RE_RECHARGE_TAG.test(ent.name) || this._RE_RECHARGE_TEXT.test(ent.name); + } + + static getRechargeMeta(name) { + if (!name) + return null; + + let rechargeValue = null; + + name = name.replace(this._RE_RECHARGE_TAG, (...m)=>{ + rechargeValue ??= Number(m.last().rechargeValue || 6); + return ""; + } + ).trim().replace(/ +/g, " ").replace(this._RE_RECHARGE_TEXT, (...m)=>{ + rechargeValue ??= Number(m.last().rechargeValue); + return ""; + } + ).trim().replace(/ +/g, " "); + + return { + name, + rechargeValue, + }; + } +} +//#endregion + + +//#region UtilActors +class UtilActors { + static init() { + UtilActors.VALID_DAMAGE_TYPES = Object.keys(MiscUtil.get(CONFIG, "DND5E", "damageTypes") || {}); + UtilActors.VALID_CONDITIONS = Object.keys(MiscUtil.get(CONFIG, "DND5E", "conditionTypes") || {}); + } + + static async pGetActorSpellItemOpts({actor, isAllowAutoDetectPreparationMode=false}={}) { + const opts = { + isActorItem: true, + isActorItemNpc: actor?.type === "npc", + isPrepared: !!Config.get("importSpell", "prepareActorSpells"), + preparationMode: Config.get("importSpell", "actorSpellPreparationMode"), + }; + + if (!actor || this.isImporterTempActor(actor)) + return opts; + + const spellcastingAbility = MiscUtil.get(actor, "system", "attributes", "spellcasting"); + if (spellcastingAbility) + opts.ability = spellcastingAbility.value; + + if (actor && isAllowAutoDetectPreparationMode) { + const autoPreparationMode = await this._pGetActorSpellItemOpts_getAutoPreparationMode({ + actor + }); + if (autoPreparationMode != null) + opts.preparationMode = autoPreparationMode; + } + + return opts; + } + + static isImporterTempActor(actor) { + return !!MiscUtil.get(actor, "flags", SharedConsts.MODULE_ID, "isImporterTempActor"); + } + + static async _pGetActorSpellItemOpts_getAutoPreparationMode({actor}) { + if (!Config.get("importSpell", "isAutoDetectActorSpellPreparationMode")) + return null; + + const classItems = actor.items.filter(it=>it.type === "class" && it.system?.spellcasting?.progression !== "none"); + if (!classItems.length || classItems.length > 1) + return null; + + const sheetItem = classItems[0]; + + const spellProgression = sheetItem.system.spellcasting.progression; + switch (spellProgression) { + case "full": + case "half": + case "third": + case "artificer": + { + const classSubclassMeta = await UtilDataConverter.pGetClassItemClassAndSubclass({ + sheetItem, + subclassSheetItems: actor.items.filter(it=>it.type === "subclass") + }); + if (classSubclassMeta.matchingClasses.length !== 1) + return null; + return (classSubclassMeta.matchingClasses[0].preparedSpells || classSubclassMeta.matchingClasses[0].preparedSpellsProgression) ? "prepared" : "always"; + } + case "pact": + return "pact"; + default: + return null; + } + } + + static getSpellItemItemOpts() { + const opts = {}; + + opts.isPrepared = !!Config.get("importSpell", "prepareSpellItems"); + opts.preparationMode = Config.get("importSpell", "spellItemPreparationMode"); + + return opts; + } + + static getMappedTool(str) { + str = str.toLowerCase().trim(); + if (this.VALID_TOOL_PROFICIENCIES[str]) + return this.VALID_TOOL_PROFICIENCIES[str]; + str = str.split("|")[0]; + return this.VALID_TOOL_PROFICIENCIES[str]; + } + + static getUnmappedTool(str) { + if (!str) + return null; + return Parser._parse_bToA(this.VALID_TOOL_PROFICIENCIES, str, null); + } + + static getMappedLanguage(str) { + str = str.toLowerCase().trim(); + return this.VALID_LANGUAGES[str]; + } + + static getMappedCasterType(str) { + if (!str) + return str; + return this._VET_CASTER_TYPE_TO_FVTT[str]; + } + + static getMappedArmorProficiency(str) { + if (!str) + return null; + return Parser._parse_aToB(this.VALID_ARMOR_PROFICIENCIES, str, null); + } + + static getUnmappedArmorProficiency(str) { + if (!str) + return null; + return Parser._parse_bToA(this.VALID_ARMOR_PROFICIENCIES, str, null); + } + + static getMappedWeaponProficiency(str) { + if (!str) + return null; + return Parser._parse_aToB(this.VALID_WEAPON_PROFICIENCIES, str, null); + } + + static getUnmappedWeaponProficiency(str) { + if (!str) + return null; + return Parser._parse_bToA(this.VALID_WEAPON_PROFICIENCIES, str, null); + } + + static getItemUIdFromWeaponProficiency(str) { + if (!str) + return null; + str = str.trim(); + const tagItemUid = this._getItemUidFromTag(str); + if (tagItemUid) + return tagItemUid; + return Parser._parse_aToB(this._WEAPON_PROFICIENCIES_TO_ITEM_UIDS, str, null); + } + + static getItemUIdFromToolProficiency(str) { + if (!str) + return null; + str = str.trim(); + const tagItemUid = this._getItemUidFromTag(str); + if (tagItemUid) + return tagItemUid; + return Parser._parse_aToB(this._TOOL_PROFICIENCIES_TO_ITEM_UIDS, str, null); + } + + static _getItemUidFromTag(str) { + const mItem = /^{@item ([^}]+)}$/.exec(str); + if (!mItem) + return null; + const {name, source} = DataUtil.generic.unpackUid(mItem[1], "item", { + isLower: true + }); + return `${name}|${source}`; + } + + static getActorBarAttributes(actor) { + if (!actor) + return []; + + const attributeSource = actor?.system instanceof foundry.abstract.DataModel ? actor?.type : actor?.system; + const attributes = MiscUtil.copyFast(TokenDocument.implementation.getTrackedAttributes(attributeSource), ); + + return TokenDocument.implementation.getTrackedAttributeChoices(attributes); + } + + static getTotalClassLevels(actor) { + return actor.items.filter(it=>it.type === "class").map(it=>it.system.levels || 0).reduce((a,b)=>a + b, 0); + } + + static isLevelUp(actor) { + let xpCur = Number(actor?.system?.details?.xp?.value); + if (isNaN(xpCur)) + xpCur = 0; + + const lvlTarget = actor.items.filter(it=>it.type === "class").map(it=>it.system.levels || 0).sum(); + let xpMax = game.system.config.CHARACTER_EXP_LEVELS[lvlTarget]; + if (isNaN(xpMax)) + xpMax = Number.MAX_SAFE_INTEGER; + + return xpCur >= xpMax; + } + + static ICON_SPELL_POINTS_ = "icons/magic/light/explosion-star-glow-silhouette.webp"; + static _SPELL_POINTS_SLOT_COUNT = 99; + static async pGetCreateActorSpellPointsSlotsEffect({actor, isTemporary, isRender}) { + if (this.hasActorSpellPointSlotEffect({ + actor + })) + return; + + await UtilDocuments.pCreateEmbeddedDocuments(actor, this.getActorSpellPointsSlotsEffectData({ + actor + }), { + ClsEmbed: ActiveEffect, + isTemporary, + isRender + }, ); + + await UtilDocuments.pUpdateDocument(actor, this.getActorSpellPointsSlotsUpdateSys()); + } + + static hasActorSpellPointSlotEffect({actor}) { + return (actor?.effects || []).some(it=>it.flags[SharedConsts.MODULE_ID]?.["isSpellPointsSlotUnlocker"]); + } + + static getActorSpellPointsSlotsEffectData({actor=null, sheetItem=null}={}) { + return UtilActiveEffects.getExpandedEffects([{ + name: `Spell Points Spell Slot Unlock`, + changes: [...new Array(9)].map((_,i)=>({ + "key": `system.spells.spell${i + 1}.override`, + "mode": "OVERRIDE", + "value": this._SPELL_POINTS_SLOT_COUNT, + })), + flags: { + [SharedConsts.MODULE_ID]: { + isSpellPointsSlotUnlocker: true, + dedupeId: "spellPointsSlotUnlocker", + }, + }, + }, ], { + img: this.ICON_SPELL_POINTS_, + actor, + sheetItem, + }, ); + } + + static getActorSpellPointsSlotsUpdateSys() { + return { + system: { + spells: [...new Array(9)].mergeMap((_,i)=>({ + [`spell${i + 1}`]: { + value: 99, + }, + })), + }, + }; + } + + static getActorSpellPointsItem({actor}) { + return SpellPointsItemBuilder.getItem({ + actor + }); + } + + static async pGetCreateActorSpellPointsItem({actor, totalSpellcastingLevels=null}) { + return SpellPointsItemBuilder.pGetCreateItem({ + actor, + totalLevels: totalSpellcastingLevels + }); + } + + static getActorPsiPointsItem({actor}) { + return PsiPointsItemBuilder.getItem({ + actor + }); + } + + static async pGetCreateActorPsiPointsItem({actor, totalMysticLevels=null}) { + return PsiPointsItemBuilder.pGetCreateItem({ + actor, + totalLevels: totalMysticLevels + }); + } + + static getActorSpellcastingInfo({actor, sheetItems, isForceSpellcastingMulticlass=false, }={}, ) { + if (actor && sheetItems) + throw new Error(`Only one of "actor" or "sheetItems" may be specified!`); + + const spellcastingClassItems = (actor?.items || sheetItems).filter(it=>it.type === "class").filter(it=>it.system?.spellcasting); + + if (!spellcastingClassItems.length) { + return { + totalSpellcastingLevels: 0, + casterClassCount: 0, + maxPactCasterLevel: 0, + isSpellcastingMulticlass: isForceSpellcastingMulticlass, + }; + } + + let totalSpellcastingLevels = 0; + let maxPactCasterLevel = 0; + + const isSpellcastingMulticlass = isForceSpellcastingMulticlass || spellcastingClassItems.length > 1; + + const getSpellcastingLevel = (lvl,type)=>{ + switch (type) { + case "half": + return Math.ceil(lvl / 2); + case "third": + return Math.ceil(lvl / 3); + case "artificer": + return lvl === 1 ? 1 : getSpellcastingLevel(lvl, "half"); + default: + throw new Error(`Unhandled spellcaster type "${type}"`); + } + } + ; + + const getSpellcastingLevelMulticlass = (lvl,type)=>{ + switch (type) { + case "half": + return Math.floor(lvl / 2); + case "third": + return Math.floor(lvl / 3); + case "artificer": + return Math.ceil(lvl / 2); + default: + throw new Error(`Unhandled spellcaster type "${type}"`); + } + } + ; + + const fnGetSpellcastingLevelHalfThird = isSpellcastingMulticlass ? getSpellcastingLevelMulticlass : getSpellcastingLevel; + + spellcastingClassItems.forEach(it=>{ + const lvl = it.system.levels || 0; + + switch (it.system.spellcasting.progression) { + case "full": + totalSpellcastingLevels += lvl; + break; + case "half": + totalSpellcastingLevels += fnGetSpellcastingLevelHalfThird(lvl, it.system.spellcasting.progression); + break; + case "third": + totalSpellcastingLevels += fnGetSpellcastingLevelHalfThird(lvl, it.system.spellcasting.progression); + break; + case "pact": + Math.max(maxPactCasterLevel, lvl); + break; + case "artificer": + totalSpellcastingLevels += fnGetSpellcastingLevelHalfThird(lvl, it.system.spellcasting.progression); + break; + } + } + ); + + return { + totalSpellcastingLevels, + casterClassCount: spellcastingClassItems.length, + maxPactCasterLevel, + isSpellcastingMulticlass + }; + } + + static async pLinkTempUuids({actor}) { + const SENTINEL = `__${SharedConsts.MODULE_ID_FAKE}_REPLACE_TARGET__`; + + const reUuid = new RegExp(`(?@UUID\\[[^\\]]+\\.)temp-${SharedConsts.MODULE_ID_FAKE}-(?[^.\\]]+)(?](?:\\{[^}]+})?)`,"g"); + const reSentinelLi = new RegExp(`]*>\\s*${SENTINEL}\\s*<\\/li>`,"g"); + const reSentinelP = new RegExp(`]*>\\s*${SENTINEL}\\s*<\\/p>`,"g"); + const reSentinel = new RegExp(SENTINEL,"g"); + + const updates = actor.items.map(item=>{ + const desc = item.system.description.value || ""; + const nxtDesc = desc.replace(reUuid, (...m)=>{ + const packed = m.last().packed; + try { + const {page, source, hash} = JSON.parse(decodeURIComponent(atob(packed))); + if (!page || !source || !hash) + return SENTINEL; + + const matchedItem = actor.items.find(it=>it.flags?.[SharedConsts.MODULE_ID]?.page === page && it.flags?.[SharedConsts.MODULE_ID]?.source === source && it.flags?.[SharedConsts.MODULE_ID]?.hash === hash); + + if (!matchedItem) + return SENTINEL; + + return `${m.last().prefix}${matchedItem.id}${m.last().suffix}`; + } catch (e) { + console.error(...LGT, `Failed to unpack temp page/source/hash`, e); + return ""; + } + } + ).replace(reSentinelLi, "").replace(reSentinelP, "").replace(reSentinel, ""); + + if (desc === nxtDesc) + return null; + + return { + _id: item.id, + system: { + description: { + value: nxtDesc, + }, + }, + }; + } + ).filter(Boolean); + + if (!updates.length) + return; + + await UtilDocuments.pUpdateEmbeddedDocuments(actor, updates, { + ClsEmbed: Item + }); + } + + static isSetMaxHp({actor}) { + if (!UtilVersions.getSystemVersion().isVersionTwoOnePlus) + return true; + return actor._source.system.attributes.hp.max != null; + } + + static getProficiencyBonusNumber({actor}) { + const prof = actor.getRollData().prof; + if (typeof prof === "number") + return prof; + return prof.flat; + } +} +UtilActors.SKILL_ABV_TO_FULL = { + acr: "acrobatics", + ani: "animal handling", + arc: "arcana", + ath: "athletics", + dec: "deception", + his: "history", + ins: "insight", + itm: "intimidation", + inv: "investigation", + med: "medicine", + nat: "nature", + prc: "perception", + prf: "performance", + per: "persuasion", + rel: "religion", + slt: "sleight of hand", + ste: "stealth", + sur: "survival", +}; +UtilActors.TOOL_ABV_TO_FULL = { + art: "artisan's tools", + alchemist: "alchemist's supplies", + brewer: "brewer's supplies", + calligrapher: "calligrapher's supplies", + carpenter: "carpenter's tools", + cartographer: "cartographer's tools", + cobbler: "cobbler's tools", + cook: "cook's utensils", + glassblower: "glassblower's tools", + jeweler: "jeweler's tools", + leatherworker: "leatherworker's tools", + mason: "mason's tools", + painter: "painter's supplies", + potter: "potter's tools", + smith: "smith's tools", + tinker: "tinker's tools", + weaver: "weaver's tools", + woodcarver: "woodcarver's tools", + + disg: "disguise kit", + forg: "forgery kit", + + game: "gaming set", + chess: "dragonchess set", + dice: "dice set", + card: "three-dragon ante set", + + herb: "herbalism kit", + + music: "musical instrument", + bagpipes: "bagpipes", + drum: "drum", + dulcimer: "dulcimer", + flute: "flute", + horn: "horn", + lute: "lute", + lyre: "lyre", + panflute: "pan flute", + shawm: "shawm", + viol: "viol", + + navg: "navigator's tools", + + pois: "poisoner's kit", + + thief: "thieves' tools", + + vehicle: "vehicles", + air: "vehicles (air)", + land: "vehicles (land)", + space: "vehicles (space)", + water: "vehicles (water)", +}; +UtilActors.PROF_TO_ICON_CLASS = { + "1": "fa-check", + "2": "fa-check-double", + "0.5": "fa-adjust", +}; +UtilActors.PROF_TO_TEXT = { + "1": "Proficient", + "2": "Proficient with Expertise", + "0.5": "Half-Proficient", + "0": "", +}; +UtilActors.VET_SIZE_TO_ABV = { + [Parser.SZ_TINY]: "tiny", + [Parser.SZ_SMALL]: "sm", + [Parser.SZ_MEDIUM]: "med", + [Parser.SZ_LARGE]: "lg", + [Parser.SZ_HUGE]: "huge", + [Parser.SZ_GARGANTUAN]: "grg", +}; +UtilActors.VET_SPELL_SCHOOL_TO_ABV = { + A: "abj", + C: "con", + D: "div", + E: "enc", + V: "evo", + I: "ill", + N: "nec", + T: "trs", +}; + +UtilActors.PACT_CASTER_MAX_SPELL_LEVEL = 5; + +UtilActors.VALID_DAMAGE_TYPES = null; +UtilActors.VALID_CONDITIONS = null; + +UtilActors.TOOL_PROFICIENCIES_TO_UID = { + "alchemist's supplies": "alchemist's supplies|phb", + "brewer's supplies": "brewer's supplies|phb", + "calligrapher's supplies": "calligrapher's supplies|phb", + "carpenter's tools": "carpenter's tools|phb", + "cartographer's tools": "cartographer's tools|phb", + "cobbler's tools": "cobbler's tools|phb", + "cook's utensils": "cook's utensils|phb", + "glassblower's tools": "glassblower's tools|phb", + "jeweler's tools": "jeweler's tools|phb", + "leatherworker's tools": "leatherworker's tools|phb", + "mason's tools": "mason's tools|phb", + "painter's supplies": "painter's supplies|phb", + "potter's tools": "potter's tools|phb", + "smith's tools": "smith's tools|phb", + "tinker's tools": "tinker's tools|phb", + "weaver's tools": "weaver's tools|phb", + "woodcarver's tools": "woodcarver's tools|phb", + "disguise kit": "disguise kit|phb", + "forgery kit": "forgery kit|phb", + "gaming set": "gaming set|phb", + "herbalism kit": "herbalism kit|phb", + "musical instrument": "musical instrument|phb", + "navigator's tools": "navigator's tools|phb", + "thieves' tools": "thieves' tools|phb", + "poisoner's kit": "poisoner's kit|phb", +}; +UtilActors.VALID_TOOL_PROFICIENCIES = { + "artisan's tools": "art", + "alchemist's supplies": "alchemist", + "brewer's supplies": "brewer", + "calligrapher's supplies": "calligrapher", + "carpenter's tools": "carpenter", + "cartographer's tools": "cartographer", + "cobbler's tools": "cobbler", + "cook's utensils": "cook", + "glassblower's tools": "glassblower", + "jeweler's tools": "jeweler", + "leatherworker's tools": "leatherworker", + "mason's tools": "mason", + "painter's supplies": "painter", + "potter's tools": "potter", + "smith's tools": "smith", + "tinker's tools": "tinker", + "weaver's tools": "weaver", + "woodcarver's tools": "woodcarver", + + "disguise kit": "disg", + + "forgery kit": "forg", + + "gaming set": "game", + "dice set": "dice", + "dragonchess set": "chess", + "playing card set": "card", + "three-dragon ante set": "card", + + "herbalism kit": "herb", + + "musical instrument": "music", + "bagpipes": "bagpipes", + "drum": "drum", + "dulcimer": "dulcimer", + "flute": "flute", + "lute": "lute", + "lyre": "lyre", + "horn": "horn", + "pan flute": "panflute", + "shawm": "shawm", + "viol": "viol", + + "navigator's tools": "navg", + + "poisoner's kit": "pois", + + "thieves' tools": "thief", + + "vehicle (land or water)": "vehicle", + "vehicle (air)": "air", + "vehicle (land)": "land", + "vehicle (water)": "water", + "vehicle (space)": "space", +}; +UtilActors.VALID_LANGUAGES = { + "common": "common", + "aarakocra": "aarakocra", + "abyssal": "abyssal", + "aquan": "aquan", + "auran": "auran", + "celestial": "celestial", + "deep speech": "deep", + "draconic": "draconic", + "druidic": "druidic", + "dwarvish": "dwarvish", + "elvish": "elvish", + "giant": "giant", + "gith": "gith", + "gnomish": "gnomish", + "goblin": "goblin", + "gnoll": "gnoll", + "halfling": "halfling", + "ignan": "ignan", + "infernal": "infernal", + "orc": "orc", + "primordial": "primordial", + "sylvan": "sylvan", + "terran": "terran", + "thieves' cant": "cant", + "undercommon": "undercommon", +}; +UtilActors.LANGUAGES_PRIMORDIAL = ["aquan", "auran", "ignan", "terran", ]; +UtilActors._VET_CASTER_TYPE_TO_FVTT = { + "full": "full", + "1/2": "half", + "1/3": "third", + "pact": "pact", + "artificer": "artificer", +}; +UtilActors.ARMOR_PROFICIENCIES = ["light", "medium", "heavy", "shield|phb", ]; +UtilActors.VALID_ARMOR_PROFICIENCIES = { + "light": "lgt", + "medium": "med", + "heavy": "hvy", + "shield|phb": "shl", +}; +UtilActors.WEAPON_PROFICIENCIES = ["battleaxe|phb", "club|phb", "dagger|phb", "flail|phb", "glaive|phb", "greataxe|phb", "greatclub|phb", "greatsword|phb", "halberd|phb", "handaxe|phb", "javelin|phb", "lance|phb", "light hammer|phb", "longsword|phb", "mace|phb", "maul|phb", "morningstar|phb", "pike|phb", "quarterstaff|phb", "rapier|phb", "scimitar|phb", "shortsword|phb", "sickle|phb", "spear|phb", "staff|phb", "trident|phb", "war pick|phb", "warhammer|phb", "whip|phb", "blowgun|phb", "dart|phb", "hand crossbow|phb", "heavy crossbow|phb", "light crossbow|phb", "longbow|phb", "net|phb", "shortbow|phb", "sling|phb", ]; +UtilActors.VALID_WEAPON_PROFICIENCIES = { + "simple": "sim", + "martial": "mar", + + "club|phb": "club", + "dagger|phb": "dagger", + "dart|phb": "dart", + "greatclub|phb": "greatclub", + "handaxe|phb": "handaxe", + "javelin|phb": "javelin", + "light crossbow|phb": "lightcrossbow", + "light hammer|phb": "lighthammer", + "mace|phb": "mace", + "quarterstaff|phb": "quarterstaff", + "shortbow|phb": "shortbow", + "sickle|phb": "sickle", + "sling|phb": "sling", + "spear|phb": "spear", + + "battleaxe|phb": "battleaxe", + "blowgun|phb": "blowgun", + "flail|phb": "flail", + "glaive|phb": "glaive", + "greataxe|phb": "greataxe", + "greatsword|phb": "greatsword", + "halberd|phb": "halberd", + "hand crossbow|phb": "handcrossbow", + "heavy crossbow|phb": "heavycrossbow", + "lance|phb": "lance", + "longbow|phb": "longbow", + "longsword|phb": "longsword", + "maul|phb": "maul", + "morningstar|phb": "morningstar", + "net|phb": "net", + "pike|phb": "pike", + "rapier|phb": "rapier", + "scimitar|phb": "scimitar", + "shortsword|phb": "shortsword", + "trident|phb": "trident", + "war pick|phb": "warpick", + "warhammer|phb": "warhammer", + "whip|phb": "whip", +}; +UtilActors._WEAPON_PROFICIENCIES_TO_ITEM_UIDS = { + "battleaxes": "battleaxe|phb", + "clubs": "club|phb", + "daggers": "dagger|phb", + "flails": "flail|phb", + "glaives": "glaive|phb", + "greataxes": "greataxe|phb", + "greatclubs": "greatclub|phb", + "greatswords": "greatsword|phb", + "halberds": "halberd|phb", + "handaxes": "handaxe|phb", + "javelins": "javelin|phb", + "lances": "lance|phb", + "light hammers": "light hammer|phb", + "longswords": "longsword|phb", + "maces": "mace|phb", + "mauls": "maul|phb", + "morningstars": "morningstar|phb", + "pikes": "pike|phb", + "quarterstaffs": "quarterstaff|phb", + "rapiers": "rapier|phb", + "scimitars": "scimitar|phb", + "shortswords": "shortsword|phb", + "sickles": "sickle|phb", + "spears": "spear|phb", + "staffs": "staff|phb", + "tridents": "trident|phb", + "war picks": "war pick|phb", + "warhammers": "warhammer|phb", + "whips": "whip|phb", + + "blowguns": "blowgun|phb", + "darts": "dart|phb", + "hand crossbows": "hand crossbow|phb", + "heavy crossbows": "heavy crossbow|phb", + "light crossbows": "light crossbow|phb", + "longbows": "longbow|phb", + "nets": "net|phb", + "shortbows": "shortbow|phb", + "slings": "sling|phb", + + "battleaxe": "battleaxe|phb", + "club": "club|phb", + "dagger": "dagger|phb", + "flail": "flail|phb", + "glaive": "glaive|phb", + "greataxe": "greataxe|phb", + "greatclub": "greatclub|phb", + "greatsword": "greatsword|phb", + "halberd": "halberd|phb", + "handaxe": "handaxe|phb", + "javelin": "javelin|phb", + "lance": "lance|phb", + "light hammer": "light hammer|phb", + "longsword": "longsword|phb", + "mace": "mace|phb", + "maul": "maul|phb", + "morningstar": "morningstar|phb", + "pike": "pike|phb", + "quarterstaff": "quarterstaff|phb", + "rapier": "rapier|phb", + "scimitar": "scimitar|phb", + "shortsword": "shortsword|phb", + "sickle": "sickle|phb", + "spear": "spear|phb", + "staff": "staff|phb", + "trident": "trident|phb", + "war pick": "war pick|phb", + "warhammer": "warhammer|phb", + "whip": "whip|phb", + + "blowgun": "blowgun|phb", + "dart": "dart|phb", + "hand crossbow": "hand crossbow|phb", + "heavy crossbow": "heavy crossbow|phb", + "light crossbow": "light crossbow|phb", + "longbow": "longbow|phb", + "net": "net|phb", + "shortbow": "shortbow|phb", + "sling": "sling|phb", +}; +UtilActors._TOOL_PROFICIENCIES_TO_ITEM_UIDS = { + "alchemist's supplies": "alchemist's supplies|phb", + "artisan's tools": "artisan's tools|phb", + "bagpipes": "bagpipes|phb", + "brewer's supplies": "brewer's supplies|phb", + "calligrapher's supplies": "calligrapher's supplies|phb", + "carpenter's tools": "carpenter's tools|phb", + "cartographer's tools": "cartographer's tools|phb", + "cobbler's tools": "cobbler's tools|phb", + "cook's utensils": "cook's utensils|phb", + "disguise kit": "disguise kit|phb", + "drum": "drum|phb", + "dulcimer": "dulcimer|phb", + "flute": "flute|phb", + "forgery kit": "forgery kit|phb", + "glassblower's tools": "glassblower's tools|phb", + "herbalism kit": "herbalism kit|phb", + "horn": "horn|phb", + "jeweler's tools": "jeweler's tools|phb", + "leatherworker's tools": "leatherworker's tools|phb", + "lute": "lute|phb", + "lyre": "lyre|phb", + "mason's tools": "mason's tools|phb", + "musical instrument": "musical instrument|phb", + "navigator's tools": "navigator's tools|phb", + "painter's supplies": "painter's supplies|phb", + "pan flute": "pan flute|phb", + "poisoner's kit": "poisoner's kit|phb", + "potter's tools": "potter's tools|phb", + "shawm": "shawm|phb", + "smith's tools": "smith's tools|phb", + "thieves' tools": "thieves' tools|phb", + "tinker's tools": "tinker's tools|phb", + "viol": "viol|phb", + "weaver's tools": "weaver's tools|phb", + "woodcarver's tools": "woodcarver's tools|phb", +}; + +UtilActors.BG_SKILL_PROFS_CUSTOMIZE = [{ + choose: { + from: Object.keys(Parser.SKILL_TO_ATB_ABV), + count: 2, + }, +}, ]; + +UtilActors.LANG_TOOL_PROFS_CUSTOMIZE = [{ + anyStandardLanguage: 2, +}, { + anyStandardLanguage: 1, + anyTool: 1, +}, { + anyTool: 2, +}, ]; +//#endregion \ No newline at end of file diff --git a/charbuilder/js/plutonium/utils.js b/charbuilder/js/plutonium/utils.js new file mode 100644 index 0000000..d3c7656 --- /dev/null +++ b/charbuilder/js/plutonium/utils.js @@ -0,0 +1,12978 @@ +//#region UtilDataSource +class UtilDataSource { + + static sortListItems(a, b, o) { + const ixTypeA = Math.min(...a.values.filterTypes.map(it=>UtilDataSource.SOURCE_TYPE_ORDER.indexOf(it))); + const ixTypeB = Math.min(...b.values.filterTypes.map(it=>UtilDataSource.SOURCE_TYPE_ORDER.indexOf(it))); + + return SortUtil.ascSort(ixTypeA, ixTypeB) || SortUtil.compareListNames(a, b); + } + + static _PROPS_NO_BLOCKLIST = new Set(["itemProperty", "itemType", "spellList"]); + static _PROP_RE_FOUNDRY = /^foundry[A-Z]/; + + static getMergedData(data, {isFilterBlocklisted=true}={}) { + const mergedData = {}; + + data.forEach(sourceData=>{ + Object.entries(sourceData).forEach(([prop,arr])=>{ + if (!arr || !(arr instanceof Array)) + return; + if (mergedData[prop]) + mergedData[prop] = [...mergedData[prop], ...MiscUtil.copy(arr)]; + else + mergedData[prop] = MiscUtil.copy(arr); + } + ); + }); + + if (isFilterBlocklisted) { + Object.entries(mergedData).forEach(([prop,arr])=>{ + if (!arr || !(arr instanceof Array)) + return; + mergedData[prop] = mergedData[prop].filter(it=>{ + if (SourceUtil.getEntitySource(it) === VeCt.STR_GENERIC) + return false; + + if (it.__prop && this._PROPS_NO_BLOCKLIST.has(it.__prop)) + return true; + if (it.__prop && this._PROP_RE_FOUNDRY.test(it.__prop)) + return false; + + if (!SourceUtil.getEntitySource(it)) { + console.warn(`Entity did not have a "source"! This should never occur.`); + return true; + } + if (!it.__prop) { + console.warn(`Entity did not have a "__prop"! This should never occur.`); + return true; + } + if (!UrlUtil.URL_TO_HASH_BUILDER[it.__prop]) { + console.warn(`No hash builder found for "__prop" "${it.__prop}"! This should never occur.`); + return true; + } + + switch (it.__prop) { + case "class": + { + if (!it.subclasses?.length) + break; + + it.subclasses = it.subclasses.filter(sc=>{ + if (sc.source === VeCt.STR_GENERIC) + return false; + + return !ExcludeUtil.isExcluded(UrlUtil.URL_TO_HASH_BUILDER["subclass"](sc), "subclass", sc.source, { + isNoCount: true + }, ); + } + ); + + break; + } + + case "item": + case "baseitem": + case "itemGroup": + case "magicvariant": + case "_specificVariant": + { + return !Renderer.item.isExcluded(it); + } + + case "race": + { + if (this._isExcludedRaceSubrace(it)) + return false; + } + } + + return !ExcludeUtil.isExcluded(UrlUtil.URL_TO_HASH_BUILDER[it.__prop](it), it.__prop, SourceUtil.getEntitySource(it), { + isNoCount: true + }, ); + } + ); + } + ); + } + + return mergedData; + } + + static async pHandleBackgroundLoad({pLoad, isBackground=false, cntSources=null}) { + const pTimeout = isBackground ? MiscUtil.pDelay(500, VeCt.SYM_UTIL_TIMEOUT) : null; + + const promises = [pLoad, pTimeout].filter(Boolean); + + const winner = await Promise.race(promises); + if (winner === VeCt.SYM_UTIL_TIMEOUT) + ui.notifications.info(`Please wait while ${cntSources != null ? `${cntSources} source${cntSources === 1 ? " is" : "s are"}` : "data is being"} loaded...`); + return pLoad; + } + + static _IGNORED_KEYS = new Set(["_meta", "$schema", ]); + + static async pGetAllContent({sources, uploadedFileMetas, customUrls, isBackground=false, userData, cacheKeys=null, + page, isDedupable=false, fnGetDedupedData=null, fnGetBlocklistFilteredData=null, isAutoSelectAll=false, }, ) { + const allContent = []; + + if (isAutoSelectAll && this.isTooManySources({ cntSources: sources.length })) { + const ptHelp = `This may take a (very) long time! If this seems like too much, ${game.user.isGM ? "your GM" : "you"} may have to adjust ${game.user.isGM ? "your" : "the"} "Data Sources" Config options/${game.user.isGM ? "your" : "the"} "World Data Source Selector" list to limit the number of sources selected by default.`; + + console.warn(...LGT, `${sources.length} source${sources.length === 1 ? "" : "s"} are being loaded! ${ptHelp}`); + + if (!(await InputUiUtil.pGetUserBoolean({ + title: "Too Many Sources", + htmlDescription: `You are about to load ${sources.length} source${sources.length === 1 ? "" + : "s"}. ${ptHelp}
    Would you like to load ${sources.length} source${sources.length === 1 ? "" : "s"}?`, + textNo: "Cancel", + textYes: "Continue", + }))) + return null; + } + + const pLoad = sources.pMap(async source=>{ + await source.pLoadAndAddToAllContent({ uploadedFileMetas, customUrls, allContent, cacheKeys }); + }); + + await UtilDataSource.pHandleBackgroundLoad({ + pLoad, + isBackground, + cntSources: sources.length + }); + + const allContentMerged = {}; + + if (allContent.length === 1) + Object.assign(allContentMerged, allContent[0]); + else { + allContent.forEach(obj=>{ + Object.entries(obj).forEach(([k,v])=>{ + if (v == null) + return; + if (this._IGNORED_KEYS.has(k)) + return; + + if (!(v instanceof Array)) + console.warn(`Could not merge "${typeof v}" for key "${k}"!`); + + allContentMerged[k] = allContentMerged[k] || []; + allContentMerged[k] = [...allContentMerged[k], ...v]; + } + ); + }); + } + + let dedupedAllContentMerged = fnGetDedupedData ? fnGetDedupedData({ + allContentMerged, + isDedupable + }) : this._getDedupedAllContentMerged({ + allContentMerged, + isDedupable + }); + + dedupedAllContentMerged = fnGetBlocklistFilteredData ? fnGetBlocklistFilteredData({ + dedupedAllContentMerged, + page + }) : this._getBlocklistFilteredData({ + dedupedAllContentMerged, + page + }); + + if (Config.get("import", "isShowVariantsInLists")) { + Object.entries(dedupedAllContentMerged).forEach(([k,arr])=>{ + if (!(arr instanceof Array)) + return; + dedupedAllContentMerged[k] = arr.map(it=>[it, ...DataUtil.proxy.getVersions(it.__prop, it)]).flat(); + } + ); + } + + Object.entries(dedupedAllContentMerged).forEach(([k,arr])=>{ + if (!(arr instanceof Array)) + return; + if (!arr.length) + delete dedupedAllContentMerged[k]; + } + ); + + return { + dedupedAllContentMerged, + cacheKeys, + userData + }; + } + + static isTooManySources({cntSources}) { + return Config.get("dataSources", "tooManySourcesWarningThreshold") != null && cntSources >= Config.get("dataSources", "tooManySourcesWarningThreshold"); + } + + static _getBlocklistFilteredData({dedupedAllContentMerged, page}) { + if (!UrlUtil.URL_TO_HASH_BUILDER[page]) + return dedupedAllContentMerged; + dedupedAllContentMerged = { + ...dedupedAllContentMerged + }; + Object.entries(dedupedAllContentMerged).forEach(([k,arr])=>{ + if (!(arr instanceof Array)) + return; + dedupedAllContentMerged[k] = arr.filter(it=>{ + if (it.source === VeCt.STR_GENERIC) + return false; + + if (!SourceUtil.getEntitySource(it)) { + console.warn(`Entity did not have a "source"! This should never occur.`); + return true; + } + if (!it.__prop) { + console.warn(`Entity did not have a "__prop"! This should never occur.`); + return true; + } + + switch (it.__prop) { + case "item": + case "baseitem": + case "itemGroup": + case "magicvariant": + case "_specificVariant": + { + return !Renderer.item.isExcluded(it); + } + + case "race": + { + if (this._isExcludedRaceSubrace(it)) + return false; + } + } + + return !ExcludeUtil.isExcluded((UrlUtil.URL_TO_HASH_BUILDER[it.__prop] || UrlUtil.URL_TO_HASH_BUILDER[page])(it), it.__prop, SourceUtil.getEntitySource(it), { + isNoCount: true + }, ); + } + ); + } + ); + return dedupedAllContentMerged; + } + + static _isExcludedRaceSubrace(it) { + if (it.__prop !== "race") + return false; + return it._subraceName && ExcludeUtil.isExcluded(UrlUtil.URL_TO_HASH_BUILDER["subrace"]({ + name: it._subraceName, + source: it.source, + raceName: it._baseName, + raceSource: it._baseSource + }), "subrace", SourceUtil.getEntitySource(it), { + isNoCount: true + }, ); + } + + static _getDedupedAllContentMerged({allContentMerged, page, isDedupable=false}) { + if (!isDedupable) + return allContentMerged; + return this._getDedupedData({ + allContentMerged, + page + }); + } + + static _getDedupedData({allContentMerged, page}) { + if (!UrlUtil.URL_TO_HASH_BUILDER[page]) + return allContentMerged; + + const contentHashes = new Set(); + Object.entries(allContentMerged).forEach(([k,arr])=>{ + if (!(arr instanceof Array)) + return; + allContentMerged[k] = arr.filter(it=>{ + const fnGetHash = UrlUtil.URL_TO_HASH_BUILDER[page]; + if (!fnGetHash) + return true; + const hash = fnGetHash(it); + if (contentHashes.has(hash)) + return false; + contentHashes.add(hash); + return true; + } + ); + } + ); + + return allContentMerged; + } + + static async pPostLoadGeneric({isPrerelease, isBrew}, out) { + out = { ...out }; + + if ((isBrew || isPrerelease) && (out.race || out.subrace)) { + const nxt = await Charactermancer_Race_Util.pPostLoadPrereleaseBrew(out); + Object.assign(out, nxt || {}); + } + + if ((isBrew || isPrerelease) && (out.item || out.baseitem || out.magicvariant || out.itemGroup)) { + if (isBrew) + out.item = await Vetools.pGetBrewItems(out); + else if (isPrerelease) + out.item = await Vetools.pGetPrereleaseItems(out); + + delete out.baseitem; + delete out.magicvariant; + delete out.itemProperty; + delete out.itemType; + delete out.itemGroup; + } + + return out; + } + + static getSourceFilterTypes(src) { + return SourceUtil.isPrereleaseSource(src) ? [UtilDataSource.SOURCE_TYP_PRERELEASE] : SourceUtil.isNonstandardSource(src) ? [UtilDataSource.SOURCE_TYP_EXTRAS] : [UtilDataSource.SOURCE_TYP_OFFICIAL_SINGLE]; + } + + static getSourcesCustomUrl(nxtOpts={}) { + return [new UtilDataSource.DataSourceUrl("Custom URL","",{ + ...nxtOpts, + filterTypes: [UtilDataSource.SOURCE_TYP_CUSTOM], + isAutoDetectPrereleaseBrew: true, + },), ]; + } + + static getSourcesUploadFile(nxtOpts={}) { + return [new UtilDataSource.DataSourceFile("Upload File",{ + ...nxtOpts, + filterTypes: [UtilDataSource.SOURCE_TYP_CUSTOM], + isAutoDetectPrereleaseBrew: true, + },), ]; + } + + static async pGetSourcesPrerelease(dirsPrerelease, nxtOpts={}) { + return this._pGetSourcesPrereleaseBrew({ + brewUtil: PrereleaseUtil, + localSources: await Vetools.pGetLocalPrereleaseSources(...dirsPrerelease), + sources: await Vetools.pGetPrereleaseSources(...dirsPrerelease), + filterTypesLocal: [UtilDataSource.SOURCE_TYP_PRERELEASE, UtilDataSource.SOURCE_TYP_PRERELEASE_LOCAL], + filterTypes: [UtilDataSource.SOURCE_TYP_PRERELEASE], + nxtOpts, + }); + } + + /** + * @param {string[]} dirsHomebrew + * @param {{pPostLoad:Function}} nxtOpts + * @returns {Promise} + */ + static async pGetSourcesBrew(dirsHomebrew, nxtOpts={}) { + return this._pGetSourcesPrereleaseBrew({ + brewUtil: BrewUtil2, + localSources: await Vetools.pGetLocalBrewSources(...dirsHomebrew), + sources: await Vetools.pGetBrewSources(...dirsHomebrew), + filterTypesLocal: [UtilDataSource.SOURCE_TYP_BREW, UtilDataSource.SOURCE_TYP_BREW_LOCAL], + filterTypes: [UtilDataSource.SOURCE_TYP_BREW], + nxtOpts, + }); + } + + /** + * @param {{localSources:any[], sources:{name:string, url:string, abbreviations:string[]}[], nxtOpts:{pPostLoad:Function}, brewUtil:any, + * filterTypesLocal:string[], filterTypes:string[]}} + */ + static async _pGetSourcesPrereleaseBrew({localSources, sources, nxtOpts, brewUtil, filterTypesLocal, filterTypes}) { + return [...localSources.map(({name, url, abbreviations})=>new UtilDataSource.DataSourceUrl(name,url,{ + ...nxtOpts, + filterTypes: [...filterTypesLocal], + abbreviations, + brewUtil, + isExistingPrereleaseBrew: true, + },)), ...sources.map(({name, url, abbreviations})=>new UtilDataSource.DataSourceUrl(name,url,{ + ...nxtOpts, + filterTypes: [...filterTypes], + abbreviations, + brewUtil, + },)), ]; + } + + + /** + * Returns the prerelease and brew sources from the inputted material + * @param {{_meta:{sources:{json:string}[]}}} json + * @param {boolean} isErrorOnMultiple + * @returns {{isPrerelease: Array, isBrew: Array}} + */ + static getSourceType(json, {isErrorOnMultiple=false}={}) { + const isPrereleasePerSource = (json._meta?.sources || []).map(it=>SourceUtil.isPrereleaseSource(it.json || "")); + const isPrerelease = isPrereleasePerSource.every(it=>it); + const isBrew = isPrereleasePerSource.every(it=>!it); + + if (isPrerelease && isBrew && isErrorOnMultiple) + throw new Error(`Could not determine if data contained homebrew or if data contained prerelease content! Please ensure all homebrew/prerelease files have a valid "_meta.sources", and that no file contains both homebrew and prerelease sources.`); + + return { isPrerelease, isBrew }; + } +} +UtilDataSource.SOURCE_TYP_OFFICIAL_BASE = "Official"; +UtilDataSource.SOURCE_TYP_OFFICIAL_ALL = `${UtilDataSource.SOURCE_TYP_OFFICIAL_BASE} (All)`; +UtilDataSource.SOURCE_TYP_OFFICIAL_SINGLE = `${UtilDataSource.SOURCE_TYP_OFFICIAL_BASE} (Single Source)`; +UtilDataSource.SOURCE_TYP_CUSTOM = "Custom/User"; +UtilDataSource.SOURCE_TYP_EXTRAS = "Extras"; +UtilDataSource.SOURCE_TYP_PRERELEASE = "Prerelease"; +UtilDataSource.SOURCE_TYP_PRERELEASE_LOCAL = "Local Prerelease"; +UtilDataSource.SOURCE_TYP_BREW = "Homebrew"; +UtilDataSource.SOURCE_TYP_BREW_LOCAL = "Local Homebrew"; +UtilDataSource.SOURCE_TYP_UNKNOWN = "Unknown"; + +UtilDataSource.SOURCE_TYPE_ORDER = [UtilDataSource.SOURCE_TYP_OFFICIAL_ALL, UtilDataSource.SOURCE_TYP_CUSTOM, UtilDataSource.SOURCE_TYP_OFFICIAL_SINGLE, UtilDataSource.SOURCE_TYP_EXTRAS, UtilDataSource.SOURCE_TYP_PRERELEASE_LOCAL, UtilDataSource.SOURCE_TYP_PRERELEASE, UtilDataSource.SOURCE_TYP_BREW_LOCAL, UtilDataSource.SOURCE_TYP_BREW, UtilDataSource.SOURCE_TYP_UNKNOWN, ]; + +UtilDataSource.SOURCE_TYPE_ORDER__FILTER = [UtilDataSource.SOURCE_TYP_OFFICIAL_ALL, UtilDataSource.SOURCE_TYP_OFFICIAL_SINGLE, UtilDataSource.SOURCE_TYP_EXTRAS, UtilDataSource.SOURCE_TYP_PRERELEASE_LOCAL, UtilDataSource.SOURCE_TYP_PRERELEASE, UtilDataSource.SOURCE_TYP_BREW_LOCAL, UtilDataSource.SOURCE_TYP_BREW, UtilDataSource.SOURCE_TYP_CUSTOM, UtilDataSource.SOURCE_TYP_UNKNOWN, ]; + +UtilDataSource.DataSourceBase = class { + /** + * @param {string} name + * @param {{pPostLoad:Function, filterTypes:string[], abbreviations:string[], brewUtil:any, isExistingPrereleaseBrew:boolean}} opts + */ + constructor(name, opts) { + this.name = name; + + this._pPostLoad = opts.pPostLoad; + this._brewUtil = opts.brewUtil; + this._isAutoDetectPrereleaseBrew = !!opts.isAutoDetectPrereleaseBrew; + this._isExistingPrereleaseBrew = !!opts.isExistingPrereleaseBrew; + this.filterTypes = opts.filterTypes || [UtilDataSource.SOURCE_TYP_UNKNOWN]; + this.isDefault = !!opts.isDefault; + this.abbreviations = opts.abbreviations; + this.isWorldSelectable = !!opts.isWorldSelectable; + } + + get identifier() { + throw new Error(`Unimplemented!`); + } + get identifierWorld() { + return this.isDefault ? "5etools" : this.identifier; + } + + isCacheable() { + throw new Error("Unimplemented!"); + } + async pGetOutputs({uploadedFileMetas, customUrls}) { + throw new Error("Unimplemented!"); + } + + async _pGetBrewUtil(...args) { + if (this._brewUtil) + return this._brewUtil; + if (!this._isAutoDetectPrereleaseBrew) + return null; + return this._pGetBrewUtilAutodetected(...args); + } + + async _pGetBrewUtilAutodetected(...args) { + throw new Error("Unimplemented!"); + } + + async pLoadAndAddToAllContent({uploadedFileMetas, customUrls, allContent, cacheKeys=null}) { + const meta = await this.pGetOutputs({ + uploadedFileMetas, + customUrls + }); + allContent.push(...meta.contents); + if (cacheKeys && this.isCacheable()) + cacheKeys.push(...meta.cacheKeys); + } +}; + +UtilDataSource.DataSourceUrl = class extends UtilDataSource.DataSourceBase { + /** + * @param {string} name + * @param {string} url + * @param {{pPostLoad:Function, filterTypes:string[], abbreviations:string[], brewUtil:any, isExistingPrereleaseBrew:boolean}} opts + */ + constructor(name, url, opts) { + opts = opts || {}; + + super(name, { + isWorldSelectable: !!url, + ...opts + }); + + this.url = url; + this.source = opts.source; + this.userData = opts.userData; + } + + get identifier() { + return this.url === "" ? `VE_SOURCE_CUSTOM_URL` : this.url; + } + get identifierWorld() { + return this.source ?? super.identifierWorld; + } + + isCacheable() { + return true; + } + + async pGetOutputs({uploadedFileMetas, customUrls}) { + if (this.url === "") { + customUrls = customUrls || []; + + let loadedDatas; + try { + loadedDatas = await Promise.all(customUrls.map(async url=>{ + const brewUtil = await this._pGetBrewUtil(url); + if (brewUtil && !this._isExistingPrereleaseBrew) + await brewUtil.pAddBrewFromUrl(url); + + const data = await DataUtil.loadJSON(url); + return this._pPostLoad ? this._pPostLoad(data, this.userData) : data; + } + )); + } catch (e) { + ui.notifications.error(`Failed to load one or more URLs! ${VeCt.STR_SEE_CONSOLE}`); + throw e; + } + + return { + cacheKeys: customUrls, + contents: loadedDatas, + }; + } + + let data; + try { + const brewUtil = await this._pGetBrewUtil(this.url); + if (brewUtil && !this._isExistingPrereleaseBrew) + await brewUtil.pAddBrewFromUrl(this.url); + + data = await DataUtil.loadJSON(this.url); + if (this._pPostLoad) + data = await this._pPostLoad(data, this.userData); + } catch (e) { + const msg = `Failed to load URL "${this.url}"!`; + //console.ui.notifications.error(`${msg} ${VeCt.STR_SEE_CONSOLE}`); + console.error(msg); + throw e; + } + return { + cacheKeys: [this.url], + contents: [data], + }; + } + + async _pGetBrewUtilAutodetected(url) { + const json = await DataUtil.loadJSON(url); + const {isPrerelease, isBrew} = UtilDataSource.getSourceType(json, { + isErrorOnMultiple: true + }); + if (isPrerelease) + return PrereleaseUtil; + if (isBrew) + return BrewUtil2; + return null; + } +}; + +UtilDataSource.DataSourceFile = class extends UtilDataSource.DataSourceBase { + constructor(name, opts) { + opts = opts || {}; + + super(name, { + isWorldSelectable: false, + ...opts + }); + + this.isFile = true; + } + + get identifier() { + return `VE_SOURCE_CUSTOM_FILE`; + } + + isCacheable() { + return false; + } + + async pGetOutputs({uploadedFileMetas, customUrls}) { + uploadedFileMetas = uploadedFileMetas || []; + + const allContent = await uploadedFileMetas.pMap(async fileMeta=>{ + if (!fileMeta) + return null; + + const brewUtil = await this._pGetBrewUtil(fileMeta.contents); + if (brewUtil && !this._isExistingPrereleaseBrew) + await brewUtil.pAddBrewsFromFiles([{ + json: fileMeta.contents, + name: fileMeta.name + }]); + + const contents = await DataUtil.pDoMetaMerge(CryptUtil.uid(), MiscUtil.copyFast(fileMeta.contents)); + + return this._pPostLoad ? this._pPostLoad(contents, this.userData) : contents; + } + ); + + return { + contents: allContent.filter(it=>it != null), + }; + } + + async _pGetBrewUtilAutodetected(json) { + const {isPrerelease, isBrew} = UtilDataSource.getSourceType(json, { + isErrorOnMultiple: true + }); + if (isPrerelease) + return PrereleaseUtil; + if (isBrew) + return BrewUtil2; + return null; + } +}; + +UtilDataSource.DataSourceSpecial = class extends UtilDataSource.DataSourceBase { + + /** + * @param {string} name + * @param {any} pGet + * @param {{cacheKey:string}} opts + * @returns {any} + */ + constructor(name, pGet, opts) { + opts = opts || {}; + + super(name, { isWorldSelectable: true, ...opts }); + this.special = { pGet }; + if (!opts.cacheKey) { throw new Error(`No cache key specified!`); } + this.cacheKey = opts.cacheKey; + } + + get identifier() { + return this.cacheKey; + } + + isCacheable() { + return true; + } + + async pGetOutputs({uploadedFileMetas, customUrls}) { + let loadedData; + try { + const json = await Vetools.pLoadImporterSourceSpecial(this); + loadedData = json; + if (this._pPostLoad) + loadedData = await this._pPostLoad(loadedData, json, this.userData); + } catch (e) { + //ui.notifications + console.error(`Failed to load pre-defined source "${this.cacheKey}"! ${VeCt.STR_SEE_CONSOLE}`); + throw e; + } + return { + cacheKeys: [this.cacheKey], + contents: [loadedData], + }; + } + + async _pGetBrewUtilAutodetected() { + throw new Error("Unimplemented!"); + } +}; +//#endregion + +//#region List +class ListItem { + constructor(ix, ele, name, values, data) { + this.ix = ix; + this.ele = ele; + this.name = name; + this.values = values || {}; + this.data = data || {}; + + this.searchText = null; + this.mutRegenSearchText(); + + this._isSelected = false; + } + + mutRegenSearchText() { + let searchText = `${this.name} - `; + for (const k in this.values) { + const v = this.values[k]; + if (!v) + continue; + searchText += `${v} - `; + } + this.searchText = searchText.toAscii().toLowerCase(); + } + + set isSelected(val) { + if (this._isSelected === val) + return; + this._isSelected = val; + + if (this.ele instanceof $) { + if (this._isSelected) + this.ele.addClass("list-multi-selected"); + else + this.ele.removeClass("list-multi-selected"); + } else { + if (this._isSelected) + this.ele.classList.add("list-multi-selected"); + else + this.ele.classList.remove("list-multi-selected"); + } + } + + get isSelected() { + return this._isSelected; + } +} + +class _ListSearch { + #isInterrupted = false; + + #term = null; + #fn = null; + #items = null; + + constructor({term, fn, items}) { + this.#term = term; + this.#fn = fn; + this.#items = [...items]; + } + + interrupt() { + this.#isInterrupted = true; + } + + async pRun() { + const out = []; + for (const item of this.#items) { + if (this.#isInterrupted) + break; + if (await this.#fn(item, this.#term)) + out.push(item); + } + return { + isInterrupted: this.#isInterrupted, + searchedItems: out + }; + } +} + +class List { + #activeSearch = null; + + constructor(opts) { + if (opts.fnSearch && opts.isFuzzy) + throw new Error(`The options "fnSearch" and "isFuzzy" are mutually incompatible!`); + + this._$iptSearch = opts.$iptSearch; + this._$wrpList = opts.$wrpList; + this._fnSort = opts.fnSort === undefined ? SortUtil.listSort : opts.fnSort; + this._fnSearch = opts.fnSearch; + this._syntax = opts.syntax; + this._isFuzzy = !!opts.isFuzzy; + this._isSkipSearchKeybindingEnter = !!opts.isSkipSearchKeybindingEnter; + this._helpText = opts.helpText; + + this._items = []; + this._eventHandlers = {}; + + this._searchTerm = List._DEFAULTS.searchTerm; + this._sortBy = opts.sortByInitial || List._DEFAULTS.sortBy; + this._sortDir = opts.sortDirInitial || List._DEFAULTS.sortDir; + this._sortByInitial = this._sortBy; + this._sortDirInitial = this._sortDir; + this._fnFilter = null; + this._isUseJquery = opts.isUseJquery; + + if (this._isFuzzy) + this._initFuzzySearch(); + + this._searchedItems = []; + this._filteredItems = []; + this._sortedItems = []; + + this._isInit = false; + this._isDirty = false; + + this._prevList = null; + this._nextList = null; + this._lastSelection = null; + this._isMultiSelection = false; + } + + get items() { + return this._items; + } + get visibleItems() { + return this._sortedItems; + } + get sortBy() { + return this._sortBy; + } + get sortDir() { + return this._sortDir; + } + set nextList(list) { + this._nextList = list; + } + set prevList(list) { + this._prevList = list; + } + + setFnSearch(fn) { + this._fnSearch = fn; + this._isDirty = true; + } + + init() { + if (this._isInit){return;} + + if (this._$iptSearch) { + UiUtil.bindTypingEnd({ + $ipt: this._$iptSearch, + fnKeyup: ()=>this.search(this._$iptSearch.val()) + }); + this._searchTerm = List.getCleanSearchTerm(this._$iptSearch.val()); + this._init_bindKeydowns(); + + const helpText = [...(this._helpText || []), ...Object.values(this._syntax || {}).filter(({help})=>help).map(({help})=>help), ]; + + if (helpText.length) + this._$iptSearch.title(helpText.join(" ")); + } + + this._doSearch(); + this._isInit = true; + } + + _init_bindKeydowns() { + this._$iptSearch.on("keydown", evt=>{ + if (evt._List__isHandled) + return; + + switch (evt.key) { + case "Escape": + return this._handleKeydown_escape(evt); + case "Enter": + return this._handleKeydown_enter(evt); + } + } + ); + } + + _handleKeydown_escape(evt) { + evt._List__isHandled = true; + + if (!this._$iptSearch.val()) { + $(document.activeElement).blur(); + return; + } + + this._$iptSearch.val(""); + this.search(""); + } + + _handleKeydown_enter(evt) { + if (this._isSkipSearchKeybindingEnter) + return; + + if (IS_VTT) + return; + if (!EventUtil.noModifierKeys(evt)) + return; + + const firstVisibleItem = this.visibleItems[0]; + if (!firstVisibleItem) + return; + + evt._List__isHandled = true; + + $(firstVisibleItem.ele).click(); + if (firstVisibleItem.values.hash) + window.location.hash = firstVisibleItem.values.hash; + } + + _initFuzzySearch() { + elasticlunr.clearStopWords(); + this._fuzzySearch = elasticlunr(function() { + this.addField("s"); + this.setRef("ix"); + }); + SearchUtil.removeStemmer(this._fuzzySearch); + } + + update({isForce=false}={}) { + if (!this._isInit || !this._isDirty || isForce) + return false; + this._doSearch(); + return true; + } + + _doSearch() { + this._doSearch_doInterruptExistingSearch(); + this._doSearch_doSearchTerm(); + this._doSearch_doPostSearchTerm(); + } + + _doSearch_doInterruptExistingSearch() { + if (!this.#activeSearch) + return; + this.#activeSearch.interrupt(); + this.#activeSearch = null; + } + + _doSearch_doSearchTerm() { + if (this._doSearch_doSearchTerm_preSyntax()) + return; + + const matchingSyntax = this._doSearch_getMatchingSyntax(); + if (matchingSyntax) { + if (this._doSearch_doSearchTerm_syntax(matchingSyntax)) + return; + + this._searchedItems = []; + this._doSearch_doSearchTerm_pSyntax(matchingSyntax).then(isContinue=>{ + if (!isContinue) + return; + this._doSearch_doPostSearchTerm(); + } + ); + + return; + } + + if (this._isFuzzy) + return this._searchedItems = this._doSearch_doSearchTerm_fuzzy(); + + if (this._fnSearch) + return this._searchedItems = this._items.filter(it=>this._fnSearch(it, this._searchTerm)); + + this._searchedItems = this._items.filter(it=>this.constructor.isVisibleDefaultSearch(it, this._searchTerm)); + } + + _doSearch_doSearchTerm_preSyntax() { + if (!this._searchTerm && !this._fnSearch) { + this._searchedItems = [...this._items]; + return true; + } + } + + _doSearch_getMatchingSyntax() { + const [command,term] = this._searchTerm.split(/^([a-z]+):/).filter(Boolean); + if (!command || !term || !this._syntax?.[command]) + return null; + return { + term: this._doSearch_getSyntaxSearchTerm(term), + syntax: this._syntax[command] + }; + } + + _doSearch_getSyntaxSearchTerm(term) { + if (!term.startsWith("/") || !term.endsWith("/")) + return term; + try { + return new RegExp(term.slice(1, -1)); + } catch (ignored) { + return term; + } + } + + _doSearch_doSearchTerm_syntax({term, syntax: {fn, isAsync}}) { + if (isAsync) + return false; + + this._searchedItems = this._items.filter(it=>fn(it, term)); + return true; + } + + async _doSearch_doSearchTerm_pSyntax({term, syntax: {fn, isAsync}}) { + if (!isAsync) + return false; + + this.#activeSearch = new _ListSearch({ + term, + fn, + items: this._items, + }); + const {isInterrupted, searchedItems} = await this.#activeSearch.pRun(); + + if (isInterrupted) + return false; + this._searchedItems = searchedItems; + return true; + } + + static isVisibleDefaultSearch(li, searchTerm) { + return li.searchText.includes(searchTerm); + } + + _doSearch_doSearchTerm_fuzzy() { + const results = this._fuzzySearch.search(this._searchTerm, { + fields: { + s: { + expand: true + }, + }, + bool: "AND", + expand: true, + }, ); + + return results.map(res=>this._items[res.doc.ix]); + } + + _doSearch_doPostSearchTerm() { + this._searchedItems = this._searchedItems.filter(it=>!it.data.isExcluded); + + this._doFilter(); + } + + getFilteredItems({items=null, fnFilter}={}) { + items = items || this._searchedItems; + fnFilter = fnFilter || this._fnFilter; + + if (!fnFilter) + return items; + + return items.filter(it=>fnFilter(it)); + } + + _doFilter() { + this._filteredItems = this.getFilteredItems(); + this._doSort(); + } + + getSortedItems({items=null}={}) { + items = items || [...this._filteredItems]; + + const opts = { + sortBy: this._sortBy, + sortDir: this._sortDir, + }; + if (this._fnSort) + items.sort((a,b)=>this._fnSort(a, b, opts)); + if (this._sortDir === "desc") + items.reverse(); + + return items; + } + + _doSort() { + this._sortedItems = this.getSortedItems(); + this._doRender(); + } + + _doRender() { + const len = this._sortedItems.length; + + if (this._isUseJquery) { + this._$wrpList.children().detach(); + for (let i = 0; i < len; ++i) + this._$wrpList.append(this._sortedItems[i].ele); + } else { + this._$wrpList[0].innerHTML = ""; + const frag = document.createDocumentFragment(); + for (let i = 0; i < len; ++i) + frag.appendChild(this._sortedItems[i].ele); + this._$wrpList[0].appendChild(frag); + } + + this._isDirty = false; + this._trigger("updated"); + } + + search(searchTerm) { + const nextTerm = List.getCleanSearchTerm(searchTerm); + if (nextTerm === this._searchTerm) + return; + this._searchTerm = nextTerm; + return this._doSearch(); + } + + filter(fnFilter) { + if (this._fnFilter === fnFilter) + return; + this._fnFilter = fnFilter; + this._doFilter(); + } + + sort(sortBy, sortDir) { + if (this._sortBy !== sortBy || this._sortDir !== sortDir) { + this._sortBy = sortBy; + this._sortDir = sortDir; + this._doSort(); + } + } + + reset() { + if (this._searchTerm !== List._DEFAULTS.searchTerm) { + this._searchTerm = List._DEFAULTS.searchTerm; + return this._doSearch(); + } else if (this._sortBy !== this._sortByInitial || this._sortDir !== this._sortDirInitial) { + this._sortBy = this._sortByInitial; + this._sortDir = this._sortDirInitial; + } + } + + addItem(listItem) { + this._isDirty = true; + this._items.push(listItem); + + if (this._isFuzzy) + this._fuzzySearch.addDoc({ + ix: listItem.ix, + s: listItem.searchText + }); + } + + removeItem(listItem) { + const ixItem = this._items.indexOf(listItem); + return this.removeItemByIndex(listItem.ix, ixItem); + } + + removeItemByIndex(ix, ixItem) { + ixItem = ixItem ?? this._items.findIndex(it=>it.ix === ix); + if (!~ixItem) + return; + + this._isDirty = true; + const removed = this._items.splice(ixItem, 1); + + if (this._isFuzzy) + this._fuzzySearch.removeDocByRef(ix); + + return removed[0]; + } + + removeItemBy(valueName, value) { + const ixItem = this._items.findIndex(it=>it.values[valueName] === value); + return this.removeItemByIndex(ixItem, ixItem); + } + + removeItemByData(dataName, value) { + const ixItem = this._items.findIndex(it=>it.data[dataName] === value); + return this.removeItemByIndex(ixItem, ixItem); + } + + removeAllItems() { + this._isDirty = true; + this._items = []; + if (this._isFuzzy) + this._initFuzzySearch(); + } + + on(eventName, handler) { + (this._eventHandlers[eventName] = this._eventHandlers[eventName] || []).push(handler); + } + + off(eventName, handler) { + if (!this._eventHandlers[eventName]) + return false; + const ix = this._eventHandlers[eventName].indexOf(handler); + if (!~ix) + return false; + this._eventHandlers[eventName].splice(ix, 1); + return true; + } + + _trigger(eventName) { + (this._eventHandlers[eventName] || []).forEach(fn=>fn()); + } + + doAbsorbItems(dataArr, opts) { + const children = [...this._$wrpList[0].children]; + + const len = children.length; + if (len !== dataArr.length) + throw new Error(`Data source length and list element length did not match!`); + + for (let i = 0; i < len; ++i) { + const node = children[i]; + const dataItem = dataArr[i]; + const listItem = new ListItem(i,node,opts.fnGetName(dataItem),opts.fnGetValues ? opts.fnGetValues(dataItem) : {},{},); + if (opts.fnGetData) + listItem.data = opts.fnGetData(listItem, dataItem); + if (opts.fnBindListeners) + opts.fnBindListeners(listItem, dataItem); + this.addItem(listItem); + } + } + + doSelect(item, evt) { + if (evt && evt.shiftKey) { + evt.preventDefault(); + if (this._prevList && this._prevList._lastSelection) { + this._prevList._selectFromItemToEnd(this._prevList._lastSelection, true); + this._selectToItemFromStart(item); + } else if (this._nextList && this._nextList._lastSelection) { + this._nextList._selectToItemFromStart(this._nextList._lastSelection, true); + this._selectFromItemToEnd(item); + } else if (this._lastSelection && this.visibleItems.includes(item)) { + this._doSelect_doMulti(item); + } else { + this._doSelect_doSingle(item); + } + } else + this._doSelect_doSingle(item); + } + + _doSelect_doSingle(item) { + if (this._isMultiSelection) { + this.deselectAll(); + if (this._prevList) + this._prevList.deselectAll(); + if (this._nextList) + this._nextList.deselectAll(); + } else if (this._lastSelection) + this._lastSelection.isSelected = false; + + item.isSelected = true; + this._lastSelection = item; + } + + _doSelect_doMulti(item) { + this._selectFromItemToItem(this._lastSelection, item); + + if (this._prevList && this._prevList._isMultiSelection) { + this._prevList.deselectAll(); + } + + if (this._nextList && this._nextList._isMultiSelection) { + this._nextList.deselectAll(); + } + } + + _selectFromItemToEnd(item, isKeepLastSelection=false) { + this.deselectAll(isKeepLastSelection); + this._isMultiSelection = true; + const ixStart = this.visibleItems.indexOf(item); + const len = this.visibleItems.length; + for (let i = ixStart; i < len; ++i) { + this.visibleItems[i].isSelected = true; + } + } + + _selectToItemFromStart(item, isKeepLastSelection=false) { + this.deselectAll(isKeepLastSelection); + this._isMultiSelection = true; + const ixEnd = this.visibleItems.indexOf(item); + for (let i = 0; i <= ixEnd; ++i) { + this.visibleItems[i].isSelected = true; + } + } + + _selectFromItemToItem(item1, item2) { + this.deselectAll(true); + + if (item1 === item2) { + if (this._lastSelection) + this._lastSelection.isSelected = false; + item1.isSelected = true; + this._lastSelection = item1; + return; + } + + const ix1 = this.visibleItems.indexOf(item1); + const ix2 = this.visibleItems.indexOf(item2); + + this._isMultiSelection = true; + const [ixStart,ixEnd] = [ix1, ix2].sort(SortUtil.ascSort); + for (let i = ixStart; i <= ixEnd; ++i) { + this.visibleItems[i].isSelected = true; + } + } + + deselectAll(isKeepLastSelection=false) { + if (!isKeepLastSelection) + this._lastSelection = null; + this._isMultiSelection = false; + this._items.forEach(it=>it.isSelected = false); + } + + updateSelected(item) { + if (this.visibleItems.includes(item)) { + if (this._isMultiSelection) + this.deselectAll(true); + + if (this._lastSelection && this._lastSelection !== item) + this._lastSelection.isSelected = false; + + item.isSelected = true; + this._lastSelection = item; + } else + this.deselectAll(); + } + + getSelected() { + return this.visibleItems.filter(it=>it.isSelected); + } + + static getCleanSearchTerm(str) { + return (str || "").toAscii().trim().toLowerCase().split(/\s+/g).join(" "); + } +} +; +List._DEFAULTS = { + searchTerm: "", + sortBy: "name", + sortDir: "asc", + fnFilter: null, +}; +//#endregion + + +//#region TabUIUtil +class TabUiUtilBase { + static decorate(obj, {isInitMeta=false}={}) { + if (isInitMeta) { + obj.__meta = {}; + obj._meta = obj._getProxy("meta", obj.__meta); + } + + obj.__tabState = {}; + + obj._getTabProps = function({propProxy=TabUiUtilBase._DEFAULT_PROP_PROXY, tabGroup=TabUiUtilBase._DEFAULT_TAB_GROUP}={}) { + return { + propProxy, + _propProxy: `_${propProxy}`, + __propProxy: `__${propProxy}`, + propActive: `ixActiveTab__${tabGroup}`, + }; + } + ; + + obj._renderTabs = function(tabMetas, {$parent, propProxy=TabUiUtilBase._DEFAULT_PROP_PROXY, tabGroup=TabUiUtilBase._DEFAULT_TAB_GROUP, cbTabChange, additionalClassesWrpHeads}={}) { + if (!tabMetas.length) + throw new Error(`One or more tab meta must be specified!`); + obj._resetTabs({ + tabGroup + }); + + const isSingleTab = tabMetas.length === 1; + + const {propActive, _propProxy, __propProxy} = obj._getTabProps({ + propProxy, + tabGroup + }); + + this[__propProxy][propActive] = this[__propProxy][propActive] || 0; + + const $dispTabTitle = obj.__$getDispTabTitle({ + isSingleTab + }); + + const renderTabMetas_standard = (it,i)=>{ + const $btnTab = obj.__$getBtnTab({ + isSingleTab, + tabMeta: it, + _propProxy, + propActive, + ixTab: i, + }); + + const $wrpTab = obj.__$getWrpTab({ + tabMeta: it, + ixTab: i + }); + + return { + ...it, + ix: i, + $btnTab, + $wrpTab, + }; + } + ; + + const tabMetasOut = tabMetas.map((it,i)=>{ + if (it.type) + return obj.__renderTypedTabMeta({ + tabMeta: it, + ixTab: i + }); + return renderTabMetas_standard(it, i); + } + ).filter(Boolean); + + if ($parent) + obj.__renderTabs_addToParent({ + $dispTabTitle, + $parent, + tabMetasOut, + additionalClassesWrpHeads + }); + + const hkActiveTab = ()=>{ + tabMetasOut.forEach(it=>{ + if (it.type) + return; + const isActive = it.ix === this[_propProxy][propActive]; + if (isActive && $dispTabTitle) + $dispTabTitle.text(isSingleTab ? "" : it.name); + if (it.$btnTab) + it.$btnTab.toggleClass("active", isActive); + it.$wrpTab.toggleVe(isActive); + } + ); + + if (cbTabChange) + cbTabChange(); + } + ; + this._addHook(propProxy, propActive, hkActiveTab); + hkActiveTab(); + + obj.__tabState[tabGroup] = { + fnReset: ()=>{ + this._removeHook(propProxy, propActive, hkActiveTab); + } + , + tabMetasOut, + }; + + return tabMetasOut; + } + ; + + obj.__renderTabs_addToParent = function({$dispTabTitle, $parent, tabMetasOut, additionalClassesWrpHeads}) { + const hasBorder = tabMetasOut.some(it=>it.hasBorder); + $$`
    + ${$dispTabTitle} +
    +
    ${tabMetasOut.map(it=>it.$btnTab)}
    +
    ${tabMetasOut.map(it=>it.$wrpTab).filter(Boolean)}
    +
    +
    `.appendTo($parent); + } + ; + + obj._resetTabs = function({tabGroup=TabUiUtilBase._DEFAULT_TAB_GROUP}={}) { + if (!obj.__tabState[tabGroup]) + return; + obj.__tabState[tabGroup].fnReset(); + delete obj.__tabState[tabGroup]; + } + ; + + obj._hasPrevTab = function({propProxy=TabUiUtilBase._DEFAULT_PROP_PROXY, tabGroup=TabUiUtilBase._DEFAULT_TAB_GROUP}={}) { + return obj.__hasTab({ + propProxy, + tabGroup, + offset: -1 + }); + } + ; + obj._hasNextTab = function({propProxy=TabUiUtilBase._DEFAULT_PROP_PROXY, tabGroup=TabUiUtilBase._DEFAULT_TAB_GROUP}={}) { + return obj.__hasTab({ + propProxy, + tabGroup, + offset: 1 + }); + } + ; + + obj.__hasTab = function({propProxy=TabUiUtilBase._DEFAULT_PROP_PROXY, tabGroup=TabUiUtilBase._DEFAULT_TAB_GROUP, offset}) { + const {propActive, _propProxy} = obj._getTabProps({ + propProxy, + tabGroup + }); + const ixActive = obj[_propProxy][propActive]; + return !!(obj.__tabState[tabGroup]?.tabMetasOut && obj.__tabState[tabGroup]?.tabMetasOut[ixActive + offset]); + } + ; + + obj._doSwitchToPrevTab = function({propProxy=TabUiUtilBase._DEFAULT_PROP_PROXY, tabGroup=TabUiUtilBase._DEFAULT_TAB_GROUP}={}) { + return obj.__doSwitchToTab({ + propProxy, + tabGroup, + offset: -1 + }); + } + ; + obj._doSwitchToNextTab = function({propProxy=TabUiUtilBase._DEFAULT_PROP_PROXY, tabGroup=TabUiUtilBase._DEFAULT_TAB_GROUP}={}) { + return obj.__doSwitchToTab({ + propProxy, + tabGroup, + offset: 1 + }); + } + ; + + obj.__doSwitchToTab = function({propProxy=TabUiUtilBase._DEFAULT_PROP_PROXY, tabGroup=TabUiUtilBase._DEFAULT_TAB_GROUP, offset}) { + if (!obj.__hasTab({ + propProxy, + tabGroup, + offset + })) + return; + const {propActive, _propProxy} = obj._getTabProps({ + propProxy, + tabGroup + }); + obj[_propProxy][propActive] = obj[_propProxy][propActive] + offset; + } + ; + + obj._addHookActiveTab = function(hook, {propProxy=TabUiUtilBase._DEFAULT_PROP_PROXY, tabGroup=TabUiUtilBase._DEFAULT_TAB_GROUP}={}) { + const {propActive} = obj._getTabProps({ + propProxy, + tabGroup + }); + this._addHook(propProxy, propActive, hook); + } + ; + + obj._getIxActiveTab = function({propProxy=TabUiUtilBase._DEFAULT_PROP_PROXY, tabGroup=TabUiUtilBase._DEFAULT_TAB_GROUP}={}) { + const {propActive, _propProxy} = obj._getTabProps({ + propProxy, + tabGroup + }); + return obj[_propProxy][propActive]; + } + ; + + obj._setIxActiveTab = function({propProxy=TabUiUtilBase._DEFAULT_PROP_PROXY, tabGroup=TabUiUtilBase._DEFAULT_TAB_GROUP, ixActiveTab}={}) { + const {propActive, _propProxy} = obj._getTabProps({ + propProxy, + tabGroup + }); + obj[_propProxy][propActive] = ixActiveTab; + } + ; + + obj._getActiveTab = function({propProxy=TabUiUtilBase._DEFAULT_PROP_PROXY, tabGroup=TabUiUtilBase._DEFAULT_TAB_GROUP}={}) { + const tabState = obj.__tabState[tabGroup]; + const ixActiveTab = obj._getIxActiveTab({ + propProxy, + tabGroup + }); + return tabState.tabMetasOut[ixActiveTab]; + } + ; + + obj._setActiveTab = function({propProxy=TabUiUtilBase._DEFAULT_PROP_PROXY, tabGroup=TabUiUtilBase._DEFAULT_TAB_GROUP, tab}) { + const tabState = obj.__tabState[tabGroup]; + const ix = tabState.tabMetasOut.indexOf(tab); + obj._setIxActiveTab({ + propProxy, + tabGroup, + ixActiveTab: ix + }); + } + ; + + obj.__$getBtnTab = function() { + throw new Error("Unimplemented!"); + } + ; + obj.__$getWrpTab = function() { + throw new Error("Unimplemented!"); + } + ; + obj.__renderTypedTabMeta = function() { + throw new Error("Unimplemented!"); + } + ; + obj.__$getDispTabTitle = function() { + throw new Error("Unimplemented!"); + } + ; + } +} +TabUiUtilBase._DEFAULT_TAB_GROUP = "_default"; +TabUiUtilBase._DEFAULT_PROP_PROXY = "meta"; + +TabUiUtilBase.TabMeta = class { + constructor({name, icon=null, type=null, buttons=null}={}) { + this.name = name; + this.icon = icon; + this.type = type; + this.buttons = buttons; + } +} +; + +let TabUiUtil$1 = class TabUiUtil extends TabUiUtilBase { + static decorate(obj, {isInitMeta=false}={}) { + super.decorate(obj, { + isInitMeta + }); + + obj.__$getBtnTab = function({tabMeta, _propProxy, propActive, ixTab}) { + return $(``).click(()=>obj[_propProxy][propActive] = ixTab); + } + ; + + obj.__$getWrpTab = function({tabMeta}) { + return $(`
    `); + } + ; + + obj.__renderTypedTabMeta = function({tabMeta, ixTab}) { + switch (tabMeta.type) { + case "buttons": + return obj.__renderTypedTabMeta_buttons({ + tabMeta, + ixTab + }); + default: + throw new Error(`Unhandled tab type "${tabMeta.type}"`); + } + } + ; + + obj.__renderTypedTabMeta_buttons = function({tabMeta, ixTab}) { + const $btns = tabMeta.buttons.map((meta,j)=>{ + const $btn = $(``).click(evt=>meta.pFnClick(evt, $btn)); + return $btn; + } + ); + + const $btnTab = $$`
    ${$btns}
    `; + + return { + ...tabMeta, + ix: ixTab, + $btns, + $btnTab, + }; + } + ; + + obj.__$getDispTabTitle = function() { + return null; + } + ; + } +} +; + +globalThis.TabUiUtil = TabUiUtil$1; + +TabUiUtil$1.TabMeta = class extends TabUiUtilBase.TabMeta { + constructor(opts) { + super(opts); + this.hasBorder = !!opts.hasBorder; + this.hasBackground = !!opts.hasBackground; + this.isHeadHidden = !!opts.isHeadHidden; + this.isNoPadding = !!opts.isNoPadding; + } +} +; + +let TabUiUtilSide$1 = class TabUiUtilSide extends TabUiUtilBase { + static decorate(obj, {isInitMeta=false}={}) { + super.decorate(obj, { + isInitMeta + }); + + obj.__$getBtnTab = function({isSingleTab, tabMeta, _propProxy, propActive, ixTab}) { + return isSingleTab ? null : $(``).click(()=>this[_propProxy][propActive] = ixTab); + } + ; + + obj.__$getWrpTab = function({tabMeta}) { + return $(`
    `); + } + ; + + obj.__renderTabs_addToParent = function({$dispTabTitle, $parent, tabMetasOut}) { + $$`
    + ${$dispTabTitle} +
    +
    ${tabMetasOut.map(it=>it.$btnTab)}
    +
    ${tabMetasOut.map(it=>it.$wrpTab).filter(Boolean)}
    +
    +
    `.appendTo($parent); + } + ; + + obj.__renderTypedTabMeta = function({tabMeta, ixTab}) { + switch (tabMeta.type) { + case "buttons": + return obj.__renderTypedTabMeta_buttons({ + tabMeta, + ixTab + }); + default: + throw new Error(`Unhandled tab type "${tabMeta.type}"`); + } + } + ; + + obj.__renderTypedTabMeta_buttons = function({tabMeta, ixTab}) { + const $btns = tabMeta.buttons.map((meta,j)=>{ + const $btn = $(``).click(evt=>meta.pFnClick(evt, $btn)); + + if (j === tabMeta.buttons.length - 1) + $btn.addClass(`br-0 btr-0 bbr-0`); + + return $btn; + } + ); + + const $btnTab = $$`
    ${$btns}
    `; + + return { + ...tabMeta, + ix: ixTab, + $btnTab, + }; + } + ; + + obj.__$getDispTabTitle = function({isSingleTab}) { + return $(`
    `); + } + ; + } +} +; + +globalThis.TabUiUtilSide = TabUiUtilSide$1; +//#endregion + +//#region ElementUtil +jQuery.fn.disableSpellcheck = function(){ + return this.attr("autocomplete", "new-password").attr("autocapitalize", "off").attr("spellcheck", "false"); +} +jQuery.fn.hideVe = function() { + this.classList.add("ve-hidden"); + return this; +} +globalThis.ElementUtil = { + _ATTRS_NO_FALSY: new Set(["checked", "disabled", ]), + + getOrModify({tag, clazz, style, click, contextmenu, change, mousedown, mouseup, mousemove, pointerdown, pointerup, keydown, html, text, txt, ele, children, outer, + id, name, title, val, href, type, tabindex, value, placeholder, attrs, data, }) { + ele = ele || (outer ? (new DOMParser()).parseFromString(outer, "text/html").body.childNodes[0] : document.createElement(tag)); + + if (clazz) + ele.className = clazz; + if (style) + ele.setAttribute("style", style); + if (click) + ele.addEventListener("click", click); + if (contextmenu) + ele.addEventListener("contextmenu", contextmenu); + if (change) + ele.addEventListener("change", change); + if (mousedown) + ele.addEventListener("mousedown", mousedown); + if (mouseup) + ele.addEventListener("mouseup", mouseup); + if (mousemove) + ele.addEventListener("mousemove", mousemove); + if (pointerdown) + ele.addEventListener("pointerdown", pointerdown); + if (pointerup) + ele.addEventListener("pointerup", pointerup); + if (keydown) + ele.addEventListener("keydown", keydown); + if (html != null) + ele.innerHTML = html; + if (text != null || txt != null) + ele.textContent = text; + if (id != null) + ele.setAttribute("id", id); + if (name != null) + ele.setAttribute("name", name); + if (title != null) + ele.setAttribute("title", title); + if (href != null) + ele.setAttribute("href", href); + if (val != null) + ele.setAttribute("value", val); + if (type != null) + ele.setAttribute("type", type); + if (tabindex != null) + ele.setAttribute("tabindex", tabindex); + if (value != null) + ele.setAttribute("value", value); + if (placeholder != null) + ele.setAttribute("placeholder", placeholder); + + if (attrs != null) { + for (const k in attrs) { + if (attrs[k] === undefined) + continue; + if (!attrs[k] && ElementUtil._ATTRS_NO_FALSY.has(k)) + continue; + ele.setAttribute(k, attrs[k]); + } + } + + if (data != null) { + for (const k in data) { + if (data[k] === undefined) + continue; + ele.dataset[k] = data[k]; + } + } + + if (children) + for (let i = 0, len = children.length; i < len; ++i) + if (children[i] != null) + ele.append(children[i]); + + ele.appends = ele.appends || ElementUtil._appends.bind(ele); + ele.appendTo = ele.appendTo || ElementUtil._appendTo.bind(ele); + ele.prependTo = ele.prependTo || ElementUtil._prependTo.bind(ele); + ele.insertAfter = ele.insertAfter || ElementUtil._insertAfter.bind(ele); + ele.addClass = ele.addClass || ElementUtil._addClass.bind(ele); + ele.removeClass = ele.removeClass || ElementUtil._removeClass.bind(ele); + ele.toggleClass = ele.toggleClass || ElementUtil._toggleClass.bind(ele); + ele.showVe = ele.showVe || ElementUtil._showVe.bind(ele); + ele.hideVe = ele.hideVe || ElementUtil._hideVe.bind(ele); + ele.toggleVe = ele.toggleVe || ElementUtil._toggleVe.bind(ele); + ele.empty = ele.empty || ElementUtil._empty.bind(ele); + ele.detach = ele.detach || ElementUtil._detach.bind(ele); + ele.attr = ele.attr || ElementUtil._attr.bind(ele); + ele.val = ele.val || ElementUtil._val.bind(ele); + ele.html = ele.html || ElementUtil._html.bind(ele); + ele.txt = ele.txt || ElementUtil._txt.bind(ele); + ele.tooltip = ele.tooltip || ElementUtil._tooltip.bind(ele); + ele.disableSpellcheck = ele.disableSpellcheck || ElementUtil._disableSpellcheck.bind(ele); + ele.on = ele.on || ElementUtil._onX.bind(ele); + ele.onClick = ele.onClick || ElementUtil._onX.bind(ele, "click"); + ele.onContextmenu = ele.onContextmenu || ElementUtil._onX.bind(ele, "contextmenu"); + ele.onChange = ele.onChange || ElementUtil._onX.bind(ele, "change"); + ele.onKeydown = ele.onKeydown || ElementUtil._onX.bind(ele, "keydown"); + ele.onKeyup = ele.onKeyup || ElementUtil._onX.bind(ele, "keyup"); + + return ele; + }, + + _appends(child) { + this.appendChild(child); + return this; + }, + + _appendTo(parent) { + parent.appendChild(this); + return this; + }, + + _prependTo(parent) { + parent.prepend(this); + return this; + }, + + _insertAfter(parent) { + parent.after(this); + return this; + }, + + _addClass(clazz) { + this.classList.add(clazz); + return this; + }, + + _removeClass(clazz) { + this.classList.remove(clazz); + return this; + }, + + _toggleClass(clazz, isActive) { + if (isActive == null) + this.classList.toggle(clazz); + else if (isActive) + this.classList.add(clazz); + else + this.classList.remove(clazz); + return this; + }, + + _showVe() { + this.classList.remove("ve-hidden"); + return this; + }, + + _hideVe() { + this.classList.add("ve-hidden"); + return this; + }, + + _toggleVe(isActive) { + this.toggleClass("ve-hidden", isActive == null ? isActive : !isActive); + return this; + }, + + _empty() { + this.innerHTML = ""; + return this; + }, + + _detach() { + if (this.parentElement) + this.parentElement.removeChild(this); + return this; + }, + + _attr(name, value) { + this.setAttribute(name, value); + return this; + }, + + _html(html) { + if (html === undefined) + return this.innerHTML; + this.innerHTML = html; + return this; + }, + + _txt(txt) { + if (txt === undefined) + return this.innerText; + this.innerText = txt; + return this; + }, + + _tooltip(title) { + return this.attr("title", title); + }, + + _disableSpellcheck() { + return this.attr("autocomplete", "new-password").attr("autocapitalize", "off").attr("spellcheck", "false"); + }, + + _onX(evtName, fn) { + this.addEventListener(evtName, fn); + return this; + }, + + _val(val) { + if (val !== undefined) { + switch (this.tagName) { + case "SELECT": + { + let selectedIndexNxt = -1; + for (let i = 0, len = this.options.length; i < len; ++i) { + if (this.options[i]?.value === val) { + selectedIndexNxt = i; + break; + } + } + this.selectedIndex = selectedIndexNxt; + return this; + } + + default: + { + this.value = val; + return this; + } + } + } + + switch (this.tagName) { + case "SELECT": + return this.options[this.selectedIndex]?.value; + + default: + return this.value; + } + }, + + getIndexPathToParent(parent, child) { + if (!parent.contains(child)) + return null; + const path = []; + + while (child !== parent) { + if (!child.parentElement) + return null; + const ix = [...child.parentElement.children].indexOf(child); + if (!~ix) + return null; + path.push(ix); + + child = child.parentElement; + } + + return path.reverse(); + }, + + getChildByIndexPath(parent, indexPath) { + for (let i = 0; i < indexPath.length; ++i) { + const ix = indexPath[i]; + parent = parent.children[ix]; + if (!parent) + return null; + } + return parent; + }, +}; +if (typeof window !== "undefined"){window.e_ = ElementUtil.getOrModify;} +//#endregion + +//#region CollectionUtil +globalThis.CollectionUtil = { + ObjectSet: class ObjectSet { + constructor() { + this.map = new Map(); + this[Symbol.iterator] = this.values; + } + add(item) { + this.map.set(item._toIdString(), item); + } + + values() { + return this.map.values(); + } + } + , + + setEq(a, b) { + if (a.size !== b.size) + return false; + for (const it of a) + if (!b.has(it)) + return false; + return true; + }, + + setDiff(set1, set2) { + return new Set([...set1].filter(it=>!set2.has(it))); + }, + + objectDiff(obj1, obj2) { + const out = {}; + + [...new Set([...Object.keys(obj1), ...Object.keys(obj2)])].forEach(k=>{ + const diff = CollectionUtil._objectDiff_recurse(obj1[k], obj2[k]); + if (diff !== undefined) + out[k] = diff; + } + ); + + return out; + }, + + _objectDiff_recurse(a, b) { + if (CollectionUtil.deepEquals(a, b)) + return undefined; + + if (a && b && typeof a === "object" && typeof b === "object") { + return CollectionUtil.objectDiff(a, b); + } + + return b; + }, + + objectIntersect(obj1, obj2) { + const out = {}; + + [...new Set([...Object.keys(obj1), ...Object.keys(obj2)])].forEach(k=>{ + const diff = CollectionUtil._objectIntersect_recurse(obj1[k], obj2[k]); + if (diff !== undefined) + out[k] = diff; + } + ); + + return out; + }, + + _objectIntersect_recurse(a, b) { + if (CollectionUtil.deepEquals(a, b)) + return a; + + if (a && b && typeof a === "object" && typeof b === "object") { + return CollectionUtil.objectIntersect(a, b); + } + + return undefined; + }, + + deepEquals(a, b) { + if (Object.is(a, b)) + return true; + if (a && b && typeof a === "object" && typeof b === "object") { + if (CollectionUtil._eq_isPlainObject(a) && CollectionUtil._eq_isPlainObject(b)) + return CollectionUtil._eq_areObjectsEqual(a, b); + const isArrayA = Array.isArray(a); + const isArrayB = Array.isArray(b); + if (isArrayA || isArrayB) + return isArrayA === isArrayB && CollectionUtil._eq_areArraysEqual(a, b); + const isSetA = a instanceof Set; + const isSetB = b instanceof Set; + if (isSetA || isSetB) + return isSetA === isSetB && CollectionUtil.setEq(a, b); + return CollectionUtil._eq_areObjectsEqual(a, b); + } + return false; + }, + + _eq_isPlainObject: (value)=>value.constructor === Object || value.constructor == null, + _eq_areObjectsEqual(a, b) { + const keysA = Object.keys(a); + const {length} = keysA; + if (Object.keys(b).length !== length) + return false; + for (let i = 0; i < length; i++) { + if (!b.hasOwnProperty(keysA[i])) + return false; + if (!CollectionUtil.deepEquals(a[keysA[i]], b[keysA[i]])) + return false; + } + return true; + }, + _eq_areArraysEqual(a, b) { + const {length} = a; + if (b.length !== length) + return false; + for (let i = 0; i < length; i++) + if (!CollectionUtil.deepEquals(a[i], b[i])) + return false; + return true; + }, + + dfs(obj, opts) { + const {prop=null, fnMatch=null} = opts; + if (!prop && !fnMatch) + throw new Error(`One of "prop" or "fnMatch" must be specified!`); + + if (obj instanceof Array) { + for (const child of obj) { + const n = CollectionUtil.dfs(child, opts); + if (n) + return n; + } + return; + } + + if (obj instanceof Object) { + if (prop && obj[prop]) + return obj[prop]; + if (fnMatch && fnMatch(obj)) + return obj; + + for (const child of Object.values(obj)) { + const n = CollectionUtil.dfs(child, opts); + if (n) + return n; + } + } + }, + + bfs(obj, opts) { + const {prop=null, fnMatch=null} = opts; + if (!prop && !fnMatch) + throw new Error(`One of "prop" or "fnMatch" must be specified!`); + + if (obj instanceof Array) { + for (const child of obj) { + if (!(child instanceof Array) && child instanceof Object) { + if (prop && child[prop]) + return child[prop]; + if (fnMatch && fnMatch(child)) + return child; + } + } + + for (const child of obj) { + const n = CollectionUtil.bfs(child, opts); + if (n) + return n; + } + + return; + } + + if (obj instanceof Object) { + if (prop && obj[prop]) + return obj[prop]; + if (fnMatch && fnMatch(obj)) + return obj; + + return CollectionUtil.bfs(Object.values(obj)); + } + }, +}; +//#endregion + +//#region UIUtil +let UiUtil$1 = class UiUtil { + static strToInt(string, fallbackEmpty=0, opts) { + return UiUtil$1._strToNumber(string, fallbackEmpty, opts, true); + } + + static strToNumber(string, fallbackEmpty=0, opts) { + return UiUtil$1._strToNumber(string, fallbackEmpty, opts, false); + } + + static _strToNumber(string, fallbackEmpty=0, opts, isInt) { + opts = opts || {}; + let out; + string = string.trim(); + if (!string) + out = fallbackEmpty; + else { + const num = UiUtil$1._parseStrAsNumber(string, isInt); + out = isNaN(num) || !isFinite(num) ? opts.fallbackOnNaN !== undefined ? opts.fallbackOnNaN : 0 : num; + } + if (opts.max != null) + out = Math.min(out, opts.max); + if (opts.min != null) + out = Math.max(out, opts.min); + return out; + } + + static strToBool(string, fallbackEmpty=null, opts) { + opts = opts || {}; + if (!string) + return fallbackEmpty; + string = string.trim().toLowerCase(); + if (!string) + return fallbackEmpty; + return string === "true" ? true : string === "false" ? false : opts.fallbackOnNaB; + } + + static intToBonus(int, {isPretty=false}={}) { + return `${int >= 0 ? "+" : int < 0 ? (isPretty ? "\u2012" : "-") : ""}${Math.abs(int)}`; + } + + static getEntriesAsText(entryArray) { + if (!entryArray || !entryArray.length) + return ""; + if (!(entryArray instanceof Array)) + return UiUtil$1.getEntriesAsText([entryArray]); + + return entryArray.map(it=>{ + if (typeof it === "string" || typeof it === "number") + return it; + + return JSON.stringify(it, null, 2).split("\n").map(it=>` ${it}`); + } + ).flat().join("\n"); + } + + static getTextAsEntries(text) { + try { + const lines = text.split("\n").filter(it=>it.trim()).map(it=>{ + if (/^\s/.exec(it)) + return it; + return `"${it.replace(/"/g, `\\"`)}",`; + } + ).map(it=>{ + if (/[}\]]$/.test(it.trim())) + return `${it},`; + return it; + } + ); + const json = `[\n${lines.join("")}\n]`.replace(/(.*?)(,)(:?\s*]|\s*})/g, "$1$3"); + return JSON.parse(json); + } catch (e) { + const lines = text.split("\n").filter(it=>it.trim()); + const slice = lines.join(" \\ ").substring(0, 30); + JqueryUtil.doToast({ + content: `Could not parse entries! Error was: ${e.message}
    Text was: ${slice}${slice.length === 30 ? "..." : ""}`, + type: "danger", + }); + return lines; + } + } + + static getShowModal(opts) { + opts = opts || {}; + + const doc = (opts.window || window).document; + + UiUtil$1._initModalEscapeHandler({ + doc + }); + UiUtil$1._initModalMouseupHandlers({ + doc + }); + if (doc.activeElement) + doc.activeElement.blur(); + let resolveModal; + const pResolveModal = new Promise(resolve=>{ + resolveModal = resolve; + } + ); + + const pHandleCloseClick = async(isDataEntered,...args)=>{ + if (opts.cbClose) + await opts.cbClose(isDataEntered, ...args); + resolveModal([isDataEntered, ...args]); + + if (opts.isIndestructible) + wrpOverlay.detach(); + else + wrpOverlay.remove(); + + ContextUtil.closeAllMenus(); + + doTeardown(); + } + ; + + const doTeardown = ()=>{ + UiUtil$1._popFromModalStack(modalStackMeta); + if (!UiUtil$1._MODAL_STACK.length) + doc.body.classList.remove(`ui-modal__body-active`); + } + ; + + const doOpen = ()=>{ + wrpOverlay.appendTo(doc.body); + doc.body.classList.add(`ui-modal__body-active`); + } + ; + + const wrpOverlay = e_({ + tag: "div", + clazz: "ui-modal__overlay" + }); + if (opts.zIndex != null) + wrpOverlay.style.zIndex = `${opts.zIndex}`; + if (opts.overlayColor != null) + wrpOverlay.style.backgroundColor = `${opts.overlayColor}`; + + const overlayBlind = opts.isFullscreenModal ? e_({ + tag: "div", + clazz: `ui-modal__overlay-blind w-100 h-100 ve-flex-col`, + }).appendTo(wrpOverlay) : null; + + const wrpScroller = e_({ + tag: "div", + clazz: `ui-modal__scroller ve-flex-col`, + }); + + const modalWindowClasses = [opts.isWidth100 ? `w-100` : "", opts.isHeight100 ? "h-100" : "", opts.isUncappedHeight ? "ui-modal__inner--uncap-height" : "", opts.isUncappedWidth ? "ui-modal__inner--uncap-width" : "", opts.isMinHeight0 ? `ui-modal__inner--no-min-height` : "", opts.isMinWidth0 ? `ui-modal__inner--no-min-width` : "", opts.isMaxWidth640p ? `ui-modal__inner--max-width-640p` : "", opts.isFullscreenModal ? `ui-modal__inner--mode-fullscreen my-0 pt-0` : "", opts.hasFooter ? `pb-0` : "", ].filter(Boolean); + + const btnCloseModal = opts.isFullscreenModal ? e_({ + tag: "button", + clazz: `btn btn-danger btn-xs`, + html: `{ + if (evt.target !== wrpOverlay) + return; + if (evt.target !== UiUtil$1._MODAL_LAST_MOUSEDOWN) + return; + if (opts.isPermanent) + return; + evt.stopPropagation(); + evt.preventDefault(); + return pHandleCloseClick(false); + } + ); + + if (!opts.isClosed) + doOpen(); + + const modalStackMeta = { + isPermanent: opts.isPermanent, + pHandleCloseClick, + doTeardown, + }; + if (!opts.isClosed) + UiUtil$1._pushToModalStack(modalStackMeta); + + const out = { + $modal: $(modal), + $modalInner: $(wrpScroller), + $modalFooter: $(modalFooter), + doClose: pHandleCloseClick, + doTeardown, + pGetResolved: ()=>pResolveModal, + }; + + if (opts.isIndestructible || opts.isClosed) { + out.doOpen = ()=>{ + UiUtil$1._pushToModalStack(modalStackMeta); + doOpen(); + } + ; + } + + return out; + } + + static async pGetShowModal(opts) { + return UiUtil$1.getShowModal(opts); + } + + static _pushToModalStack(modalStackMeta) { + if (!UiUtil$1._MODAL_STACK.includes(modalStackMeta)) { + UiUtil$1._MODAL_STACK.push(modalStackMeta); + } + } + + static _popFromModalStack(modalStackMeta) { + const ixStack = UiUtil$1._MODAL_STACK.indexOf(modalStackMeta); + if (~ixStack) + UiUtil$1._MODAL_STACK.splice(ixStack, 1); + } + + static _initModalEscapeHandler({doc}) { + if (UiUtil$1._MODAL_STACK) + return; + UiUtil$1._MODAL_STACK = []; + + doc.addEventListener("keydown", evt=>{ + if (evt.which !== 27) + return; + if (!UiUtil$1._MODAL_STACK.length) + return; + if (EventUtil.isInInput(evt)) + return; + + const outerModalMeta = UiUtil$1._MODAL_STACK.last(); + if (!outerModalMeta) + return; + evt.stopPropagation(); + if (!outerModalMeta.isPermanent) + return outerModalMeta.pHandleCloseClick(false); + } + ); + } + + static _initModalMouseupHandlers({doc}) { + doc.addEventListener("mousedown", evt=>{ + UiUtil$1._MODAL_LAST_MOUSEDOWN = evt.target; + } + ); + } + + static isAnyModalOpen() { + return !!UiUtil$1._MODAL_STACK?.length; + } + + static addModalSep($modalInner) { + $modalInner.append(`
    `); + } + + static $getAddModalRow($modalInner, tag="div") { + return $(`<${tag} class="ui-modal__row">`).appendTo($modalInner); + } + + static $getAddModalRowHeader($modalInner, headerText, opts) { + opts = opts || {}; + const $row = UiUtil$1.$getAddModalRow($modalInner, "h5").addClass("bold"); + if (opts.$eleRhs) + $$`
    ${headerText}${opts.$eleRhs}
    `.appendTo($row); + else + $row.text(headerText); + if (opts.helpText) + $row.title(opts.helpText); + return $row; + } + + static $getAddModalRowCb($modalInner, labelText, objectWithProp, propName, helpText) { + const $row = UiUtil$1.$getAddModalRow($modalInner, "label").addClass(`ui-modal__row--cb`); + if (helpText) + $row.title(helpText); + $row.append(`${labelText}`); + const $cb = $(``).appendTo($row).keydown(evt=>{ + if (evt.key === "Escape") + $cb.blur(); + } + ).prop("checked", objectWithProp[propName]).on("change", ()=>objectWithProp[propName] = $cb.prop("checked")); + return $cb; + } + + static $getAddModalRowCb2({$wrp, comp, prop, text, title=null}) { + const $cb = ComponentUiUtil$1.$getCbBool(comp, prop); + + const $row = $$``.appendTo($wrp); + if (title) + $row.title(title); + + return $cb; + } + + static $getAddModalRowSel($modalInner, labelText, objectWithProp, propName, values, opts) { + opts = opts || {}; + const $row = UiUtil$1.$getAddModalRow($modalInner, "label").addClass(`ui-modal__row--sel`); + if (opts.helpText) + $row.title(opts.helpText); + $row.append(`${labelText}`); + const $sel = $(``)).disableSpellcheck().keydown(evt=>{ + if (evt.key === "Escape") + $ipt.blur(); + } + ).change(()=>{ + const raw = $ipt.val().trim(); + const cur = component._state[prop]; + + if (opts.isAllowNull && !raw) + return component._state[prop] = null; + + if (raw.startsWith("=")) { + component._state[prop] = fnConvert(raw.slice(1), fallbackEmpty, opts) - opts.offset; + } else { + const mUnary = prevValue != null && prevValue < 0 ? /^[+/*^]/.exec(raw) : /^[-+/*^]/.exec(raw); + if (mUnary) { + let proc = raw; + proc = proc.slice(1).trim(); + const mod = fnConvert(proc, fallbackEmpty, opts); + const full = `${cur}${mUnary[0]}${mod}`; + component._state[prop] = fnConvert(full, fallbackEmpty, opts) - opts.offset; + } else { + component._state[prop] = fnConvert(raw, fallbackEmpty, opts) - opts.offset; + } + } + + if (cur === component._state[prop]) + setIptVal(); + } + ); + + let prevValue; + const hook = ()=>{ + prevValue = component._state[prop]; + setIptVal(); + } + ; + if (opts.hookTracker) + ComponentUiUtil.trackHook(opts.hookTracker, prop, hook); + component._addHookBase(prop, hook); + hook(); + + if (opts.asMeta) + return this._getIptDecoratedMeta(component, prop, $ipt, hook, opts); + else + return $ipt; + } + + static $getIptStr(component, prop, opts) { + opts = opts || {}; + + if ((opts.decorationLeft || opts.decorationRight) && !opts.asMeta) + throw new Error(`Input must be created with "asMeta" option`); + + const $ipt = (opts.$ele || $(opts.html || ``)).keydown(evt=>{ + if (evt.key === "Escape") + $ipt.blur(); + } + ).disableSpellcheck(); + UiUtil.bindTypingEnd({ + $ipt, + fnKeyup: ()=>{ + const nxtVal = opts.isNoTrim ? $ipt.val() : $ipt.val().trim(); + component._state[prop] = opts.isAllowNull && !nxtVal ? null : nxtVal; + } + , + }); + + if (opts.placeholder) + $ipt.attr("placeholder", opts.placeholder); + + if (opts.autocomplete && opts.autocomplete.length) + $ipt.typeahead({ + source: opts.autocomplete + }); + const hook = ()=>{ + if (component._state[prop] == null) + $ipt.val(null); + else { + if ($ipt.val().trim() !== component._state[prop]) + $ipt.val(component._state[prop]); + } + } + ; + component._addHookBase(prop, hook); + hook(); + + if (opts.asMeta) + return this._getIptDecoratedMeta(component, prop, $ipt, hook, opts); + else + return $ipt; + } + + static _getIptDecoratedMeta(component, prop, $ipt, hook, opts) { + const out = { + $ipt, + unhook: ()=>component._removeHookBase(prop, hook) + }; + + if (opts.decorationLeft || opts.decorationRight) { + let $decorLeft; + let $decorRight; + + if (opts.decorationLeft) { + $ipt.addClass(`ui-ideco__ipt ui-ideco__ipt--left`); + $decorLeft = ComponentUiUtil._$getDecor(component, prop, $ipt, opts.decorationLeft, "left", opts); + } + + if (opts.decorationRight) { + $ipt.addClass(`ui-ideco__ipt ui-ideco__ipt--right`); + $decorRight = ComponentUiUtil._$getDecor(component, prop, $ipt, opts.decorationRight, "right", opts); + } + + out.$wrp = $$`
    ${$ipt}${$decorLeft}${$decorRight}
    `; + } + + return out; + } + + static _$getDecor(component, prop, $ipt, decorType, side, opts) { + switch (decorType) { + case "search": + { + return $(`
    `); + } + case "clear": + { + return $(`
    `).click(()=>$ipt.val("").change().keydown().keyup()); + } + case "ticker": + { + const isValidValue = val=>{ + if (opts.max != null && val > opts.max) + return false; + if (opts.min != null && val < opts.min) + return false; + return true; + } + ; + + const handleClick = (delta)=>{ + const nxt = component._state[prop] + delta; + if (!isValidValue(nxt)) + return; + component._state[prop] = nxt; + $ipt.focus(); + } + ; + + const $btnUp = $(``).click(()=>handleClick(1)); + + const $btnDown = $(``).click(()=>handleClick(-1)); + + return $$`
    + ${$btnUp} + ${$btnDown} +
    `; + } + case "spacer": + { + return ""; + } + default: + throw new Error(`Unimplemented!`); + } + } + + static $getIptEntries(component, prop, opts) { + opts = opts || {}; + + const $ipt = (opts.$ele || $(``)).keydown(evt=>{ + if (evt.key === "Escape") + $ipt.blur(); + } + ).change(()=>component._state[prop] = UiUtil$1.getTextAsEntries($ipt.val().trim())); + const hook = ()=>$ipt.val(UiUtil$1.getEntriesAsText(component._state[prop])); + component._addHookBase(prop, hook); + hook(); + return $ipt; + } + + static $getIptColor(component, prop, opts) { + opts = opts || {}; + + const $ipt = (opts.$ele || $(opts.html || ``)).change(()=>component._state[prop] = $ipt.val()); + const hook = ()=>$ipt.val(component._state[prop]); + component._addHookBase(prop, hook); + hook(); + return $ipt; + } + + static getBtnBool(component, prop, opts) { + opts = opts || {}; + + let ele = opts.ele; + if (opts.html) + ele = e_({ + outer: opts.html + }); + + const activeClass = opts.activeClass || "active"; + const stateName = opts.stateName || "state"; + const stateProp = opts.stateProp || `_${stateName}`; + + const btn = (ele ? e_({ + ele + }) : e_({ + ele: ele, + tag: "button", + clazz: "btn btn-xs btn-default", + text: opts.text || "Toggle", + })).onClick(()=>component[stateProp][prop] = !component[stateProp][prop]).onContextmenu(evt=>{ + evt.preventDefault(); + component[stateProp][prop] = !component[stateProp][prop]; + } + ); + + const hk = ()=>{ + btn.toggleClass(activeClass, opts.isInverted ? !component[stateProp][prop] : !!component[stateProp][prop]); + if (opts.activeTitle || opts.inactiveTitle) + btn.title(component[stateProp][prop] ? (opts.activeTitle || opts.title || "") : (opts.inactiveTitle || opts.title || "")); + if (opts.fnHookPost) + opts.fnHookPost(component[stateProp][prop]); + } + ; + component._addHook(stateName, prop, hk); + hk(); + + return btn; + } + + static $getBtnBool(component, prop, opts) { + const nxtOpts = { + ...opts + }; + if (nxtOpts.$ele) { + nxtOpts.ele = nxtOpts.$ele[0]; + delete nxtOpts.$ele; + } + return $(this.getBtnBool(component, prop, nxtOpts)); + } + + static $getCbBool(component, prop, opts) { + opts = opts || {}; + + const stateName = opts.stateName || "state"; + const stateProp = opts.stateProp || `_${stateName}`; + + const cb = e_({ + tag: "input", + type: "checkbox", + keydown: evt=>{ + if (evt.key === "Escape") + cb.blur(); + } + , + change: ()=>{ + if (opts.isTreatIndeterminateNullAsPositive && component[stateProp][prop] == null) { + component[stateProp][prop] = false; + return; + } + + component[stateProp][prop] = cb.checked; + }, + }); + + const hook = ()=>{ + cb.checked = !!component[stateProp][prop]; + if (opts.isDisplayNullAsIndeterminate) + cb.indeterminate = component[stateProp][prop] == null; + } + ; + component._addHook(stateName, prop, hook); + hook(); + + const $cb = $(cb); + + return opts.asMeta ? ({ + $cb, + unhook: ()=>component._removeHook(stateName, prop, hook) + }) : $cb; + } + + /**Create a dropdown menu with options to click on (used to create a class dropdown menu at least) */ + static $getSelSearchable(comp, prop, opts) { + opts = opts || {}; + + //UI Dropdown element + const $iptDisplay = (opts.$ele || $(opts.html || ``)) + .addClass("ui-sel2__ipt-display").attr("tabindex", "-1").click(()=>{ + if (opts.isDisabled){return;} + $iptSearch.focus().select(); + } + ).prop("disabled", !!opts.isDisabled); + //$iptDisplay.disableSpellcheck(); + $iptDisplay.attr("autocomplete", "new-password").attr("autocapitalize", "off").attr("spellcheck", "false"); + + const handleSearchChange = ()=>{ + const cleanTerm = this._$getSelSearchable_getSearchString($iptSearch.val()); + metaOptions.forEach(it=>{ + it.isVisible = it.searchTerm.includes(cleanTerm); + it.$ele.toggleVe(it.isVisible && !it.isForceHidden); + }); + }; + + const handleSearchChangeDebounced = MiscUtil.debounce(handleSearchChange, 30); + + const $iptSearch = (opts.$ele || $(opts.html || ``)).addClass("absolute ui-sel2__ipt-search").keydown(evt=>{ + if (opts.isDisabled) + return; + + switch (evt.key) { + case "Escape": + evt.stopPropagation(); + return $iptSearch.blur(); + + case "ArrowDown": + { + evt.preventDefault(); + const visibleMetaOptions = metaOptions.filter(it=>it.isVisible && !it.isForceHidden); + if (!visibleMetaOptions.length) + return; + visibleMetaOptions[0].$ele.focus(); + break; + } + + case "Enter": + case "Tab": + { + const visibleMetaOptions = metaOptions.filter(it=>it.isVisible && !it.isForceHidden); + if (!visibleMetaOptions.length) + return; + comp._state[prop] = visibleMetaOptions[0].value; + $iptSearch.blur(); + break; + } + + default: + handleSearchChangeDebounced(); + } + } + ).change(()=>handleSearchChangeDebounced()).click(()=>{ + if (opts.isDisabled) + return; + $iptSearch.focus().select(); + } + ).prop("disabled", !!opts.isDisabled)//.disableSpellcheck(); + .attr("autocomplete", "new-password").attr("autocapitalize", "off").attr("spellcheck", "false"); + + //This object will be the parent of our choices in the dropdown menu + const $wrpChoices = $$`
    `; + const $wrp = $$`
    + ${$iptDisplay} + ${$iptSearch} +
    `; + + + + const procValues = opts.isAllowNull ? [null, ...opts.values] : opts.values; + //Create dropdown options here + const metaOptions = procValues.map((v,i)=>{ + const display = v == null ? (opts.displayNullAs || "\u2014") : opts.fnDisplay ? opts.fnDisplay(v) : v; + const additionalStyleClasses = opts.fnGetAdditionalStyleClasses ? opts.fnGetAdditionalStyleClasses(v) : null; + + //V is an index that points to a class + + //Here we create an option in the dropdown menu + const $ele = $(`
    ${display}
    `) + .click(()=>{ //When an option is clicked + if (opts.isDisabled){return;} + //here is where _state first gets set with the [propIxClass] thingy + //this should probably trigger an event (because _state is a proxy and can run events when something is setted) + comp._state[prop] = v; + $(document.activeElement).blur(); + $wrp.addClass("no-events"); + setTimeout(()=>$wrp.removeClass("no-events"), 50); + }) + .keydown(evt=>{ + if (opts.isDisabled) + return; + + switch (evt.key) { + case "Escape": + evt.stopPropagation(); + return $ele.blur(); + + case "ArrowDown": + { + evt.preventDefault(); + const visibleMetaOptions = metaOptions.filter(it=>it.isVisible && !it.isForceHidden); + if (!visibleMetaOptions.length) + return; + const ixCur = visibleMetaOptions.indexOf(out); + const nxt = visibleMetaOptions[ixCur + 1]; + if (nxt) + nxt.$ele.focus(); + break; + } + + case "ArrowUp": + { + evt.preventDefault(); + const visibleMetaOptions = metaOptions.filter(it=>it.isVisible && !it.isForceHidden); + if (!visibleMetaOptions.length) + return; + const ixCur = visibleMetaOptions.indexOf(out); + const prev = visibleMetaOptions[ixCur - 1]; + if (prev) + return prev.$ele.focus(); + $iptSearch.focus(); + break; + } + + case "Enter": + { + comp._state[prop] = v; + $ele.blur(); + break; + } + } + } + ).appendTo($wrpChoices); + + + //TEMPFIX + const isForceHidden = false; //opts.isHiddenPerValue && !!(opts.isAllowNull ? opts.isHiddenPerValue[i - 1] : opts.isHiddenPerValue[i]); + if (isForceHidden){$ele.hideVe();} + + + + $wrp.append($wrpChoices); + + const out = { + value: v, + isVisible: true, + isForceHidden, + searchTerm: this._$getSelSearchable_getSearchString(display), + $ele, + }; + + return out; + }); + + const fnUpdateHidden = (isHiddenPerValue,isHideNull=false)=>{ + let metaOptions_ = metaOptions; + + if (opts.isAllowNull) { + metaOptions_[0].isForceHidden = isHideNull; + metaOptions_ = metaOptions_.slice(1); + } + + metaOptions_.forEach((it,i)=>it.isForceHidden = !!isHiddenPerValue[i]); + handleSearchChange(); + }; + + const hk = ()=>{ + if (comp._state[prop] == null) + $iptDisplay.addClass("italic").addClass("ve-muted").val(opts.displayNullAs || "\u2014"); + else + $iptDisplay.removeClass("italic").removeClass("ve-muted").val(opts.fnDisplay ? opts.fnDisplay(comp._state[prop]) : comp._state[prop]); + + metaOptions.forEach(it=>it.$ele.removeClass("active")); + const metaActive = metaOptions.find(it=>it.value == null ? comp._state[prop] == null : it.value === comp._state[prop]); + if (metaActive) + metaActive.$ele.addClass("active"); + }; + comp._addHookBase(prop, hk); + hk(); + + const arrow = $(`
    + +
    `); + $wrp.append(arrow); + + return opts.asMeta ? ({ + $wrp, + unhook: ()=>comp._removeHookBase(prop, hk), + $iptDisplay, + $iptSearch, + fnUpdateHidden, + }) : $wrp; + } + + static _$getSelSearchable_getSearchString(str) { + if (str == null) + return ""; + return CleanUtil.getCleanString(str.trim().toLowerCase().replace(/\s+/g, " ")); + } + + static $getSelEnum(component, prop, {values, $ele, html, isAllowNull, fnDisplay, displayNullAs, asMeta, propProxy="state", isSetIndexes=false}={}) { + const _propProxy = `_${propProxy}`; + + let values_; + + let $sel = $ele || (html ? $(html) : null); + if (!$sel) { + const sel = document.createElement("select"); + sel.className = "form-control input-xs"; + $sel = $(sel); + } + + $sel.change(()=>{ + const ix = Number($sel.val()); + if (~ix) + return void (component[_propProxy][prop] = isSetIndexes ? ix : values_[ix]); + + if (isAllowNull) + return void (component[_propProxy][prop] = null); + component[_propProxy][prop] = isSetIndexes ? 0 : values_[0]; + } + ); + + const setValues_handleResetOnMissing = ({isResetOnMissing, nxtValues})=>{ + if (!isResetOnMissing) + return; + + if (component[_propProxy][prop] == null) + return; + + if (isSetIndexes) { + if (component[_propProxy][prop] >= 0 && component[_propProxy][prop] < nxtValues.length) { + if (isAllowNull) + return component[_propProxy][prop] = null; + return component[_propProxy][prop] = 0; + } + + return; + } + + if (!nxtValues.includes(component[_propProxy][prop])) { + if (isAllowNull) + return component[_propProxy][prop] = null; + return component[_propProxy][prop] = nxtValues[0]; + } + } + ; + + const setValues = (nxtValues,{isResetOnMissing=false, isForce=false}={})=>{ + if (!isForce && CollectionUtil.deepEquals(values_, nxtValues)) + return; + values_ = nxtValues; + $sel.empty(); + if (isAllowNull) { + const opt = document.createElement("option"); + opt.value = "-1"; + opt.text = displayNullAs || "\u2014"; + $sel.append(opt); + } + values_.forEach((it,i)=>{ + const opt = document.createElement("option"); + opt.value = `${i}`; + opt.text = fnDisplay ? fnDisplay(it) : it; + $sel.append(opt); + } + ); + + setValues_handleResetOnMissing({ + isResetOnMissing, + nxtValues + }); + + hook(); + } + ; + + const hook = ()=>{ + if (isSetIndexes) { + const ix = component[_propProxy][prop] == null ? -1 : component[_propProxy][prop]; + $sel.val(`${ix}`); + return; + } + + const searchFor = component[_propProxy][prop] === undefined ? null : component[_propProxy][prop]; + const ix = values_.indexOf(searchFor); + $sel.val(`${ix}`); + } + ; + component._addHookBase(prop, hook); + + setValues(values); + + if (!asMeta) + return $sel; + + return { + $sel, + unhook: ()=>component._removeHookBase(prop, hook), + setValues, + }; + } + + static $getPickEnum(component, prop, opts) { + return this._$getPickEnumOrString(component, prop, opts); + } + + static $getPickString(component, prop, opts) { + return this._$getPickEnumOrString(component, prop, { + ...opts, + isFreeText: true + }); + } + + static _$getPickEnumOrString(component, prop, opts) { + opts = opts || {}; + + const getSubcompValues = ()=>{ + const initialValuesArray = (opts.values || []).concat(opts.isFreeText ? MiscUtil.copyFast((component._state[prop] || [])) : []); + const initialValsCompWith = opts.isCaseInsensitive ? component._state[prop].map(it=>it.toLowerCase()) : component._state[prop]; + return initialValuesArray.map(v=>opts.isCaseInsensitive ? v.toLowerCase() : v).mergeMap(v=>({ + [v]: component._state[prop] && initialValsCompWith.includes(v) + })); + } + ; + + const initialVals = getSubcompValues(); + + let $btnAdd; + if (opts.isFreeText) { + $btnAdd = $(``).click(async()=>{ + const input = await InputUiUtil.pGetUserString(); + if (input == null || input === VeCt.SYM_UI_SKIP) + return; + const inputClean = opts.isCaseInsensitive ? input.trim().toLowerCase() : input.trim(); + pickComp.getPod().set(inputClean, true); + } + ); + } + else { + const menu = ContextUtil.getMenu(opts.values.map(it=>new ContextUtil.Action(opts.fnDisplay ? opts.fnDisplay(it) : it,()=>pickComp.getPod().set(it, true),))); + + $btnAdd = $(``).click(evt=>ContextUtil.pOpenMenu(evt, menu)); + } + + const pickComp = BaseComponent.fromObject(initialVals); + pickComp.render = function($parent) { + $parent.empty(); + + Object.entries(this._state).forEach(([k,v])=>{ + if (v === false) + return; + + const $btnRemove = $(``).click(()=>this._state[k] = false); + const txt = `${opts.fnDisplay ? opts.fnDisplay(k) : k}`; + $$`
    ${txt}
    ${$btnRemove}
    `.appendTo($parent); + } + ); + }; + + const $wrpPills = $(`
    `); + const $wrp = $$`
    ${$btnAdd}${$wrpPills}
    `; + pickComp._addHookAll("state", ()=>{ + component._state[prop] = Object.keys(pickComp._state).filter(k=>pickComp._state[k]); + pickComp.render($wrpPills); + } + ); + pickComp.render($wrpPills); + + const hkParent = ()=>pickComp._proxyAssignSimple("state", getSubcompValues(), true); + component._addHookBase(prop, hkParent); + + return $wrp; + } + + static $getCbsEnum(component, prop, opts) { + opts = opts || {}; + + const $wrp = $(`
    `); + const metas = opts.values.map(it=>{ + const $cb = $(``).keydown(evt=>{ + if (evt.key === "Escape") + $cb.blur(); + } + ).change(()=>{ + let didUpdate = false; + const ix = (component._state[prop] || []).indexOf(it); + if (~ix) + component._state[prop].splice(ix, 1); + else { + if (component._state[prop]) + component._state[prop].push(it); + else { + didUpdate = true; + component._state[prop] = [it]; + } + } + if (!didUpdate) + component._state[prop] = [...component._state[prop]]; + } + ); + + $$``.appendTo($wrp); + + return { + $cb, + value: it + }; + } + ); + + const hook = ()=>metas.forEach(meta=>meta.$cb.prop("checked", component._state[prop] && component._state[prop].includes(meta.value))); + component._addHookBase(prop, hook); + hook(); + + return opts.asMeta ? { + $wrp, + unhook: ()=>component._removeHookBase(prop, hook) + } : $wrp; + } + + /** + * @param {BaseComponent} comp + * @param {string} prop usually "_state" + * @param {any} opts + * @returns {any} + */ + static getMetaWrpMultipleChoice(comp, prop, opts) { + opts = opts || {}; + this._getMetaWrpMultipleChoice_doValidateOptions(opts); + + const rowMetas = []; + const $eles = []; + const ixsSelectionOrder = []; + const $elesSearchable = {}; + + const propIsAcceptable = this.getMetaWrpMultipleChoice_getPropIsAcceptable(prop); + const propPulse = this.getMetaWrpMultipleChoice_getPropPulse(prop); + const propIxMax = this._getMetaWrpMultipleChoice_getPropValuesLength(prop); + + const cntRequired = ((opts.required || []).length) + ((opts.ixsRequired || []).length); + const count = opts.count != null ? opts.count - cntRequired : null; + const countIncludingRequired = opts.count != null ? count + cntRequired : null; + const min = opts.min != null ? opts.min - cntRequired : null; + const max = opts.max != null ? opts.max - cntRequired : null; + + const valueGroups = opts.valueGroups || [{ + values: opts.values + }]; + + let ixValue = 0; + valueGroups.forEach((group,i)=>{ + if (i !== 0) + $eles.push($(`
    `)); + + if (group.name) { + const $wrpName = $$`
    +
    โ€’${group.name}
    + ${opts.valueGroupSplitControlsLookup?.[group.name]} +
    `; + $eles.push($wrpName); + } + + if (group.text) + $eles.push($(`
    ${group.text}
    `)); + + group.values.forEach(v=>{ + const ixValueFrozen = ixValue; + + const propIsActive = this.getMetaWrpMultipleChoice_getPropIsActive(prop, ixValueFrozen); + const propIsRequired = this.getMetaWrpMultipleChoice_getPropIsRequired(prop, ixValueFrozen); + + const isHardRequired = (opts.required && opts.required.includes(v)) || (opts.ixsRequired && opts.ixsRequired.includes(ixValueFrozen)); + const isRequired = isHardRequired || comp._state[propIsRequired]; + + if (comp._state[propIsActive] && !comp._state[propIsRequired]) + ixsSelectionOrder.push(ixValueFrozen); + + let hk; + const $cb = isRequired ? $(``) : ComponentUiUtil.$getCbBool(comp, propIsActive); + + if (isRequired) + comp._state[propIsActive] = true; + + if (!isRequired) { + hk = ()=>{ + const ixIx = ixsSelectionOrder.findIndex(it=>it === ixValueFrozen); + if (~ixIx) + ixsSelectionOrder.splice(ixIx, 1); + if (comp._state[propIsActive]) + ixsSelectionOrder.push(ixValueFrozen); + + const activeRows = rowMetas.filter(it=>comp._state[it.propIsActive]); + + if (count != null) { + if (activeRows.length > countIncludingRequired) { + const ixFirstSelected = ixsSelectionOrder.splice(ixsSelectionOrder.length - 2, 1)[0]; + if (ixFirstSelected != null) { + const propIsActiveOther = this.getMetaWrpMultipleChoice_getPropIsActive(prop, ixFirstSelected); + comp._state[propIsActiveOther] = false; + + comp._state[propPulse] = !comp._state[propPulse]; + } + return; + } + } + + let isAcceptable = false; + if (count != null) { + if (activeRows.length === countIncludingRequired) + isAcceptable = true; + } else { + if (activeRows.length >= (min || 0) && activeRows.length <= (max || Number.MAX_SAFE_INTEGER)) + isAcceptable = true; + } + + comp._state[propIsAcceptable] = isAcceptable; + + comp._state[propPulse] = !comp._state[propPulse]; + } + ; + comp._addHookBase(propIsActive, hk); + hk(); + } + + const displayValue = opts.fnDisplay ? opts.fnDisplay(v, ixValueFrozen) : v; + + rowMetas.push({ + $cb, + displayValue, + value: v, + propIsActive, + unhook: ()=>{ + if (hk) + comp._removeHookBase(propIsActive, hk); + } + , + }); + + const $ele = $$``; + $eles.push($ele); + + if (opts.isSearchable) { + const searchText = `${opts.fnGetSearchText ? opts.fnGetSearchText(v, ixValueFrozen) : v}`.toLowerCase().trim(); + ($elesSearchable[searchText] = $elesSearchable[searchText] || []).push($ele); + } + + ixValue++; + } + ); + } + ); + + ixsSelectionOrder.sort((a,b)=>SortUtil.ascSort(a, b)); + + comp.__state[propIxMax] = ixValue; + + let $iptSearch; + if (opts.isSearchable) { + const compSub = BaseComponent.fromObject({ + search: "" + }); + $iptSearch = ComponentUiUtil.$getIptStr(compSub, "search"); + const hkSearch = ()=>{ + const cleanSearch = compSub._state.search.trim().toLowerCase(); + if (!cleanSearch) { + Object.values($elesSearchable).forEach($eles=>$eles.forEach($ele=>$ele.removeClass("ve-hidden"))); + return; + } + + Object.entries($elesSearchable).forEach(([searchText,$eles])=>$eles.forEach($ele=>$ele.toggleVe(searchText.includes(cleanSearch)))); + } + ; + compSub._addHookBase("search", hkSearch); + hkSearch(); + } + + const unhook = ()=>rowMetas.forEach(it=>it.unhook()); + return { + $ele: $$`
    ${$eles}
    `, + $iptSearch, + rowMetas, + propIsAcceptable, + propPulse, + unhook, + cleanup: ()=>{ + unhook(); + Object.keys(comp._state).filter(it=>it.startsWith(`${prop}__`)).forEach(it=>delete comp._state[it]); + } + , + }; + } + + static getMetaWrpMultipleChoice_getPropIsAcceptable(prop) { + return `${prop}__isAcceptable`; + } + static getMetaWrpMultipleChoice_getPropPulse(prop) { + return `${prop}__pulse`; + } + static _getMetaWrpMultipleChoice_getPropValuesLength(prop) { + return `${prop}__length`; + } + static getMetaWrpMultipleChoice_getPropIsActive(prop, ixValue) { + return `${prop}__isActive_${ixValue}`; + } + static getMetaWrpMultipleChoice_getPropIsRequired(prop, ixValue) { + return `${prop}__isRequired_${ixValue}`; + } + + static getMetaWrpMultipleChoice_getSelectedIxs(comp, prop) { + const out = []; + const len = comp._state[this._getMetaWrpMultipleChoice_getPropValuesLength(prop)] || 0; + for (let i = 0; i < len; ++i) { + if (comp._state[this.getMetaWrpMultipleChoice_getPropIsActive(prop, i)]) + out.push(i); + } + return out; + } + + static getMetaWrpMultipleChoice_getSelectedValues(comp, prop, {values, valueGroups}) { + const selectedIxs = this.getMetaWrpMultipleChoice_getSelectedIxs(comp, prop); + if (values) + return selectedIxs.map(ix=>values[ix]); + + const selectedIxsSet = new Set(selectedIxs); + const out = []; + let ixValue = 0; + valueGroups.forEach(group=>{ + group.values.forEach(v=>{ + if (selectedIxsSet.has(ixValue)) + out.push(v); + ixValue++; + } + ); + } + ); + return out; + } + + static _getMetaWrpMultipleChoice_doValidateOptions(opts) { + if ((Number(!!opts.values) + Number(!!opts.valueGroups)) !== 1) + throw new Error(`Exactly one of "values" and "valueGroups" must be specified!`); + + if (opts.count != null && (opts.min != null || opts.max != null)) + throw new Error(`Chooser must be either in "count" mode or "min/max" mode!`); + if (opts.count == null && opts.min == null && opts.max == null) + opts.count = 1; + } + + static $getSliderRange(comp, opts) { + opts = opts || {}; + const slider = new ComponentUiUtil.RangeSlider({ + comp, + ...opts + }); + return slider.$get(); + } + + static $getSliderNumber(comp, prop, {min, max, step, $ele, asMeta, }={}, ) { + const $slider = ($ele || $(``)).change(()=>comp._state[prop] = Number($slider.val())); + + if (min != null) + $slider.attr("min", min); + if (max != null) + $slider.attr("max", max); + if (step != null) + $slider.attr("step", step); + + const hk = ()=>$slider.val(comp._state[prop]); + comp._addHookBase(prop, hk); + hk(); + + return asMeta ? ({ + $slider, + unhook: ()=>comp._removeHookBase(prop, hk) + }) : $slider; + } +} + +ComponentUiUtil.RangeSlider = class { + constructor({comp, propMin, propMax, propCurMin, propCurMax, fnDisplay, fnDisplayTooltip, sparseValues, }, ) { + this._comp = comp; + this._propMin = propMin; + this._propMax = propMax; + this._propCurMin = propCurMin; + this._propCurMax = propCurMax; + this._fnDisplay = fnDisplay; + this._fnDisplayTooltip = fnDisplayTooltip; + this._sparseValues = sparseValues; + + this._isSingle = !this._propCurMax; + + const compCpyState = { + [this._propMin]: this._comp._state[this._propMin], + [this._propCurMin]: this._comp._state[this._propCurMin], + [this._propMax]: this._comp._state[this._propMax], + }; + if (!this._isSingle) + compCpyState[this._propCurMax] = this._comp._state[this._propCurMax]; + this._compCpy = BaseComponent.fromObject(compCpyState); + + this._comp._addHook("state", this._propMin, ()=>this._compCpy._state[this._propMin] = this._comp._state[this._propMin]); + this._comp._addHook("state", this._propCurMin, ()=>this._compCpy._state[this._propCurMin] = this._comp._state[this._propCurMin]); + this._comp._addHook("state", this._propMax, ()=>this._compCpy._state[this._propMax] = this._comp._state[this._propMax]); + + if (!this._isSingle) + this._comp._addHook("state", this._propCurMax, ()=>this._compCpy._state[this._propCurMax] = this._comp._state[this._propCurMax]); + + this._cacheRendered = null; + this._dispTrackOuter = null; + this._dispTrackInner = null; + this._thumbLow = null; + this._thumbHigh = null; + this._dragMeta = null; + } + + $get() { + const out = this.get(); + return $(out); + } + + get() { + this.constructor._init(); + this.constructor._ALL_SLIDERS.add(this); + + if (this._cacheRendered) + return this._cacheRendered; + + const dispValueLeft = this._isSingle ? this._getSpcSingleValue() : this._getDispValue({ + isVisible: true, + side: "left" + }); + const dispValueRight = this._getDispValue({ + isVisible: true, + side: "right" + }); + + this._dispTrackInner = this._isSingle ? null : e_({ + tag: "div", + clazz: "ui-slidr__track-inner h-100 absolute", + }); + + this._thumbLow = this._getThumb(); + this._thumbHigh = this._isSingle ? null : this._getThumb(); + + this._dispTrackOuter = e_({ + tag: "div", + clazz: `relative w-100 ui-slidr__track-outer`, + children: [this._dispTrackInner, this._thumbLow, this._thumbHigh, ].filter(Boolean), + }); + + const wrpTrack = e_({ + tag: "div", + clazz: `ve-flex-v-center w-100 h-100 ui-slidr__wrp-track clickable`, + mousedown: evt=>{ + const thumb = this._getClosestThumb(evt); + this._handleMouseDown(evt, thumb); + } + , + children: [this._dispTrackOuter, ], + }); + + const wrpTop = e_({ + tag: "div", + clazz: "ve-flex-v-center w-100 ui-slidr__wrp-top", + children: [dispValueLeft, wrpTrack, dispValueRight, ].filter(Boolean), + }); + + const wrpPips = e_({ + tag: "div", + clazz: `w-100 ve-flex relative clickable h-100 ui-slidr__wrp-pips`, + mousedown: evt=>{ + const thumb = this._getClosestThumb(evt); + this._handleMouseDown(evt, thumb); + } + , + }); + + const wrpBottom = e_({ + tag: "div", + clazz: "w-100 ve-flex-vh-center ui-slidr__wrp-bottom", + children: [this._isSingle ? this._getSpcSingleValue() : this._getDispValue({ + side: "left" + }), wrpPips, this._getDispValue({ + side: "right" + }), ].filter(Boolean), + }); + + const hkChangeValue = ()=>{ + const curMin = this._compCpy._state[this._propCurMin]; + const pctMin = this._getLeftPositionPercentage({ + value: curMin + }); + this._thumbLow.style.left = `calc(${pctMin}% - ${this.constructor._W_THUMB_PX / 2}px)`; + const toDisplayLeft = this._fnDisplay ? `${this._fnDisplay(curMin)}`.qq() : curMin; + const toDisplayLeftTooltip = this._fnDisplayTooltip ? `${this._fnDisplayTooltip(curMin)}`.qq() : null; + if (!this._isSingle) { + dispValueLeft.html(toDisplayLeft).tooltip(toDisplayLeftTooltip); + } + + if (!this._isSingle) { + this._dispTrackInner.style.left = `${pctMin}%`; + + const curMax = this._compCpy._state[this._propCurMax]; + const pctMax = this._getLeftPositionPercentage({ + value: curMax + }); + this._dispTrackInner.style.right = `${100 - pctMax}%`; + this._thumbHigh.style.left = `calc(${pctMax}% - ${this.constructor._W_THUMB_PX / 2}px)`; + dispValueRight.html(this._fnDisplay ? `${this._fnDisplay(curMax)}`.qq() : curMax).tooltip(this._fnDisplayTooltip ? `${this._fnDisplayTooltip(curMax)}`.qq() : null); + } else { + dispValueRight.html(toDisplayLeft).tooltip(toDisplayLeftTooltip); + } + } + ; + + const hkChangeLimit = ()=>{ + const pips = []; + + if (!this._sparseValues) { + const numPips = this._compCpy._state[this._propMax] - this._compCpy._state[this._propMin]; + let pipIncrement = 1; + if (numPips > ComponentUiUtil.RangeSlider._MAX_PIPS) + pipIncrement = Math.ceil(numPips / ComponentUiUtil.RangeSlider._MAX_PIPS); + + let i, len; + for (i = this._compCpy._state[this._propMin], + len = this._compCpy._state[this._propMax] + 1; i < len; i += pipIncrement) { + pips.push(this._getWrpPip({ + isMajor: i === this._compCpy._state[this._propMin] || i === (len - 1), + value: i, + })); + } + + if (i !== this._compCpy._state[this._propMax]) + pips.push(this._getWrpPip({ + isMajor: true, + value: this._compCpy._state[this._propMax] + })); + } else { + const len = this._sparseValues.length; + this._sparseValues.forEach((val,i)=>{ + pips.push(this._getWrpPip({ + isMajor: i === 0 || i === (len - 1), + value: val, + })); + } + ); + } + + wrpPips.empty(); + e_({ + ele: wrpPips, + children: pips, + }); + + hkChangeValue(); + } + ; + + this._compCpy._addHook("state", this._propMin, hkChangeLimit); + this._compCpy._addHook("state", this._propMax, hkChangeLimit); + this._compCpy._addHook("state", this._propCurMin, hkChangeValue); + if (!this._isSingle) + this._compCpy._addHook("state", this._propCurMax, hkChangeValue); + + hkChangeLimit(); + + const wrp = e_({ + tag: "div", + clazz: "ve-flex-col w-100 ui-slidr__wrp", + children: [wrpTop, wrpBottom, ], + }); + + return this._cacheRendered = wrp; + } + + destroy() { + this.constructor._ALL_SLIDERS.delete(this); + if (this._cacheRendered) + this._cacheRendered.remove(); + } + + _getDispValue({isVisible, side}) { + return e_({ + tag: "div", + clazz: `overflow-hidden ui-slidr__disp-value no-shrink no-grow ve-flex-vh-center bold no-select ${isVisible ? `ui-slidr__disp-value--visible` : ""} ui-slidr__disp-value--${side}`, + }); + } + + _getSpcSingleValue() { + return e_({ + tag: "div", + clazz: `px-2`, + }); + } + + _getThumb() { + const thumb = e_({ + tag: "div", + clazz: "ui-slidr__thumb absolute clickable", + mousedown: evt=>this._handleMouseDown(evt, thumb), + }).attr("draggable", true); + + return thumb; + } + + _getWrpPip({isMajor, value}={}) { + const style = this._getWrpPip_getStyle({ + value + }); + + const pip = e_({ + tag: "div", + clazz: `ui-slidr__pip ${isMajor ? `ui-slidr__pip--major` : `absolute`}`, + }); + + const dispLabel = e_({ + tag: "div", + clazz: "absolute ui-slidr__pip-label ve-flex-vh-center ve-small no-wrap", + html: isMajor ? this._fnDisplay ? `${this._fnDisplay(value)}`.qq() : value : "", + title: isMajor && this._fnDisplayTooltip ? `${this._fnDisplayTooltip(value)}`.qq() : null, + }); + + return e_({ + tag: "div", + clazz: "ve-flex-col ve-flex-vh-center absolute no-select", + children: [pip, dispLabel, ], + style, + }); + } + + _getWrpPip_getStyle({value}) { + return `left: ${this._getLeftPositionPercentage({ + value + })}%`; + } + + _getLeftPositionPercentage({value}) { + if (this._sparseValues) { + const ix = this._sparseValues.sort(SortUtil.ascSort).indexOf(value); + if (!~ix) + throw new Error(`Value "${value}" was not in the list of sparse values!`); + return (ix / (this._sparseValues.length - 1)) * 100; + } + + const min = this._compCpy._state[this._propMin]; + const max = this._compCpy._state[this._propMax]; + return ((value - min) / (max - min)) * 100; + } + + _getRelativeValue(evt, {trackOriginX, trackWidth}) { + const xEvt = EventUtil.getClientX(evt) - trackOriginX; + + if (this._sparseValues) { + const ixMax = this._sparseValues.length - 1; + const rawVal = Math.round((xEvt / trackWidth) * ixMax); + return this._sparseValues[Math.min(ixMax, Math.max(0, rawVal))]; + } + + const min = this._compCpy._state[this._propMin]; + const max = this._compCpy._state[this._propMax]; + + const rawVal = min + Math.round((xEvt / trackWidth) * (max - min), ); + + return Math.min(max, Math.max(min, rawVal)); + } + + _getClosestThumb(evt) { + if (this._isSingle) + return this._thumbLow; + + const {x: trackOriginX, width: trackWidth} = this._dispTrackOuter.getBoundingClientRect(); + const value = this._getRelativeValue(evt, { + trackOriginX, + trackWidth + }); + + if (value < this._compCpy._state[this._propCurMin]) + return this._thumbLow; + if (value > this._compCpy._state[this._propCurMax]) + return this._thumbHigh; + + const {distToMin, distToMax} = this._getDistsToCurrentMinAndMax(value); + if (distToMax < distToMin) + return this._thumbHigh; + return this._thumbLow; + } + + _getDistsToCurrentMinAndMax(value) { + if (this._isSingle) + throw new Error(`Can not get distance to max value for singleton slider!`); + + const distToMin = Math.abs(this._compCpy._state[this._propCurMin] - value); + const distToMax = Math.abs(this._compCpy._state[this._propCurMax] - value); + return { + distToMin, + distToMax + }; + } + + _handleClick(evt, value) { + evt.stopPropagation(); + evt.preventDefault(); + + if (value < this._compCpy._state[this._propCurMin]) + this._compCpy._state[this._propCurMin] = value; + + if (value > this._compCpy._state[this._propCurMax]) + this._compCpy._state[this._propCurMax] = value; + + const {distToMin, distToMax} = this._getDistsToCurrentMinAndMax(value); + + if (distToMax < distToMin) + this._compCpy._state[this._propCurMax] = value; + else + this._compCpy._state[this._propCurMin] = value; + } + + _handleMouseDown(evt, thumb) { + evt.preventDefault(); + evt.stopPropagation(); + + const {x: trackOriginX, width: trackWidth} = this._dispTrackOuter.getBoundingClientRect(); + + thumb.addClass(`ui-slidr__thumb--hover`); + + this._dragMeta = { + trackOriginX, + trackWidth, + thumb, + }; + + this._handleMouseMove(evt); + } + + _handleMouseUp() { + const wasActive = this._doDragCleanup(); + + if (wasActive) { + const nxtState = { + [this._propMin]: this._compCpy._state[this._propMin], + [this._propMax]: this._compCpy._state[this._propMax], + [this._propCurMin]: this._compCpy._state[this._propCurMin], + }; + if (!this._isSingle) + nxtState[this._propCurMax] = this._compCpy._state[this._propCurMax]; + + this._comp._proxyAssignSimple("state", nxtState); + } + } + + _handleMouseMove(evt) { + if (!this._dragMeta) + return; + + const val = this._getRelativeValue(evt, this._dragMeta); + + if (this._dragMeta.thumb === this._thumbLow) { + if (val > this._compCpy._state[this._propCurMax]) + return; + this._compCpy._state[this._propCurMin] = val; + } else if (this._dragMeta.thumb === this._thumbHigh) { + if (val < this._compCpy._state[this._propCurMin]) + return; + this._compCpy._state[this._propCurMax] = val; + } + } + + _doDragCleanup() { + const isActive = this._dragMeta != null; + + if (this._dragMeta?.thumb) + this._dragMeta.thumb.removeClass(`ui-slidr__thumb--hover`); + + this._dragMeta = null; + + return isActive; + } + + static _init() { + if (this._isInit) + return; + document.addEventListener("mousemove", evt=>{ + for (const slider of this._ALL_SLIDERS) { + slider._handleMouseMove(evt); + } + } + ); + + document.addEventListener("mouseup", evt=>{ + for (const slider of this._ALL_SLIDERS) { + slider._handleMouseUp(evt); + } + } + ); + } +} +; +ComponentUiUtil.RangeSlider._isInit = false; +ComponentUiUtil.RangeSlider._ALL_SLIDERS = new Set(); +ComponentUiUtil.RangeSlider._W_THUMB_PX = 16; +ComponentUiUtil.RangeSlider._W_LABEL_PX = 24; +ComponentUiUtil.RangeSlider._MAX_PIPS = 40; +//#endregion + +//#region UtilGameSettings +class UtilGameSettings { + static prePreInit() { + //TEMPFIX + /* game.settings.register(SharedConsts.MODULE_ID, "isDbgMode", { + name: `Debug Mode`, + hint: `Enable additional developer-only debugging functionality. Not recommended, as it may reduce stability.`, + default: false, + type: Boolean, + scope: "world", + config: true, + }); */ + } + + static isDbg() { + return !!this.getSafe(SharedConsts.MODULE_ID, "isDbgMode"); + } + + static getSafe(module, key) { + //TEMPFIX + return null; + /* try { + return game.settings.get(module, key); + } catch (e) { + return null; + } */ + } +} +//#endregion + +//#region UtilPrePreInit +class UtilPrePreInit { + static _IS_GM = null; + + static isGM() { + return true; + //return UtilPrePreInit._IS_GM = UtilPrePreInit._IS_GM ?? game.data.users.find(it=>it._id === game.userId).role >= CONST.USER_ROLES.ASSISTANT; + } +} +//#endregion +//#region ContextUtil +globalThis.ContextUtil = { + _isInit: false, + _menus: [], + + _init() { + if (ContextUtil._isInit) + return; + ContextUtil._isInit = true; + + document.body.addEventListener("click", ()=>ContextUtil.closeAllMenus()); + }, + + getMenu(actions) { + ContextUtil._init(); + + const menu = new ContextUtil.Menu(actions); + ContextUtil._menus.push(menu); + return menu; + }, + + deleteMenu(menu) { + if (!menu) + return; + + menu.remove(); + const ix = ContextUtil._menus.findIndex(it=>it === menu); + if (~ix) + ContextUtil._menus.splice(ix, 1); + }, + + pOpenMenu(evt, menu, {userData=null}={}) { + evt.preventDefault(); + evt.stopPropagation(); + + ContextUtil._init(); + + ContextUtil._menus.filter(it=>it !== menu).forEach(it=>it.close()); + + return menu.pOpen(evt, { + userData + }); + }, + + closeAllMenus() { + ContextUtil._menus.forEach(menu=>menu.close()); + }, + + Menu: class { + constructor(actions) { + this._actions = actions; + this._pResult = null; + this.resolveResult_ = null; + + this.userData = null; + + this._$ele = null; + this._metasActions = []; + + this._menusSub = []; + } + + remove() { + if (!this._$ele) + return; + this._$ele.remove(); + this._$ele = null; + } + + width() { + return this._$ele ? this._$ele.width() : undefined; + } + height() { + return this._$ele ? this._$ele.height() : undefined; + } + + pOpen(evt, {userData=null, offsetY=null, boundsX=null}={}) { + evt.stopPropagation(); + evt.preventDefault(); + + this._initLazy(); + + if (this.resolveResult_) + this.resolveResult_(null); + this._pResult = new Promise(resolve=>{ + this.resolveResult_ = resolve; + } + ); + this.userData = userData; + + this._$ele.css({ + left: 0, + top: 0, + opacity: 0, + pointerEvents: "none", + }).showVe().css({ + left: this._getMenuPosition(evt, "x", { + bounds: boundsX + }), + top: this._getMenuPosition(evt, "y", { + offset: offsetY + }), + opacity: "", + pointerEvents: "", + }); + + this._metasActions[0].$eleRow.focus(); + + return this._pResult; + } + + close() { + if (!this._$ele) + return; + this._$ele.hideVe(); + + this.closeSubMenus(); + } + + isOpen() { + if (!this._$ele) + return false; + return !this._$ele.hasClass("ve-hidden"); + } + + _initLazy() { + if (this._$ele) { + this._metasActions.forEach(meta=>meta.action.update()); + return; + } + + const $elesAction = this._actions.map(it=>{ + if (it == null) + return $(`
    `); + + const rdMeta = it.render({ + menu: this + }); + this._metasActions.push(rdMeta); + return rdMeta.$eleRow; + } + ); + + this._$ele = $$`
    ${$elesAction}
    `.hideVe().appendTo(document.body); + } + + _getMenuPosition(evt, axis, {bounds=null, offset=null}={}) { + const {fnMenuSize, fnGetEventPos, fnWindowSize, fnScrollDir} = axis === "x" ? { + fnMenuSize: "width", + fnGetEventPos: "getClientX", + fnWindowSize: "width", + fnScrollDir: "scrollLeft" + } : { + fnMenuSize: "height", + fnGetEventPos: "getClientY", + fnWindowSize: "height", + fnScrollDir: "scrollTop" + }; + + const posMouse = EventUtil[fnGetEventPos](evt); + const szWin = $(window)[fnWindowSize](); + const posScroll = $(window)[fnScrollDir](); + let position = posMouse + posScroll; + + if (offset) + position += offset; + + const szMenu = this[fnMenuSize](); + + if (bounds != null) { + const {trailingLower, leadingUpper} = bounds; + + const posTrailing = position; + const posLeading = position + szMenu; + + if (posTrailing < trailingLower) { + position += trailingLower - posTrailing; + } else if (posLeading > leadingUpper) { + position -= posLeading - leadingUpper; + } + } + + if (position + szMenu > szWin && szMenu < position) + position -= szMenu; + + return position; + } + + addSubMenu(menu) { + this._menusSub.push(menu); + } + + closeSubMenus(menuSubExclude=null) { + this._menusSub.filter(menuSub=>menuSubExclude == null || menuSub !== menuSubExclude).forEach(menuSub=>menuSub.close()); + } + } + , + + Action: function(text, fnAction, opts) { + opts = opts || {}; + + this.text = text; + this.fnAction = fnAction; + + this.isDisabled = opts.isDisabled; + this.title = opts.title; + this.style = opts.style; + + this.fnActionAlt = opts.fnActionAlt; + this.textAlt = opts.textAlt; + this.titleAlt = opts.titleAlt; + + this.render = function({menu}) { + const $btnAction = this._render_$btnAction({ + menu + }); + const $btnActionAlt = this._render_$btnActionAlt({ + menu + }); + + return { + action: this, + $eleRow: $$`
    ${$btnAction}${$btnActionAlt}
    `, + $eleBtn: $btnAction, + }; + } + ; + + this._render_$btnAction = function({menu}) { + const $btnAction = $(`
    ${this.text}
    `).on("click", async evt=>{ + if (this.isDisabled) + return; + + evt.preventDefault(); + evt.stopPropagation(); + + menu.close(); + + const result = await this.fnAction(evt, { + userData: menu.userData + }); + if (menu.resolveResult_) + menu.resolveResult_(result); + } + ).keydown(evt=>{ + if (evt.key !== "Enter") + return; + $btnAction.click(); + } + ); + if (this.title) + $btnAction.title(this.title); + + return $btnAction; + } + ; + + this._render_$btnActionAlt = function({menu}) { + if (!this.fnActionAlt) + return null; + + const $btnActionAlt = $(`
    ${this.textAlt ?? ``}
    `).on("click", async evt=>{ + if (this.isDisabled) + return; + + evt.preventDefault(); + evt.stopPropagation(); + + menu.close(); + + const result = await this.fnActionAlt(evt, { + userData: menu.userData + }); + if (menu.resolveResult_) + menu.resolveResult_(result); + } + ); + if (this.titleAlt) + $btnActionAlt.title(this.titleAlt); + + return $btnActionAlt; + } + ; + + this.update = function() {} + ; + }, + + ActionLink: function(text, fnHref, opts) { + ContextUtil.Action.call(this, text, null, opts); + + this.fnHref = fnHref; + this._$btnAction = null; + + this._render_$btnAction = function() { + this._$btnAction = $(`${this.text}`); + if (this.title) + this._$btnAction.title(this.title); + + return this._$btnAction; + } + ; + + this.update = function() { + this._$btnAction.attr("href", this.fnHref()); + } + ; + }, + + ActionSelect: function({values, fnOnChange=null, fnGetDisplayValue=null, }, ) { + this._values = values; + this._fnOnChange = fnOnChange; + this._fnGetDisplayValue = fnGetDisplayValue; + + this._sel = null; + + this._ixInitial = null; + + this.render = function({menu}) { + this._sel = this._render_sel({ + menu + }); + + if (this._ixInitial != null) { + this._sel.val(`${this._ixInitial}`); + this._ixInitial = null; + } + + return { + action: this, + $eleRow: $$`
    ${this._sel}
    `, + }; + } + ; + + this._render_sel = function({menu}) { + const sel = e_({ + tag: "select", + clazz: "w-100 min-w-0 mx-5 py-1", + tabindex: 0, + children: this._values.map((val,i)=>{ + return e_({ + tag: "option", + value: i, + text: this._fnGetDisplayValue ? this._fnGetDisplayValue(val) : val, + }); + } + ), + click: async evt=>{ + evt.preventDefault(); + evt.stopPropagation(); + } + , + keydown: evt=>{ + if (evt.key !== "Enter") + return; + sel.click(); + } + , + change: ()=>{ + menu.close(); + + const ix = Number(sel.val() || 0); + const val = this._values[ix]; + + if (this._fnOnChange) + this._fnOnChange(val); + if (menu.resolveResult_) + menu.resolveResult_(val); + } + , + }); + + return sel; + } + ; + + this.setValue = function(val) { + const ix = this._values.indexOf(val); + if (!this._sel) + return this._ixInitial = ix; + this._sel.val(`${ix}`); + } + ; + + this.update = function() {} + ; + }, + + ActionSubMenu: class { + constructor(name, actions) { + this._name = name; + this._actions = actions; + } + + render({menu}) { + const menuSub = ContextUtil.getMenu(this._actions); + menu.addSubMenu(menuSub); + + const $eleRow = $$`
    +
    ${this._name}
    +
    +
    `.on("click", async evt=>{ + evt.stopPropagation(); + if (menuSub.isOpen()) + return menuSub.close(); + + menu.closeSubMenus(menuSub); + + const bcr = $eleRow[0].getBoundingClientRect(); + + await menuSub.pOpen(evt, { + offsetY: bcr.top - EventUtil.getClientY(evt), + boundsX: { + trailingLower: bcr.right, + leadingUpper: bcr.left, + }, + }, ); + + menu.close(); + } + ); + + return { + action: this, + $eleRow, + }; + } + + update() {} + } + , +}; +//#endregion +//#region StrUtil +globalThis.StrUtil = { + COMMAS_NOT_IN_PARENTHESES_REGEX: /,\s?(?![^(]*\))/g, + COMMA_SPACE_NOT_IN_PARENTHESES_REGEX: /, (?![^(]*\))/g, + + uppercaseFirst: function(string) { + return string.uppercaseFirst(); + }, + TITLE_LOWER_WORDS: ["a", "an", "the", "and", "but", "or", "for", "nor", "as", "at", "by", "for", "from", "in", "into", "near", "of", "on", "onto", "to", "with", "over", "von"], + TITLE_UPPER_WORDS: ["Id", "Tv", "Dm", "Ok", "Npc", "Pc", "Tpk", "Wip", "Dc"], + TITLE_UPPER_WORDS_PLURAL: ["Ids", "Tvs", "Dms", "Oks", "Npcs", "Pcs", "Tpks", "Wips", "Dcs"], + IRREGULAR_PLURAL_WORDS: { + "cactus": "cacti", + "child": "children", + "die": "dice", + "djinni": "djinn", + "dwarf": "dwarves", + "efreeti": "efreet", + "elf": "elves", + "fey": "fey", + "foot": "feet", + "goose": "geese", + "ki": "ki", + "man": "men", + "mouse": "mice", + "ox": "oxen", + "person": "people", + "sheep": "sheep", + "slaad": "slaadi", + "tooth": "teeth", + "undead": "undead", + "woman": "women", + }, + + padNumber: (n,len,padder)=>{ + return String(n).padStart(len, padder); + } + , + + elipsisTruncate(str, atLeastPre=5, atLeastSuff=0, maxLen=20) { + if (maxLen >= str.length) + return str; + + maxLen = Math.max(atLeastPre + atLeastSuff + 3, maxLen); + let out = ""; + let remain = maxLen - (3 + atLeastPre + atLeastSuff); + for (let i = 0; i < str.length - atLeastSuff; ++i) { + const c = str[i]; + if (i < atLeastPre) + out += c; + else if ((remain--) > 0) + out += c; + } + if (remain < 0) + out += "..."; + out += str.substring(str.length - atLeastSuff, str.length); + return out; + }, + + toTitleCase(str) { + return str.toTitleCase(); + }, + qq(str) { + return (str = str || "").qq(); + }, +}; +//#endregion +//#region CleanUtil +globalThis.CleanUtil = { + getCleanJson(data, {isMinify=false, isFast=true}={}) { + data = MiscUtil.copy(data); + data = MiscUtil.getWalker().walk(data, { + string: (str)=>CleanUtil.getCleanString(str, { + isFast + }) + }); + let str = isMinify ? JSON.stringify(data) : `${JSON.stringify(data, null, "\t")}\n`; + return str.replace(CleanUtil.STR_REPLACEMENTS_REGEX, (match)=>CleanUtil.STR_REPLACEMENTS[match]); + }, + + getCleanString(str, {isFast=true}={}) { + str = str.replace(CleanUtil.SHARED_REPLACEMENTS_REGEX, (match)=>CleanUtil.SHARED_REPLACEMENTS[match]).replace(CleanUtil._SOFT_HYPHEN_REMOVE_REGEX, ""); + + if (isFast) + return str; + + const ptrStack = { + _: "" + }; + CleanUtil._getCleanString_walkerStringHandler(ptrStack, 0, str); + return ptrStack._; + }, + + _getCleanString_walkerStringHandler(ptrStack, tagCount, str) { + const tagSplit = Renderer.splitByTags(str); + const len = tagSplit.length; + for (let i = 0; i < len; ++i) { + const s = tagSplit[i]; + if (!s) + continue; + if (s.startsWith("{@")) { + const [tag,text] = Renderer.splitFirstSpace(s.slice(1, -1)); + + ptrStack._ += `{${tag}${text.length ? " " : ""}`; + this._getCleanString_walkerStringHandler(ptrStack, tagCount + 1, text); + ptrStack._ += `}`; + } else { + if (tagCount) { + ptrStack._ += s; + } else { + ptrStack._ += s.replace(CleanUtil._DASH_COLLAPSE_REGEX, "$1").replace(CleanUtil._ELLIPSIS_COLLAPSE_REGEX, "$1"); + } + } + } + }, +}; +CleanUtil.SHARED_REPLACEMENTS = { + "โ€™": "'", + "โ€˜": "'", + "ย’": "'", + "โ€ฆ": "...", + "\u200B": "", + "\u2002": " ", + "๏ฌ€": "ff", + "๏ฌƒ": "ffi", + "๏ฌ„": "ffl", + "๏ฌ": "fi", + "๏ฌ‚": "fl", + "ฤฒ": "IJ", + "ฤณ": "ij", + "ว‡": "LJ", + "วˆ": "Lj", + "ว‰": "lj", + "วŠ": "NJ", + "ว‹": "Nj", + "วŒ": "nj", + "๏ฌ…": "ft", + "โ€œ": `"`, + "โ€": `"`, + "\u201a": ",", +}; +CleanUtil.STR_REPLACEMENTS = { + "โ€”": "\\u2014", + "โ€“": "\\u2013", + "โ€‘": "\\u2011", + "โˆ’": "\\u2212", + "ย ": "\\u00A0", + "โ€‡": "\\u2007", +}; +CleanUtil.SHARED_REPLACEMENTS_REGEX = new RegExp(Object.keys(CleanUtil.SHARED_REPLACEMENTS).join("|"),"g"); +CleanUtil.STR_REPLACEMENTS_REGEX = new RegExp(Object.keys(CleanUtil.STR_REPLACEMENTS).join("|"),"g"); +CleanUtil._SOFT_HYPHEN_REMOVE_REGEX = /\u00AD *\r?\n?\r?/g; +CleanUtil._ELLIPSIS_COLLAPSE_REGEX = /\s*(\.\s*\.\s*\.)/g; +CleanUtil._DASH_COLLAPSE_REGEX = /[ ]*([\u2014\u2013])[ ]*/g; + +//#endregion +//#region ExcludeUtil +globalThis.ExcludeUtil = { + isInitialised: false, + _excludes: null, + _cache_excludesLookup: null, + _lock: null, + + async pInitialise({lockToken=null}={}) { + try { + await ExcludeUtil._lock.pLock({ + token: lockToken + }); + await ExcludeUtil._pInitialise(); + } finally { + ExcludeUtil._lock.unlock(); + } + }, + + async _pInitialise() { + if (ExcludeUtil.isInitialised) + return; + + ExcludeUtil.pSave = MiscUtil.throttle(ExcludeUtil._pSave, 50); + try { + ExcludeUtil._excludes = await StorageUtil.pGet(VeCt.STORAGE_EXCLUDES) || []; + ExcludeUtil._excludes = ExcludeUtil._excludes.filter(it=>it.hash); + } catch (e) { + JqueryUtil.doToast({ + content: "Error when loading content blocklist! Purged blocklist data. (See the log for more information.)", + type: "danger", + }); + try { + await StorageUtil.pRemove(VeCt.STORAGE_EXCLUDES); + } catch (e) { + setTimeout(()=>{ + throw e; + } + ); + } + ExcludeUtil._excludes = null; + window.location.hash = ""; + setTimeout(()=>{ + throw e; + } + ); + } + ExcludeUtil.isInitialised = true; + }, + + getList() { + return MiscUtil.copyFast(ExcludeUtil._excludes || []); + }, + + async pSetList(toSet) { + ExcludeUtil._excludes = toSet; + ExcludeUtil._cache_excludesLookup = null; + await ExcludeUtil.pSave(); + }, + + async pExtendList(toAdd) { + try { + const lockToken = await ExcludeUtil._lock.pLock(); + await ExcludeUtil._pExtendList({ + toAdd, + lockToken + }); + } finally { + ExcludeUtil._lock.unlock(); + } + }, + + async _pExtendList({toAdd, lockToken}) { + await ExcludeUtil.pInitialise({ + lockToken + }); + this._doBuildCache(); + + const out = MiscUtil.copyFast(ExcludeUtil._excludes || []); + MiscUtil.copyFast(toAdd || []).filter(({hash, category, source})=>{ + if (!hash || !category || !source) + return false; + const cacheUid = ExcludeUtil._getCacheUids(hash, category, source, true); + return !ExcludeUtil._cache_excludesLookup[cacheUid]; + } + ).forEach(it=>out.push(it)); + + await ExcludeUtil.pSetList(out); + }, + + _doBuildCache() { + if (ExcludeUtil._cache_excludesLookup) + return; + if (!ExcludeUtil._excludes) + return; + + ExcludeUtil._cache_excludesLookup = {}; + ExcludeUtil._excludes.forEach(({source, category, hash})=>{ + const cacheUid = ExcludeUtil._getCacheUids(hash, category, source, true); + ExcludeUtil._cache_excludesLookup[cacheUid] = true; + } + ); + }, + + _getCacheUids(hash, category, source, isExact) { + hash = (hash || "").toLowerCase(); + category = (category || "").toLowerCase(); + source = (source?.source || source || "").toLowerCase(); + + const exact = `${hash}__${category}__${source}`; + if (isExact) + return [exact]; + + return [`${hash}__${category}__${source}`, `*__${category}__${source}`, `${hash}__*__${source}`, `${hash}__${category}__*`, `*__*__${source}`, `*__${category}__*`, `${hash}__*__*`, `*__*__*`, ]; + }, + + _excludeCount: 0, + isExcluded(hash, category, source, opts) { + if (!ExcludeUtil._excludes || !ExcludeUtil._excludes.length) + return false; + if (!source) + throw new Error(`Entity had no source!`); + opts = opts || {}; + + this._doBuildCache(); + + hash = (hash || "").toLowerCase(); + category = (category || "").toLowerCase(); + source = (source.source || source || "").toLowerCase(); + + const isExcluded = ExcludeUtil._isExcluded(hash, category, source); + if (!isExcluded) + return isExcluded; + + if (!opts.isNoCount) + ++ExcludeUtil._excludeCount; + + return isExcluded; + }, + + _isExcluded(hash, category, source) { + for (const cacheUid of ExcludeUtil._getCacheUids(hash, category, source)) { + if (ExcludeUtil._cache_excludesLookup[cacheUid]) + return true; + } + return false; + }, + + isAllContentExcluded(list) { + return (!list.length && ExcludeUtil._excludeCount) || (list.length > 0 && list.length === ExcludeUtil._excludeCount); + }, + getAllContentBlocklistedHtml() { + return `
    (All content blocklisted)
    `; + }, + + async _pSave() { + return StorageUtil.pSet(VeCt.STORAGE_EXCLUDES, ExcludeUtil._excludes); + }, + + async pSave() {}, +}; +//#endregion + +//#region SourceUtil +globalThis.SourceUtil = { + ADV_BOOK_GROUPS: [{ + group: "core", + displayName: "Core" + }, { + group: "supplement", + displayName: "Supplements" + }, { + group: "setting", + displayName: "Settings" + }, { + group: "setting-alt", + displayName: "Additional Settings" + }, { + group: "supplement-alt", + displayName: "Extras" + }, { + group: "prerelease", + displayName: "Prerelease" + }, { + group: "homebrew", + displayName: "Homebrew" + }, { + group: "screen", + displayName: "Screens" + }, { + group: "recipe", + displayName: "Recipes" + }, { + group: "other", + displayName: "Miscellaneous" + }, ], + + _subclassReprintLookup: {}, + async pInitSubclassReprintLookup() { + SourceUtil._subclassReprintLookup = await DataUtil.loadJSON(`${Renderer.get().baseUrl}data/generated/gendata-subclass-lookup.json`); + }, + + isSubclassReprinted(className, classSource, subclassShortName, subclassSource) { + const fromLookup = MiscUtil.get(SourceUtil._subclassReprintLookup, classSource, className, subclassSource, subclassShortName); + return fromLookup ? fromLookup.isReprinted : false; + }, + + isSiteSource(source) { + return !!Parser.SOURCE_JSON_TO_FULL[source]; + }, + + isAdventure(source) { + if (source instanceof FilterItem) + source = source.item; + return Parser.SOURCES_ADVENTURES.has(source); + }, + + isCoreOrSupplement(source) { + if (source instanceof FilterItem) + source = source.item; + return Parser.SOURCES_CORE_SUPPLEMENTS.has(source); + }, + + isNonstandardSource(source) { + if (source == null) + return false; + return ((typeof BrewUtil2 === "undefined" || !BrewUtil2.hasSourceJson(source)) && SourceUtil.isNonstandardSourceWotc(source)) || SourceUtil.isPrereleaseSource(source); + }, + + + /** + * Returns true if the source is partnered with WOTC + * @param {string} source + * @returns {Boolean} + */ + isPartneredSourceWotc(source) { + if (source == null) { return false; } + return Parser.SOURCES_PARTNERED_WOTC.has(source); + }, + + + /** + * Returns true if the source is a prerelease source + * @param {string} source + * @returns {boolean} + */ + isPrereleaseSource(source) { + if (source == null) + return false; + if (typeof PrereleaseUtil !== "undefined" && PrereleaseUtil.hasSourceJson(source)) + return true; + return source.startsWith(Parser.SRC_UA_PREFIX) || source.startsWith(Parser.SRC_UA_ONE_PREFIX); + }, + + isNonstandardSourceWotc(source) { + return SourceUtil.isPrereleaseSource(source) || source.startsWith(Parser.SRC_PS_PREFIX) || source.startsWith(Parser.SRC_AL_PREFIX) || source.startsWith(Parser.SRC_MCVX_PREFIX) || Parser.SOURCES_NON_STANDARD_WOTC.has(source); + }, + + FILTER_GROUP_STANDARD: 0, + FILTER_GROUP_PARTNERED: 1, + FILTER_GROUP_NON_STANDARD: 2, + FILTER_GROUP_HOMEBREW: 3, + + getFilterGroup(source) { + if (source instanceof FilterItem) + source = source.item; + if ((typeof PrereleaseUtil !== "undefined" && PrereleaseUtil.hasSourceJson(source)) || SourceUtil.isNonstandardSource(source)) + return SourceUtil.FILTER_GROUP_NON_STANDARD; + if (typeof BrewUtil2 !== "undefined" && BrewUtil2.hasSourceJson(source)) + return SourceUtil.FILTER_GROUP_HOMEBREW; + if (SourceUtil.isPartneredSourceWotc(source)) + return SourceUtil.FILTER_GROUP_PARTNERED; + return SourceUtil.FILTER_GROUP_STANDARD; + }, + + getAdventureBookSourceHref(source, page) { + if (!source) + return null; + source = source.toLowerCase(); + + let docPage, mappedSource; + if (Parser.SOURCES_AVAILABLE_DOCS_BOOK[source]) { + docPage = UrlUtil.PG_BOOK; + mappedSource = Parser.SOURCES_AVAILABLE_DOCS_BOOK[source]; + } else if (Parser.SOURCES_AVAILABLE_DOCS_ADVENTURE[source]) { + docPage = UrlUtil.PG_ADVENTURE; + mappedSource = Parser.SOURCES_AVAILABLE_DOCS_ADVENTURE[source]; + } + if (!docPage) + return null; + + mappedSource = mappedSource.toLowerCase(); + + return `${docPage}#${[mappedSource, page ? `page:${page}` : null].filter(Boolean).join(HASH_PART_SEP)}`; + }, + + getEntitySource(it) { + return it.source || it.inherits?.source; + }, +}; +//#endregion +//#region MiscUtil +globalThis.MiscUtil = { + COLOR_HEALTHY: "#00bb20", + COLOR_HURT: "#c5ca00", + COLOR_BLOODIED: "#f7a100", + COLOR_DEFEATED: "#cc0000", + + copy(obj, {isSafe=false, isPreserveUndefinedValueKeys=false}={}) { + if (isSafe && obj === undefined) + return undefined; + return JSON.parse(JSON.stringify(obj)); + }, + + copyFast(obj) { + if ((typeof obj !== "object") || obj == null) + return obj; + + if (obj instanceof Array) + return obj.map(MiscUtil.copyFast); + + const cpy = {}; + for (const k of Object.keys(obj)) + cpy[k] = MiscUtil.copyFast(obj[k]); + return cpy; + }, + + async pCopyTextToClipboard(text) { + function doCompatibilityCopy() { + const $iptTemp = $(``).appendTo(document.body).val(text).select(); + document.execCommand("Copy"); + $iptTemp.remove(); + } + + if (navigator && navigator.permissions) { + try { + const access = await navigator.permissions.query({ + name: "clipboard-write" + }); + if (access.state === "granted" || access.state === "prompt") { + await navigator.clipboard.writeText(text); + } else + doCompatibilityCopy(); + } catch (e) { + doCompatibilityCopy(); + } + } else + doCompatibilityCopy(); + }, + + checkProperty(object, ...path) { + for (let i = 0; i < path.length; ++i) { + object = object[path[i]]; + if (object == null) + return false; + } + return true; + }, + + /**Returns null if object doesn't have all the child (and grandchild) properties listed in the path. + * Returns the final property in the path if it exists */ + get(object, ...path) { + if (object == null) + return null; + for (let i = 0; i < path.length; ++i) { + object = object[path[i]]; + if (object == null) + return object; + } + return object; + }, + + set(object, ...pathAndVal) { + if (object == null) + return null; + + const val = pathAndVal.pop(); + if (!pathAndVal.length) + return null; + + const len = pathAndVal.length; + for (let i = 0; i < len; ++i) { + const pathPart = pathAndVal[i]; + if (i === len - 1) + object[pathPart] = val; + else + object = (object[pathPart] = object[pathPart] || {}); + } + + return val; + }, + + getOrSet(object, ...pathAndVal) { + if (pathAndVal.length < 2) + return null; + const existing = MiscUtil.get(object, ...pathAndVal.slice(0, -1)); + if (existing != null) + return existing; + return MiscUtil.set(object, ...pathAndVal); + }, + + getThenSetCopy(object1, object2, ...path) { + const val = MiscUtil.get(object1, ...path); + return MiscUtil.set(object2, ...path, MiscUtil.copyFast(val, { + isSafe: true + })); + }, + + delete(object, ...path) { + if (object == null) + return object; + for (let i = 0; i < path.length - 1; ++i) { + object = object[path[i]]; + if (object == null) + return object; + } + return delete object[path.last()]; + }, + + deleteObjectPath(object, ...path) { + const stack = [object]; + + if (object == null) + return object; + for (let i = 0; i < path.length - 1; ++i) { + object = object[path[i]]; + stack.push(object); + if (object === undefined) + return object; + } + const out = delete object[path.last()]; + + for (let i = path.length - 1; i > 0; --i) { + if (!Object.keys(stack[i]).length) + delete stack[i - 1][path[i - 1]]; + } + + return out; + }, + + merge(obj1, obj2) { + obj2 = MiscUtil.copyFast(obj2); + + Object.entries(obj2).forEach(([k,v])=>{ + if (obj1[k] == null) { + obj1[k] = v; + return; + } + + if (typeof obj1[k] === "object" && typeof v === "object" && !(obj1[k]instanceof Array) && !(v instanceof Array)) { + MiscUtil.merge(obj1[k], v); + return; + } + + obj1[k] = v; + } + ); + + return obj1; + }, + + mix: (superclass)=>new MiscUtil._MixinBuilder(superclass), + _MixinBuilder: function(superclass) { + this.superclass = superclass; + + this.with = function(...mixins) { + return mixins.reduce((c,mixin)=>mixin(c), this.superclass); + } + ; + }, + + clearSelection() { + if (document.getSelection) { + document.getSelection().removeAllRanges(); + document.getSelection().addRange(document.createRange()); + } else if (window.getSelection) { + if (window.getSelection().removeAllRanges) { + window.getSelection().removeAllRanges(); + window.getSelection().addRange(document.createRange()); + } else if (window.getSelection().empty) { + window.getSelection().empty(); + } + } else if (document.selection) { + document.selection.empty(); + } + }, + + randomColor() { + let r; + let g; + let b; + const h = RollerUtil.randomise(30, 0) / 30; + const i = ~~(h * 6); + const f = h * 6 - i; + const q = 1 - f; + switch (i % 6) { + case 0: + r = 1; + g = f; + b = 0; + break; + case 1: + r = q; + g = 1; + b = 0; + break; + case 2: + r = 0; + g = 1; + b = f; + break; + case 3: + r = 0; + g = q; + b = 1; + break; + case 4: + r = f; + g = 0; + b = 1; + break; + case 5: + r = 1; + g = 0; + b = q; + break; + } + return `#${`00${(~~(r * 255)).toString(16)}`.slice(-2)}${`00${(~~(g * 255)).toString(16)}`.slice(-2)}${`00${(~~(b * 255)).toString(16)}`.slice(-2)}`; + }, + + invertColor(hex, opts) { + opts = opts || {}; + + hex = hex.slice(1); + let r = parseInt(hex.slice(0, 2), 16); + let g = parseInt(hex.slice(2, 4), 16); + let b = parseInt(hex.slice(4, 6), 16); + + const isDark = (r * 0.299 + g * 0.587 + b * 0.114) > 186; + if (opts.dark && opts.light) + return isDark ? opts.dark : opts.light; + else if (opts.bw) + return isDark ? "#000000" : "#FFFFFF"; + + r = (255 - r).toString(16); + g = (255 - g).toString(16); + b = (255 - b).toString(16); + return `#${[r, g, b].map(it=>it.padStart(2, "0")).join("")}`; + }, + + scrollPageTop() { + document.body.scrollTop = document.documentElement.scrollTop = 0; + }, + + expEval(str) { + return new Function(`return ${str.replace(/[^-()\d/*+.]/g, "")}`)(); + }, + + parseNumberRange(input, min=Number.MIN_SAFE_INTEGER, max=Number.MAX_SAFE_INTEGER) { + if (!input || !input.trim()) + return null; + + const errInvalid = input=>{ + throw new Error(`Could not parse range input "${input}"`); + } + ; + + const errOutOfRange = ()=>{ + throw new Error(`Number was out of range! Range was ${min}-${max} (inclusive).`); + } + ; + + const isOutOfRange = (num)=>num < min || num > max; + + const addToRangeVal = (range,num)=>range.add(num); + + const addToRangeLoHi = (range,lo,hi)=>{ + for (let i = lo; i <= hi; ++i) + range.add(i); + } + ; + + const clean = input.replace(/\s*/g, ""); + if (!/^((\d+-\d+|\d+),)*(\d+-\d+|\d+)$/.exec(clean)) + errInvalid(); + + const parts = clean.split(","); + const out = new Set(); + + for (const part of parts) { + if (part.includes("-")) { + const spl = part.split("-"); + const numLo = Number(spl[0]); + const numHi = Number(spl[1]); + + if (isNaN(numLo) || isNaN(numHi) || numLo === 0 || numHi === 0 || numLo > numHi) + errInvalid(); + + if (isOutOfRange(numLo) || isOutOfRange(numHi)) + errOutOfRange(); + + if (numLo === numHi) + addToRangeVal(out, numLo); + else + addToRangeLoHi(out, numLo, numHi); + continue; + } + + const num = Number(part); + if (isNaN(num) || num === 0) + errInvalid(); + + if (isOutOfRange(num)) + errOutOfRange(); + addToRangeVal(out, num); + } + + return out; + }, + + findCommonPrefix(strArr, {isRespectWordBoundaries}={}) { + if (isRespectWordBoundaries) { + return MiscUtil._findCommonPrefixSuffixWords({ + strArr + }); + } + + let prefix = null; + strArr.forEach(s=>{ + if (prefix == null) { + prefix = s; + return; + } + + const minLen = Math.min(s.length, prefix.length); + for (let i = 0; i < minLen; ++i) { + const cp = prefix[i]; + const cs = s[i]; + if (cp !== cs) { + prefix = prefix.substring(0, i); + break; + } + } + } + ); + return prefix; + }, + + findCommonSuffix(strArr, {isRespectWordBoundaries}={}) { + if (!isRespectWordBoundaries) + throw new Error(`Unimplemented!`); + + return MiscUtil._findCommonPrefixSuffixWords({ + strArr, + isSuffix: true + }); + }, + + _findCommonPrefixSuffixWords({strArr, isSuffix}) { + let prefixTks = null; + let lenMax = -1; + + strArr.map(str=>{ + lenMax = Math.max(lenMax, str.length); + return str.split(" "); + } + ).forEach(tks=>{ + if (isSuffix) + tks.reverse(); + + if (prefixTks == null) + return prefixTks = [...tks]; + + const minLen = Math.min(tks.length, prefixTks.length); + while (prefixTks.length > minLen) + prefixTks.pop(); + + for (let i = 0; i < minLen; ++i) { + const cp = prefixTks[i]; + const cs = tks[i]; + if (cp !== cs) { + prefixTks = prefixTks.slice(0, i); + break; + } + } + } + ); + + if (isSuffix) + prefixTks.reverse(); + + if (!prefixTks.length) + return ""; + + const out = prefixTks.join(" "); + if (out.length === lenMax) + return out; + + return isSuffix ? ` ${prefixTks.join(" ")}` : `${prefixTks.join(" ")} `; + }, + + calculateBlendedColor(fgHexTarget, fgOpacity, bgHex) { + const fgDcTarget = CryptUtil.hex2Dec(fgHexTarget); + const bgDc = CryptUtil.hex2Dec(bgHex); + return ((fgDcTarget - ((1 - fgOpacity) * bgDc)) / fgOpacity).toString(16); + }, + + debounce(func, wait, options) { + let lastArgs; + let lastThis; + let maxWait; + let result; + let timerId; + let lastCallTime; + let lastInvokeTime = 0; + let leading = false; + let maxing = false; + let trailing = true; + + wait = Number(wait) || 0; + if (typeof options === "object") { + leading = !!options.leading; + maxing = "maxWait"in options; + maxWait = maxing ? Math.max(Number(options.maxWait) || 0, wait) : maxWait; + trailing = "trailing"in options ? !!options.trailing : trailing; + } + + function invokeFunc(time) { + let args = lastArgs; + let thisArg = lastThis; + + lastArgs = lastThis = undefined; + lastInvokeTime = time; + result = func.apply(thisArg, args); + return result; + } + + function leadingEdge(time) { + lastInvokeTime = time; + timerId = setTimeout(timerExpired, wait); + return leading ? invokeFunc(time) : result; + } + + function remainingWait(time) { + let timeSinceLastCall = time - lastCallTime; + let timeSinceLastInvoke = time - lastInvokeTime; + let result = wait - timeSinceLastCall; + return maxing ? Math.min(result, maxWait - timeSinceLastInvoke) : result; + } + + function shouldInvoke(time) { + let timeSinceLastCall = time - lastCallTime; + let timeSinceLastInvoke = time - lastInvokeTime; + + return (lastCallTime === undefined || (timeSinceLastCall >= wait) || (timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait)); + } + + function timerExpired() { + const time = Date.now(); + if (shouldInvoke(time)) { + return trailingEdge(time); + } + timerId = setTimeout(timerExpired, remainingWait(time)); + } + + function trailingEdge(time) { + timerId = undefined; + + if (trailing && lastArgs) + return invokeFunc(time); + lastArgs = lastThis = undefined; + return result; + } + + function cancel() { + if (timerId !== undefined) + clearTimeout(timerId); + lastInvokeTime = 0; + lastArgs = lastCallTime = lastThis = timerId = undefined; + } + + function flush() { + return timerId === undefined ? result : trailingEdge(Date.now()); + } + + function debounced() { + let time = Date.now(); + let isInvoking = shouldInvoke(time); + lastArgs = arguments; + lastThis = this; + lastCallTime = time; + + if (isInvoking) { + if (timerId === undefined) + return leadingEdge(lastCallTime); + if (maxing) { + timerId = setTimeout(timerExpired, wait); + return invokeFunc(lastCallTime); + } + } + if (timerId === undefined) + timerId = setTimeout(timerExpired, wait); + return result; + } + + debounced.cancel = cancel; + debounced.flush = flush; + return debounced; + }, + + throttle(func, wait, options) { + let leading = true; + let trailing = true; + + if (typeof options === "object") { + leading = "leading"in options ? !!options.leading : leading; + trailing = "trailing"in options ? !!options.trailing : trailing; + } + + return this.debounce(func, wait, { + leading, + maxWait: wait, + trailing + }); + }, + + pDelay(msecs, resolveAs) { + return new Promise(resolve=>setTimeout(()=>resolve(resolveAs), msecs)); + }, + + GENERIC_WALKER_ENTRIES_KEY_BLOCKLIST: new Set(["caption", "type", "colLabels", "colLabelGroups", "name", "colStyles", "style", "shortName", "subclassShortName", "id", "path"]), + + getWalker(opts) { + opts = opts || {}; + + if (opts.isBreakOnReturn && !opts.isNoModification) + throw new Error(`"isBreakOnReturn" may only be used in "isNoModification" mode!`); + + const keyBlocklist = opts.keyBlocklist || new Set(); + + const getMappedPrimitive = (obj,primitiveHandlers,lastKey,stack,prop,propPre,propPost)=>{ + if (primitiveHandlers[propPre]) + MiscUtil._getWalker_runHandlers({ + handlers: primitiveHandlers[propPre], + obj, + lastKey, + stack + }); + if (primitiveHandlers[prop]) { + const out = MiscUtil._getWalker_applyHandlers({ + opts, + handlers: primitiveHandlers[prop], + obj, + lastKey, + stack + }); + if (out === VeCt.SYM_WALKER_BREAK) + return out; + if (!opts.isNoModification) + obj = out; + } + if (primitiveHandlers[propPost]) + MiscUtil._getWalker_runHandlers({ + handlers: primitiveHandlers[propPost], + obj, + lastKey, + stack + }); + return obj; + } + ; + + const doObjectRecurse = (obj,primitiveHandlers,stack)=>{ + for (const k of Object.keys(obj)) { + if (keyBlocklist.has(k)) + continue; + + const out = fn(obj[k], primitiveHandlers, k, stack); + if (out === VeCt.SYM_WALKER_BREAK) + return VeCt.SYM_WALKER_BREAK; + if (!opts.isNoModification) + obj[k] = out; + } + } + ; + + const fn = (obj,primitiveHandlers,lastKey,stack)=>{ + if (obj === null) + return getMappedPrimitive(obj, primitiveHandlers, lastKey, stack, "null", "preNull", "postNull"); + + switch (typeof obj) { + case "undefined": + return getMappedPrimitive(obj, primitiveHandlers, lastKey, stack, "undefined", "preUndefined", "postUndefined"); + case "boolean": + return getMappedPrimitive(obj, primitiveHandlers, lastKey, stack, "boolean", "preBoolean", "postBoolean"); + case "number": + return getMappedPrimitive(obj, primitiveHandlers, lastKey, stack, "number", "preNumber", "postNumber"); + case "string": + return getMappedPrimitive(obj, primitiveHandlers, lastKey, stack, "string", "preString", "postString"); + case "object": + { + if (obj instanceof Array) { + if (primitiveHandlers.preArray) + MiscUtil._getWalker_runHandlers({ + handlers: primitiveHandlers.preArray, + obj, + lastKey, + stack + }); + if (opts.isDepthFirst) { + if (stack) + stack.push(obj); + const out = new Array(obj.length); + for (let i = 0, len = out.length; i < len; ++i) { + out[i] = fn(obj[i], primitiveHandlers, lastKey, stack); + if (out[i] === VeCt.SYM_WALKER_BREAK) + return out[i]; + } + if (!opts.isNoModification) + obj = out; + if (stack) + stack.pop(); + + if (primitiveHandlers.array) { + const out = MiscUtil._getWalker_applyHandlers({ + opts, + handlers: primitiveHandlers.array, + obj, + lastKey, + stack + }); + if (out === VeCt.SYM_WALKER_BREAK) + return out; + if (!opts.isNoModification) + obj = out; + } + if (obj == null) { + if (!opts.isAllowDeleteArrays) + throw new Error(`Array handler(s) returned null!`); + } + } else { + if (primitiveHandlers.array) { + const out = MiscUtil._getWalker_applyHandlers({ + opts, + handlers: primitiveHandlers.array, + obj, + lastKey, + stack + }); + if (out === VeCt.SYM_WALKER_BREAK) + return out; + if (!opts.isNoModification) + obj = out; + } + if (obj != null) { + const out = new Array(obj.length); + for (let i = 0, len = out.length; i < len; ++i) { + if (stack) + stack.push(obj); + out[i] = fn(obj[i], primitiveHandlers, lastKey, stack); + if (stack) + stack.pop(); + if (out[i] === VeCt.SYM_WALKER_BREAK) + return out[i]; + } + if (!opts.isNoModification) + obj = out; + } else { + if (!opts.isAllowDeleteArrays) + throw new Error(`Array handler(s) returned null!`); + } + } + if (primitiveHandlers.postArray) + MiscUtil._getWalker_runHandlers({ + handlers: primitiveHandlers.postArray, + obj, + lastKey, + stack + }); + return obj; + } + + if (primitiveHandlers.preObject) + MiscUtil._getWalker_runHandlers({ + handlers: primitiveHandlers.preObject, + obj, + lastKey, + stack + }); + if (opts.isDepthFirst) { + if (stack) + stack.push(obj); + const flag = doObjectRecurse(obj, primitiveHandlers, stack); + if (stack) + stack.pop(); + if (flag === VeCt.SYM_WALKER_BREAK) + return flag; + + if (primitiveHandlers.object) { + const out = MiscUtil._getWalker_applyHandlers({ + opts, + handlers: primitiveHandlers.object, + obj, + lastKey, + stack + }); + if (out === VeCt.SYM_WALKER_BREAK) + return out; + if (!opts.isNoModification) + obj = out; + } + if (obj == null) { + if (!opts.isAllowDeleteObjects) + throw new Error(`Object handler(s) returned null!`); + } + } else { + if (primitiveHandlers.object) { + const out = MiscUtil._getWalker_applyHandlers({ + opts, + handlers: primitiveHandlers.object, + obj, + lastKey, + stack + }); + if (out === VeCt.SYM_WALKER_BREAK) + return out; + if (!opts.isNoModification) + obj = out; + } + if (obj == null) { + if (!opts.isAllowDeleteObjects) + throw new Error(`Object handler(s) returned null!`); + } else { + if (stack) + stack.push(obj); + const flag = doObjectRecurse(obj, primitiveHandlers, stack); + if (stack) + stack.pop(); + if (flag === VeCt.SYM_WALKER_BREAK) + return flag; + } + } + if (primitiveHandlers.postObject) + MiscUtil._getWalker_runHandlers({ + handlers: primitiveHandlers.postObject, + obj, + lastKey, + stack + }); + return obj; + } + default: + throw new Error(`Unhandled type "${typeof obj}"`); + } + } + ; + + return { + walk: fn + }; + }, + + _getWalker_applyHandlers({opts, handlers, obj, lastKey, stack}) { + handlers = handlers instanceof Array ? handlers : [handlers]; + const didBreak = handlers.some(h=>{ + const out = h(obj, lastKey, stack); + if (opts.isBreakOnReturn && out) + return true; + if (!opts.isNoModification) + obj = out; + } + ); + if (didBreak) + return VeCt.SYM_WALKER_BREAK; + return obj; + }, + + _getWalker_runHandlers({handlers, obj, lastKey, stack}) { + handlers = handlers instanceof Array ? handlers : [handlers]; + handlers.forEach(h=>h(obj, lastKey, stack)); + }, + + getAsyncWalker(opts) { + opts = opts || {}; + const keyBlocklist = opts.keyBlocklist || new Set(); + + const pFn = async(obj,primitiveHandlers,lastKey,stack)=>{ + if (obj == null) { + if (primitiveHandlers.null) + return MiscUtil._getAsyncWalker_pApplyHandlers({ + opts, + handlers: primitiveHandlers.null, + obj, + lastKey, + stack + }); + return obj; + } + + const pDoObjectRecurse = async()=>{ + await Object.keys(obj).pSerialAwaitMap(async k=>{ + const v = obj[k]; + if (keyBlocklist.has(k)) + return; + const out = await pFn(v, primitiveHandlers, k, stack); + if (!opts.isNoModification) + obj[k] = out; + } + ); + } + ; + + const to = typeof obj; + switch (to) { + case undefined: + if (primitiveHandlers.preUndefined) + await MiscUtil._getAsyncWalker_pRunHandlers({ + handlers: primitiveHandlers.preUndefined, + obj, + lastKey, + stack + }); + if (primitiveHandlers.undefined) { + const out = await MiscUtil._getAsyncWalker_pApplyHandlers({ + opts, + handlers: primitiveHandlers.undefined, + obj, + lastKey, + stack + }); + if (!opts.isNoModification) + obj = out; + } + if (primitiveHandlers.postUndefined) + await MiscUtil._getAsyncWalker_pRunHandlers({ + handlers: primitiveHandlers.postUndefined, + obj, + lastKey, + stack + }); + return obj; + case "boolean": + if (primitiveHandlers.preBoolean) + await MiscUtil._getAsyncWalker_pRunHandlers({ + handlers: primitiveHandlers.preBoolean, + obj, + lastKey, + stack + }); + if (primitiveHandlers.boolean) { + const out = await MiscUtil._getAsyncWalker_pApplyHandlers({ + opts, + handlers: primitiveHandlers.boolean, + obj, + lastKey, + stack + }); + if (!opts.isNoModification) + obj = out; + } + if (primitiveHandlers.postBoolean) + await MiscUtil._getAsyncWalker_pRunHandlers({ + handlers: primitiveHandlers.postBoolean, + obj, + lastKey, + stack + }); + return obj; + case "number": + if (primitiveHandlers.preNumber) + await MiscUtil._getAsyncWalker_pRunHandlers({ + handlers: primitiveHandlers.preNumber, + obj, + lastKey, + stack + }); + if (primitiveHandlers.number) { + const out = await MiscUtil._getAsyncWalker_pApplyHandlers({ + opts, + handlers: primitiveHandlers.number, + obj, + lastKey, + stack + }); + if (!opts.isNoModification) + obj = out; + } + if (primitiveHandlers.postNumber) + await MiscUtil._getAsyncWalker_pRunHandlers({ + handlers: primitiveHandlers.postNumber, + obj, + lastKey, + stack + }); + return obj; + case "string": + if (primitiveHandlers.preString) + await MiscUtil._getAsyncWalker_pRunHandlers({ + handlers: primitiveHandlers.preString, + obj, + lastKey, + stack + }); + if (primitiveHandlers.string) { + const out = await MiscUtil._getAsyncWalker_pApplyHandlers({ + opts, + handlers: primitiveHandlers.string, + obj, + lastKey, + stack + }); + if (!opts.isNoModification) + obj = out; + } + if (primitiveHandlers.postString) + await MiscUtil._getAsyncWalker_pRunHandlers({ + handlers: primitiveHandlers.postString, + obj, + lastKey, + stack + }); + return obj; + case "object": + { + if (obj instanceof Array) { + if (primitiveHandlers.preArray) + await MiscUtil._getAsyncWalker_pRunHandlers({ + handlers: primitiveHandlers.preArray, + obj, + lastKey, + stack + }); + if (opts.isDepthFirst) { + if (stack) + stack.push(obj); + const out = await obj.pSerialAwaitMap(it=>pFn(it, primitiveHandlers, lastKey, stack)); + if (!opts.isNoModification) + obj = out; + if (stack) + stack.pop(); + + if (primitiveHandlers.array) { + const out = await MiscUtil._getAsyncWalker_pApplyHandlers({ + opts, + handlers: primitiveHandlers.array, + obj, + lastKey, + stack + }); + if (!opts.isNoModification) + obj = out; + } + if (obj == null) { + if (!opts.isAllowDeleteArrays) + throw new Error(`Array handler(s) returned null!`); + } + } else { + if (primitiveHandlers.array) { + const out = await MiscUtil._getAsyncWalker_pApplyHandlers({ + opts, + handlers: primitiveHandlers.array, + obj, + lastKey, + stack + }); + if (!opts.isNoModification) + obj = out; + } + if (obj != null) { + const out = await obj.pSerialAwaitMap(it=>pFn(it, primitiveHandlers, lastKey, stack)); + if (!opts.isNoModification) + obj = out; + } else { + if (!opts.isAllowDeleteArrays) + throw new Error(`Array handler(s) returned null!`); + } + } + if (primitiveHandlers.postArray) + await MiscUtil._getAsyncWalker_pRunHandlers({ + handlers: primitiveHandlers.postArray, + obj, + lastKey, + stack + }); + return obj; + } else { + if (primitiveHandlers.preObject) + await MiscUtil._getAsyncWalker_pRunHandlers({ + handlers: primitiveHandlers.preObject, + obj, + lastKey, + stack + }); + if (opts.isDepthFirst) { + if (stack) + stack.push(obj); + await pDoObjectRecurse(); + if (stack) + stack.pop(); + + if (primitiveHandlers.object) { + const out = await MiscUtil._getAsyncWalker_pApplyHandlers({ + opts, + handlers: primitiveHandlers.object, + obj, + lastKey, + stack + }); + if (!opts.isNoModification) + obj = out; + } + if (obj == null) { + if (!opts.isAllowDeleteObjects) + throw new Error(`Object handler(s) returned null!`); + } + } else { + if (primitiveHandlers.object) { + const out = await MiscUtil._getAsyncWalker_pApplyHandlers({ + opts, + handlers: primitiveHandlers.object, + obj, + lastKey, + stack + }); + if (!opts.isNoModification) + obj = out; + } + if (obj == null) { + if (!opts.isAllowDeleteObjects) + throw new Error(`Object handler(s) returned null!`); + } else { + await pDoObjectRecurse(); + } + } + if (primitiveHandlers.postObject) + await MiscUtil._getAsyncWalker_pRunHandlers({ + handlers: primitiveHandlers.postObject, + obj, + lastKey, + stack + }); + return obj; + } + } + default: + throw new Error(`Unhandled type "${to}"`); + } + } + ; + + return { + pWalk: pFn + }; + }, + + async _getAsyncWalker_pApplyHandlers({opts, handlers, obj, lastKey, stack}) { + handlers = handlers instanceof Array ? handlers : [handlers]; + await handlers.pSerialAwaitMap(async pH=>{ + const out = await pH(obj, lastKey, stack); + if (!opts.isNoModification) + obj = out; + } + ); + return obj; + }, + + async _getAsyncWalker_pRunHandlers({handlers, obj, lastKey, stack}) { + handlers = handlers instanceof Array ? handlers : [handlers]; + await handlers.pSerialAwaitMap(pH=>pH(obj, lastKey, stack)); + }, + + pDefer(fn) { + return (async()=>fn())(); + }, +}; +//#endregion +//#region UtilDataConverter +class UtilDataConverter { + static getNameWithSourcePart(ent, {displayName=null, isActorItem=false}={}) { + return `${displayName || `${ent.type === "variant" ? "Variant: " : ""}${Renderer.stripTags(UtilEntityGeneric.getName(ent))}`}${!isActorItem && ent.source && Config.get("import", "isAddSourceToName") ? ` (${Parser.sourceJsonToAbv(ent.source)})` : ""}`; + } + + static async pGetItemWeaponType(uid) { + uid = uid.toLowerCase().trim(); + + if (UtilDataConverter.WEAPONS_MARTIAL.includes(uid)){return "martial";} + if (UtilDataConverter.WEAPONS_SIMPLE.includes(uid)){return "simple";} + + let[name,source] = Renderer.splitTagByPipe(uid); + source = source || "phb"; + const hash = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_ITEMS]({name, source}); + + //TEMPFIX + return null; + const found = await DataLoader.pCacheAndGet(UrlUtil.PG_ITEMS, source, hash); + return found?.weaponCategory; + } + + static async _pGetClassSubclass_pInitCache({cache}) { + cache = cache || {}; + if (!cache._allClasses && !cache._allSubclasses) { + const classData = await DataUtil.class.loadJSON(); + const prerelease = await PrereleaseUtil.pGetBrewProcessed(); + const brew = await BrewUtil2.pGetBrewProcessed(); + + cache._allClasses = [...(classData.class || []), ...(prerelease?.class || []), ...(brew?.class || []), ]; + + cache._allSubclasses = [...(classData.subclass || []), ...(prerelease?.subclass || []), ...(brew?.subclass || []), ]; + } + return cache; + } + + static async pGetClassItemClassAndSubclass({sheetItem, subclassSheetItems, cache=null}={}) { + cache = await this._pGetClassSubclass_pInitCache({ + cache + }); + + const nameLowerClean = sheetItem.name.toLowerCase().trim(); + const sourceLowerClean = (UtilDocumentSource.getDocumentSource(sheetItem).source || "").toLowerCase(); + + const matchingClasses = cache._allClasses.filter(cls=>cls.name.toLowerCase() === nameLowerClean && (!Config.get("import", "isStrictMatching") || sourceLowerClean === Parser.sourceJsonToAbv(cls.source).toLowerCase()), ); + if (!matchingClasses.length) + return { + matchingClasses: [], + matchingSubclasses: [], + sheetItem + }; + + if (!subclassSheetItems?.length) + return { + matchingClasses, + matchingSubclasses: [], + sheetItem + }; + + const matchingSubclasses = matchingClasses.map(cls=>{ + const classSubclassSheetItems = subclassSheetItems.filter(scItem=>scItem.system.classIdentifier === sheetItem.system.identifier); + return cache._allSubclasses.filter(sc=>{ + if (sc.className !== cls.name || sc.classSource !== cls.source) + return false; + + return classSubclassSheetItems.some(scItem=>sc.name.toLowerCase() === scItem.name.toLowerCase().trim() && (!Config.get("import", "isStrictMatching") || (UtilDocumentSource.getDocumentSource(scItem).source || "").toLowerCase() === Parser.sourceJsonToAbv(sc.source).toLowerCase()), ); + } + ); + } + ).flat(); + + return { + matchingClasses, + matchingSubclasses, + sheetItem + }; + } + + static getSpellPointTotal({totalSpellcastingLevels}) { + if (!totalSpellcastingLevels) + return 0; + + const spellSlotCounts = UtilDataConverter.CASTER_TYPE_TO_PROGRESSION.full[totalSpellcastingLevels - 1] || UtilDataConverter.CASTER_TYPE_TO_PROGRESSION.full[0]; + + return spellSlotCounts.map((countSlots,ix)=>{ + const spellLevel = ix + 1; + return Parser.spLevelToSpellPoints(spellLevel) * countSlots; + } + ).sum(); + } + + static getPsiPointTotal({totalMysticLevels}) { + if (!totalMysticLevels || isNaN(totalMysticLevels) || totalMysticLevels < 0) + return 0; + + totalMysticLevels = Math.round(Math.min(totalMysticLevels, Consts.CHAR_MAX_LEVEL)); + + return [4, 6, 14, 17, 27, 32, 38, 44, 57, 64, 64, 64, 64, 64, 64, 64, 64, 71, 71, 71][totalMysticLevels - 1]; + } + + static async pGetWithDescriptionPlugins(pFn, {actorId=null, tagHashItemIdMap=null}={}) { + const hkLink = (entry,procHash)=>this._pGetWithDescriptionPlugins_fnPlugin(entry, procHash); + + const hkStr = (tag,text)=>{ + const inn = `{${tag} ${text}}`; + const itemId = this._pGetWithDescriptionPlugins_getTagItemId({ + tag, + text, + tagHashItemIdMap + }); + const out = this._getConvertedTagLinkString(inn, { + actorId, + itemId + }); + if (inn === out) + return null; + return out; + } + ; + + const hkStrFont = (tag,text)=>{ + if (!game.user.isGM) + return; + + const [,fontFamily] = Renderer.splitTagByPipe(text); + + if (UtilDataConverter._DESCRIPTION_FONTS_TRACKED[fontFamily]) + return; + UtilDataConverter._DESCRIPTION_FONTS_TRACKED[fontFamily] = true; + + if (FontConfig.getAvailableFontChoices()[fontFamily]) + return; + + if (!Config.get("import", "isAutoAddAdditionalFonts")) { + ui.notifications.warn(`The "${fontFamily}" font, used by recently-rendered content, is not available in your game. You may need to manually add it via the "Additional Fonts" setting, or text using the "${fontFamily}" font may not display correctly.`); + } + + const url = BrewUtil2.getMetaLookup("fonts")?.[fontFamily] || PrereleaseUtil.getMetaLookup("fonts")?.[fontFamily]; + + if (!url) + return void ui.notifications.warn(`Failed to load font "${fontFamily}". You may need to manually add it via the "Additional Fonts" setting, or text using the "${fontFamily}" font may not display correctly.`); + + this._pDoLoadAdditionalFont(fontFamily, url).then(null); + } + ; + + const hkImg = (entry,url)=>{ + const out = Vetools.getImageSavedToServerUrl({ + originalUrl: url + }); + Vetools.pSaveImageToServerAndGetUrl({ + originalUrl: url, + force: true + }).then(null).catch(()=>{} + ); + return out; + } + ; + + Renderer.get().addPlugin("link_attributesHover", hkLink); + Renderer.get().addPlugin("string_@font", hkStrFont); + if (Config.get("import", "isRenderLinksAsTags")) + Renderer.get().addPlugin("string_tag", hkStr); + if (Config.get("import", "isSaveImagesToServer")) { + Renderer.get().addPlugin("image_urlPostProcess", hkImg); + Renderer.get().addPlugin("image_urlThumbnailPostProcess", hkImg); + } + + let out; + try { + out = await pFn(); + } finally { + Renderer.get().removePlugin("link_attributesHover", hkLink); + Renderer.get().removePlugin("string_@font", hkStrFont); + Renderer.get().removePlugin("string_tag", hkStr); + Renderer.get().removePlugin("image_urlPostProcess", hkImg); + Renderer.get().removePlugin("image_urlThumbnailPostProcess", hkImg); + } + + return out; + } + + static _DESCRIPTION_FONTS_TRACKED = {}; + static _HAS_NOTIFIED_FONTS_RELOAD = false; + + static async _pDoLoadAdditionalFont(family, url) { + const hasNotified = this._HAS_NOTIFIED_FONTS_RELOAD; + this._HAS_NOTIFIED_FONTS_RELOAD = true; + + const definitions = game.settings.get("core", FontConfig.SETTING); + definitions[family] ??= { + editor: true, + fonts: [] + }; + const definition = definitions[family]; + definition.fonts.push({ + urls: [url], + weight: 400, + style: "normal" + }); + await game.settings.set("core", FontConfig.SETTING, definitions); + await FontConfig.loadFont(family, definition); + + if (hasNotified) + return; + + ChatNotificationHandlers.getHandler("ReloadFonts").pDoPostChatMessage(); + } + + static _pGetWithDescriptionPlugins_getTagItemId({tag, text, tagHashItemIdMap}) { + const tagName = tag.slice(1); + if (!tagHashItemIdMap?.[tagName]) + return null; + const defaultSource = Renderer.tag.TAG_LOOKUP[tagName]?.defaultSource; + if (!defaultSource) + return null; + const page = Renderer.tag.getPage(tagName); + if (!page) + return null; + const hashBuilder = UrlUtil.URL_TO_HASH_BUILDER[page]; + if (!hashBuilder) + return null; + let[name,source] = text.split("|"); + source = source || defaultSource; + const hash = hashBuilder({ + name, + source + }); + return tagHashItemIdMap?.[tagName]?.[hash]; + } + + static _pGetWithDescriptionPlugins_fnPlugin(entry, procHash) { + const page = entry.href.hover.page; + const source = entry.href.hover.source; + const hash = procHash; + const preloadId = entry.href.hover.preloadId; + return { + attributesHoverReplace: [`data-plut-hover="${true}" data-plut-hover-page="${page.qq()}" data-plut-hover-source="${source.qq()}" data-plut-hover-hash="${hash.qq()}" ${preloadId ? `data-plut-hover-preload-id="${preloadId.qq()}"` : ""}`, ], + }; + } + + static _getConvertedTagLinkString(str, {actorId, itemId}={}) { + this._getConvertedTagLinkString_initLinkTagMetas(); + for (const {tag, re} of this._LINK_TAG_METAS_REPLACE) + str = str.replace(re, (...m)=>this._replaceEntityLinks_getReplacement({ + tag, + text: m.last().text, + actorId, + itemId + })); + for (const {tag, re} of this._LINK_TAG_METAS_REMOVE) + str = str.replace(re, (...m)=>this._replaceEntityLinks_getRemoved({ + tag, + text: m.last().text + })); + return str; + } + + static _LINK_TAGS_TO_REMOVE = new Set(["quickref", ]); + static _LINK_TAG_METAS_REPLACE = null; + static _LINK_TAG_METAS_REMOVE = null; + + static _getConvertedTagLinkString_initLinkTagMetas() { + if (!this._LINK_TAG_METAS_REPLACE) { + this._LINK_TAG_METAS_REPLACE = Renderer.tag.TAGS.filter(it=>it.defaultSource).map(it=>it.tagName).filter(tag=>!this._LINK_TAGS_TO_REMOVE.has(tag)).map(tag=>({ + tag, + re: this._getConvertedTagLinkString_getRegex({ + tag + }) + })); + } + + if (!this._LINK_TAG_METAS_REMOVE) { + this._LINK_TAG_METAS_REMOVE = Renderer.tag.TAGS.filter(it=>it.defaultSource).map(it=>it.tagName).filter(tag=>this._LINK_TAGS_TO_REMOVE.has(tag)).map(tag=>({ + tag, + re: this._getConvertedTagLinkString_getRegex({ + tag + }) + })); + } + } + + static _getConvertedTagLinkString_getRegex({tag}) { + return RegExp(`^{@${tag} (?[^}]+)}$`, "g"); + } + + static getConvertedTagLinkEntries(entries) { + if (!entries) + return entries; + + return UtilDataConverter.WALKER_GENERIC.walk(MiscUtil.copy(entries), { + string: str=>{ + const textStack = [""]; + this._getConvertedTagLinkEntries_recurse(str, textStack); + return textStack.join(""); + } + , + }, ); + } + + static _getConvertedTagLinkEntries_recurse(str, textStack) { + const tagSplit = Renderer.splitByTags(str); + const len = tagSplit.length; + for (let i = 0; i < len; ++i) { + const s = tagSplit[i]; + if (!s) + continue; + + if (s.startsWith("{@")) { + const converted = this._getConvertedTagLinkString(s); + + if (converted !== s) { + textStack[0] += (converted); + continue; + } + + textStack[0] += s.slice(0, 1); + this._getConvertedTagLinkEntries_recurse(s.slice(1, -1), textStack); + textStack[0] += s.slice(-1); + + continue; + } + + textStack[0] += s; + } + } + + static _replaceEntityLinks_getReplacement({tag, text, actorId, itemId}) { + if (actorId && itemId) { + const [,,displayText] = text.split("|"); + return `@UUID[Actor.${actorId}.Item.${itemId}]${displayText ? `{${displayText}}` : ""}`; + } + return `@${tag}[${text}]`; + } + + static _replaceEntityLinks_getRemoved({tag, text}) { + return Renderer.stripTags(`{@${tag} ${text}}`); + } + + static async _pReplaceEntityLinks_pReplace({str, re, tag}) { + let m; + while ((m = re.exec(str))) { + const prefix = str.slice(0, m.index); + const suffix = str.slice(re.lastIndex); + const replacement = this._replaceEntityLinks_getReplacement({ + tag, + m + }); + str = `${prefix}${replacement}${suffix}`; + re.lastIndex = prefix.length + replacement.length; + } + return str; + } + + static _RECHARGE_TYPES = { + "round": null, + "restShort": "sr", + "restLong": "lr", + "dawn": "dawn", + "dusk": "dusk", + "midnight": "day", + + "special": null, + + "week": null, + "month": null, + "year": null, + "decade": null, + "century": null, + }; + + static getFvttUsesPer(it, {isStrict=true}={}) { + if (isStrict && !this._RECHARGE_TYPES[it]) + return null; + return Parser._parse_aToB(this._RECHARGE_TYPES, it); + } + + static getTempDocumentDefaultOwnership({documentType}) { + if (game.user.isGM) + return undefined; + + const clazz = CONFIG[documentType].documentClass; + + if (game.user.can(clazz.metadata.permissions.create)) + return undefined; + + return CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER; + } +} +UtilDataConverter.WALKER_READONLY_GENERIC = MiscUtil.getWalker({ + isNoModification: true, + keyBlocklist: MiscUtil.GENERIC_WALKER_ENTRIES_KEY_BLOCKLIST +}); +UtilDataConverter.WALKER_GENERIC = MiscUtil.getWalker({ + keyBlocklist: MiscUtil.GENERIC_WALKER_ENTRIES_KEY_BLOCKLIST +}); + +UtilDataConverter.WEAPONS_MARTIAL = ["battleaxe|phb", "blowgun|phb", "flail|phb", "glaive|phb", "greataxe|phb", "greatsword|phb", "halberd|phb", "hand crossbow|phb", "heavy crossbow|phb", "lance|phb", "longbow|phb", "longsword|phb", "maul|phb", "morningstar|phb", "net|phb", "pike|phb", "rapier|phb", "scimitar|phb", "shortsword|phb", "trident|phb", "war pick|phb", "warhammer|phb", "whip|phb", ]; +UtilDataConverter.WEAPONS_SIMPLE = ["club|phb", "dagger|phb", "dart|phb", "greatclub|phb", "handaxe|phb", "javelin|phb", "light crossbow|phb", "light hammer|phb", "mace|phb", "quarterstaff|phb", "shortbow|phb", "sickle|phb", "sling|phb", "spear|phb", ]; + +UtilDataConverter.CASTER_TYPE_TO_PROGRESSION = { + "full": [[2, 0, 0, 0, 0, 0, 0, 0, 0], [3, 0, 0, 0, 0, 0, 0, 0, 0], [4, 2, 0, 0, 0, 0, 0, 0, 0], [4, 3, 0, 0, 0, 0, 0, 0, 0], [4, 3, 2, 0, 0, 0, 0, 0, 0], [4, 3, 3, 0, 0, 0, 0, 0, 0], [4, 3, 3, 1, 0, 0, 0, 0, 0], [4, 3, 3, 2, 0, 0, 0, 0, 0], [4, 3, 3, 3, 1, 0, 0, 0, 0], [4, 3, 3, 3, 2, 0, 0, 0, 0], [4, 3, 3, 3, 2, 1, 0, 0, 0], [4, 3, 3, 3, 2, 1, 0, 0, 0], [4, 3, 3, 3, 2, 1, 1, 0, 0], [4, 3, 3, 3, 2, 1, 1, 0, 0], [4, 3, 3, 3, 2, 1, 1, 1, 0], [4, 3, 3, 3, 2, 1, 1, 1, 0], [4, 3, 3, 3, 2, 1, 1, 1, 1], [4, 3, 3, 3, 3, 1, 1, 1, 1], [4, 3, 3, 3, 3, 2, 1, 1, 1], [4, 3, 3, 3, 3, 2, 2, 1, 1], ], + "artificer": [[2, 0, 0, 0, 0], [2, 0, 0, 0, 0], [3, 0, 0, 0, 0], [3, 0, 0, 0, 0], [4, 2, 0, 0, 0], [4, 2, 0, 0, 0], [4, 3, 0, 0, 0], [4, 3, 0, 0, 0], [4, 3, 2, 0, 0], [4, 3, 2, 0, 0], [4, 3, 3, 0, 0], [4, 3, 3, 0, 0], [4, 3, 3, 1, 0], [4, 3, 3, 1, 0], [4, 3, 3, 2, 0], [4, 3, 3, 2, 0], [4, 3, 3, 3, 1], [4, 3, 3, 3, 1], [4, 3, 3, 3, 2], [4, 3, 3, 3, 2], ], + "1/2": [[0, 0, 0, 0, 0], [2, 0, 0, 0, 0], [3, 0, 0, 0, 0], [3, 0, 0, 0, 0], [4, 2, 0, 0, 0], [4, 2, 0, 0, 0], [4, 3, 0, 0, 0], [4, 3, 0, 0, 0], [4, 3, 2, 0, 0], [4, 3, 2, 0, 0], [4, 3, 3, 0, 0], [4, 3, 3, 0, 0], [4, 3, 3, 1, 0], [4, 3, 3, 1, 0], [4, 3, 3, 2, 0], [4, 3, 3, 2, 0], [4, 3, 3, 3, 1], [4, 3, 3, 3, 1], [4, 3, 3, 3, 2], [4, 3, 3, 3, 2], ], + "1/3": [[0, 0, 0, 0], [0, 0, 0, 0], [2, 0, 0, 0], [3, 0, 0, 0], [3, 0, 0, 0], [3, 0, 0, 0], [4, 2, 0, 0], [4, 2, 0, 0], [4, 2, 0, 0], [4, 3, 0, 0], [4, 3, 0, 0], [4, 3, 0, 0], [4, 3, 2, 0], [4, 3, 2, 0], [4, 3, 2, 0], [4, 3, 3, 0], [4, 3, 3, 0], [4, 3, 3, 0], [4, 3, 3, 1], [4, 3, 3, 1], ], + "pact": [[1, 0, 0, 0, 0], [2, 0, 0, 0, 0], [0, 2, 0, 0, 0], [0, 2, 0, 0, 0], [0, 0, 2, 0, 0], [0, 0, 2, 0, 0], [0, 0, 0, 2, 0], [0, 0, 0, 2, 0], [0, 0, 0, 0, 2], [0, 0, 0, 0, 2], [0, 0, 0, 0, 3], [0, 0, 0, 0, 3], [0, 0, 0, 0, 3], [0, 0, 0, 0, 3], [0, 0, 0, 0, 3], [0, 0, 0, 0, 3], [0, 0, 0, 0, 4], [0, 0, 0, 0, 4], [0, 0, 0, 0, 4], [0, 0, 0, 0, 4], ], +}; +//#endregion +//#region DataConverter +class DataConverter { + static _configGroup; + + static _SideDataInterface; + static _ImageFetcher; + + static async pGetDocumentJson(ent, opts) { + throw new Error("Unimplemented!"); + } + + static isStubEntity(ent) { + return false; + } + + static getTagUids(tag, str) { + const re = new RegExp(`{@${tag} ([^}]+)}`,"gi"); + const out = []; + str.replace(re, (...m)=>out.push(m[1])); + return out; + } + + static getCombinedFoundrySystem(foundrySystem, _foundryData) { + if (!_foundryData && !foundrySystem) + return {}; + + const combinedFoundrySystem = MiscUtil.copy(_foundryData || {}); + Object.assign(combinedFoundrySystem, MiscUtil.copy(foundrySystem || {})); + + return combinedFoundrySystem; + } + + static getCombinedFoundryFlags(foundryFlags, _foundryFlags) { + if (!foundryFlags && !_foundryFlags) + return {}; + + const combinedFoundryFlags = MiscUtil.copy(_foundryFlags || {}); + + Object.entries(MiscUtil.copy(foundryFlags || {})).forEach(([flagNamespace,flagData])=>{ + if (!combinedFoundryFlags[flagNamespace]) + return combinedFoundryFlags[flagNamespace] = flagData; + Object.assign(combinedFoundryFlags[flagNamespace], flagData); + } + ); + + return combinedFoundryFlags; + } + + static async pGetEntryDescription(entry, opts) { + opts = opts || {}; + opts.prop = opts.prop || "entries"; + + Renderer.get().setFirstSection(true).resetHeaderIndex(); + + let description = ""; + if (entry[opts.prop]) { + let cpyEntries = MiscUtil.copy(entry[opts.prop]); + + cpyEntries = UtilDataConverter.WALKER_GENERIC.walk(cpyEntries, { + string: (str)=>{ + return str.replace(/{@hitYourSpellAttack}/gi, ()=>`{@dice 1d20 + @${SharedConsts.MODULE_ID_FAKE}.userchar.spellAttackRanged|your spell attack modifier}`).replace(/{(@dice|@damage|@scaledice|@scaledamage|@hit) ([^}]+)}/gi, (...m)=>{ + const [,tag,text] = m; + let[rollText,displayText,name,...others] = Renderer.splitTagByPipe(text); + const originalRollText = rollText; + + rollText = this._pGetEntryDescription_getCleanDicePart(rollText, opts); + displayText = this._pGetEntryDescription_getCleanDisplayPart({ + displayText, + originalText: originalRollText, + text: rollText + }); + + return `{${tag} ${[rollText, displayText || "", name || "", ...others].join("|")}}`; + } + ).replace(/{(@dc) ([^}]+)}/gi, (...m)=>{ + const [,tag,text] = m; + let[dcText,displayText] = Renderer.splitTagByPipe(text); + const originalDcText = dcText; + + dcText = this._pGetEntryDescription_getCleanDicePart(dcText, opts); + displayText = this._pGetEntryDescription_getCleanDisplayPart({ + displayText, + originalText: originalDcText, + text: dcText + }); + + return `{${tag} ${[dcText, displayText || ""].join("|")}}`; + } + ); + } + , + }, ); + + description = await UtilDataConverter.pGetWithDescriptionPlugins(()=>Renderer.get().setFirstSection(true).render({ + type: "entries", + entries: cpyEntries, + }, opts.depth != null ? opts.depth : 2, ), ); + } + + return description; + } + + static _pGetEntryDescription_getCleanDicePart(str, opts) { + return str.replace(/\bPB\b/gi, `@${SharedConsts.MODULE_ID_FAKE}.userchar.pb`).replace(/\bsummonSpellLevel\b/gi, `${opts.summonSpellLevel ?? 0}`); + } + + static _pGetEntryDescription_getCleanDisplayPart({displayText, originalText, text}) { + if (!displayText && originalText !== text) { + displayText = originalText.replace(/\bsummonSpellLevel\b/gi, `the spell's level`); + } + return displayText; + } + + static mutActorUpdate(actor, actorUpdate, entry, opts) { + opts = opts || {}; + + this._mutActorUpdate_mutFromSideDataMod(actor, actorUpdate, opts); + this._mutActorUpdate_mutFromSideTokenMod(actor, actorUpdate, opts); + } + + static _mutActorUpdate_mutFromSideDataMod(actor, actorUpdate, opts) { + return this._mutActorUpdate_mutFromSideMod(actor, actorUpdate, opts, "actorDataMod", "data"); + } + + static _mutActorUpdate_mutFromSideTokenMod(actor, actorUpdate, opts) { + return this._mutActorUpdate_mutFromSideMod(actor, actorUpdate, opts, "actorTokenMod", "token"); + } + + static _mutActorUpdate_mutFromSideMod(actor, actorUpdate, opts, sideProp, actorProp) { + if (!opts.sideData || !opts.sideData[sideProp]) + return; + + Object.entries(opts.sideData[sideProp]).forEach(([path,modMetas])=>this._mutActorUpdate_mutFromSideMod_handleProp(actor, actorUpdate, opts, sideProp, actorProp, path, modMetas)); + } + + static _mutActorUpdate_mutFromSideMod_handleProp(actor, actorUpdate, opts, sideProp, actorProp, path, modMetas) { + const pathParts = path.split("."); + + if (path === "_") { + modMetas.forEach(modMeta=>{ + switch (modMeta.mode) { + case "conditionals": + { + for (const cond of modMeta.conditionals) { + + window.PLUT_CONTEXT = { + actor + }; + + if (cond.condition && !eval(cond.condition)) + continue; + + Object.entries(cond.mod).forEach(([path,modMetas])=>this._mutActorUpdate_mutFromSideMod_handleProp(actor, actorUpdate, opts, sideProp, actorProp, path, modMetas)); + + break; + } + + break; + } + + default: + throw new Error(`Unhandled mode "${modMeta.mode}"`); + } + } + ); + return; + } + + const fromActor = MiscUtil.get(actor, "system", actorProp, ...pathParts); + const fromUpdate = MiscUtil.get(actorUpdate, actorProp, ...pathParts); + const existing = fromUpdate || fromActor; + + modMetas.forEach(modMeta=>{ + switch (modMeta.mode) { + case "appendStr": + { + const existing = MiscUtil.get(actorUpdate, actorProp, ...pathParts); + const next = existing ? `${existing}${modMeta.joiner || ""}${modMeta.str}` : modMeta.str; + MiscUtil.set(actorUpdate, actorProp, ...pathParts, next); + break; + } + + case "appendIfNotExistsArr": + { + const existingArr = MiscUtil.copy(existing || []); + const out = [...existingArr]; + out.push(...modMeta.items.filter(it=>!existingArr.some(x=>CollectionUtil.deepEquals(it, x)))); + MiscUtil.set(actorUpdate, actorProp, ...pathParts, out); + break; + } + + case "scalarAdd": + { + MiscUtil.set(actorUpdate, actorProp, ...pathParts, modMeta.scalar + existing || 0); + break; + } + + case "scalarAddUnit": + { + const existingLower = `${existing || 0}`.toLowerCase(); + + const handle = (toFind)=>{ + const ix = existingLower.indexOf(toFind.toLowerCase()); + let numPart = existing.slice(0, ix); + const rest = existing.slice(ix); + const isSep = numPart.endsWith(" "); + numPart = numPart.trim(); + + if (!isNaN(numPart)) { + const out = `${modMeta.scalar + Number(numPart)}${isSep ? " " : ""}${rest}`; + MiscUtil.set(actorUpdate, actorProp, ...pathParts, out); + } + } + ; + + if (!existing) + MiscUtil.set(actorUpdate, actorProp, ...pathParts, `${modMeta.scalar} ${modMeta.unitShort || modMeta.unit}`); + else if (modMeta.unit && existingLower.includes(modMeta.unit.toLowerCase())) { + handle(modMeta.unit); + } else if (modMeta.unitShort && existingLower.includes(modMeta.unitShort.toLowerCase())) { + handle(modMeta.unitShort); + } + break; + } + + case "setMax": + { + const existingLower = `${existing || 0}`.toLowerCase(); + let asNum = Number(existingLower); + if (isNaN(asNum)) + asNum = 0; + const maxValue = Math.max(asNum, modMeta.value); + MiscUtil.set(actorUpdate, actorProp, ...pathParts, maxValue); + break; + } + + case "set": + { + MiscUtil.set(actorUpdate, actorProp, ...pathParts, MiscUtil.copy(modMeta.value)); + break; + } + + default: + throw new Error(`Unhandled mode "${modMeta.mode}"`); + } + } + ); + } + + static _getProfBonusExpressionParts(str) { + const parts = str.split(/([-+]\s*[^-+]+)/g).map(it=>it.trim().replace(/\s*/g, "")).filter(Boolean); + + const [partsNumerical,partsNonNumerical] = parts.segregate(it=>!isNaN(it)); + + const totalNumerical = partsNumerical.map(it=>Number(it)).sum(); + + return { + partsNumerical, + partsNonNumerical, + totalNumerical + }; + } + + static _PassiveEntryParseState = class { + constructor({entry, img, name}, opts) { + this._entry = entry; + this._opts = opts; + + this.name = name; + this.img = img; + + let {id, + description, + activationType, activationCost, activationCondition, + saveAbility, saveDc, saveScaling, + damageParts, + attackBonus, + requirements, + actionType, + durationValue, durationUnits, + consumeType, consumeTarget, consumeAmount, consumeScale, + formula, + targetValue, targetUnits, targetType, targetPrompt, + rangeShort, rangeLong, rangeUnits, + ability, + usesValue, usesMax, usesPer, + rechargeValue, + isProficient, + typeType, typeSubtype, + foundrySystem, _foundryData, foundryFlags, _foundryFlags, } = opts; + + this.combinedFoundrySystem = DataConverter.getCombinedFoundrySystem(foundrySystem, _foundryData); + this.combinedFoundryFlags = DataConverter.getCombinedFoundryFlags(foundryFlags, _foundryFlags); + + if (entry._foundryId && id && entry._foundryId !== id) + throw new Error(`Item given two different IDs (${this.id} and ${id})! This is a bug!`); + + this.id = entry._foundryId || id; + + this.description = description; + + this.activationType = activationType; + this.activationCost = activationCost; + this.activationCondition = activationCondition; + + this.saveAbility = saveAbility; + this.saveDc = saveDc; + this.saveScaling = saveScaling; + + this.damageParts = damageParts; + + this.attackBonus = attackBonus; + + this.requirements = requirements; + + this.actionType = actionType; + + this.durationValue = durationValue; + this.durationUnits = durationUnits; + + this.consumeType = consumeType; + this.consumeTarget = consumeTarget; + this.consumeAmount = consumeAmount; + this.consumeScale = consumeScale; + + this.formula = formula; + + this.targetValue = targetValue; + this.targetUnits = targetUnits; + this.targetType = targetType; + this.targetPrompt = targetPrompt; + + this.rangeShort = rangeShort; + this.rangeLong = rangeLong; + this.rangeUnits = rangeUnits; + + this.ability = ability; + + this.usesValue = usesValue; + this.usesMax = usesMax; + this.usesPer = usesPer; + + this.rechargeValue = rechargeValue; + + this.isProficient = isProficient; + + this.typeType = typeType; + this.typeSubtype = typeSubtype; + + this.effectsParsed = []; + + this.flagsParsed = {}; + } + + async pInit({isSkipDescription=false, isSkipImg=false}={}) { + if (!isSkipDescription && !this.description && !this._opts.isSkipDescription) { + this.description = await DataConverter.pGetEntryDescription(this._entry, { + depth: this._opts.renderDepth, + summonSpellLevel: this._opts.summonSpellLevel + }); + } + + if (!isSkipImg && this._opts.img) { + this.img = await Vetools.pOptionallySaveImageToServerAndGetUrl(this._opts.img); + } + } + } + ; + + static async _pGetItemActorPassive(entry, opts) { + opts = opts || {}; + + Renderer.get().setFirstSection(true).resetHeaderIndex(); + + opts.modeOptions = opts.modeOptions || {}; + + if (opts.mode === "object") + opts.mode = "creature"; + + const state = new this._PassiveEntryParseState({ + entry, + name: UtilApplications.getCleanEntityName(UtilDataConverter.getNameWithSourcePart(entry, { + displayName: opts.displayName, + isActorItem: opts.isActorItem ?? true + })), + },opts,); + await state.pInit(); + + const strEntries = entry.entries ? JSON.stringify(entry.entries) : null; + + this._pGetItemActorPassive_mutRecharge({ + entry, + opts, + state + }); + this._pGetItemActorPassive_mutActivation({ + entry, + opts, + state + }); + this._pGetItemActorPassive_mutUses({ + entry, + opts, + strEntries, + state + }); + this._pGetItemActorPassive_mutSave({ + entry, + opts, + strEntries, + state + }); + this._pGetItemActorPassive_mutDuration({ + entry, + opts, + state + }); + this._pGetItemActorPassive_mutDamageAndFormula({ + entry, + opts, + strEntries, + state + }); + this._pGetItemActorPassive_mutTarget({ + entry, + opts, + strEntries, + state + }); + this._pGetItemActorPassive_mutActionType({ + entry, + opts, + state + }); + this._pGetItemActorPassive_mutEffects({ + entry, + opts, + state + }); + + try { + state.activationCondition = Renderer.stripTags(state.activationCondition); + } catch (e) { + console.error(...LGT, e); + } + + if ((state.consumeType || state.usesPer || opts.additionalData?.["consume.type"] || opts.additionalData?.consume?.type || opts.additionalData?.["uses.per"] || opts.additionalData?.uses?.per) && !state.activationType) + state.activationType = "special"; + + state.name = state.name.trim().replace(/\s+/g, " "); + if (!state.name) + state.name = "(Unnamed)"; + const fauxEntrySourcePage = { + ...entry + }; + if (opts.source != null) + fauxEntrySourcePage.source = opts.source; + if (opts.page != null) + fauxEntrySourcePage.page = opts.page; + + this._pGetItemActorPassive_mutFlags({ + entry, + opts, + state + }); + + const {name: translatedName, description: translatedDescription, flags: translatedFlags} = this._getTranslationMeta({ + translationData: opts.translationData, + name: state.name, + description: state.description, + }); + + return { + ...this._getIdObj({ + id: state.id + }), + name: translatedName, + type: opts.fvttType || "feat", + system: { + source: opts.fvttSource !== undefined ? opts.fvttSource : UtilDocumentSource.getSourceObjectFromEntity(fauxEntrySourcePage), + description: { + value: translatedDescription, + chat: "", + unidentified: "" + }, + + damage: { + parts: state.damageParts ?? [], + versatile: "", + }, + duration: { + value: state.durationValue, + units: state.durationUnits, + }, + range: { + value: state.rangeShort, + long: state.rangeLong, + units: state.rangeUnits || ((state.rangeShort != null || state.rangeLong != null) ? "ft" : ""), + }, + proficient: state.isProficient, + requirements: state.requirements, + + save: { + ability: state.saveAbility, + dc: state.saveDc, + scaling: state.saveScaling || "flat", + }, + + activation: { + type: state.activationType, + cost: state.activationCost, + condition: state.activationCondition, + }, + + target: { + value: state.targetValue, + units: state.targetUnits, + type: state.targetType, + prompt: state.targetPrompt, + }, + + uses: { + value: state.usesValue, + max: state.usesMax, + per: state.usesPer, + }, + ability: state.ability, + actionType: state.actionType, + attackBonus: state.attackBonus, + chatFlavor: "", + critical: { + threshold: null, + damage: "" + }, + + formula: state.formula, + + recharge: { + value: state.rechargeValue, + charged: state.rechargeValue != null, + }, + + consume: { + type: state.consumeType, + target: state.consumeTarget, + amount: state.consumeAmount, + scale: state.consumeScale, + }, + + type: { + value: state.typeType, + subtype: state.typeSubtype, + }, + + ...(state.combinedFoundrySystem || {}), + ...(opts.additionalData || {}), + }, + ownership: { + default: 0 + }, + img: state.img, + flags: { + ...translatedFlags, + ...state.flagsParsed, + ...(UtilCompat.getFeatureFlags({ + isReaction: ["reaction", "reactiondamage", "reactionmanual"].includes(state.activationType) + })), + ...(state.combinedFoundryFlags || {}), + ...opts.additionalFlags, + }, + effects: DataConverter.getEffectsMutDedupeId([...(opts.effects || []), ...state.effectsParsed, ]), + }; + } + + static _pGetItemActorPassive_mutRecharge({entry, opts, state}) { + if (!state.name) + return; + + const rechargeMeta = UtilEntityGeneric.getRechargeMeta(state.name); + if (rechargeMeta == null) + return; + + state.name = rechargeMeta.name; + if (state.rechargeValue === undefined && rechargeMeta.rechargeValue != null) + state.rechargeValue = rechargeMeta.rechargeValue; + } + + static _pGetItemActorPassive_mutActivation({entry, opts, state}) { + this._pGetItemActorPassive_mutActivation_player({ + entry, + opts, + state + }); + this._pGetItemActorPassive_mutActivation_creature({ + entry, + opts, + state + }); + } + + static _pGetItemActorPassive_mutActivation_player({entry, opts, state}) { + if (opts.mode !== "player" || !entry.entries?.length) + return; + + if (state.activationType || state.activationCost) { + this._pGetItemActorPassive_mutActivation_playerCompat({ + entry, + opts, + state + }); + return; + } + + let isAction = false; + let isBonusAction = false; + let isReaction = false; + + UtilDataConverter.WALKER_READONLY_GENERIC.walk(entry.entries, { + string: (str)=>{ + if (state.activationType) + return str; + + const sentences = Util.getSentences(str); + for (const sentence of sentences) { + if (/\b(?:as an action|can take an action|can use your action)\b/i.test(sentence)) { + isAction = true; + break; + } + + if (/\bbonus action\b/i.test(sentence)) { + isBonusAction = true; + break; + } + + const mReact = /\b(?:your reaction|this special reaction|as a reaction)\b/i.exec(sentence); + if (mReact) { + isReaction = true; + + let preceding = sentence.slice(0, mReact.index).trim().replace(/,$/, ""); + const mCondition = /(^|\W)(?:if|when)(?:|\W)/i.exec(preceding); + if (mCondition) { + preceding = preceding.slice(mCondition.index + mCondition[1].length).trim(); + state.activationCondition = state.activationCondition || preceding; + } + + break; + } + } + } + , + }, ); + + if (isAction) + state.activationType = "action"; + else if (isBonusAction) + state.activationType = "bonus"; + else if (isReaction) + state.activationType = "reaction"; + + if (state.activationType) + state.activationCost = 1; + + if (!state.activationType) { + UtilDataConverter.WALKER_READONLY_GENERIC.walk(entry.entries, { + string: (str)=>{ + if (state.activationType) + return str; + + const sentences = Util.getSentences(str); + + for (const sentence of sentences) { + if (/you can't use this feature again|once you use this feature/i.test(sentence)) + state.activationType = "special"; + } + } + , + }, ); + } + + this._pGetItemActorPassive_mutActivation_playerCompat({ + entry, + opts, + state + }); + } + + static _pGetItemActorPassive_mutActivation_creature({entry, opts, state}) { + if (opts.mode !== "creature" || !entry.entries?.length || !entry.name) { + this._pGetItemActorPassive_mutActivation_creature_enableOtherFields({ + entry, + opts, + state + }); + return; + } + + if (state.activationType || state.activationCost) { + this._pGetItemActorPassive_mutActivation_creatureCompat({ + entry, + opts, + state + }); + this._pGetItemActorPassive_mutActivation_creature_enableOtherFields({ + entry, + opts, + state + }); + return; + } + + MiscUtil.getWalker({ + isNoModification: true, + keyBlocklist: MiscUtil.GENERIC_WALKER_ENTRIES_KEY_BLOCKLIST, + isBreakOnReturn: true, + }).walk(entry.entries, { + string: str=>{ + if (/\bbonus action\b/i.test(str)) { + state.activationType = "bonus"; + state.activationCost = 1; + return true; + } + } + , + }, ); + + if (/^legendary resistance/i.test(entry.name)) { + state.activationType = "special"; + } + + this._pGetItemActorPassive_mutActivation_creature_enableOtherFields({ + entry, + opts, + state + }); + + this._pGetItemActorPassive_mutActivation_creatureCompat({ + entry, + opts, + state + }); + } + + static _pGetItemActorPassive_mutActivation_creature_enableOtherFields({entry, opts, state}) { + if (state.rechargeValue !== undefined) { + if (state.activationType == null) + state.activationType = "special"; + } + } + + static _pGetItemActorPassive_mutActivation_playerCompat({entry, opts, state}) { + if (!UtilCompat.isMidiQolActive() || state.activationType !== "reaction") + return null; + + } + + static _pGetItemActorPassive_mutActivation_creatureCompat({entry, opts, state}) { + if (!UtilCompat.isMidiQolActive() || state.activationType !== "reaction") + return; + + state.activationType = "reactionmanual"; + + let firstEntry = entry.entries[0]; + if (typeof firstEntry !== "string") + return; + + firstEntry.replace(/\bcauses the attack to miss\b/i, ()=>{ + state.activationType = "reaction"; + return ""; + } + ) + .replace(/\badds? (?\d+) to (its|their|his|her) AC\b/i, (...m)=>{ + const argsDuration = UtilCompat.isDaeActive() ? { + flags: { + [UtilCompat.MODULE_DAE]: { + specialDuration: ["1Reaction"] + } + } + } : { + durationTurns: 1 + }; + + state.effectsParsed.push(UtilActiveEffects.getGenericEffect({ + ...argsDuration, + key: `system.attributes.ac.bonus`, + value: UiUtil.intToBonus(Number(m.last().ac)), + mode: CONST.ACTIVE_EFFECT_MODES.ADD, + name: `${entry.name}`, + icon: state.img, + disabled: false, + transfer: false, + priority: UtilActiveEffects.PRIORITY_BONUS, + })); + + state.targetType = state.targetType || "self"; + + return ""; + } + ) + .replace(/\battack that would (?:hit|miss) (?:it|them|him|her|or miss)\b/i, ()=>{ + state.activationType = "reaction"; + return ""; + } + ).replace(/\bin response to being (?:hit|missed)\b/i, ()=>{ + state.activationType = "reaction"; + return ""; + } + ) + .replace(/\bafter taking damage from\b/i, ()=>{ + state.activationType = "reactiondamage"; + return ""; + } + ).replace(/\bIf [^.!?:]+ takes damage(?:,| while it)\b/i, ()=>{ + state.activationType = "reactiondamage"; + return ""; + } + ).replace(/\bIn response to taking damage\b/i, ()=>{ + state.activationType = "reactiondamage"; + return ""; + } + ); + } + + static _pGetItemActorPassive_mutSave({entry, opts, strEntries, state}) { + this._pGetItemActorPassive_mutSave_player({ + entry, + opts, + strEntries, + state + }); + this._pGetItemActorPassive_mutSave_creature({ + entry, + opts, + strEntries, + state + }); + } + + static _pGetItemActorPassive_mutSave_player({entry, opts, strEntries, state}) { + if (opts.mode !== "player" || !entry.entries?.length) + return; + + UtilDataConverter.WALKER_READONLY_GENERIC.walk(entry.entries, { + object: (obj)=>{ + if (obj.type !== "abilityDc") + return obj; + + if (state.actionType && state.saveScaling) + return obj; + + state.actionType = state.actionType || "save"; + state.saveScaling = obj.attributes[0]; + return obj; + } + , + string: (str)=>{ + if (state.actionType && state.saveAbility && state.saveScaling) + return str; + + str.replace(/8\s*\+\s*your proficiency bonus\s*\+\s*your (.*?) modifier/i, (...m)=>{ + const customAbilities = []; + m[1].replace(/(Strength|Dexterity|Constitution|Intelligence|Wisdom|Charisma)/i, (...m2)=>{ + customAbilities.push(m2[1].toLowerCase().slice(0, 3)); + } + ); + if (!customAbilities.length) + return; + + state.actionType = state.actionType || "save"; + state.saveScaling = customAbilities[0]; + } + ); + + str.replace(/(Strength|Dexterity|Constitution|Intelligence|Wisdom|Charisma) saving throw against your (.*? )spell save DC/i, (...m)=>{ + state.actionType = state.actionType || "save"; + state.saveAbility = state.saveAbility || m[1].toLowerCase().slice(0, 3); + state.saveScaling = state.saveScaling || "spell"; + } + ); + + str.replace(/(?:make a|succeed on a) (Strength|Dexterity|Constitution|Intelligence|Wisdom|Charisma) saving throw/gi, (...m)=>{ + state.actionType = state.actionType || "save"; + state.saveAbility = state.saveAbility || m[1].toLowerCase().slice(0, 3); + state.saveScaling = state.saveScaling || "spell"; + } + ); + + return str; + } + , + }, ); + } + + static _pGetItemActorPassive_mutSave_creature({entry, opts, strEntries, state}) { + if (opts.mode !== "creature" || !entry.entries?.length) + return; + + const m = /{@dc (?[^|}]+)(?:\|[^}]+)?}\s+(?Strength|Dexterity|Constitution|Intelligence|Wisdom|Charisma)/i.exec(strEntries); + if (!m) + return; + + const {partsNonNumerical, totalNumerical} = this._getProfBonusExpressionParts(m.groups.save); + + state.actionType = state.actionType === undefined ? "save" : state.actionType; + state.saveAbility = state.saveAbility === undefined ? m.groups.abil.toLowerCase().slice(0, 3) : state.saveAbility; + state.saveDc = state.saveDc === undefined ? totalNumerical : state.saveDc; + + if (partsNonNumerical.length || opts.pb == null || ((opts.entity == null || Parser.ABIL_ABVS.some(ab=>opts.entity[ab] == null || typeof opts.entity[ab] !== "number")) && (opts.entity != null || Parser.ABIL_ABVS.some(ab=>{ + const abNamespaced = UtilEntityCreatureFeature.getNamespacedProp(ab); + return entry[abNamespaced] == null || typeof entry[abNamespaced] !== "number"; + } + )))) { + state.saveScaling = state.saveScaling === undefined ? "flat" : state.saveScaling; + return; + } + + if (state.saveScaling) + return; + + const fromAbil = state.saveDc - opts.pb - 8; + const abilToBonus = Parser.ABIL_ABVS.map(ab=>({ + ability: ab, + bonus: Parser.getAbilityModNumber(opts.entity != null ? Renderer.monster.getSafeAbilityScore(opts.entity, ab, { + isDefaultTen: true + }) : Renderer.monster.getSafeAbilityScore(entry, UtilEntityCreatureFeature.getNamespacedProp(ab), { + isDefaultTen: true + }), ), + })); + const matchingAbils = abilToBonus.filter(it=>it.bonus === fromAbil); + + if (matchingAbils.length === 1) + state.saveScaling = state.saveScaling || matchingAbils[0].ability; + else + state.saveScaling = "flat"; + } + + static _pGetItemActorPassive_mutUses({entry, opts, strEntries, state}) { + this._pGetItemActorPassive_mutUses_creature({ + entry, + opts, + strEntries, + state + }); + this._pGetItemActorPassive_mutUses_player({ + entry, + opts, + strEntries, + state + }); + } + + static _pGetItemActorPassive_mutUses_creature({entry, opts, strEntries, state}) { + if (opts.mode !== "creature" || !entry.name) + return; + + const isLegendary = /legendary resistance/gi.test(state.name); + + let isFound = false; + + state.name = state.name.replace(/\(Recharges after a (?[^)]+)\)/i, (...m)=>{ + isFound = true; + + if (isLegendary) + return ""; + + if (state.usesValue === undefined) + state.usesValue = 1; + if (state.usesMax === undefined) + state.usesMax = `${state.usesValue}`; + + const restPartClean = m.last().restPart.toLowerCase(); + if (/\bshort\b/.test(restPartClean)) { + if (state.usesPer === undefined) + state.usesPer = "sr"; + } else if (/\blong\b/.test(restPartClean)) { + if (state.usesPer === undefined) + state.usesPer = "lr"; + } + + return ""; + } + ); + + if (state.usesPer === undefined) { + state.name = state.name.replace(/\(\s*(\d+)\s*\/\s*(Day|Short Rest|Long Rest)\s*\)/i, (...m)=>{ + isFound = true; + + if (isLegendary) + return ""; + + if (state.usesValue === undefined) + state.usesValue = Number(m[1]); + if (state.usesMax === undefined) + state.usesMax = `${state.usesValue}`; + + if (state.usesPer === undefined) { + const cleanTime = m[2].trim().toLowerCase(); + switch (cleanTime) { + case "day": + state.usesPer = "day"; + break; + case "short rest": + state.usesPer = "sr"; + break; + case "long rest": + state.usesPer = "lr"; + break; + } + } + + return ""; + } + ); + } + + if (state.usesPer === undefined) { + state.name = state.name.replace(/\(\s*(\d+)\s+Charges\s*\)/i, (...m)=>{ + isFound = true; + + if (isLegendary) + return ""; + + if (state.usesValue === undefined) + state.usesValue = Number(m[1]); + if (state.usesMax === undefined) + state.usesMax = `${state.usesValue}`; + if (state.usesPer === undefined) + state.usesPer = "charges"; + + return ""; + } + ); + } + + if (!isFound) + return; + + state.name = state.name.trim().replace(/ +/g, " "); + + if (state.activationType === undefined) + state.activationType = state.activationType || "none"; + + if (entry.entries && typeof entry.entries[0] === "string" && /^(?:If |When )/i.test(entry.entries[0].trim())) { + if (state.activationCondition === undefined) + state.activationCondition = entry.entries[0].trim(); + } + } + + static _pGetItemActorPassive_mutUses_player({entry, opts, strEntries, state}) { + if (opts.mode !== "player" || !entry.entries) + return; + + if (state.consumeType === "charges") + return; + + const isShortRest = /\b(?:finish|complete) a short rest\b/.test(strEntries) || /\b(?:finish|complete) a short or long rest\b/.test(strEntries) || /\b(?:finish|complete) a short rest or a long rest\b/.test(strEntries) || /\b(?:finish|complete) a short or long rest\b/.test(strEntries); + const isLongRest = !isShortRest && /\b(?:finish|complete) a long rest\b/.test(strEntries); + + if (state.usesPer === undefined) { + if (isShortRest) + state.usesPer = "sr"; + else if (isLongRest) + state.usesPer = "lr"; + } + + const mAbilModifier = new RegExp(`a number of times equal to(?: (${Consts.TERMS_COUNT.map(it=>it.tokens.join("")).join("|")}))? your (Strength|Dexterity|Constitution|Intelligence|Wisdom|Charisma) modifier(?: \\(minimum of (${Consts.TERMS_COUNT.map(it=>it.tokens.join("")).join("|")})\\))?`,"i").exec(strEntries); + if (mAbilModifier && opts.actor) { + const abv = mAbilModifier[2].slice(0, 3).toLowerCase(); + const abilScore = MiscUtil.get(opts.actor, "system", "abilities", abv, "value"); + if (abilScore != null) { + let mod = Parser.getAbilityModNumber(abilScore); + let modFormula = `floor((@abilities.${abv}.value - 10) / 2)`; + + if (mAbilModifier[1]) { + const multiplier = (Consts.TERMS_COUNT.find(it=>it.tokens.join(" ") === mAbilModifier[1].trim().toLowerCase()) || {}).count || 1; + mod = mod * multiplier; + modFormula = `${modFormula} * ${multiplier}`; + } + + if (mAbilModifier[3]) { + const min = (Consts.TERMS_COUNT.find(it=>it.tokens.join("") === mAbilModifier[3].trim().toLowerCase()) || {}).count || 1; + mod = Math.max(min, mod); + modFormula = `max(${min}, ${modFormula})`; + } + + if (state.usesValue === undefined) + state.usesValue = mod; + if (state.usesMax === undefined) + state.usesMax = modFormula; + } + } + + strEntries.replace(/(you can ([^.!?]+)) a number of times equal to(? twice)? your proficiency bonus/i, (...m)=>{ + const mult = m.last().mult ? (Consts.TERMS_COUNT.find(meta=>CollectionUtil.deepEquals(meta.tokens, m.last().mult.trim().toLowerCase().split(/( )/g)))?.count || 1) : 1; + if (state.usesValue === undefined) + state.usesValue = opts.actor ? (UtilActors.getProficiencyBonusNumber({ + actor: opts.actor + }) * mult) : null; + if (state.usesMax === undefined) + state.usesMax = `@prof${mult > 1 ? ` * ${mult}` : ""}`; + } + ); + + strEntries.replace(/you can use this (?:feature|ability) (?once|twice|[a-zA-Z]+ times)/i, (...m)=>{ + const mult = (Consts.TERMS_COUNT.find(meta=>CollectionUtil.deepEquals(meta.tokens, m.last().mult.trim().toLowerCase().split(/( )/g)))?.count || 1); + if (state.usesValue === undefined) + state.usesValue = mult; + if (state.usesMax === undefined) + state.usesMax = mult; + } + ); + + if (state.usesPer && !state.usesValue && (!state.usesMax || state.usesMax === "0")) { + if (state.usesValue === undefined) + state.usesValue = 1; + if (state.usesMax === undefined) + state.usesMax = `${state.usesValue}`; + } + } + + static _pGetItemActorPassive_mutDuration({entry, opts, state}) { + this._pGetItemActorPassive_mutDuration_creature({ + entry, + opts, + state + }); + this._pGetItemActorPassive_mutDuration_player({ + entry, + opts, + state + }); + } + + static _pGetItemActorPassive_mutDuration_creature({entry, opts, state}) { + if (opts.mode !== "creature" || !entry.entries) + return; + + return "stubbed"; + } + + static _pGetItemActorPassive_mutDuration_player({entry, opts, state}) { + if (opts.mode !== "player" || !entry.entries) + return; + + UtilDataConverter.WALKER_READONLY_GENERIC.walk(entry.entries, { + string: (str)=>{ + if (state.durationValue || state.durationUnits) + return; + + str.replace(/(?:^|\W)lasts for (\d+) (minute|hour|day|month|year|turn|round)s?(?:\W|$)/gi, (...m)=>{ + state.durationValue = Number(m[1]); + state.durationUnits = m[2].toLowerCase(); + } + ); + + str.replace(/(?:^|\W)for the next (\d+) (minute|hour|day|month|year|turn|round)s?(?:\W|$)/gi, (...m)=>{ + state.durationValue = Number(m[1]); + state.durationUnits = m[2].toLowerCase(); + } + ); + + str.replace(/(?:^|\W)turned for (\d+) (minute|hour|day|month|year|turn|round)s?(?:\W|$)/gi, (...m)=>{ + state.durationValue = Number(m[1]); + state.durationUnits = m[2].toLowerCase(); + } + ); + + str.replace(/(?:^|\W)this effect lasts for (\d+) (minute|hour|day|month|year|turn|round)s?(?:\W|$)/gi, (...m)=>{ + state.durationValue = Number(m[1]); + state.durationUnits = m[2].toLowerCase(); + } + ); + + str.replace(/(?:^|\W)until the end of your next turn(?:\W|$)/gi, ()=>{ + state.durationValue = 1; + state.durationUnits = "turn"; + } + ); + + Renderer.stripTags(str).replace(/(?:^|\W)is \w+ by you for (\d+) (minute|hour|day|month|year|turn|round)s(?:\W|$)/gi, (...m)=>{ + state.durationValue = Number(m[1]); + state.durationUnits = m[2].toLowerCase(); + } + ); + } + , + }, ); + } + + static _pGetItemActorPassive_getTargetMeta(strEntries) { + let targetValue, targetUnits, targetType; + let found = false; + + let tmpEntries = strEntries.replace(/exhales [^.]*a (?\d+)-foot[- ](?cone|line)/, (...m)=>{ + targetValue = Number(m.last().size); + targetUnits = "ft"; + targetType = m.last().shape; + found = true; + + return ""; + } + ); + + if (found) + return this._pGetItemActorPassive_getTargetMetricAdjusted({ + targetValue, + targetUnits, + targetType + }); + + tmpEntries = tmpEntries.replace(/(?\d+)-foot-radius,? \d+-foot-tall cylinder/, (...m)=>{ + targetValue = Number(m.last().size); + targetUnits = "ft"; + targetType = "cylinder"; + + found = true; + return ""; + } + ); + + if (found) + return this._pGetItemActorPassive_getTargetMetricAdjusted({ + targetValue, + targetUnits, + targetType + }); + + tmpEntries = tmpEntries.replace(/(?\d+)-foot[- ]radius(? sphere)?/, (...m)=>{ + targetValue = Number(m.last().size); + targetUnits = "ft"; + targetType = (m.last().ptSphere ? "sphere" : "radius"); + + found = true; + return ""; + } + ); + + if (found) + return this._pGetItemActorPassive_getTargetMetricAdjusted({ + targetValue, + targetUnits, + targetType + }); + + tmpEntries = tmpEntries.replace(/(?\d+)-foot[- ]cube/, (...m)=>{ + targetValue = Number(m.last().size); + targetUnits = "ft"; + targetType = "cube"; + + found = true; + return ""; + } + ); + + if (found) + return this._pGetItemActorPassive_getTargetMetricAdjusted({ + targetValue, + targetUnits, + targetType + }); + + tmpEntries = tmpEntries.replace(/(?\d+)-foot[- ]square/, (...m)=>{ + targetValue = Number(m.last().size); + targetUnits = "ft"; + targetType = "square"; + + found = true; + return ""; + } + ); + + if (found) + return this._pGetItemActorPassive_getTargetMetricAdjusted({ + targetValue, + targetUnits, + targetType + }); + + tmpEntries = tmpEntries.replace(/(?\d+)-foot line/, (...m)=>{ + targetValue = Number(m.last().size); + targetUnits = "ft"; + targetType = "line"; + + found = true; + return ""; + } + ); + + tmpEntries = tmpEntries.replace(/(?\d+)-foot cone/, (...m)=>{ + targetValue = Number(m.last().size); + targetUnits = "ft"; + targetType = "cone"; + + found = true; + return ""; + } + ); + + if (found) + return this._pGetItemActorPassive_getTargetMetricAdjusted({ + targetValue, + targetUnits, + targetType + }); + + return {}; + } + + static _pGetItemActorPassive_getTargetMetricAdjusted({targetValue, targetUnits, targetType}) { + targetValue = Config.getMetricNumberDistance({ + configGroup: this._configGroup, + originalValue: targetValue, + originalUnit: "feet" + }); + if (targetUnits) + targetUnits = Config.getMetricUnitDistance({ + configGroup: this._configGroup, + originalUnit: targetUnits + }); + + return { + targetValue, + targetUnits, + targetType + }; + } + + static _pGetItemActorPassive_mutDamageAndFormula({entry, opts, strEntries, state}) { + this._pGetItemActorPassive_mutDamageAndFormula_playerOrVehicle({ + entry, + opts, + strEntries, + state + }); + this._pGetItemActorPassive_mutDamageAndFormula_creature({ + entry, + opts, + strEntries, + state + }); + } + + static _pGetItemActorPassive_mutDamageAndFormula_playerOrVehicle({entry, opts, state, strEntries}) { + if (opts.mode !== "player" && opts.mode !== "vehicle") + return; + if (!entry.entries) + return; + + let strEntriesNoDamageDice = strEntries; + if (!state.damageParts?.length) { + const {str, damageTupleMetas} = this._getDamageTupleMetas(strEntries, { + summonSpellLevel: opts.summonSpellLevel + }); + strEntriesNoDamageDice = str; + + const {damageParts: damageParts_, formula: formula_} = this._getDamagePartsAndOtherFormula(damageTupleMetas); + + state.damageParts = damageParts_; + state.formula = state.formula ?? formula_; + } + + if (state.formula == null) { + strEntriesNoDamageDice.replace(/{(?:@dice|@scaledice) ([^|}]+)(?:\|[^}]+)?}/i, (...m)=>{ + const [dice] = m[1].split("|"); + state.formula = dice; + } + ); + } + } + + static _pGetItemActorPassive_mutDamageAndFormula_creature({entry, opts, strEntries, state}) { + if (opts.mode !== "creature") + return; + if (!entry.entries?.length) + return; + + if (!state.damageParts?.length && state.formula == null) { + const str = entry.entries[0]; + if (typeof str !== "string") + return; + + const {damageTupleMetas} = this._getDamageTupleMetas(str); + const {damageParts, formula} = this._getDamagePartsAndOtherFormula(damageTupleMetas); + + state.damageParts = damageParts; + state.formula = formula; + } + } + + static _pGetItemActorPassive_mutTarget({entry, opts, strEntries, state}) { + this._pGetItemActorPassive_mutTarget_player({ + entry, + opts, + strEntries, + state + }); + this._pGetItemActorPassive_mutTarget_creature({ + entry, + opts, + strEntries, + state + }); + } + + static _pGetItemActorPassive_mutTarget_player({entry, opts, strEntries, state}) { + if (opts.mode !== "player") + return; + + if (state.targetPrompt === undefined) + state.targetPrompt = Config.getSafe(this._configGroup, "isTargetTemplatePrompt"); + } + + static _pGetItemActorPassive_mutTarget_creature({entry, opts, strEntries, state}) { + if (opts.mode !== "creature") + return; + if (!strEntries) + return; + + if (!state.targetValue && !state.targetUnits && !state.targetType) { + const targetMeta = this._pGetItemActorPassive_getTargetMeta(strEntries); + state.targetValue = targetMeta.targetValue || state.targetValue; + state.targetUnits = targetMeta.targetUnits || state.targetUnits; + state.targetType = targetMeta.targetType || state.targetType; + } + + if (state.targetPrompt === undefined) + state.targetPrompt = Config.getSafe(this._configGroup, "isTargetTemplatePrompt"); + } + + static _pGetItemActorPassive_mutActionType({entry, opts, state}) { + this._pGetItemActorPassive_mutActionType_player({ + entry, + opts, + state + }); + this._pGetItemActorPassive_mutActionType_creature({ + entry, + opts, + state + }); + } + + static _pGetItemActorPassive_mutActionType_player({entry, opts, state}) { + if (state.actionType || opts.mode !== "player" || !entry.entries?.length) + return; + + const walker = MiscUtil.getWalker({ + keyBlocklist: MiscUtil.GENERIC_WALKER_ENTRIES_KEY_BLOCKLIST, + isNoModification: true, + isBreakOnReturn: true + }); + walker.walk(entry.entries, { + string: str=>{ + const mMeleeRangedWeaponAttack = /you\b[^.!?]*\bmake a (?melee|ranged) weapon attack/i.exec(str); + if (mMeleeRangedWeaponAttack) { + state.actionType = mMeleeRangedWeaponAttack.groups.type.toLowerCase() === "melee" ? "mwak" : "rwak"; + return true; + } + + const mMeleeRangedSpellAttack = /you\b[^.!?]*\bmake a (?melee|ranged) spell attack/i.exec(str); + if (mMeleeRangedSpellAttack) { + state.actionType = mMeleeRangedSpellAttack.groups.type.toLowerCase() === "melee" ? "msak" : "rsak"; + return true; + } + + const mHeal = /creature\b[^.!?]*\bregains\b[^.!?]*\bhit points?/i.exec(str); + if (mHeal) { + state.actionType = "heal"; + return true; + } + } + , + }, ); + + state.actionType = state.actionType || "other"; + } + + static _pGetItemActorPassive_mutActionType_creature({entry, opts, state}) { + if (state.actionType || opts.mode !== "creature" || !entry.entries?.length) + return; + + state.actionType = "other"; + } + + static _pGetItemActorPassive_mutEffects({entry, opts, state}) { + this._pGetItemActorPassive_mutEffects_player({ + entry, + opts, + state + }); + this._pGetItemActorPassive_mutEffects_creature({ + entry, + opts, + state + }); + } + + static _pGetItemActorPassive_mutEffects_player({entry, opts, state}) { + if (opts.mode !== "player" || !entry.entries?.length) + return; + + void 0; + } + + static _pGetItemActorPassive_mutEffects_creature({entry, opts, state}) { + if (opts.mode !== "creature" || !entry.entries?.length) + return; + + if (!UtilCompat.isPlutoniumAddonAutomationActive()) + return; + + const effects = UtilAutomation.getCreatureFeatureEffects({ + entry, + img: state.img + }); + if (effects.length) + state.effectsParsed.push(...effects); + } + + static _pGetItemActorPassive_mutFlags({entry, opts, state}) { + this._pGetItemActorPassive_mutFlags_player({ + entry, + opts, + state + }); + this._pGetItemActorPassive_mutFlags_creature({ + entry, + opts, + state + }); + } + + static _pGetItemActorPassive_mutFlags_player({entry, opts, state}) { + if (opts.mode !== "player" || !entry.entries?.length) + return; + + void 0; + } + + static _pGetItemActorPassive_mutFlags_creature({entry, opts, state}) { + if (opts.mode !== "creature" || !entry.entries?.length) + return; + + if (!UtilCompat.isPlutoniumAddonAutomationActive()) + return; + + const flags = UtilAutomation.getCreatureFeatureFlags({ + entry, + hasDamageParts: !!state.damageParts?.length, + hasSavingThrow: !!state.saveDc, + }); + + foundry.utils.mergeObject(state.flagsParsed, flags); + } + + static _DEFAULT_SAVING_THROW_DATA = { + saveAbility: undefined, + saveScaling: undefined, + saveDc: undefined, + }; + + static getSavingThrowData(entries) { + if (!entries?.length) + return MiscUtil.copy(this._DEFAULT_SAVING_THROW_DATA); + + let isFoundParse = false; + let {saveAbility, saveScaling, saveDc, } = MiscUtil.copy(this._DEFAULT_SAVING_THROW_DATA); + + const walker = MiscUtil.getWalker({ + keyBlocklist: MiscUtil.GENERIC_WALKER_ENTRIES_KEY_BLOCKLIST, + isNoModification: true, + isBreakOnReturn: true + }); + const reDc = /(?:{@dc (?\d+)}|DC\s*(?\d+))\s*(?Strength|Dexterity|Constitution|Intelligence|Wisdom|Charisma)/i; + + walker.walk(entries, { + string: (str)=>{ + const mDc = reDc.exec(str); + if (!mDc) + return; + + saveDc = Number(mDc.groups.dc || mDc.groups.dcAlt); + saveAbility = mDc.groups.ability.toLowerCase().substring(0, 3); + saveScaling = "flat"; + isFoundParse = true; + + return true; + } + , + }, ); + + return { + saveAbility, + saveScaling, + saveDc, + isFoundParse + }; + } + + static getMaxCasterProgression(...casterProgressions) { + casterProgressions = casterProgressions.filter(Boolean); + const ixs = casterProgressions.map(it=>this._CASTER_PROGRESSIONS.indexOf(it)).filter(ix=>~ix); + if (!ixs.length){return null;} + return this._CASTER_PROGRESSIONS[Math.min(...ixs)]; + } + + static getMaxCantripProgression(...casterProgressions) { + const out = []; + casterProgressions.filter(Boolean).forEach(progression=>{ + progression.forEach((cnt,i)=>{ + if (out[i] == null) + return out[i] = cnt; + out[i] = Math.max(out[i], cnt); + }); + }); + return out; + } + + static async pFillActorSkillToolLanguageData({existingProficienciesSkills, existingProficienciesTools, existingProficienciesLanguages, skillProficiencies, languageProficiencies, toolProficiencies, skillToolLanguageProficiencies, actorData, importOpts, titlePrefix, }, ) { + skillToolLanguageProficiencies = this._pFillActorSkillToolLanguageData_getMergedProfs({ + skillProficiencies, + languageProficiencies, + toolProficiencies, + skillToolLanguageProficiencies, + }); + + const formData = await Charactermancer_OtherProficiencySelect.pGetUserInput({ + titlePrefix, + existingFvtt: { + skillProficiencies: existingProficienciesSkills, + toolProficiencies: existingProficienciesTools, + languageProficiencies: existingProficienciesLanguages, + }, + available: skillToolLanguageProficiencies, + }); + if (!formData) + return importOpts.isCancelled = true; + if (formData === VeCt.SYM_UI_SKIP) + return; + + this.doApplySkillFormDataToActorUpdate({ + existingProfsActor: existingProficienciesSkills, + formData, + actorData, + }); + + this.doApplyOtherProficienciesFormData({ + existingProfsActor: existingProficienciesLanguages, + formData, + formDataProp: "languageProficiencies", + actorData, + opts: { + fnGetMappedItem: it=>UtilActors.getMappedLanguage(it), + fnGetMappedCustomItem: it=>Renderer.splitTagByPipe(it)[0].toTitleCase(), + actorTraitProp: "languages", + }, + }); + + this.doApplyToolFormDataToActorUpdate({ + existingProfsActor: existingProficienciesTools, + formData, + actorData, + }); + } + + static _pFillActorSkillToolLanguageData_getMergedProfs({skillProficiencies, languageProficiencies, toolProficiencies, skillToolLanguageProficiencies, }, ) { + const hasAnySingles = skillProficiencies?.length || languageProficiencies?.length || toolProficiencies?.length; + if (!hasAnySingles) + return skillToolLanguageProficiencies; + + if (!skillToolLanguageProficiencies?.length) { + const out = []; + this._pFillActorSkillToolLanguageData_doMergeToSingleArray({ + targetArray: out, + skillProficiencies, + languageProficiencies, + toolProficiencies, + }); + return out; + } + + if (skillToolLanguageProficiencies?.length && hasAnySingles) + console.warn(...LGT, `Founds individual skill/language/tool proficiencies alongside combined skill/language/tool; these will be merged together.`); + + const out = MiscUtil.copy(skillToolLanguageProficiencies || []); + this._pFillActorSkillToolLanguageData_doMergeToSingleArray({ + targetArray: out, + skillProficiencies, + languageProficiencies, + toolProficiencies, + }); + return out; + } + + static _pFillActorSkillToolLanguageData_doMergeToSingleArray({targetArray, skillProficiencies, languageProficiencies, toolProficiencies, }, ) { + const maxLen = Math.max(targetArray?.length || 0, skillProficiencies?.length || 0, languageProficiencies?.length || 0, toolProficiencies?.length || 0, ); + for (let i = 0; i < maxLen; ++i) { + const tgt = (targetArray[i] = {}); + + const skillProfSet = skillProficiencies?.[i]; + const langProfSet = languageProficiencies?.[i]; + const toolProfSet = toolProficiencies?.[i]; + + if (skillProfSet) { + this._pFillActorSkillToolLanguageData_doAddProfType({ + targetObject: tgt, + profSet: skillProfSet, + validKeySet: new Set(Object.keys(Parser.SKILL_TO_ATB_ABV)), + anyKeySet: new Set(["any"]), + anyKeySuffix: "Skill", + }); + } + + if (langProfSet) { + this._pFillActorSkillToolLanguageData_doAddProfType({ + targetObject: tgt, + profSet: langProfSet, + anyKeySet: new Set(["any", "anyStandard", "anyExotic"]), + anyKeySuffix: "Language", + }); + } + + if (toolProfSet) { + this._pFillActorSkillToolLanguageData_doAddProfType({ + targetObject: tgt, + profSet: toolProfSet, + anyKeySet: new Set(["any"]), + anyKeySuffix: "Tool", + }); + } + } + } + + static _pFillActorSkillToolLanguageData_doAddProfType({targetObject, profSet, validKeySet, anyKeySet, anyKeySuffix, }, ) { + Object.entries(profSet).forEach(([k,v])=>{ + switch (k) { + case "choose": + { + if (v?.from?.length) { + const choose = MiscUtil.copy(v); + choose.from = choose.from.filter(kFrom=>!validKeySet || validKeySet.has(kFrom)); + if (choose.from.length) { + const tgtChoose = (targetObject.choose = targetObject.choose || []); + tgtChoose.push(choose); + } + } + break; + } + + default: + { + if (anyKeySet && anyKeySet.has(k)) { + targetObject[`${k}${anyKeySuffix}`] = MiscUtil.copy(v); + break; + } + + if (!validKeySet || validKeySet.has(k)) + targetObject[k] = MiscUtil.copy(v); + } + } + } + ); + } + + static async pFillActorSkillData(existingProfsActor, skillProficiencies, actorData, dataBuilderOpts, opts) { + return this._pFillActorSkillToolData({ + existingProfsActor, + proficiencies: skillProficiencies, + actorData, + dataBuilderOpts, + opts, + fnGetMapped: Charactermancer_OtherProficiencySelect.getMappedSkillProficiencies.bind(Charactermancer_OtherProficiencySelect), + propProficiencies: "skillProficiencies", + pFnApplyToActorUpdate: this.doApplySkillFormDataToActorUpdate.bind(this), + }); + } + + static async pFillActorToolData(existingProfsActor, toolProficiencies, actorData, dataBuilderOpts, opts) { + return this._pFillActorSkillToolData({ + existingProfsActor, + proficiencies: toolProficiencies, + actorData, + dataBuilderOpts, + opts, + fnGetMapped: Charactermancer_OtherProficiencySelect.getMappedToolProficiencies.bind(Charactermancer_OtherProficiencySelect), + propProficiencies: "toolProficiencies", + pFnApplyToActorUpdate: this.doApplyToolFormDataToActorUpdate.bind(this), + }); + } + + static async _pFillActorSkillToolData({existingProfsActor, proficiencies, actorData, dataBuilderOpts, opts, fnGetMapped, propProficiencies, pFnApplyToActorUpdate, }, ) { + opts = opts || {}; + + if (!proficiencies) + return {}; + proficiencies = fnGetMapped(proficiencies); + + const formData = await Charactermancer_OtherProficiencySelect.pGetUserInput({ + ...opts, + existingFvtt: { + [propProficiencies]: existingProfsActor, + }, + available: proficiencies, + }); + if (!formData) + return dataBuilderOpts.isCancelled = true; + if (formData === VeCt.SYM_UI_SKIP) + return; + + return pFnApplyToActorUpdate({ + existingProfsActor, + formData, + actorData + }); + } + + static doApplySkillFormDataToActorUpdate({existingProfsActor, formData, actorData}) { + return this._doApplySkillToolFormDataToActorUpdate({ + existingProfsActor, + formData, + actorData, + mapAbvToFull: UtilActors.SKILL_ABV_TO_FULL, + propFormData: "skillProficiencies", + propActorData: "skills", + }, ); + } + + static doApplyToolFormDataToActorUpdate({existingProfsActor, formData, actorData}) { + return this._doApplySkillToolFormDataToActorUpdate({ + existingProfsActor, + formData, + actorData, + mapAbvToFull: UtilActors.TOOL_ABV_TO_FULL, + propFormData: "toolProficiencies", + propActorData: "tools", + }, ); + } + + static _doApplySkillToolFormDataToActorUpdate({existingProfsActor, formData, actorData, mapAbvToFull, propFormData, propActorData}) { + if (!formData?.data?.[propFormData]) + return; + + const out = {}; + + actorData[propActorData] = actorData[propActorData] || {}; + Object.entries(mapAbvToFull).filter(([_,name])=>formData.data[propFormData][name]).forEach(([abv,name])=>{ + out[abv] = formData.data[propFormData][name]; + + const maxValue = Math.max((existingProfsActor[abv] || {}).value || 0, formData.data[propFormData][name] != null ? Number(formData.data[propFormData][name]) : 0, (actorData[propActorData][abv] || {}).value || 0, ); + + const isUpdate = maxValue > (MiscUtil.get(actorData[propActorData], abv, "value") || 0); + if (isUpdate) + (actorData[propActorData][abv] = actorData[propActorData][abv] || {}).value = maxValue; + } + ); + + return out; + } + + static async pFillActorLanguageData(existingProfsActor, importingProfs, data, importOpts, opts) { + opts = opts || {}; + + if (!importingProfs) + return; + importingProfs = Charactermancer_OtherProficiencySelect.getMappedLanguageProficiencies(importingProfs); + + const formData = await Charactermancer_OtherProficiencySelect.pGetUserInput({ + ...opts, + existingFvtt: { + languageProficiencies: existingProfsActor, + }, + available: importingProfs, + }); + if (!formData) + return importOpts.isCancelled = true; + if (formData === VeCt.SYM_UI_SKIP) + return; + + this.doApplyLanguageProficienciesFormDataToActorUpdate({ + existingProfsActor, + formData, + actorData: data + }); + } + + static doApplyLanguageProficienciesFormDataToActorUpdate({existingProfsActor, formData, actorData}) { + this.doApplyOtherProficienciesFormData({ + existingProfsActor, + formData, + formDataProp: "languageProficiencies", + actorData, + opts: { + fnGetMappedItem: it=>UtilActors.getMappedLanguage(it), + fnGetMappedCustomItem: it=>Renderer.splitTagByPipe(it)[0].toTitleCase(), + actorTraitProp: "languages", + }, + }); + } + + static async pFillActorToolProfData(existingProfsActor, importingProfs, data, dataBuilderOpts, opts) { + opts = opts || {}; + + if (!importingProfs) + return; + importingProfs = Charactermancer_OtherProficiencySelect.getMappedToolProficiencies(importingProfs); + + const formData = await Charactermancer_OtherProficiencySelect.pGetUserInput({ + ...opts, + existingFvtt: { + toolProficiencies: existingProfsActor, + }, + available: importingProfs, + }); + if (!formData) + return dataBuilderOpts.isCancelled = true; + if (formData === VeCt.SYM_UI_SKIP) + return; + + this.doApplyToolProficienciesFormDataToActorUpdate({ + existingProfsActor, + formData, + actorData: data + }); + } + + static doApplyToolProficienciesFormDataToActorUpdate({existingProfsActor, formData, actorData}) { + this.doApplyToolFormDataToActorUpdate({ + existingProfsActor: existingProfsActor, + formData, + actorData, + }); + } + + static async pFillActorLanguageOrToolData(existingProfsLanguages, existingProfsTools, importingProfs, actorData, importOpts, opts) { + opts = opts || {}; + + if (!importingProfs) + return; + importingProfs = Charactermancer_OtherProficiencySelect.getMappedLanguageProficiencies(importingProfs); + importingProfs = Charactermancer_OtherProficiencySelect.getMappedToolProficiencies(importingProfs); + + const formData = await Charactermancer_OtherProficiencySelect.pGetUserInput({ + ...opts, + existingFvtt: { + languageProficiencies: existingProfsLanguages, + toolProficiencies: existingProfsTools, + }, + available: importingProfs, + }); + if (!formData) + return importOpts.isCancelled = true; + if (formData === VeCt.SYM_UI_SKIP) + return; + + this.doApplyOtherProficienciesFormData({ + existingProfsActor: existingProfsLanguages, + formData, + formDataProp: "languageProficiencies", + actorData, + opts: { + fnGetMappedItem: it=>UtilActors.getMappedLanguage(it), + fnGetMappedCustomItem: it=>Renderer.splitTagByPipe(it)[0].toTitleCase(), + actorTraitProp: "languages", + }, + }); + + this.doApplyToolFormDataToActorUpdate({ + existingProfsActor: existingProfsTools, + formData, + actorData, + }); + } + + static async pFillActorArmorProfData(existingProfsActor, importingProfs, data, importOpts, opts) { + opts = opts || {}; + + if (!importingProfs) + return; + importingProfs = Charactermancer_OtherProficiencySelect.getMappedArmorProficiencies(importingProfs); + + const formData = await Charactermancer_OtherProficiencySelect.pGetUserInput({ + ...opts, + existingFvtt: { + armorProficiencies: existingProfsActor, + }, + available: importingProfs, + }); + if (!formData) + return importOpts.isCancelled = true; + if (formData === VeCt.SYM_UI_SKIP) + return; + + this.doApplyArmorProficienciesFormDataToActorUpdate({ + existingProfsActor, + formData, + actorData: data + }); + } + + static doApplyArmorProficienciesFormDataToActorUpdate({existingProfsActor, formData, actorData}) { + this.doApplyOtherProficienciesFormData({ + existingProfsActor, + formData, + formDataProp: "armorProficiencies", + actorData, + opts: { + fnGetMappedItem: it=>UtilActors.getMappedArmorProficiency(it), + fnGetMappedCustomItem: it=>Renderer.splitTagByPipe(it)[0].toTitleCase(), + actorTraitProp: "armorProf", + }, + }); + } + + static async pFillActorWeaponProfData(existingProfsActor, importingProfs, data, importOpts, opts) { + opts = opts || {}; + + if (!importingProfs) + return; + importingProfs = Charactermancer_OtherProficiencySelect.getMappedWeaponProficiencies(importingProfs); + + const formData = await Charactermancer_OtherProficiencySelect.pGetUserInput({ + ...opts, + existingFvtt: { + weaponProficiencies: existingProfsActor, + }, + available: importingProfs, + }); + if (!formData) + return importOpts.isCancelled = true; + if (formData === VeCt.SYM_UI_SKIP) + return; + + this.doApplyWeaponProficienciesFormDataToActorUpdate({ + existingProfsActor, + formData, + actorData: data + }); + } + + static doApplyWeaponProficienciesFormDataToActorUpdate({existingProfsActor, formData, actorData}) { + this.doApplyOtherProficienciesFormData({ + existingProfsActor, + formData, + formDataProp: "weaponProficiencies", + actorData, + opts: { + fnGetMappedItem: it=>UtilActors.getMappedWeaponProficiency(it), + fnGetMappedCustomItem: it=>Renderer.splitTagByPipe(it)[0].toTitleCase(), + actorTraitProp: "weaponProf", + }, + }); + } + + static doApplyOtherProficienciesFormData({existingProfsActor, formData, formDataProp, actorData, opts}) { + if (!formData?.data?.[formDataProp]) + return false; + + existingProfsActor = existingProfsActor || {}; + + const formDataSet = formData.data[formDataProp]; + + if (!Object.keys(formDataSet).length) + return false; + const cpyFormDataSet = MiscUtil.copy(formDataSet); + + const profSet = new Set(); + Object.keys(cpyFormDataSet).filter(k=>cpyFormDataSet[k]).forEach(k=>profSet.add(k)); + + const mappedValidItems = new Set(); + const customItems = []; + + (existingProfsActor.value || []).forEach(it=>mappedValidItems.add(it)); + (existingProfsActor.custom || "").split(";").map(it=>it.trim()).filter(Boolean).forEach(it=>this._doApplyFormData_doCheckAddCustomItem(customItems, it)); + + const existingProfsActorData = MiscUtil.get(actorData, "traits", opts.actorTraitProp); + (existingProfsActorData?.value || []).forEach(it=>mappedValidItems.add(it)); + (existingProfsActorData?.custom || "").split(";").map(it=>it.trim()).filter(Boolean).forEach(it=>this._doApplyFormData_doCheckAddCustomItem(customItems, it)); + + profSet.forEach(it=>{ + const mapped = opts.fnGetMappedItem ? opts.fnGetMappedItem(it) : it; + if (mapped) + mappedValidItems.add(mapped); + else { + const toAdd = opts.fnGetMappedCustomItem ? opts.fnGetMappedCustomItem(it) : it.toTitleCase(); + this._doApplyFormData_doCheckAddCustomItem(customItems, toAdd); + } + } + ); + + const dataTarget = MiscUtil.set(actorData, "traits", opts.actorTraitProp, {}); + dataTarget.value = [...mappedValidItems].map(it=>it.toLowerCase()).sort(SortUtil.ascSortLower); + dataTarget.custom = customItems.join(";"); + } + + static _doApplyFormData_doCheckAddCustomItem(customItems, item) { + const cleanItem = item.trim().toLowerCase(); + if (!customItems.some(it=>it.trim().toLowerCase() === cleanItem)) + customItems.push(item); + } + + static doApplySavingThrowProficienciesFormDataToActorUpdate({existingProfsActor, formData, actorData}) { + if (!formData?.data?.savingThrowProficiencies) + return; + + actorData.abilities = actorData.abilities || {}; + Parser.ABIL_ABVS.filter(ab=>formData.data.savingThrowProficiencies[ab]).forEach(ab=>{ + const maxValue = Math.max(existingProfsActor[ab]?.proficient || 0, formData.data.savingThrowProficiencies[ab] ? 1 : 0, actorData.abilities[ab]?.proficient || 0, ); + const isUpdate = maxValue > (MiscUtil.get(actorData.abilities, ab, "proficient") || 0); + if (isUpdate) + MiscUtil.set(actorData.abilities, ab, "proficient", maxValue); + } + ); + } + + static async pFillActorImmunityData(existingProfsActor, importing, data, importOpts, opts) { + opts = opts || {}; + + const formData = await Charactermancer_DamageImmunitySelect.pGetUserInput({ + ...opts, + existingFvtt: { + immune: existingProfsActor, + }, + available: importing, + }); + if (!formData) + return importOpts.isCancelled = true; + if (formData === VeCt.SYM_UI_SKIP) + return; + + this.doApplyDamageImmunityFormDataToActorUpdate({ + existingProfsActor, + formData, + actorData: data + }); + } + + static async pFillActorResistanceData(existingProfsActor, importing, data, importOpts, opts) { + opts = opts || {}; + + const formData = await Charactermancer_DamageResistanceSelect.pGetUserInput({ + ...opts, + existingFvtt: { + resist: existingProfsActor, + }, + available: importing, + }); + if (!formData) + return importOpts.isCancelled = true; + if (formData === VeCt.SYM_UI_SKIP) + return; + + this.doApplyDamageResistanceFormDataToActorUpdate({ + existingProfsActor, + formData, + actorData: data + }); + } + + static async pFillActorVulnerabilityData(existingProfsActor, importing, data, importOpts, opts) { + opts = opts || {}; + + const formData = await Charactermancer_DamageVulnerabilitySelect.pGetUserInput({ + ...opts, + existingFvtt: { + vulnerable: existingProfsActor, + }, + available: importing, + }); + if (!formData) + return importOpts.isCancelled = true; + if (formData === VeCt.SYM_UI_SKIP) + return; + + this.doApplyDamageVulnerabilityFormDataToActorUpdate({ + existingProfsActor, + formData, + actorData: data + }); + } + + static async pFillActorConditionImmunityData(existing, importing, data, importOpts, opts) { + opts = opts || {}; + + const formData = await Charactermancer_ConditionImmunitySelect.pGetUserInput({ + ...opts, + existingFvtt: { + conditionImmune: existing, + }, + available: importing, + }); + if (!formData) + return importOpts.isCancelled = true; + if (formData === VeCt.SYM_UI_SKIP) + return; + + this.doApplyConditionImmunityFormDataToActorUpdate({ + existingProfsActor: existing, + formData, + actorData: data + }); + } + + static doApplyDamageImmunityFormDataToActorUpdate({existingProfsActor, formData, actorData}) { + this.doApplyOtherProficienciesFormData({ + existingProfsActor, + formData, + formDataProp: "immune", + actorData, + opts: { + actorTraitProp: "di", + }, + }); + } + + static doApplyDamageResistanceFormDataToActorUpdate({existingProfsActor, formData, actorData}) { + this.doApplyOtherProficienciesFormData({ + existingProfsActor, + formData, + formDataProp: "resist", + actorData, + opts: { + actorTraitProp: "dr", + }, + }); + } + + static doApplyDamageVulnerabilityFormDataToActorUpdate({existingProfsActor, formData, actorData}) { + this.doApplyOtherProficienciesFormData({ + existingProfsActor, + formData, + formDataProp: "vulnerable", + actorData, + opts: { + actorTraitProp: "dv", + }, + }); + } + + static doApplyConditionImmunityFormDataToActorUpdate({existingProfsActor, formData, actorData}) { + this.doApplyOtherProficienciesFormData({ + existingProfsActor, + formData, + formDataProp: "conditionImmune", + actorData, + opts: { + fnGetMappedItem: it=>it === "disease" ? "diseased" : it, + actorTraitProp: "ci", + }, + }); + } + + static async pFillActorExpertiseData({existingProficienciesSkills, existingProficienciesTools, expertise, actorData, importOpts, titlePrefix, }, ) { + const mergedExistingProficienciesSkills = existingProficienciesSkills ? MiscUtil.copy(existingProficienciesSkills) : existingProficienciesSkills; + const mergedExistingProficienciesTools = existingProficienciesTools ? MiscUtil.copy(existingProficienciesTools) : existingProficienciesTools; + + if (mergedExistingProficienciesSkills && actorData.skills) { + Object.entries(actorData.skills).forEach(([key,meta])=>{ + if (!meta) + return; + + mergedExistingProficienciesSkills[key] = mergedExistingProficienciesSkills[key] || MiscUtil.copy(meta); + mergedExistingProficienciesSkills[key].value = Math.max(mergedExistingProficienciesSkills[key].value, meta.value); + } + ); + } + + if (mergedExistingProficienciesSkills && actorData.tools) { + Object.entries(actorData.tools).forEach(([key,meta])=>{ + if (!meta) + return; + + mergedExistingProficienciesTools[key] = mergedExistingProficienciesTools[key] || MiscUtil.copy(meta); + mergedExistingProficienciesTools[key].value = Math.max(mergedExistingProficienciesTools[key].value, meta.value); + } + ); + } + + const formData = await Charactermancer_ExpertiseSelect.pGetUserInput({ + titlePrefix, + existingFvtt: { + skillProficiencies: mergedExistingProficienciesSkills, + toolProficiencies: mergedExistingProficienciesTools, + }, + available: expertise, + }); + if (!formData) + return importOpts.isCancelled = true; + if (formData === VeCt.SYM_UI_SKIP) + return; + + this.doApplyExpertiseFormDataToActorUpdate({ + existingProfsActor: { + skillProficiencies: existingProficienciesSkills, + toolProficiencies: existingProficienciesTools, + }, + formData, + actorData: actorData, + }); + } + + static doApplyExpertiseFormDataToActorUpdate({existingProfsActor, formData, actorData}) { + this.doApplySkillFormDataToActorUpdate({ + existingProfsActor: existingProfsActor.skillProficiencies, + formData, + actorData, + }); + + this.doApplyToolFormDataToActorUpdate({ + existingProfsActor: existingProfsActor.toolProficiencies, + formData, + actorData, + }); + } + + static doApplySensesFormDataToActorUpdate({existingSensesActor, existingTokenActor, formData, actorData, actorToken, configGroup}) { + if (!Object.keys(formData?.data).length) + return; + + const dataTarget = MiscUtil.getOrSet(actorData, "attributes", "senses", {}); + Object.assign(dataTarget, MiscUtil.copy(existingSensesActor)); + + const foundrySenseData = this._getFoundrySenseData({ + configGroup, + formData + }); + + this._getSensesNumericalKeys(foundrySenseData).forEach(kSense=>{ + const range = foundrySenseData[kSense]; + delete foundrySenseData[kSense]; + + if (range == null) + return; + dataTarget[kSense] = Math.max(dataTarget[kSense], range); + } + ); + + Object.assign(dataTarget, foundrySenseData); + + let {sight: {range: curSightRange}} = existingTokenActor || { + sight: {} + }; + if (curSightRange == null || isNaN(curSightRange) || Number(curSightRange) !== curSightRange) { + const cleanedSightRange = curSightRange == null || isNaN(curSightRange) ? 0 : Number(curSightRange); + if (curSightRange === 0) + MiscUtil.set(actorToken, "sight", "range", cleanedSightRange); + } + + MiscUtil.set(actorToken, "sight", "enabled", true); + + this.mutTokenSight({ + dataAttributesSenses: dataTarget, + dataToken: actorToken, + configGroup, + }); + } + + static _getFoundrySenseData({configGroup, formData}) { + const out = {}; + + Object.entries(formData.data).forEach(([sense,range])=>{ + if (!range) + return out[sense] = null; + + range = Config.getMetricNumberDistance({ + configGroup, + originalValue: range, + originalUnit: "feet" + }); + range = Number(range.toFixed(2)); + + out[sense] = range; + } + ); + + const units = Config.getMetricUnitDistance({ + configGroup, + originalUnit: "ft" + }); + if (!out.units || units !== "ft") + out.units = units; + + return out; + } + + static _getSensesNumericalKeys() { + const sensesModel = CONFIG.Item.dataModels.race.defineSchema().senses; + return Object.entries(sensesModel.fields).filter(([,v])=>v instanceof foundry.data.fields.NumberField).map(([k])=>k); + } + + static mutTokenSight({dataAttributesSenses, dataToken, configGroup}) { + if (!dataAttributesSenses) + return { + dataAttributesSenses, + dataToken + }; + + if (dataAttributesSenses.darkvision) { + MiscUtil.set(dataToken, "sight", "range", Math.max(dataToken.sight?.dim ?? 0, dataAttributesSenses.darkvision)); + if (dataToken.sight?.visionMode == null || dataToken.sight?.visionMode === "basic") + MiscUtil.set(dataToken, "sight", "visionMode", "darkvision"); + } + + let hasNonDarkvisionSense = false; + for (const prop of ["blindsight", "tremorsense", "truesight"]) { + if (!dataAttributesSenses[prop]) + continue; + + hasNonDarkvisionSense = true; + + const isUse = dataAttributesSenses[prop] > (dataToken.sight?.range ?? 0); + if (!isUse) + continue; + + MiscUtil.set(dataToken, "sight", "range", dataAttributesSenses[prop]); + + if (dataToken.sight?.visionMode === "basic") { + MiscUtil.set(dataToken, "sight", "visionMode", prop === "tremorsense" ? "tremorsense" : "darkvision"); + } + } + + if (dataAttributesSenses.truesight) + this._mutTokenSight_addUpdateDetectionMode({ + dataToken, + id: "seeAll", + range: dataAttributesSenses.truesight + }); + if (dataAttributesSenses.tremorsense) + this._mutTokenSight_addUpdateDetectionMode({ + dataToken, + id: "feelTremor", + range: dataAttributesSenses.tremorsense + }); + if (dataAttributesSenses.blindsight) + this._mutTokenSight_addUpdateDetectionMode({ + dataToken, + id: "blindsight", + range: dataAttributesSenses.blindsight + }); + + if (dataAttributesSenses.darkvision && !hasNonDarkvisionSense && Config.getSafe(configGroup, "tokenVisionSaturation") !== ConfigConsts.C_USE_GAME_DEFAULT) { + MiscUtil.set(dataToken, "sight", "saturation", -1); + } + + return { + dataAttributesSenses, + dataToken + }; + } + + static _mutTokenSight_addUpdateDetectionMode({dataToken, id, range}) { + const detectionModeArr = MiscUtil.getOrSet(dataToken, "detectionModes", []); + const existing = detectionModeArr.find(mode=>mode?.id === id); + if (existing) + return existing.range = Math.max(existing.range, range); + detectionModeArr.push({ + id, + range, + enabled: true + }); + } + + static _RE_IS_VERSATILE = / (?:two|both) hands/i; + static _getDamageTupleMetas(str, {summonSpellLevel=0}={}) { + const damageTupleMetas = []; + + const ixFirstDc = str.indexOf(`{@dc `); + + let ixLastMatch = null; + let lenLastMatch = null; + + const strOut = str.replace(/(?:(?\d+)|\(?{@(?:dice|damage) (?[^|}]+)(?:\|[^}]+)?}(?:\s+[-+]\s+the spell's level)?(?: plus {@(?:dice|damage) (?[^|}]+)(?:\|[^}]+)?})?\)?)(?:\s+[-+]\s+[-+a-zA-Z0-9 ]*?)?(?: (?[^ ]+))? damage/gi, (...mDamage)=>{ + const [fullMatch] = mDamage; + const [ixMatch,,{dmgFlat, dmgDice1, dmgDice2, dmgType}] = mDamage.slice(-3); + + const dmgDice1Clean = dmgDice1 ? dmgDice1.split("|")[0] : null; + const dmgDice2Clean = dmgDice2 ? dmgDice2.split("|")[0] : null; + const dmgTypeClean = dmgType || ""; + + const isFlatDamage = dmgFlat != null; + let dmg = isFlatDamage ? dmgFlat : dmgDice2Clean ? `${dmgDice1Clean} + ${dmgDice2Clean}` : dmgDice1Clean; + + if (isFlatDamage) { + const tokens = str.split(/( )/g); + let lenTokenStack = 0; + const tokenStack = []; + for (let i = 0; i < tokens.length; ++i) { + tokenStack.push(tokens[i]); + lenTokenStack += tokens[i].length; + + if (lenTokenStack === ixMatch) { + const lastFourTokens = tokenStack.slice(-4); + if (/^by dealing$/i.test(lastFourTokens.join("").trim())) { + return ""; + } + } + } + } + + dmg = dmg.replace(/\bPB\b/gi, `@${SharedConsts.MODULE_ID_FAKE}.userchar.pb`); + + dmg = dmg.replace(/\bsummonSpellLevel\b/gi, `${summonSpellLevel ?? 0}`); + + const tupleMeta = { + tuple: [dmg, dmgTypeClean], + isOnFailSavingThrow: false, + isAlternateRoll: false, + }; + + if (~ixFirstDc && ixMatch >= ixFirstDc) { + tupleMeta.isOnFailSavingThrow = true; + } + + if (damageTupleMetas.last()?.isAlternateRoll || (damageTupleMetas.length && /\bor\b/.test(str.slice(ixLastMatch + lenLastMatch, ixMatch)) && !this._RE_IS_VERSATILE.test(str.slice(ixMatch + fullMatch.length)))) { + tupleMeta.isAlternateRoll = true; + } + + damageTupleMetas.push(tupleMeta); + + ixLastMatch = ixMatch; + lenLastMatch = fullMatch.length; + + return ""; + } + ).replace(/ +/g, " "); + + return { + str: strOut, + damageTupleMetas: damageTupleMetas.filter(it=>it.tuple.length), + }; + } + + static _getDamagePartsAndOtherFormula(damageTupleMetas) { + damageTupleMetas = damageTupleMetas || []; + + const damageTuples = []; + const otherFormulaParts = []; + + damageTupleMetas.forEach(meta=>{ + if ((!Config.get("import", "isUseOtherFormulaFieldForSaveHalvesDamage") || !meta.isOnFailSavingThrow) && (!Config.get("import", "isUseOtherFormulaFieldForSaveHalvesDamage") || !meta.isAlternateRoll)) + return damageTuples.push(meta.tuple); + + otherFormulaParts.push(`${meta.tuple[1] ? "(" : ""}${meta.tuple[0]}${meta.tuple[1] ? `)[${meta.tuple[1]}]` : ""}`); + } + ); + + if (!damageTuples.length) + return { + damageParts: damageTupleMetas.map(it=>it.tuple), + formula: "" + }; + + return { + damageParts: damageTuples, + formula: otherFormulaParts.join(" + ") + }; + } + + static _getSpeedValue(speeds, prop, configGroup) { + if (speeds == null) + return null; + + if (typeof speeds === "number") { + return prop === "walk" ? Config.getMetricNumberDistance({ + configGroup, + originalValue: speeds, + originalUnit: "feet" + }) : null; + } + + const speed = speeds[prop]; + + if (speed == null) + return null; + if (typeof speed === "boolean") + return null; + if (speed.number != null) + return Config.getMetricNumberDistance({ + configGroup, + originalValue: speed.number, + originalUnit: "feet" + }); + if (isNaN(speed)) + return null; + return Config.getMetricNumberDistance({ + configGroup, + originalValue: Number(speed), + originalUnit: "feet" + }); + } + + static _SPEED_PROPS_IS_EQUAL_MAP = { + burrow: "burrow", + climb: "climb", + fly: "fly", + swim: "swim", + }; + + static async _pGetSpeedEffects(speeds, {actor, actorItem, iconEntity, iconPropCompendium, taskRunner=null}={}) { + if (speeds == null) + return []; + + const icon = iconEntity && iconPropCompendium ? await this._ImageFetcher.pGetSaveImagePath(iconEntity, { + propCompendium: iconPropCompendium, + taskRunner + }) : undefined; + + if (typeof speeds === "number") + return []; + + const toMap = Object.entries(speeds).filter(([k,v])=>this._SPEED_PROPS_IS_EQUAL_MAP[k] && v === true); + + if (!toMap.length) + return []; + + return [...toMap.map(([k])=>{ + return UtilActiveEffects.getGenericEffect({ + key: `system.attributes.movement.${this._SPEED_PROPS_IS_EQUAL_MAP[k]}`, + value: `@attributes.movement.walk`, + mode: CONST.ACTIVE_EFFECT_MODES.OVERRIDE, + name: `${k.toTitleCase()} Speed`, + icon, + disabled: false, + priority: UtilActiveEffects.PRIORITY_BASE, + originActor: actor, + originActorItem: actorItem, + }); + } + ), ]; + } + + static _isSpeedHover(speed) { + if (typeof speed === "number") + return false; + return !!speed.canHover; + } + + static getMovement(speed, {configGroup=null, propAllowlist=null}={}) { + return { + burrow: (!propAllowlist || propAllowlist.has("burrow")) ? this._getSpeedValue(speed, "burrow", configGroup) : null, + climb: (!propAllowlist || propAllowlist.has("climb")) ? this._getSpeedValue(speed, "climb", configGroup) : null, + fly: (!propAllowlist || propAllowlist.has("fly")) ? this._getSpeedValue(speed, "fly", configGroup) : null, + swim: (!propAllowlist || propAllowlist.has("swim")) ? this._getSpeedValue(speed, "swim", configGroup) : null, + walk: (!propAllowlist || propAllowlist.has("walk")) ? this._getSpeedValue(speed, "walk", configGroup) : null, + units: Config.getMetricUnitDistance({ + configGroup, + originalUnit: "ft" + }), + hover: this._isSpeedHover(speed), + }; + } + + static _getParsedWeaponEntryData(ent) { + if (!(ent.entries && ent.entries[0] && typeof ent.entries[0] === "string")) + return; + + const damageTupleMetas = []; + let attackBonus = 0; + + const str = ent.entries[0]; + + damageTupleMetas.push(...this._getDamageTupleMetas(str).damageTupleMetas); + + const {rangeShort, rangeLong, rangeUnits} = this._getAttackRange(str); + + const mHit = /{@hit ([^|}]+)(?:\|[^}]+)?}/gi.exec(str); + if (mHit) { + const hitBonus = Number(mHit[1]); + if (!isNaN(hitBonus)) { + attackBonus = hitBonus; + } + } + + return { + damageTupleMetas, + rangeShort, + rangeLong, + rangeUnits, + attackBonus, + }; + } + + static _getAttackRange(str) { + let rangeShort = null; + let rangeLong = null; + + const mRange = /range (\d+)(?:\/(\d+))? ft/gi.exec(str); + if (mRange) { + rangeShort = Number(mRange[1]); + if (mRange[2]) + rangeLong = Number(mRange[2]); + } else { + const mReach = /reach (\d+) ft/gi.exec(str); + if (mReach) { + rangeShort = Number(mReach[1]); + } + } + + rangeShort = Config.getMetricNumberDistance({ + configGroup: this._configGroup, + originalValue: rangeShort, + originalUnit: "feet" + }); + rangeLong = Config.getMetricNumberDistance({ + configGroup: this._configGroup, + originalValue: rangeLong, + originalUnit: "feet" + }); + + return { + rangeShort, + rangeLong, + rangeUnits: rangeShort || rangeLong ? Config.getMetricUnitDistance({ + configGroup: this._configGroup, + originalUnit: "feet" + }) : null, + }; + } + + static getActorDamageResImmVulnConditionImm(ent) { + const out = {}; + + const allDis = new Set(); + const bypassDis = new Set(); + let customDis = []; + this._getActorDamageResImmVulnConditionImm_addDamageTypesOrConditionTypes({ + ent, + validTypesArr: UtilActors.VALID_DAMAGE_TYPES, + fnRender: Parser.getFullImmRes, + prop: "immune", + allSet: allDis, + bypassSet: bypassDis, + customStack: customDis, + }); + + out.di = { + value: [...allDis], + custom: customDis.join(", "), + bypasses: [...bypassDis], + }; + + const allDrs = new Set(); + const bypassDrs = new Set(); + let customDrs = []; + this._getActorDamageResImmVulnConditionImm_addDamageTypesOrConditionTypes({ + ent, + validTypesArr: UtilActors.VALID_DAMAGE_TYPES, + fnRender: Parser.getFullImmRes, + prop: "resist", + allSet: allDrs, + bypassSet: bypassDrs, + customStack: customDrs, + }); + + out.dr = { + value: [...allDrs], + custom: customDrs.join(", "), + bypasses: [...bypassDrs], + }; + + const allDvs = new Set(); + const bypassDvs = new Set(); + let customDvs = []; + this._getActorDamageResImmVulnConditionImm_addDamageTypesOrConditionTypes({ + ent, + validTypesArr: UtilActors.VALID_DAMAGE_TYPES, + fnRender: Parser.getFullImmRes, + prop: "vulnerable", + allSet: allDvs, + bypassSet: bypassDvs, + customStack: customDvs, + }); + + out.dv = { + value: [...allDvs], + custom: customDvs.join(", "), + bypasses: [...bypassDvs], + }; + + const allCis = new Set(); + let customCis = []; + this._getActorDamageResImmVulnConditionImm_addDamageTypesOrConditionTypes({ + ent, + validTypesArr: UtilActors.VALID_CONDITIONS, + fnRender: arr=>Parser.getFullCondImm(arr, { + isPlainText: true + }), + prop: "conditionImmune", + allSet: allCis, + customStack: customCis, + }); + + out.ci = { + value: [...allCis], + custom: customCis.join(", "), + }; + + return out; + } + + static _getActorDamageResImmVulnConditionImm_addDamageTypesOrConditionTypes({ent, validTypesArr, fnRender, prop, allSet, bypassSet, customStack}, ) { + if (!ent[prop]) + return; + + ent[prop].forEach(it=>{ + if (validTypesArr.includes(it)) { + allSet.add(it); + return; + } + + if (this._PROPS_DAMAGE_IMM_VULN_RES.has(prop) && it[prop] && it[prop]instanceof Array && CollectionUtil.setEq(new Set(it[prop]), this._SET_PHYSICAL_DAMAGE) && it.note && it.cond) { + const mNote = /\bnon[- ]?magical\b.*?(?:\baren't (?silvered|adamantine)\b)?$/.exec(it.note); + + if (mNote) { + bypassSet.add("mgc"); + + switch ((mNote.groups.bypass || "").toLowerCase()) { + case "silvered": + bypassSet.add("sil"); + break; + case "adamantine": + bypassSet.add("ada"); + break; + } + + it[prop].forEach(sub=>allSet.add(sub)); + return; + } + } + + const asText = fnRender([it]); + customStack.push(asText); + } + ); + } + + static getImportedEmbed(importedEmbeds, itemData) { + const importedEmbed = importedEmbeds.find(it=>it.raw === itemData); + + if (!importedEmbed) { + ui.notifications.warn(`Failed to link embedded entity for active effects! ${VeCt.STR_SEE_CONSOLE}`); + console.warn(...LGT, `Could not find loaded item data`, itemData, `in imported embedded entities`, importedEmbeds); + return null; + } + + return importedEmbed; + } + + static getConsumedSheetItem({consumes, actor}) { + const lookupNames = [consumes.name.toLowerCase().trim(), consumes.name.toLowerCase().trim().toPlural(), ]; + + return (actor?.items?.contents || []).find(it=>it.type === "feat" && lookupNames.includes(it.name.toLowerCase().trim())); + } + + static _mutApplyDocOwnership(docData, {defaultOwnership, isAddDefaultOwnershipFromConfig, userOwnership, }, ) { + if (defaultOwnership != null) + docData.ownership = { + default: defaultOwnership + }; + else if (isAddDefaultOwnershipFromConfig) + docData.ownership = { + default: Config.get(this._configGroup, "ownership") + }; + + if (userOwnership) + Object.assign(docData.ownership ||= {}, userOwnership); + } + + static mutEffectsDisabledTransfer(effects, configGroup, opts={}) { + if (!effects) + return; + + return effects.map(effect=>this.mutEffectDisabledTransfer(effect, configGroup, opts)); + } + + static mutEffectDisabledTransfer(effect, configGroup, {hintDisabled=null, hintTransfer=null, hintSelfTarget=null, }={}, ) { + if (!effect) + return; + + const disabled = Config.get(configGroup, "setEffectDisabled"); + switch (disabled) { + case ConfigConsts.C_USE_PLUT_VALUE: + effect.disabled = hintDisabled != null ? hintDisabled : false; + break; + case ConfigConsts.C_BOOL_DISABLED: + effect.disabled = false; + break; + case ConfigConsts.C_BOOL_ENABLED: + effect.disabled = true; + break; + } + + const transfer = Config.get(configGroup, "setEffectTransfer"); + switch (transfer) { + case ConfigConsts.C_USE_PLUT_VALUE: + effect.transfer = hintTransfer != null ? hintTransfer : true; + break; + case ConfigConsts.C_BOOL_DISABLED: + effect.transfer = false; + break; + case ConfigConsts.C_BOOL_ENABLED: + effect.transfer = true; + break; + } + + if (UtilCompat.isPlutoniumAddonAutomationActive()) { + const val = hintTransfer != null ? hintSelfTarget : false; + MiscUtil.set(effect, "flags", UtilCompat.MODULE_DAE, "selfTarget", val); + MiscUtil.set(effect, "flags", UtilCompat.MODULE_DAE, "selfTargetAlways", val); + } + + return effect; + } + + static getEffectsMutDedupeId(effects) { + if (!effects?.length) + return effects; + + const usedDedupeIds = new Set(); + + effects.forEach(eff=>{ + const dedupeIdExisting = eff.flags?.[SharedConsts.MODULE_ID]?.dedupeId; + if (dedupeIdExisting && !usedDedupeIds.has(dedupeIdExisting)) { + usedDedupeIds.add(dedupeIdExisting); + return; + } + + if (!eff.name) + throw new Error(`Effect did not have a name!`); + + const dedupeIdBase = dedupeIdExisting ?? eff.name.slugify({ + strict: true + }); + if (!usedDedupeIds.has(dedupeIdBase)) { + usedDedupeIds.add(dedupeIdBase); + MiscUtil.set(eff, "flags", SharedConsts.MODULE_ID, "dedupeId", dedupeIdBase); + return; + } + + for (let i = 0; i < 99; ++i) { + const dedupeId = `${dedupeIdBase}-${i}`; + if (!usedDedupeIds.has(dedupeId)) { + usedDedupeIds.add(dedupeId); + MiscUtil.set(eff, "flags", SharedConsts.MODULE_ID, "dedupeId", dedupeId); + return; + } + } + + throw new Error(`Could not find an available dedupeId for base "${dedupeIdBase}"!`); + } + ); + + return effects; + } + + static _getTranslationData({srdData, }, ) { + if (!srdData || !Config.get("integrationBabele", "isEnabled") || !UtilCompat.isBabeleActive() || !srdData.flags?.[UtilCompat.MODULE_BABELE]) + return null; + + return { + name: srdData.name, + description: srdData.system?.description?.value, + flags: { + [UtilCompat.MODULE_BABELE]: { + translated: !!srdData.flags[UtilCompat.MODULE_BABELE].translated, + hasTranslation: !!srdData.flags[UtilCompat.MODULE_BABELE].hasTranslation, + }, + }, + }; + } + + static _getTranslationMeta({name, translationData, description, }, ) { + if (translationData == null) + return { + name, + description, + flags: {} + }; + + const flags = { + [UtilCompat.MODULE_BABELE]: { + ...(translationData.flags?.[UtilCompat.MODULE_BABELE] || {}), + originalName: name, + }, + }; + + name = translationData.name; + + if (description && Config.get("integrationBabele", "isUseTranslatedDescriptions")) + description = translationData.description || description; + + return { + name, + description, + flags + }; + } + + static _getIdObj({id=null}={}) { + if (id == null) + id = foundry.utils.randomID(); + return { + _id: id, + id, + }; + } +} + +DataConverter.SYM_AT = ""; + +DataConverter.ITEM_TYPES_ACTOR_TOOLS = new Set(["AT", "GS", "INS", "T"]); +DataConverter.ITEM_TYPES_ACTOR_WEAPONS = new Set(["M", "R"]); +DataConverter.ITEM_TYPES_ACTOR_ARMOR = new Set(["LA", "MA", "HA", "S"]); + +DataConverter._PROPS_DAMAGE_IMM_VULN_RES = new Set(["immune", "resist", "vulnerable"]); +DataConverter._SET_PHYSICAL_DAMAGE = new Set(["bludgeoning", "piercing", "slashing"]); + +DataConverter._CASTER_PROGRESSIONS = ["full", "artificer", "1/2", "1/3", "pact", ]; + +var DataConverter$1 = /*#__PURE__*/ +Object.freeze({ + __proto__: null, + DataConverter: DataConverter +}); + +class DataConverterJournal extends DataConverter { + static _mutPageDataOwnershipFlags({pageData, flags}) { + pageData.ownership = { + default: CONST.DOCUMENT_OWNERSHIP_LEVELS.INHERIT + }; + pageData.flags = flags ? MiscUtil.copy(flags) : {}; + } + + static _getContentPage({name, content, flags}) { + if (!content) + return null; + + const pageData = { + name, + type: "text", + text: { + format: 1, + content, + }, + }; + this._mutPageDataOwnershipFlags({ + pageData, + flags + }); + return pageData; + } + + static _getImgPage({name, img, flags}) { + if (!img) + return null; + + const pageData = { + name: `${name} (Image)`, + type: "image", + src: img, + }; + this._mutPageDataOwnershipFlags({ + pageData, + flags + }); + return pageData; + } + + static _getPages({name, content, img, flags}) { + return [this._getContentPage({ + name, + content, + flags + }), this._getImgPage({ + name, + img, + flags + }), ].filter(Boolean); + } + + static async _pGetWithJournalDescriptionPlugins(pFn) { + return UtilDataConverter.pGetWithDescriptionPlugins(async()=>{ + const renderer = Renderer.get().setPartPageExpandCollapseDisabled(true); + const out = await pFn(); + renderer.setPartPageExpandCollapseDisabled(false); + return out; + } + ); + } +} + +class DataConverterClass extends DataConverter { + static _configGroup = "importClass"; + + static _SideDataInterface = SideDataInterfaceClass; + //TEMPFIX static _ImageFetcher = ImageFetcherClass; + + static _getDoNotUseNote() { + return UtilDataConverter.pGetWithDescriptionPlugins(()=>`

    ${Renderer.get().render(`{@note Note: importing a class as an item is provided for display purposes only. If you wish to import a class to a character sheet, please use the importer on the sheet instead.}`)}

    `); + } + + static _getDataHitDice(cls) { + if (cls.hd?.number !== 1) + return null; + if (!cls.hd?.faces) + return null; + + const asString = `d${cls.hd.faces}`; + if (!CONFIG.DND5E.hitDieTypes.includes(asString)) + return null; + return asString; + } + + static async pGetClassItem(cls, opts) { + opts = opts || {}; + if (opts.actor) + opts.isActorItem = true; + + Renderer.get().setFirstSection(true).resetHeaderIndex(); + + const itemId = foundry.utils.randomID(); + + if (!opts.isClsDereferenced) { + cls = await DataLoader.pCacheAndGet("class", cls.source, UrlUtil.URL_TO_HASH_BUILDER["class"](cls), { + isRequired: true + }); + } + + if (opts.pageFilter?.filterBox && opts.filterValues) { + cls = MiscUtil.copy(cls); + + Renderer.class.mutFilterDereferencedClassFeatures({ + cpyCls: cls, + pageFilter: opts.pageFilter, + filterValues: opts.filterValues, + }); + } + + const srdData = await UtilCompendium.getSrdCompendiumEntity("class", cls, { + taskRunner: opts.taskRunner + }); + + const {name: translatedName, description: translatedDescription, flags: translatedFlags} = this._getTranslationMeta({ + translationData: this._getTranslationData({ + srdData + }), + name: UtilApplications.getCleanEntityName(UtilDataConverter.getNameWithSourcePart(cls, { + isActorItem: opts.isActorItem + })), + description: await this._pGetClassDescription(cls, opts), + }); + + const identifierCls = UtilDocumentItem.getNameAsIdentifier(cls.name); + + //TEMPFIX + /* const img = await this._ImageFetcher.pGetSaveImagePath(cls, { + propCompendium: "class", + taskRunner: opts.taskRunner + }); */ + + const hitDice = this._getDataHitDice(cls); + + const additionalData = await this._SideDataInterface.pGetDataSideLoaded(cls); + const additionalFlags = await this._SideDataInterface.pGetFlagsSideLoaded(cls); + const additionalAdvancement = await this._SideDataInterface._pGetAdvancementSideLoaded(cls); + + const effectsSideTuples = await this._SideDataInterface.pGetEffectsSideLoadedTuples({ + ent: cls, + img, + actor: opts.actor + }); + effectsSideTuples.forEach(({effect, effectRaw})=>DataConverter.mutEffectDisabledTransfer(effect, "importClass", UtilActiveEffects.getDisabledTransferHintsSideData(effectRaw))); + + const out = { + id: itemId, + _id: itemId, + name: translatedName, + type: "class", + system: { + identifier: identifierCls, + description: { + value: translatedDescription, + chat: "", + unidentified: "" + }, + source: UtilDocumentSource.getSourceObjectFromEntity(cls), + levels: opts.level ?? 1, + hitDice, + hitDiceUsed: 0, + spellcasting: { + progression: UtilActors.getMappedCasterType(cls.casterProgression) || cls.casterProgression, + ability: cls.spellcastingAbility, + }, + advancement: [...(srdData?.system?.advancement || []).filter(it=>it.type === "ScaleValue"), ...this._getClassAdvancement(cls, opts), ...(additionalAdvancement || []), ], + + ...additionalData, + }, + ownership: { + default: 0 + }, + flags: { + ...translatedFlags, + ...this._getClassSubclassFlags({ + cls, + filterValues: opts.filterValues, + proficiencyImportMode: opts.proficiencyImportMode, + isActorItem: opts.isActorItem, + spellSlotLevelSelection: opts.spellSlotLevelSelection, + }), + ...additionalFlags, + }, + effects: DataConverter.getEffectsMutDedupeId([await this._pGetPreparedSpellsEffect({ + cls, + actorId: opts.actor?.id, + itemId, + existing: this._getExistingPreparedSpellsEffect({ + actor: opts.actor + }), + taskRunner: opts.taskRunner, + }), ...effectsSideTuples.map(it=>it.effect), ].filter(Boolean), ), + img, + }; + + this._mutApplyDocOwnership(out, opts); + + return out; + } + + static async _pGetClassDescription(cls, opts) { + const ptDoNotUse = !opts.isActorItem ? await this._getDoNotUseNote() : ""; + + const ptTable = await UtilDataConverter.pGetWithDescriptionPlugins(()=>this.pGetRenderedClassTable(cls)); + + const ptFluff = cls?.fluff?.length ? await UtilDataConverter.pGetWithDescriptionPlugins(()=>Renderer.get().setFirstSection(true).render({ + type: cls.fluff[0].type || "section", + entries: cls.fluff[0].entries || [] + })) : ""; + + const ptFeatures = !opts.isActorItem ? await UtilDataConverter.pGetWithDescriptionPlugins(()=>Renderer.get().setFirstSection(true).render({ + type: "section", + entries: cls.classFeatures.flat() + })) : ""; + + if (!Config.get("importClass", "isImportDescription")) + return `
    ${ptDoNotUse}${ptTable}
    `; + + return `
    ${ptDoNotUse}${ptTable}${ptFluff}${ptFeatures}
    `; + } + + static _getClassAdvancement(cls, opts) { + return [...this._getClassAdvancement_hitPoints(cls, opts), ...this._getClassAdvancement_saves(cls, opts), ...this._getClassAdvancement_skills(cls, opts), ]; + } + + static _getClassAdvancement_hitPoints(cls, opts) { + const hitDice = this._getDataHitDice(cls); + if (hitDice == null) + return []; + + const advancement = UtilAdvancements.getAdvancementHitPoints({ + hpAdvancementValue: opts.hpAdvancementValue, + isActorItem: opts.isActorItem, + }); + if (advancement == null) + return []; + + return [advancement]; + } + + static _getClassAdvancement_saves(cls, opts) { + const saves = (cls.proficiency || []).filter(it=>Parser.ATB_ABV_TO_FULL[it]); + if (!saves.length) + return []; + + const advancement = UtilAdvancements.getAdvancementSaves({ + savingThrowProficiencies: [saves.mergeMap(abv=>({ + [abv]: true + }))], + classRestriction: "primary", + level: 1, + }); + if (advancement == null) + return []; + + return [advancement]; + } + + static _getClassAdvancement_skills(cls, opts) { + return [UtilAdvancements.getAdvancementSkills({ + skillProficiencies: cls.startingProficiencies?.skills, + classRestriction: "primary", + skillsChosenFvtt: opts.proficiencyImportMode === Charactermancer_Class_ProficiencyImportModeSelect.MODE_PRIMARY ? opts.startingSkills : null, + level: 1, + }), UtilAdvancements.getAdvancementSkills({ + skillProficiencies: cls.multiclassing?.proficienciesGained?.skills, + classRestriction: "secondary", + skillsChosenFvtt: opts.proficiencyImportMode === Charactermancer_Class_ProficiencyImportModeSelect.MODE_MULTICLASS ? opts.startingSkills : null, + level: 1, + }), ].filter(Boolean); + } + + static _getClassSubclassFlags({cls, sc, filterValues, proficiencyImportMode, isActorItem, spellSlotLevelSelection}) { + const out = { + [SharedConsts.MODULE_ID]: { + page: UrlUtil.PG_CLASSES, + source: sc ? sc.source : cls.source, + hash: sc ? UrlUtil.URL_TO_HASH_BUILDER["subclass"](sc) : UrlUtil.URL_TO_HASH_BUILDER["class"](cls), + + propDroppable: sc ? "subclass" : "class", + filterValues, + + isPrimaryClass: proficiencyImportMode === Charactermancer_Class_ProficiencyImportModeSelect.MODE_PRIMARY, + + spellSlotLevelSelection, + }, + }; + + if (isActorItem) + out[SharedConsts.MODULE_ID].isDirectImport = true; + + return out; + } + + static _getAllSkillChoices(skillProfs) { + const allSkills = new Set(); + + skillProfs.forEach(skillProfGroup=>{ + Object.keys(Parser.SKILL_TO_ATB_ABV).filter(skill=>skillProfGroup[skill]).forEach(skill=>allSkills.add(skill)); + + if (skillProfGroup.choose?.from?.length) { + skillProfGroup.choose.from.filter(skill=>Parser.SKILL_TO_ATB_ABV[skill]).forEach(skill=>allSkills.add(skill)); + } + } + ); + + return Object.entries(UtilActors.SKILL_ABV_TO_FULL).filter(([,vetKey])=>allSkills.has(vetKey)).map(([fvttKey])=>fvttKey); + } + + static async pGetSubclassItem(cls, sc, opts) { + opts = opts || {}; + if (opts.actor) + opts.isActorItem = true; + + Renderer.get().setFirstSection(true).resetHeaderIndex(); + + const itemId = foundry.utils.randomID(); + + if (!opts.isScDereferenced) { + sc = await DataLoader.pCacheAndGet("subclass", sc.source, UrlUtil.URL_TO_HASH_BUILDER["subclass"](sc)); + } + + if (opts.pageFilter?.filterBox && opts.filterValues) { + sc = MiscUtil.copy(sc); + + Renderer.class.mutFilterDereferencedSubclassFeatures({ + cpySc: sc, + pageFilter: opts.pageFilter, + filterValues: opts.filterValues, + }); + } + + const srdData = await UtilCompendium.getSrdCompendiumEntity("subclass", sc, { + taskRunner: opts.taskRunner + }); + + const {name: translatedName, description: translatedDescription, flags: translatedFlags} = this._getTranslationMeta({ + translationData: this._getTranslationData({ + srdData + }), + name: UtilApplications.getCleanEntityName(UtilDataConverter.getNameWithSourcePart(sc, { + isActorItem: opts.isActorItem + })), + description: await this._pGetSubclassDescription(cls, sc, opts), + }); + + const identifierCls = UtilDocumentItem.getNameAsIdentifier(cls.name); + const identifierSc = UtilDocumentItem.getNameAsIdentifier(sc.name); + + //TEMPFIX + /* const imgMetaSc = await this._ImageFetcher.pGetSaveImagePathMeta(sc, { + propCompendium: "subclass", + taskRunner: opts.taskRunner + }); + const imgMetaCls = (Config.get("importClass", "isUseDefaultSubclassImage") || (imgMetaSc && !imgMetaSc.isFallback)) ? null : await this._ImageFetcher.pGetSaveImagePathMeta(cls, { + propCompendium: "class", + taskRunner: opts.taskRunner + }); */ + + const img = (imgMetaSc && !imgMetaSc.isFallback) ? imgMetaSc.img : imgMetaCls && !imgMetaCls.isFallback ? imgMetaCls.img : (imgMetaSc?.img || imgMetaCls.img); + + const additionalData = await this._SideDataInterface.pGetDataSideLoaded(sc, { + propOpts: "_SIDE_LOAD_OPTS_SUBCLASS" + }); + const additionalFlags = await this._SideDataInterface.pGetFlagsSideLoaded(sc, { + propOpts: "_SIDE_LOAD_OPTS_SUBCLASS" + }); + const additionalAdvancement = await this._SideDataInterface._pGetAdvancementSideLoaded(sc, { + propOpts: "_SIDE_LOAD_OPTS_SUBCLASS" + }); + + const effectsSideTuples = await this._SideDataInterface.pGetEffectsSideLoadedTuples({ + ent: sc, + img, + actor: opts.actor + }, { + propOpts: "_SIDE_LOAD_OPTS_SUBCLASS" + }); + effectsSideTuples.forEach(({effect, effectRaw})=>DataConverter.mutEffectDisabledTransfer(effect, "importClass", UtilActiveEffects.getDisabledTransferHintsSideData(effectRaw))); + + const out = { + id: itemId, + _id: itemId, + name: translatedName, + type: "subclass", + system: { + identifier: identifierSc, + classIdentifier: identifierCls, + description: { + value: translatedDescription, + chat: "", + unidentified: "" + }, + source: UtilDocumentSource.getSourceObjectFromEntity(sc), + spellcasting: { + progression: UtilActors.getMappedCasterType(sc.casterProgression) || sc.casterProgression, + ability: sc.spellcastingAbility, + }, + advancement: [...(srdData?.system?.advancement || []).filter(it=>it.type === "ScaleValue"), ...(additionalAdvancement || []), ], + + ...additionalData, + }, + ownership: { + default: 0 + }, + flags: { + ...translatedFlags, + ...this._getClassSubclassFlags({ + cls, + sc, + filterValues: opts.filterValues, + proficiencyImportMode: opts.proficiencyImportMode, + isActorItem: opts.isActorItem, + }), + ...additionalFlags, + }, + effects: DataConverter.getEffectsMutDedupeId([await this._pGetPreparedSpellsEffect({ + cls, + sc, + actorId: opts.actor?.id, + itemId, + existing: this._getExistingPreparedSpellsEffect({ + actor: opts.actor + }), + taskRunner: opts.taskRunner, + }), ...effectsSideTuples.map(it=>it.effect), ].filter(Boolean), ), + img, + }; + + this._mutApplyDocOwnership(out, opts); + + return out; + } + + static async _pGetSubclassDescription(cls, sc, opts) { + const ptDoNotUse = !opts.isActorItem ? await this._getDoNotUseNote() : ""; + + const fluff = MiscUtil.copy(Renderer.findEntry(sc.subclassFeatures || {})); + + const cleanEntries = MiscUtil.getWalker({ + keyBlocklist: MiscUtil.GENERIC_WALKER_ENTRIES_KEY_BLOCKLIST + }).walk(MiscUtil.copy(fluff.entries), { + array: (arr)=>{ + return arr.filter(it=>!it?.data?.isFvttSyntheticFeatureLink); + } + , + }, ); + + const ptFluff = opts.isActorItem ? Renderer.get().setFirstSection(true).render({ + type: "entries", + entries: cleanEntries + }) : ""; + + const ptFeatures = !opts.isActorItem ? await UtilDataConverter.pGetWithDescriptionPlugins(()=>Renderer.get().setFirstSection(true).render({ + type: "section", + entries: sc.subclassFeatures.flat() + })) : ""; + + if (!Config.get("importClass", "isImportDescription")) + return `
    ${ptDoNotUse}
    `; + + return `
    ${ptDoNotUse}${ptFluff}${ptFeatures}
    `; + } + + static async pGetRenderedClassTable(cls, sc, opts={}) { + if (!Config.get("importClass", "isImportClassTable")) + return ""; + + return UtilDataConverter.pGetWithDescriptionPlugins(async()=>{ + cls = await DataLoader.pCacheAndGet("class", cls.source, UrlUtil.URL_TO_HASH_BUILDER["class"](cls)); + + if (sc) { + sc = await DataLoader.pCacheAndGet("subclass", sc.source, UrlUtil.URL_TO_HASH_BUILDER["subclass"](sc)); + } + + return this.getRenderedClassTableFromDereferenced(cls, sc, opts); + } + ); + } + + static getRenderedClassTableFromDereferenced(cls, sc, {isAddHeader=false, isSpellsOnly=false}={}) { + if (!cls) + return ""; + + return Vetools.withUnpatchedDiceRendering(()=>{ + Renderer.get().setFirstSection(true).resetHeaderIndex(); + + const tblGroupHeaders = []; + const tblHeaders = []; + + const renderTableGroupHeader = (tableGroup)=>{ + let thGroupHeader; + if (tableGroup.title) { + thGroupHeader = `${tableGroup.title}`; + } else { + thGroupHeader = ``; + } + tblGroupHeaders.push(thGroupHeader); + + tableGroup.colLabels.forEach(lbl=>{ + tblHeaders.push(`
    ${Renderer.get().render(lbl)}
    `); + } + ); + } + ; + + if (cls.classTableGroups) { + cls.classTableGroups.forEach(tableGroup=>{ + if (isSpellsOnly) + tableGroup = this._getRenderedClassTableFromDereferenced_getSpellsOnlyTableGroup(tableGroup); + if (!tableGroup) + return; + renderTableGroupHeader(tableGroup); + } + ); + } + + if (sc?.subclassTableGroups) { + sc.subclassTableGroups.forEach(tableGroup=>{ + if (isSpellsOnly) + tableGroup = this._getRenderedClassTableFromDereferenced_getSpellsOnlyTableGroup(tableGroup); + if (!tableGroup) + return; + renderTableGroupHeader(tableGroup); + } + ); + } + + const tblRows = cls.classFeatures.map((lvlFeatures,ixLvl)=>{ + const pb = Math.ceil((ixLvl + 1) / 4) + 1; + + const lvlFeaturesFilt = lvlFeatures.filter(it=>it.name && it.type !== "inset"); + const dispsFeatures = lvlFeaturesFilt.map((it,ixFeature)=>`
    ${it.name}${ixFeature === lvlFeaturesFilt.length - 1 ? "" : `,`}
    `); + + const ptTableGroups = []; + + const renderTableGroupRow = (tableGroup)=>{ + const row = (tableGroup.rowsSpellProgression || tableGroup.rows)[ixLvl] || []; + const cells = row.map(cell=>`${cell === 0 ? "\u2014" : Renderer.get().render(cell)}`); + ptTableGroups.push(...cells); + } + ; + + if (cls.classTableGroups) { + cls.classTableGroups.forEach(tableGroup=>{ + if (isSpellsOnly) + tableGroup = this._getRenderedClassTableFromDereferenced_getSpellsOnlyTableGroup(tableGroup); + if (!tableGroup) + return; + renderTableGroupRow(tableGroup); + } + ); + } + + if (sc?.subclassTableGroups) { + sc.subclassTableGroups.forEach(tableGroup=>{ + if (isSpellsOnly) + tableGroup = this._getRenderedClassTableFromDereferenced_getSpellsOnlyTableGroup(tableGroup); + if (!tableGroup) + return; + renderTableGroupRow(tableGroup); + } + ); + } + + return ` + ${Parser.getOrdinalForm(ixLvl + 1)} + ${isSpellsOnly ? "" : `+${pb}`} + ${isSpellsOnly ? "" : `${dispsFeatures.join("") || `\u2014`}`} + ${ptTableGroups.join("")} + `; + } + ); + + return ` + + + ${isAddHeader ? `` : ""} + + + ${tblGroupHeaders.join("")} + + + + ${isSpellsOnly ? "" : ``} + ${isSpellsOnly ? "" : ``} + ${tblHeaders.join("")} + + ${tblRows.join("")} + + +
    ${cls.name}
    LevelProficiency BonusFeatures
    `; + } + ); + } + + static _getRenderedClassTableFromDereferenced_getSpellsOnlyTableGroup(tableGroup) { + tableGroup = MiscUtil.copy(tableGroup); + + if (/spell/i.test(`${tableGroup.title || ""}`)) + return tableGroup; + + if (!tableGroup.colLabels) + return null; + + const ixsSpellLabels = new Set(tableGroup.colLabels.map((it,ix)=>{ + const stripped = Renderer.stripTags(`${it || ""}`); + return /cantrip|spell|slot level/i.test(stripped) ? ix : null; + } + ).filter(ix=>ix != null)); + + if (!ixsSpellLabels.size) + return null; + + tableGroup.colLabels = tableGroup.colLabels.filter((_,ix)=>ixsSpellLabels.has(ix)); + if (tableGroup.rowsSpellProgression) + tableGroup.rowsSpellProgression = tableGroup.rowsSpellProgression.map(row=>row.filter((_,ix)=>ixsSpellLabels.has(ix))); + if (tableGroup.rows) + tableGroup.rows = tableGroup.rows.map(row=>row.filter((_,ix)=>ixsSpellLabels.has(ix))); + + return tableGroup; + } + + static _getExistingPreparedSpellsEffect({actor}) { + if (!actor) + return null; + return actor.effects.contents.find(it=>(it.name || "").toLowerCase().trim() === "prepared spells"); + } + + static async _pGetPreparedSpellsEffect({cls, sc, actorId, itemId, existing, taskRunner}) { + if (existing) + return null; + if (sc && !sc.preparedSpells) + return null; + if (!sc && !cls.preparedSpells) + return null; + + const spellsPreparedFormula = Charactermancer_Spell_Util.getMaxPreparedSpellsFormula({ + cls, + sc + }); + if (!spellsPreparedFormula) + return null; + + if (game) + return null; + + return UtilActiveEffects.getGenericEffect({ + key: `flags.${UtilCompat.MODULE_TIDY5E_SHEET}.maxPreparedSpells`, + value: spellsPreparedFormula, + mode: CONST.ACTIVE_EFFECT_MODES.CUSTOM, + name: `Prepared Spells`, + //TEMPFIX + /* icon: await this._ImageFetcher.pGetSaveImagePath(cls, { + propCompendium: "class", + taskRunner + }), */ + disabled: false, + priority: UtilActiveEffects.PRIORITY_BASE, + originActorId: actorId, + originActorItemId: itemId, + }); + } + + static isStubClass(cls) { + if (!cls) + return false; + return cls.name === DataConverterClass.STUB_CLASS.name && cls.source === DataConverterClass.STUB_CLASS.source; + } + + static isStubSubclass(sc) { + if (!sc) + return false; + return sc.name === DataConverterClass.STUB_SUBCLASS.name && sc.source === DataConverterClass.STUB_SUBCLASS.source; + } + + static getClassStub() { + const out = MiscUtil.copy(DataConverterClass.STUB_CLASS); + out.subclasses = [{ + ...MiscUtil.copy(DataConverterClass.STUB_SUBCLASS), + className: out.name, + classSource: out.source, + }, ]; + return out; + } + + static getSubclassStub({cls}) { + const out = MiscUtil.copy(DataConverterClass.STUB_SUBCLASS); + out.className = cls.name; + out.classSource = cls.source; + return out; + } +} + +class DataConverterFeature extends DataConverter { + static async _pGetGenericDescription(ent, configGroup, {fluff=null}={}) { + if (!Config.get(configGroup, "isImportDescription") && !fluff?.entries?.length) + return ""; + + const pts = [Config.get(configGroup, "isImportDescription") ? await UtilDataConverter.pGetWithDescriptionPlugins(()=>`
    ${Renderer.get().setFirstSection(true).render({ + entries: ent.entries + }, 2)}
    `) : null, fluff?.entries?.length ? Renderer.get().setFirstSection(true).render({ + type: "entries", + entries: fluff?.entries + }) : "", ].filter(Boolean).join(`
    `); + + return pts.length ? `
    ${pts}
    ` : ""; + } + + static _getData_getConsume({ent, actor}) { + if (!ent?.consumes) + return {}; + + const sheetItem = DataConverter.getConsumedSheetItem({ + consumes: ent.consumes, + actor + }); + if (!sheetItem) + return {}; + + return { + type: "charges", + amount: ent.consumes.amount ?? 1, + target: sheetItem.id, + }; + } + + static async pMutActorUpdateFeature(actor, actorUpdate, ent, dataBuilderOpts) { + const sideData = await this._SideDataInterface.pGetSideLoaded(ent); + this.mutActorUpdate(actor, actorUpdate, ent, { + sideData + }); + } + + static async pGetDereferencedFeatureItem(feature) { + return MiscUtil.copy(feature); + } + + static async pGetClassSubclassFeatureAdditionalEntities(actor, entity, {taskRunner=null}={}) { + const sideData = await this._SideDataInterface.pGetSideLoaded(entity); + if (!sideData) + return []; + if (!sideData.subEntities) + return []; + + const {ChooseImporter} = await Promise.resolve().then(function() { + return ChooseImporter$1; + }); + + for (const prop in sideData.subEntities) { + if (!sideData.subEntities.hasOwnProperty(prop)) + continue; + + const arr = sideData.subEntities[prop]; + if (!(arr instanceof Array)) + continue; + + const importer = ChooseImporter.getImporter(prop, { + actor + }); + await importer.pInit(); + for (const ent of arr) { + await importer.pImportEntry(ent, { + taskRunner, + }, ); + } + } + } +} +class DataConverterClassSubclassFeature extends DataConverterFeature { + static _configGroup = "importClassSubclassFeature"; + + static _SideDataInterface = SideDataInterfaceClassSubclassFeature; + //TEMPFIX static _ImageFetcher = ImageFetcherClassSubclassFeature; + + static async pGetDereferencedFeatureItem(feature) { + const type = UtilEntityClassSubclassFeature.getEntityType(feature); + const hash = UrlUtil.URL_TO_HASH_BUILDER[type](feature); + return DataLoader.pCacheAndGet(type, feature.source, hash, { + isCopy: true + }); + } + + static async pGetInitFeatureLoadeds(feature, {actor=null}={}) { + const isIgnoredLookup = await this._pGetInitFeatureLoadeds_getIsIgnoredLookup(feature); + + const type = UtilEntityClassSubclassFeature.getEntityType(feature); + switch (type) { + case "classFeature": + { + const uid = DataUtil.class.packUidClassFeature(feature); + const asClassFeatureRef = { + classFeature: uid + }; + await PageFilterClassesFoundry.pInitClassFeatureLoadeds({ + classFeature: asClassFeatureRef, + className: feature.className, + actor, + isIgnoredLookup + }); + return asClassFeatureRef; + } + case "subclassFeature": + { + const uid = DataUtil.class.packUidSubclassFeature(feature); + const asSubclassFeatureRef = { + subclassFeature: uid + }; + const subclassNameLookup = await DataUtil.class.pGetSubclassLookup(); + const subclassName = MiscUtil.get(subclassNameLookup, feature.classSource, feature.className, feature.subclassSource, feature.subclassShortName, "name"); + await PageFilterClassesFoundry.pInitSubclassFeatureLoadeds({ + subclassFeature: asSubclassFeatureRef, + className: feature.className, + subclassName: subclassName, + actor, + isIgnoredLookup + }); + return asSubclassFeatureRef; + } + default: + throw new Error(`Unhandled feature type "${type}"`); + } + } + + static async _pGetInitFeatureLoadeds_getIsIgnoredLookup(feature) { + if (!feature.entries) + return {}; + + const type = UtilEntityClassSubclassFeature.getEntityType(feature); + switch (type) { + case "classFeature": + { + return this.pGetClassSubclassFeatureIgnoredLookup({ + data: { + classFeature: [feature] + } + }); + } + case "subclassFeature": + { + return this.pGetClassSubclassFeatureIgnoredLookup({ + data: { + subclassFeature: [feature] + } + }); + } + default: + throw new Error(`Unhandled feature type "${type}"`); + } + } + + + /** + * Returns an object with boolean properties named after class and subclass features that are marked as ignored + * @param {{classFeature:any[], subclassFeature:any[]}} data + * @returns {any} + */ + static async pGetClassSubclassFeatureIgnoredLookup({data}) { + if (!data.classFeature?.length && !data.subclassFeature?.length) + return {}; + + const isIgnoredLookup = {}; + + const allRefsClassFeature = new Set(); + const allRefsSubclassFeature = new Set(); + + (data.classFeature || []).forEach(cf=>{ + const {refsClassFeature, refsSubclassFeature} = Charactermancer_Class_Util.getClassSubclassFeatureReferences(cf.entries); + + refsClassFeature.forEach(ref=>allRefsClassFeature.add((ref.classFeature || "").toLowerCase())); + refsSubclassFeature.forEach(ref=>allRefsSubclassFeature.add((ref.subclassFeature || "").toLowerCase())); + }); + + (data.subclassFeature || []).forEach(scf=>{ + const {refsClassFeature, refsSubclassFeature} = Charactermancer_Class_Util.getClassSubclassFeatureReferences(scf.entries); + + refsClassFeature.forEach(ref=>allRefsClassFeature.add((ref.classFeature || "").toLowerCase())); + refsSubclassFeature.forEach(ref=>allRefsSubclassFeature.add((ref.subclassFeature || "").toLowerCase())); + }); + + for (const uid of allRefsClassFeature) { + if (await this._SideDataInterface.pGetIsIgnoredSideLoaded(DataUtil.class.unpackUidClassFeature(uid))) { + isIgnoredLookup[uid] = true; + } + } + + for (const uid of allRefsSubclassFeature) { + if (await this._SideDataInterface.pGetIsIgnoredSideLoaded(DataUtil.class.unpackUidSubclassFeature(uid))) { + isIgnoredLookup[uid] = true; + } + } + + return isIgnoredLookup; + } + + static async pGetDocumentJson(feature, opts) { + opts = opts || {}; + if (opts.actor) + opts.isActorItem = true; + + Renderer.get().setFirstSection(true).resetHeaderIndex(); + + const out = await this._pGetClassSubclassFeatureItem(feature, opts); + + const additionalData = await this._SideDataInterface.pGetDataSideLoaded(feature); + Object.assign(out.system, additionalData); + + const additionalFlags = await this._SideDataInterface.pGetFlagsSideLoaded(feature); + Object.assign(out.flags, additionalFlags); + + this._mutApplyDocOwnership(out, opts); + + return out; + } + + static _isUnarmoredDefense(feature) { + const cleanLowerName = (feature.name || "").toLowerCase().trim(); + return /^unarmored defen[sc]e/.test(cleanLowerName); + } + + static _getUnarmoredDefenseMeta(entity) { + if (!entity.entries) + return null; + + const attribs = new Set(); + + JSON.stringify(entity.entries).replace(/(strength|dexterity|constitution|intelligence|wisdom|charisma|str|dex|con|int|wis|cha) modifier/gi, (fullMatch,ability)=>{ + ability = ability.slice(0, 3).toLowerCase(); + attribs.add(ability); + } + ); + + const predefinedKey = CollectionUtil.setEq(DataConverterClassSubclassFeature._UNARMORED_DEFENSE_BARBARIAN, attribs) ? "unarmoredBarb" : CollectionUtil.setEq(DataConverterClassSubclassFeature._UNARMORED_DEFENSE_MONK, attribs) ? "unarmoredMonk" : null; + + return { + formula: ["10", ...[...attribs].map(ab=>`@abilities.${ab}.mod`)].join(" + "), + abilities: [...attribs], + predefinedKey, + }; + } + + static _getUnarmoredDefenseEffectSideTuples({actor, feature, img}) { + if (!this._isUnarmoredDefense(feature)) + return []; + + const unarmoredDefenseMeta = this._getUnarmoredDefenseMeta(feature); + if (!unarmoredDefenseMeta) + return []; + + if (unarmoredDefenseMeta.predefinedKey) { + return UtilActiveEffects.getExpandedEffects([{ + name: "Unarmored Defense", + changes: [{ + key: "system.attributes.ac.calc", + mode: "OVERRIDE", + value: unarmoredDefenseMeta.predefinedKey, + }, ], + transfer: true, + }, ], { + actor, + img, + parentName: feature.name, + }, { + isTuples: true, + }, ); + } + + return UtilActiveEffects.getExpandedEffects([{ + name: "Unarmored Defense", + changes: [{ + key: "system.attributes.ac.calc", + mode: "OVERRIDE", + value: "custom", + }, ], + transfer: true, + }, { + name: "Unarmored Defense", + changes: [{ + key: "system.attributes.ac.formula", + mode: "UPGRADE", + value: unarmoredDefenseMeta.formula, + }, ], + transfer: true, + }, ], { + actor, + img, + parentName: feature.name, + }, { + isTuples: true, + }, ); + } + + static async _pGetClassSubclassFeatureItem(feature, opts) { + opts = opts || {}; + + let {type=null, actor} = opts; + type = type || UtilEntityClassSubclassFeature.getEntityType(feature); + + let pOut; + if (await this._pIsInSrd(feature, type, opts)) { + pOut = this._pGetClassSubclassFeatureItem_fromSrd(feature, type, actor, opts); + } else { + pOut = this._pGetClassSubclassFeatureItem_other(feature, type, actor, opts); + } + return pOut; + } + + static async _pIsInSrd(feature, type, {taskRunner=null}={}) { + const srdData = await UtilCompendium.getSrdCompendiumEntity(type, feature, { + fnGetAliases: UtilEntityClassSubclassFeature.getCompendiumAliases.bind(UtilEntityClassSubclassFeature), + taskRunner + }); + return !!srdData; + } + + static async _pGetClassSubclassFeatureItem_fromSrd(feature, type, actor, opts={}) { + const srdData = await UtilCompendium.getSrdCompendiumEntity(type, feature, { + fnGetAliases: UtilEntityClassSubclassFeature.getCompendiumAliases.bind(UtilEntityClassSubclassFeature), + taskRunner: opts.taskRunner + }); + + const {name: translatedName, description: translatedDescription, flags: translatedFlags} = this._getTranslationMeta({ + translationData: this._getTranslationData({ + srdData + }), + name: UtilApplications.getCleanEntityName(UtilDataConverter.getNameWithSourcePart(feature, { + isActorItem: actor != null + })), + description: await this.pGetEntryDescription(feature), + }); + + //TEMPFIX + const img = null;/* await this._ImageFetcher.pGetSaveImagePath(feature, { + propCompendium: type, + taskRunner: opts.taskRunner + }); */ + + const dataConsume = this._getData_getConsume({ + ent: feature, + actor: opts.actor + }); + + const srdEffects = await this._SideDataInterface.pIsIgnoreSrdEffectsSideLoaded(feature) ? [] : MiscUtil.copy(srdData.effects || []); + DataConverter.mutEffectsDisabledTransfer(srdEffects, "importClassSubclassFeature"); + + const effectsSideTuples = UtilActiveEffects.getExpandedEffects(feature.effectsRaw, { + actor, + img, + parentName: feature.name + }, { + isTuples: true + }); + effectsSideTuples.push(...this._getUnarmoredDefenseEffectSideTuples({ + actor, + feature, + img + })); + effectsSideTuples.forEach(({effect, effectRaw})=>DataConverter.mutEffectDisabledTransfer(effect, "importClassSubclassFeature", UtilActiveEffects.getDisabledTransferHintsSideData(effectRaw))); + + return { + name: translatedName, + type: srdData.type, + system: { + ...srdData.system, + + source: UtilDocumentSource.getSourceObjectFromEntity(feature), + description: { + value: translatedDescription, + chat: "", + unidentified: "" + }, + consume: dataConsume, + + ...(feature.foundryAdditionalSystem || {}), + }, + ownership: { + default: 0 + }, + effects: DataConverter.getEffectsMutDedupeId([...srdEffects, ...effectsSideTuples.map(it=>it.effect), ]), + flags: { + ...translatedFlags, + ...this._getClassSubclassFeatureFlags(feature, type, opts), + ...(feature.foundryAdditionalFlags || {}), + }, + img, + }; + } + + static _getClassSubclassFeatureFlags(feature, type, opts) { + opts = opts || {}; + + const prop = UtilEntityClassSubclassFeature.getEntityType(feature); + + const out = { + [SharedConsts.MODULE_ID]: { + page: prop, + source: feature.source, + hash: UrlUtil.URL_TO_HASH_BUILDER[prop](feature), + }, + }; + + if (opts.isAddDataFlags) { + out[SharedConsts.MODULE_ID].propDroppable = prop; + out[SharedConsts.MODULE_ID].filterValues = opts.filterValues; + } + + return out; + } + + static async _pGetClassSubclassFeatureItem_other(feature, type, actor, opts) { + const dataConsume = this._getData_getConsume({ + ent: feature, + actor: opts.actor + }); + + //TEMPFIX + const img = null;/* await this._ImageFetcher.pGetSaveImagePath(feature, { + propCompendium: type, + taskRunner: opts.taskRunner + }); */ + + const effectsSideTuples = UtilActiveEffects.getExpandedEffects(feature.effectsRaw, { + actor, + img, + parentName: feature.name + }, { + isTuples: true + }); + effectsSideTuples.push(...this._getUnarmoredDefenseEffectSideTuples({ + actor, + feature, + img + })); + effectsSideTuples.forEach(({effect, effectRaw})=>DataConverter.mutEffectDisabledTransfer(effect, "importClassSubclassFeature", UtilActiveEffects.getDisabledTransferHintsSideData(effectRaw))); + + return this._pGetItemActorPassive(feature, { + isActorItem: opts.isActorItem, + mode: "player", + modeOptions: { + isChannelDivinity: feature.className === "Cleric" && feature.name.toLowerCase().startsWith("channel divinity:"), + }, + renderDepth: 0, + fvttType: "feat", + typeType: "class", + img, + fvttSource: UtilDocumentSource.getSourceObjectFromEntity(feature), + requirements: [feature.className, feature.level, feature.subclassShortName ? `(${feature.subclassShortName})` : ""].filter(Boolean).join(" "), + additionalData: feature.foundryAdditionalSystem, + foundryFlags: this._getClassSubclassFeatureFlags(feature, type, opts), + additionalFlags: feature.foundryAdditionalFlags, + effects: DataConverter.getEffectsMutDedupeId(effectsSideTuples.map(it=>it.effect)), + actor, + consumeType: dataConsume.type, + consumeTarget: dataConsume.target, + consumeAmount: dataConsume.amount, + }, ); + } +} + +DataConverterClassSubclassFeature._UNARMORED_DEFENSE_BARBARIAN = new Set(["dex", "con"]); +DataConverterClassSubclassFeature._UNARMORED_DEFENSE_MONK = new Set(["dex", "wis"]); + +class DataConverterFeat extends DataConverterFeature { + static _configGroup = "importFeat"; + + static _SideDataInterface = SideDataInterfaceFeat; + //TEMPFIX static _ImageFetcher = ImageFetcherFeat; + + static async pGetDereferencedFeatureItem(feature) { + if (feature.entries) + return MiscUtil.copy(feature); + + const hash = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_FEATS](feature); + return DataLoader.pCacheAndGet(UrlUtil.PG_FEATS, feature.source, hash, { + isCopy: true + }); + } + + static async pGetInitFeatureLoadeds(feature, {actor=null}={}) { + const uid = DataUtil.proxy.getUid("feat", feature, { + isMaintainCase: true + }); + const asFeatRef = { + feat: uid + }; + await PageFilterClassesFoundry.pInitFeatLoadeds({ + feat: asFeatRef, + raw: feature, + actor + }); + return asFeatRef; + } + + static async pGetDocumentJson(feat, opts) { + opts = opts || {}; + if (opts.actor) + opts.isActorItem = true; + + Renderer.get().setFirstSection(true).resetHeaderIndex(); + + const fluff = opts.fluff || await Renderer.feat.pGetFluff(feat); + + const cpyFeat = Charactermancer_Feature_Util.getCleanedFeature_tmpOptionalfeatureList(feat); + + const content = await UtilDataConverter.pGetWithDescriptionPlugins(()=>{ + const fluffRender = fluff?.entries?.length ? Renderer.get().setFirstSection(true).render({ + type: "entries", + entries: fluff?.entries + }) : ""; + + const ptCategoryPrerequisite = Renderer.feat.getJoinedCategoryPrerequisites(cpyFeat.category, Renderer.utils.prerequisite.getHtml(cpyFeat.prerequisite), ); + const ptRepeatable = Renderer.utils.getRepeatableHtml(cpyFeat); + + Renderer.feat.initFullEntries(cpyFeat); + const statsRender = `
    + ${ptCategoryPrerequisite ? `

    ${ptCategoryPrerequisite}

    ` : ""} + ${ptRepeatable ? `

    ${ptRepeatable}

    ` : ""} + ${Renderer.get().setFirstSection(true).render({ + entries: cpyFeat._fullEntries || cpyFeat.entries + }, 2)} +
    `; + + return `
    ${[fluffRender, statsRender].join("
    ")}
    `; + } + ); + + //TEMPFIX + /* const img = await this._ImageFetcher.pGetSaveImagePath(cpyFeat, { + propCompendium: "feat", + fluff, + taskRunner: opts.taskRunner + }); */ + + const additionalData = await this._SideDataInterface.pGetDataSideLoaded(cpyFeat); + const additionalFlags = await this._SideDataInterface.pGetFlagsSideLoaded(cpyFeat); + + const effectsSideTuples = await this._SideDataInterface.pGetEffectsSideLoadedTuples({ + ent: cpyFeat, + img, + actor: opts.actor + }); + effectsSideTuples.forEach(({effect, effectRaw})=>DataConverter.mutEffectDisabledTransfer(effect, "importFeat", UtilActiveEffects.getDisabledTransferHintsSideData(effectRaw))); + + const out = this._pGetItemActorPassive(feat, { + isActorItem: opts.isActorItem, + mode: "player", + img, + fvttType: "feat", + typeType: "feat", + source: feat.source, + actor: opts.actor, + description: content, + isSkipDescription: !Config.get(this._configGroup, "isImportDescription"), + requirements: Renderer.utils.prerequisite.getHtml(cpyFeat.prerequisite, { + isTextOnly: true, + isSkipPrefix: true + }), + additionalData: additionalData, + additionalFlags: additionalFlags, + foundryFlags: this._getFeatFlags(cpyFeat, opts), + effects: DataConverter.getEffectsMutDedupeId(effectsSideTuples.map(it=>it.effect)), + }, ); + + this._mutApplyDocOwnership(out, opts); + + return out; + } + + static _getFeatFlags(feat, opts) { + opts = opts || {}; + + const out = { + [SharedConsts.MODULE_ID]: { + page: UrlUtil.PG_FEATS, + source: feat.source, + hash: UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_FEATS](feat), + }, + }; + + if (opts.isAddDataFlags) { + out[SharedConsts.MODULE_ID].propDroppable = "feat"; + out[SharedConsts.MODULE_ID].filterValues = opts.filterValues; + } + + return out; + } +} + + +//#endregion +//#region Util +class Util { + static _getLogTag() { + return [`%cPlutonium`, `color: #337ab7; font-weight: bold;`, `|`, ]; + } + + static isDebug() { + return !!CONFIG?.debug?.module?.[SharedConsts.MODULE_ID]; + } + + static _HEIGHT_MAX_OFFSET = 160; + static getMaxWindowHeight(desiredHeight) { + const targetHeight = Math.min(desiredHeight || Number.MAX_SAFE_INTEGER, document.documentElement.clientHeight - this._HEIGHT_MAX_OFFSET); + return Math.max(this._HEIGHT_MAX_OFFSET, targetHeight); + } + + static _WIDTH_MAX_OFFSET = 250; + static getMaxWindowWidth(desiredWidth) { + const targetWidth = Math.min(desiredWidth || Number.MAX_SAFE_INTEGER, document.documentElement.clientWidth - this._WIDTH_MAX_OFFSET); + return Math.max(this._WIDTH_MAX_OFFSET, targetWidth); + } + + static getWithoutParens(str) { + return str.replace(/\([^)]+\)/g, "").trim(); + } + static getTokens(str) { + return str.split(/([ ,:;()"])/g).filter(Boolean); + } + static isPunctuation(token) { + return /[,:;()"]/.test(token); + } + static isCapsFirst(word) { + return /^[A-Z]/.test(word); + } + static getSentences(str) { + return str.replace(/ +/g, " ").split(/[.?!]/g).map(it=>it.trim()).filter(Boolean); + } + + static getRounded(n, dp) { + return Number(n.toFixed(dp)); + } + + static trimObject(obj) { + const walker = MiscUtil.getWalker({ + isAllowDeleteObjects: true, + isDepthFirst: true, + }); + + return walker.walk(obj, { + object: (it)=>{ + Object.entries(it).forEach(([k,v])=>{ + if (v === undefined) + delete it[k]; + } + ); + if (!Object.keys(it).length) + return undefined; + return it; + } + , + }, ); + } + + static getCleanServerUrl(url) { + return url.replace(/^(.*?)\/*$/, "$1/"); + } +} + +const LGT = Util._getLogTag(); +Util.Fvtt = class { + static getOwnershipEnum({isIncludeDefault=false}={}) { + return [isIncludeDefault ? { + value: -1, + name: "Default" + } : null, ...Object.entries(CONST.DOCUMENT_OWNERSHIP_LEVELS).map(([name,value])=>({ + value, + name: name.toTitleCase(), + })), ].filter(Boolean); + } + + static getMinimumRolesEnum() { + return [...Object.entries(CONST.USER_ROLES).map(([name,value])=>({ + value, + name: name.toTitleCase(), + })), { + value: CONST.USER_ROLES.GAMEMASTER + 1, + name: `Cheater (Disable Feature)`, + }, ]; + } + + static canUserCreateFolders() { + return game.user.isGM; + } +} +; +//#endregion +//#region UtilCompat +class UtilCompat { + static isModuleActive(moduleId) { + //TEMPFIX + return false; + //return !!game.modules.get(moduleId)?.active; + } + + static _MODULE_LIB_WRAPPER = "lib-wrapper"; + static MODULE_DAE = "dae"; + static _MODULE_DRAG_UPLOAD = "dragupload"; + static MODULE_MIDI_QOL = "midi-qol"; + static MODULE_KANKA_FOUNDRY = "kanka-foundry"; + static MODULE_SMOL_FOUNDRY = "smol-foundry"; + static MODULE_PERMISSION_VIEWER = "permission_viewer"; + static _MODULE_TWILIGHT_UI = "twilight-ui"; + static MODULE_TIDY5E_SHEET = "tidy5e-sheet"; + static _MODULE_OBSIDIAN = "obsidian"; + static MODULE_BABELE = "babele"; + static MODULE_MONKS_LITTLE_DETAILS = "monks-little-details"; + static MODULE_MONKS_BLOODSPLATS = "monks-bloodsplats"; + static MODULE_MONKS_ENHANCED_JOURNAL = "monks-enhanced-journal"; + static MODULE_BETTER_ROLLTABLES = "better-rolltables"; + static _MODULE_BETTER_ROLLTABLES = "item-piles"; + static MODULE_PLUTONIUM_ADDON_AUTOMATION = "plutonium-addon-automation"; + static MODULE_LEVELS = "levels"; + static MODULE_MULTICLASS_SPELLBOOK_FILTER = "spell-class-filter-for-5e"; + static MODULE_ROLLDATA_AWARE_ACTIVE_EFFECTS = "fvtt-rolldata-aware-active-effects"; + static MODULE_QUICK_INSERT = "quick-insert"; + static MODULE_PF2E_TOKENS_BESTIARIES = "pf2e-tokens-bestiaries"; + static _MODULE_DFREDS_CONVENIENT_EFFECTS = "dfreds-convenient-effects"; + static MODULE_LEVELS_3D_PREVIEW = "levels-3d-preview"; + static _MODULE_CANVAS_3D_COMPENDIUM = "canvas3dcompendium"; + static _MODULE_CANVAS_3D_TOKEN_COMPENDIUM = "canvas3dtokencompendium"; + static _MODULE_FOUNDRY_SUMMONS = "foundry-summons"; + static _MODULE_TOKEN_ACTION_HUD = "token-action-hud"; + static _MODULE_TOKEN_ACTION_HUD_CORE = "token-action-hud-core"; + static MODULE_SIMPLE_CALENDAR = "foundryvtt-simple-calendar"; + + static isLibWrapperActive() { + return this.isModuleActive(UtilCompat._MODULE_LIB_WRAPPER); + } + static isDaeActive() { + return this.isModuleActive(UtilCompat.MODULE_DAE); + } + static isDragUploadActive() { + return this.isModuleActive(UtilCompat._MODULE_DRAG_UPLOAD); + } + static isPermissionViewerActive() { + return this.isModuleActive(UtilCompat.MODULE_PERMISSION_VIEWER); + } + static isSmolFoundryActive() { + return this.isModuleActive(UtilCompat.MODULE_SMOL_FOUNDRY); + } + static isTwilightUiActive() { + return this.isModuleActive(UtilCompat._MODULE_TWILIGHT_UI); + } + static isTidy5eSheetActive() { + return this.isModuleActive(UtilCompat.MODULE_TIDY5E_SHEET); + } + static isObsidianActive() { + return this.isModuleActive(UtilCompat._MODULE_OBSIDIAN); + } + static isBabeleActive() { + return this.isModuleActive(UtilCompat.MODULE_BABELE); + } + static isMonksLittleDetailsActive() { + return this.isModuleActive(UtilCompat.MODULE_MONKS_LITTLE_DETAILS); + } + static isMonksBloodsplatsActive() { + return this.isModuleActive(UtilCompat.MODULE_MONKS_BLOODSPLATS); + } + static isBetterRolltablesActive() { + return this.isModuleActive(UtilCompat.MODULE_BETTER_ROLLTABLES); + } + static isItemPilesActive() { + return this.isModuleActive(UtilCompat._MODULE_BETTER_ROLLTABLES); + } + static isPlutoniumAddonAutomationActive() { + return this.isModuleActive(UtilCompat.MODULE_PLUTONIUM_ADDON_AUTOMATION); + } + static isMidiQolActive() { + return this.isModuleActive(UtilCompat.MODULE_MIDI_QOL); + } + static isModuleMulticlassSpellbookFilterActive() { + return this.isModuleActive(UtilCompat.MODULE_MULTICLASS_SPELLBOOK_FILTER); + } + static isQuickInsertActive() { + return this.isModuleActive(UtilCompat.MODULE_QUICK_INSERT); + } + static isPf2eTokensBestiaryActive() { + return this.isModuleActive(UtilCompat.MODULE_PF2E_TOKENS_BESTIARIES); + } + static isDfredsConvenientEffectsActive() { + return this.isModuleActive(UtilCompat._MODULE_DFREDS_CONVENIENT_EFFECTS); + } + static isLevels3dPreviewActive() { + return this.isModuleActive(UtilCompat.MODULE_LEVELS_3D_PREVIEW); + } + static _isCanvas3dCompendiumActive() { + return this.isModuleActive(UtilCompat._MODULE_CANVAS_3D_COMPENDIUM); + } + static _iCanvas3dTokenCompendiumActive() { + return this.isModuleActive(UtilCompat._MODULE_CANVAS_3D_TOKEN_COMPENDIUM); + } + static isFoundrySummonsActive() { + return this.isModuleActive(UtilCompat._MODULE_FOUNDRY_SUMMONS); + } + static isTokenActionHudActive() { + return this.isModuleActive(UtilCompat._MODULE_TOKEN_ACTION_HUD) || this.isModuleActive(UtilCompat._MODULE_TOKEN_ACTION_HUD_CORE); + } + static isSimpleCalendarActive() { + return this.isModuleActive(UtilCompat.MODULE_SIMPLE_CALENDAR); + } + + static isThreeDiTokensActive() { + return this.isLevels3dPreviewActive() && this._isCanvas3dCompendiumActive() && this._iCanvas3dTokenCompendiumActive(); + } + + static getApi(moduleName) { + if (!this.isModuleActive(moduleName)) + return null; + return game.modules.get(moduleName).api; + } + + static getName(moduleName) { + if (!this.isModuleActive(moduleName)) + return null; + return game.modules.get(moduleName).title; + } + + static isDaeGeneratingArmorEffects() { + if (!this.isDaeActive()) + return false; + return !!UtilGameSettings.getSafe(UtilCompat.MODULE_DAE, "calculateArmor"); + } + + static getFeatureFlags({isReaction}) { + const out = {}; + + if (isReaction) { + out.adnd5e = { + itemInfo: { + type: "reaction" + } + }; + } + + return out; + } + + static MonksLittleDetails = class { + static isDefeated(token) { + return ((token.combatant && token.isDefeated) || token.actor?.effects.some(it=>it.statuses.has(CONFIG.specialStatusEffects.DEFEATED)) || token.document.overlayEffect === CONFIG.controlIcons.defeated); + } + } + ; + + static DfredsConvenientEffects = class { + static getCustomEffectsItemId() { + return UtilGameSettings.getSafe(UtilCompat._MODULE_DFREDS_CONVENIENT_EFFECTS, "customEffectsItemId"); + } + } + ; + + static FoundrySummons = class { + static getBlankNpcIds() { + return (UtilGameSettings.getSafe(UtilCompat._MODULE_FOUNDRY_SUMMONS, "blankNPC") || []).map(it=>it?.id).filter(Boolean); + } + } + ; +} +//#endregion + +//#region UtilHooks +class UtilHooks { + static callAll(name, val) { + Hooks.callAll(this._getHookName(name), val); + } + + static call(name, val) { + Hooks.callAll(this._getHookName(name), val); + } + + static on(name, fn) { + Hooks.on(this._getHookName(name), fn); + } + + static off(name, fn) { + Hooks.off(this._getHookName(name), fn); + } + + static _getHookName(name) { + return `${SharedConsts.MODULE_ID_FAKE}.${name}`; + } +} +UtilHooks.HK_CONFIG_UPDATE = "configUpdate"; +UtilHooks.HK_IMPORT_COMPLETE = "importComplete"; +//#endregion + +//#region CryptUtil +globalThis.CryptUtil = { + _md5cycle: (x,k)=>{ + let a = x[0]; + let b = x[1]; + let c = x[2]; + let d = x[3]; + + a = CryptUtil._ff(a, b, c, d, k[0], 7, -680876936); + d = CryptUtil._ff(d, a, b, c, k[1], 12, -389564586); + c = CryptUtil._ff(c, d, a, b, k[2], 17, 606105819); + b = CryptUtil._ff(b, c, d, a, k[3], 22, -1044525330); + a = CryptUtil._ff(a, b, c, d, k[4], 7, -176418897); + d = CryptUtil._ff(d, a, b, c, k[5], 12, 1200080426); + c = CryptUtil._ff(c, d, a, b, k[6], 17, -1473231341); + b = CryptUtil._ff(b, c, d, a, k[7], 22, -45705983); + a = CryptUtil._ff(a, b, c, d, k[8], 7, 1770035416); + d = CryptUtil._ff(d, a, b, c, k[9], 12, -1958414417); + c = CryptUtil._ff(c, d, a, b, k[10], 17, -42063); + b = CryptUtil._ff(b, c, d, a, k[11], 22, -1990404162); + a = CryptUtil._ff(a, b, c, d, k[12], 7, 1804603682); + d = CryptUtil._ff(d, a, b, c, k[13], 12, -40341101); + c = CryptUtil._ff(c, d, a, b, k[14], 17, -1502002290); + b = CryptUtil._ff(b, c, d, a, k[15], 22, 1236535329); + + a = CryptUtil._gg(a, b, c, d, k[1], 5, -165796510); + d = CryptUtil._gg(d, a, b, c, k[6], 9, -1069501632); + c = CryptUtil._gg(c, d, a, b, k[11], 14, 643717713); + b = CryptUtil._gg(b, c, d, a, k[0], 20, -373897302); + a = CryptUtil._gg(a, b, c, d, k[5], 5, -701558691); + d = CryptUtil._gg(d, a, b, c, k[10], 9, 38016083); + c = CryptUtil._gg(c, d, a, b, k[15], 14, -660478335); + b = CryptUtil._gg(b, c, d, a, k[4], 20, -405537848); + a = CryptUtil._gg(a, b, c, d, k[9], 5, 568446438); + d = CryptUtil._gg(d, a, b, c, k[14], 9, -1019803690); + c = CryptUtil._gg(c, d, a, b, k[3], 14, -187363961); + b = CryptUtil._gg(b, c, d, a, k[8], 20, 1163531501); + a = CryptUtil._gg(a, b, c, d, k[13], 5, -1444681467); + d = CryptUtil._gg(d, a, b, c, k[2], 9, -51403784); + c = CryptUtil._gg(c, d, a, b, k[7], 14, 1735328473); + b = CryptUtil._gg(b, c, d, a, k[12], 20, -1926607734); + + a = CryptUtil._hh(a, b, c, d, k[5], 4, -378558); + d = CryptUtil._hh(d, a, b, c, k[8], 11, -2022574463); + c = CryptUtil._hh(c, d, a, b, k[11], 16, 1839030562); + b = CryptUtil._hh(b, c, d, a, k[14], 23, -35309556); + a = CryptUtil._hh(a, b, c, d, k[1], 4, -1530992060); + d = CryptUtil._hh(d, a, b, c, k[4], 11, 1272893353); + c = CryptUtil._hh(c, d, a, b, k[7], 16, -155497632); + b = CryptUtil._hh(b, c, d, a, k[10], 23, -1094730640); + a = CryptUtil._hh(a, b, c, d, k[13], 4, 681279174); + d = CryptUtil._hh(d, a, b, c, k[0], 11, -358537222); + c = CryptUtil._hh(c, d, a, b, k[3], 16, -722521979); + b = CryptUtil._hh(b, c, d, a, k[6], 23, 76029189); + a = CryptUtil._hh(a, b, c, d, k[9], 4, -640364487); + d = CryptUtil._hh(d, a, b, c, k[12], 11, -421815835); + c = CryptUtil._hh(c, d, a, b, k[15], 16, 530742520); + b = CryptUtil._hh(b, c, d, a, k[2], 23, -995338651); + + a = CryptUtil._ii(a, b, c, d, k[0], 6, -198630844); + d = CryptUtil._ii(d, a, b, c, k[7], 10, 1126891415); + c = CryptUtil._ii(c, d, a, b, k[14], 15, -1416354905); + b = CryptUtil._ii(b, c, d, a, k[5], 21, -57434055); + a = CryptUtil._ii(a, b, c, d, k[12], 6, 1700485571); + d = CryptUtil._ii(d, a, b, c, k[3], 10, -1894986606); + c = CryptUtil._ii(c, d, a, b, k[10], 15, -1051523); + b = CryptUtil._ii(b, c, d, a, k[1], 21, -2054922799); + a = CryptUtil._ii(a, b, c, d, k[8], 6, 1873313359); + d = CryptUtil._ii(d, a, b, c, k[15], 10, -30611744); + c = CryptUtil._ii(c, d, a, b, k[6], 15, -1560198380); + b = CryptUtil._ii(b, c, d, a, k[13], 21, 1309151649); + a = CryptUtil._ii(a, b, c, d, k[4], 6, -145523070); + d = CryptUtil._ii(d, a, b, c, k[11], 10, -1120210379); + c = CryptUtil._ii(c, d, a, b, k[2], 15, 718787259); + b = CryptUtil._ii(b, c, d, a, k[9], 21, -343485551); + + x[0] = CryptUtil._add32(a, x[0]); + x[1] = CryptUtil._add32(b, x[1]); + x[2] = CryptUtil._add32(c, x[2]); + x[3] = CryptUtil._add32(d, x[3]); + } + , + + _cmn: (q,a,b,x,s,t)=>{ + a = CryptUtil._add32(CryptUtil._add32(a, q), CryptUtil._add32(x, t)); + return CryptUtil._add32((a << s) | (a >>> (32 - s)), b); + } + , + + _ff: (a,b,c,d,x,s,t)=>{ + return CryptUtil._cmn((b & c) | ((~b) & d), a, b, x, s, t); + } + , + + _gg: (a,b,c,d,x,s,t)=>{ + return CryptUtil._cmn((b & d) | (c & (~d)), a, b, x, s, t); + } + , + + _hh: (a,b,c,d,x,s,t)=>{ + return CryptUtil._cmn(b ^ c ^ d, a, b, x, s, t); + } + , + + _ii: (a,b,c,d,x,s,t)=>{ + return CryptUtil._cmn(c ^ (b | (~d)), a, b, x, s, t); + } + , + + _md51: (s)=>{ + let n = s.length; + let state = [1732584193, -271733879, -1732584194, 271733878]; + let i; + for (i = 64; i <= s.length; i += 64) { + CryptUtil._md5cycle(state, CryptUtil._md5blk(s.substring(i - 64, i))); + } + s = s.substring(i - 64); + let tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + for (i = 0; i < s.length; i++) + tail[i >> 2] |= s.charCodeAt(i) << ((i % 4) << 3); + tail[i >> 2] |= 0x80 << ((i % 4) << 3); + if (i > 55) { + CryptUtil._md5cycle(state, tail); + for (i = 0; i < 16; i++) + tail[i] = 0; + } + tail[14] = n * 8; + CryptUtil._md5cycle(state, tail); + return state; + } + , + + _md5blk: (s)=>{ + let md5blks = []; + for (let i = 0; i < 64; i += 4) { + md5blks[i >> 2] = s.charCodeAt(i) + (s.charCodeAt(i + 1) << 8) + (s.charCodeAt(i + 2) << 16) + (s.charCodeAt(i + 3) << 24); + } + return md5blks; + } + , + + _hex_chr: "0123456789abcdef".split(""), + + _rhex: (n)=>{ + let s = ""; + for (let j = 0; j < 4; j++) { + s += CryptUtil._hex_chr[(n >> (j * 8 + 4)) & 0x0F] + CryptUtil._hex_chr[(n >> (j * 8)) & 0x0F]; + } + return s; + } + , + + _add32: (a,b)=>{ + return (a + b) & 0xFFFFFFFF; + } + , + + hex: (x)=>{ + for (let i = 0; i < x.length; i++) { + x[i] = CryptUtil._rhex(x[i]); + } + return x.join(""); + } + , + + hex2Dec(hex) { + return parseInt(`0x${hex}`); + }, + + md5: (s)=>{ + return CryptUtil.hex(CryptUtil._md51(s)); + } + , + + hashCode(obj) { + if (typeof obj === "string") { + if (!obj) + return 0; + let h = 0; + for (let i = 0; i < obj.length; ++i) + h = 31 * h + obj.charCodeAt(i); + return h; + } else if (typeof obj === "number") + return obj; + else + throw new Error(`No hashCode implementation for ${obj}`); + }, + + uid() { + if (RollerUtil.isCrypto()) { + return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c=>(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)); + } else { + let d = Date.now(); + if (typeof performance !== "undefined" && typeof performance.now === "function") { + d += performance.now(); + } + return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) { + const r = (d + Math.random() * 16) % 16 | 0; + d = Math.floor(d / 16); + return (c === "x" ? r : (r & 0x3 | 0x8)).toString(16); + }); + } + }, +}; +//#endregion + +//#region CurrencyUtil +globalThis.CurrencyUtil = class { + static doSimplifyCoins(obj, opts) { + opts = opts || {}; + + const conversionTable = opts.currencyConversionTable || Parser.getCurrencyConversionTable(opts.currencyConversionId); + if (!conversionTable.length) + return obj; + + const normalized = conversionTable.map(it=>{ + return { + ...it, + normalizedMult: 1 / it.mult, + }; + } + ).sort((a,b)=>SortUtil.ascSort(a.normalizedMult, b.normalizedMult)); + + for (let i = 0; i < normalized.length - 1; ++i) { + const coinCur = normalized[i].coin; + const coinNxt = normalized[i + 1].coin; + const coinRatio = normalized[i + 1].normalizedMult / normalized[i].normalizedMult; + + if (obj[coinCur] && Math.abs(obj[coinCur]) >= coinRatio) { + const nxtVal = obj[coinCur] >= 0 ? Math.floor(obj[coinCur] / coinRatio) : Math.ceil(obj[coinCur] / coinRatio); + obj[coinCur] = obj[coinCur] % coinRatio; + obj[coinNxt] = (obj[coinNxt] || 0) + nxtVal; + } + } + + if (opts.originalCurrency) { + const normalizedHighToLow = MiscUtil.copyFast(normalized).reverse(); + + normalizedHighToLow.forEach((coinMeta,i)=>{ + const valOld = opts.originalCurrency[coinMeta.coin] || 0; + const valNew = obj[coinMeta.coin] || 0; + + const prevCoinMeta = normalizedHighToLow[i - 1]; + const nxtCoinMeta = normalizedHighToLow[i + 1]; + + if (!prevCoinMeta) { + if (nxtCoinMeta) { + const diff = valNew - valOld; + if (diff > 0) { + obj[coinMeta.coin] = valOld; + const coinRatio = coinMeta.normalizedMult / nxtCoinMeta.normalizedMult; + obj[nxtCoinMeta.coin] = (obj[nxtCoinMeta.coin] || 0) + (diff * coinRatio); + } + } + } else { + if (nxtCoinMeta) { + const diffPrevCoin = (opts.originalCurrency[prevCoinMeta.coin] || 0) - (obj[prevCoinMeta.coin] || 0); + const coinRatio = prevCoinMeta.normalizedMult / coinMeta.normalizedMult; + const capFromOld = valOld + (diffPrevCoin > 0 ? diffPrevCoin * coinRatio : 0); + const diff = valNew - capFromOld; + if (diff > 0) { + obj[coinMeta.coin] = capFromOld; + const coinRatio = coinMeta.normalizedMult / nxtCoinMeta.normalizedMult; + obj[nxtCoinMeta.coin] = (obj[nxtCoinMeta.coin] || 0) + (diff * coinRatio); + } + } + } + } + ); + } + + normalized.filter(coinMeta=>obj[coinMeta.coin] === 0 || obj[coinMeta.coin] == null).forEach(coinMeta=>{ + obj[coinMeta.coin] = null; + delete obj[coinMeta.coin]; + } + ); + + if (opts.isPopulateAllValues) + normalized.forEach(coinMeta=>obj[coinMeta.coin] = obj[coinMeta.coin] || 0); + + return obj; + } + + static getAsCopper(obj) { + return Parser.FULL_CURRENCY_CONVERSION_TABLE.map(currencyMeta=>(obj[currencyMeta.coin] || 0) * (1 / currencyMeta.mult)).reduce((a,b)=>a + b, 0); + } + + static getAsSingleCurrency(obj) { + const simplified = CurrencyUtil.doSimplifyCoins({ + ...obj + }); + + if (Object.keys(simplified).length === 1) + return simplified; + + const out = {}; + + const targetDemonination = Parser.FULL_CURRENCY_CONVERSION_TABLE.find(it=>simplified[it.coin]); + + out[targetDemonination.coin] = simplified[targetDemonination.coin]; + delete simplified[targetDemonination.coin]; + + Object.entries(simplified).forEach(([coin,amt])=>{ + const denom = Parser.FULL_CURRENCY_CONVERSION_TABLE.find(it=>it.coin === coin); + out[targetDemonination.coin] = (out[targetDemonination.coin] || 0) + (amt / denom.mult) * targetDemonination.mult; + } + ); + + return out; + } + + static getCombinedCurrency(currencyA, currencyB) { + const out = {}; + + [currencyA, currencyB].forEach(currency=>{ + Object.entries(currency).forEach(([coin,cnt])=>{ + if (cnt == null) + return; + if (isNaN(cnt)) + throw new Error(`Unexpected non-numerical value "${JSON.stringify(cnt)}" for currency key "${coin}"`); + + out[coin] = (out[coin] || 0) + cnt; + } + ); + } + ); + + return out; + } +}; +//#endregion + +//#region UtilWorldDataSourceSelector +class UtilWorldDataSourceSelector { + static _SETTINGS_KEY = "data-source-selection"; + static async pInit() { + + await game.settings.register(SharedConsts.MODULE_ID, this._SETTINGS_KEY, { + name: "World Data Source Selection", + default: {}, + type: Object, + scope: "world", + onChange: data=>{} + , + }, ); + } + + static async pSaveState(saveableState) { + await game.settings.set(SharedConsts.MODULE_ID, this._SETTINGS_KEY, saveableState); + ui.notifications.info(`Saved! Note that you (and connected players) may need to reload for any changes to take effect.`); + } + + static loadState() { + return UtilGameSettings.getSafe(SharedConsts.MODULE_ID, this._SETTINGS_KEY); + } + + static isSourceSelectionActive() { + if(!SETTINGS.USE_FVTT){return true;} + return (!game.user.isGM && Config.get("dataSources", "isPlayerEnableSourceSelection")) || (game.user.isGM && Config.get("dataSources", "isGmEnableSourceSelection")); + } + + static isFiltered(dataSource) { + if (!this.isSourceSelectionActive()) + return false; + + const savedState = this.loadState(); + if (savedState == null) + return false; + + return !savedState.state?.[dataSource.identifierWorld]; + } +} +//#endregion + +//#region InputUIUtil +class InputUiUtil { + static async _pGetShowModal(getShowModalOpts) { + return UiUtil$1.getShowModal(getShowModalOpts); + } + + static _$getBtnOk({comp=null, opts, doClose}) { + return $(``).click(evt=>{ + evt.stopPropagation(); + if (comp && !comp._state.isValid) + return JqueryUtil.doToast({ + content: `Please enter valid input!`, + type: "warning" + }); + doClose(true); + } + ); + } + + static _$getBtnCancel({comp=null, opts, doClose}) { + return $(``).click(evt=>{ + evt.stopPropagation(); + doClose(false); + } + ); + } + + static _$getBtnSkip({comp=null, opts, doClose}) { + return !opts.isSkippable ? null : $(``).click(evt=>{ + evt.stopPropagation(); + doClose(VeCt.SYM_UI_SKIP); + } + ); + } + + static async pGetUserNumber(opts) { + opts = opts || {}; + + let defaultVal = opts.default !== undefined ? opts.default : null; + if (opts.storageKey_default) { + const prev = await (opts.isGlobal_default ? StorageUtil.pGet(opts.storageKey_default) : StorageUtil.pGetForPage(opts.storageKey_default)); + if (prev != null) + defaultVal = prev; + } + + const $iptNumber = $(``).keydown(evt=>{ + if (evt.key === "Escape") { + $iptNumber.blur(); + return; + } + + evt.stopPropagation(); + if (evt.key === "Enter") { + evt.preventDefault(); + doClose(true); + } + } + ); + if (defaultVal !== undefined) + $iptNumber.val(defaultVal); + + const {$modalInner, doClose, pGetResolved, doAutoResize: doAutoResizeModal} = await InputUiUtil._pGetShowModal({ + title: opts.title || "Enter a Number", + isMinHeight0: true, + }); + + const $btnOk = this._$getBtnOk({ + opts, + doClose + }); + const $btnCancel = this._$getBtnCancel({ + opts, + doClose + }); + const $btnSkip = this._$getBtnSkip({ + opts, + doClose + }); + + if (opts.$elePre) + opts.$elePre.appendTo($modalInner); + $iptNumber.appendTo($modalInner); + if (opts.$elePost) + opts.$elePost.appendTo($modalInner); + $$`
    ${$btnOk}${$btnCancel}${$btnSkip}
    `.appendTo($modalInner); + + if (doAutoResizeModal) + doAutoResizeModal(); + + $iptNumber.focus(); + $iptNumber.select(); + + const [isDataEntered] = await pGetResolved(); + + if (typeof isDataEntered === "symbol") + return isDataEntered; + + if (!isDataEntered) + return null; + const outRaw = $iptNumber.val(); + if (!outRaw.trim()) + return null; + let out = UiUtil$1.strToInt(outRaw); + if (opts.min) + out = Math.max(opts.min, out); + if (opts.max) + out = Math.min(opts.max, out); + if (opts.int) + out = Math.round(out); + + if (opts.storageKey_default) { + opts.isGlobal_default ? StorageUtil.pSet(opts.storageKey_default, out).then(null) : StorageUtil.pSetForPage(opts.storageKey_default, out).then(null); + } + + return out; + } + + static async pGetUserBoolean(opts) { + opts = opts || {}; + + if (opts.storageKey) { + const prev = await (opts.isGlobal ? StorageUtil.pGet(opts.storageKey) : StorageUtil.pGetForPage(opts.storageKey)); + if (prev != null) + return prev; + } + + const $btnTrueRemember = opts.textYesRemember ? $(``).click(()=>{ + doClose(true, true); + if (opts.fnRemember) { + opts.fnRemember(true); + } else { + opts.isGlobal ? StorageUtil.pSet(opts.storageKey, true) : StorageUtil.pSetForPage(opts.storageKey, true); + } + } + ) : null; + + const $btnTrue = $(``).click(evt=>{ + evt.stopPropagation(); + doClose(true, true); + } + ); + + const $btnFalse = opts.isAlert ? null : $(``).click(evt=>{ + evt.stopPropagation(); + doClose(true, false); + } + ); + + const $btnSkip = !opts.isSkippable ? null : $(``).click(evt=>{ + evt.stopPropagation(); + doClose(VeCt.SYM_UI_SKIP); + } + ); + + const {$modalInner, doClose, pGetResolved, doAutoResize: doAutoResizeModal} = await InputUiUtil._pGetShowModal({ + title: opts.title || "Choose", + isMinHeight0: true, + }); + + if (opts.$eleDescription?.length) + $$`
    ${opts.$eleDescription}
    `.appendTo($modalInner); + else if (opts.htmlDescription && opts.htmlDescription.trim()) + $$`
    ${opts.htmlDescription}
    `.appendTo($modalInner); + $$`
    ${$btnTrueRemember}${$btnTrue}${$btnFalse}${$btnSkip}
    `.appendTo($modalInner); + + if (doAutoResizeModal) + doAutoResizeModal(); + + $btnTrue.focus(); + $btnTrue.select(); + + const [isDataEntered,out] = await pGetResolved(); + + if (typeof isDataEntered === "symbol") + return isDataEntered; + + if (!isDataEntered) + return null; + if (out == null) + throw new Error(`Callback must receive a value!`); + return out; + } + + static async pGetUserEnum(opts) { + opts = opts || {}; + + const $selEnum = $(``).keydown(async evt=>{ + evt.stopPropagation(); + if (evt.key === "Enter") { + evt.preventDefault(); + doClose(true); + } + } + ); + + if (opts.isAllowNull) + $(``).text(opts.fnDisplay ? opts.fnDisplay(null, -1) : "(None)").appendTo($selEnum); + + opts.values.forEach((v,i)=>$(``).text(opts.fnDisplay ? opts.fnDisplay(v, i) : v).appendTo($selEnum)); + if (opts.default != null) + $selEnum.val(opts.default); + else + $selEnum[0].selectedIndex = 0; + + const {$modalInner, doClose, pGetResolved, doAutoResize: doAutoResizeModal} = await InputUiUtil._pGetShowModal({ + title: opts.title || "Select an Option", + isMinHeight0: true, + }); + + const $btnOk = this._$getBtnOk({ + opts, + doClose + }); + const $btnCancel = this._$getBtnCancel({ + opts, + doClose + }); + const $btnSkip = this._$getBtnSkip({ + opts, + doClose + }); + + $selEnum.appendTo($modalInner); + if (opts.$elePost) + opts.$elePost.appendTo($modalInner); + $$`
    ${$btnOk}${$btnCancel}${$btnSkip}
    `.appendTo($modalInner); + + if (doAutoResizeModal) + doAutoResizeModal(); + + $selEnum.focus(); + + const [isDataEntered] = await pGetResolved(); + if (typeof isDataEntered === "symbol") + return isDataEntered; + + if (!isDataEntered) + return null; + const ix = Number($selEnum.val()); + if (!~ix) + return null; + if (opts.fnGetExtraState) { + const out = { + extraState: opts.fnGetExtraState() + }; + if (opts.isResolveItem) + out.item = opts.values[ix]; + else + out.ix = ix; + return out; + } + + return opts.isResolveItem ? opts.values[ix] : ix; + } + + static async pGetUserMultipleChoice(opts) { + const prop = "formData"; + + const initialState = {}; + if (opts.defaults) + opts.defaults.forEach(ix=>initialState[ComponentUiUtil$1.getMetaWrpMultipleChoice_getPropIsActive(prop, ix)] = true); + if (opts.required) { + opts.required.forEach(ix=>{ + initialState[ComponentUiUtil$1.getMetaWrpMultipleChoice_getPropIsActive(prop, ix)] = true; + initialState[ComponentUiUtil$1.getMetaWrpMultipleChoice_getPropIsRequired(prop, ix)] = true; + } + ); + } + + const comp = BaseComponent$1.fromObject(initialState); + + let title = opts.title; + if (!title) { + if (opts.count != null) + title = `Choose ${Parser.numberToText(opts.count).uppercaseFirst()}`; + else if (opts.min != null && opts.max != null) + title = `Choose Between ${Parser.numberToText(opts.min).uppercaseFirst()} and ${Parser.numberToText(opts.max).uppercaseFirst()} Options`; + else if (opts.min != null) + title = `Choose At Least ${Parser.numberToText(opts.min).uppercaseFirst()}`; + else + title = `Choose At Most ${Parser.numberToText(opts.max).uppercaseFirst()}`; + } + + const {$ele: $wrpList, $iptSearch, propIsAcceptable} = ComponentUiUtil$1.getMetaWrpMultipleChoice(comp, prop, opts); + $wrpList.addClass(`mb-1`); + + const {$modalInner, doClose, pGetResolved, doAutoResize: doAutoResizeModal} = await InputUiUtil._pGetShowModal({ + ...(opts.modalOpts || {}), + title, + isMinHeight0: true, + isUncappedHeight: true, + }); + + const $btnOk = this._$getBtnOk({ + opts, + doClose + }); + const $btnCancel = this._$getBtnCancel({ + opts, + doClose + }); + const $btnSkip = this._$getBtnSkip({ + opts, + doClose + }); + + const hkIsAcceptable = ()=>$btnOk.attr("disabled", !comp._state[propIsAcceptable]); + comp._addHookBase(propIsAcceptable, hkIsAcceptable); + hkIsAcceptable(); + + if (opts.htmlDescription) + $modalInner.append(opts.htmlDescription); + if ($iptSearch) { + $$``.appendTo($modalInner); + } + $wrpList.appendTo($modalInner); + $$`
    ${$btnOk}${$btnCancel}${$btnSkip}
    `.appendTo($modalInner); + + if (doAutoResizeModal) + doAutoResizeModal(); + + $wrpList.focus(); + + const [isDataEntered] = await pGetResolved(); + + if (typeof isDataEntered === "symbol") + return isDataEntered; + + if (!isDataEntered) + return null; + + const ixs = ComponentUiUtil$1.getMetaWrpMultipleChoice_getSelectedIxs(comp, prop); + + if (!opts.isResolveItems) + return ixs; + + if (opts.values) + return ixs.map(ix=>opts.values[ix]); + + if (opts.valueGroups) { + const allValues = opts.valueGroups.map(it=>it.values).flat(); + return ixs.map(ix=>allValues[ix]); + } + + throw new Error(`Should never occur!`); + } + + static async pGetUserIcon(opts) { + opts = opts || {}; + + let lastIx = opts.default != null ? opts.default : -1; + const onclicks = []; + + const {$modalInner, doClose, pGetResolved, doAutoResize: doAutoResizeModal} = await InputUiUtil._pGetShowModal({ + title: opts.title || "Select an Option", + isMinHeight0: true, + }); + + $$`
    ${opts.values.map((v,i)=>{ + const $btn = $$`
    + ${v.iconClass ? `
    ` : ""} + ${v.iconContent ? v.iconContent : ""} +
    ${v.name}
    +
    `.click(()=>{ + lastIx = i; + onclicks.forEach(it=>it()); + } + ).toggleClass(v.buttonClassActive || "active", opts.default === i); + if (v.buttonClassActive && opts.default === i) { + $btn.removeClass("btn-default").addClass(v.buttonClassActive); + } + + onclicks.push(()=>{ + $btn.toggleClass(v.buttonClassActive || "active", lastIx === i); + if (v.buttonClassActive) + $btn.toggleClass("btn-default", lastIx !== i); + } + ); + return $btn; + } + )}
    `.appendTo($modalInner); + + const $btnOk = this._$getBtnOk({ + opts, + doClose + }); + const $btnCancel = this._$getBtnCancel({ + opts, + doClose + }); + const $btnSkip = this._$getBtnSkip({ + opts, + doClose + }); + + $$`
    ${$btnOk}${$btnCancel}${$btnSkip}
    `.appendTo($modalInner); + + const [isDataEntered] = await pGetResolved(); + + if (typeof isDataEntered === "symbol") + return isDataEntered; + if (!isDataEntered) + return null; + return ~lastIx ? lastIx : null; + } + + static async pGetUserString(opts) { + opts = opts || {}; + + const propValue = "text"; + const comp = BaseComponent$1.fromObject({ + [propValue]: opts.default || "", + isValid: true, + }); + + const $iptStr = ComponentUiUtil$1.$getIptStr(comp, propValue, { + html: ``, + autocomplete: opts.autocomplete, + }, ).keydown(async evt=>{ + if (evt.key === "Escape") + return; + if (opts.autocomplete) { + await MiscUtil.pDelay(17); + if ($modalInner.find(`.typeahead.dropdown-menu`).is(":visible")) + return; + } + + evt.stopPropagation(); + if (evt.key === "Enter") { + evt.preventDefault(); + doClose(true); + } + } + ); + if (opts.isCode) + $iptStr.addClass("code"); + + if (opts.fnIsValid) { + const hkText = ()=>comp._state.isValid = !comp._state.text.trim() || !!opts.fnIsValid(comp._state.text); + comp._addHookBase(propValue, hkText); + hkText(); + + const hkIsValid = ()=>$iptStr.toggleClass("form-control--error", !comp._state.isValid); + comp._addHookBase("isValid", hkIsValid); + hkIsValid(); + } + + const {$modalInner, doClose, pGetResolved, doAutoResize: doAutoResizeModal} = await InputUiUtil._pGetShowModal({ + title: opts.title || "Enter Text", + isMinHeight0: true, + isWidth100: true, + }); + + const $btnOk = this._$getBtnOk({ + comp, + opts, + doClose + }); + const $btnCancel = this._$getBtnCancel({ + comp, + opts, + doClose + }); + const $btnSkip = this._$getBtnSkip({ + comp, + opts, + doClose + }); + + if (opts.$elePre) + opts.$elePre.appendTo($modalInner); + if (opts.$eleDescription?.length) + $$`
    ${opts.$eleDescription}
    `.appendTo($modalInner); + else if (opts.htmlDescription && opts.htmlDescription.trim()) + $$`
    ${opts.htmlDescription}
    `.appendTo($modalInner); + $iptStr.appendTo($modalInner); + if (opts.$elePost) + opts.$elePost.appendTo($modalInner); + $$`
    ${$btnOk}${$btnCancel}${$btnSkip}
    `.appendTo($modalInner); + + if (doAutoResizeModal) + doAutoResizeModal(); + + $iptStr.focus(); + $iptStr.select(); + + if (opts.cbPostRender) { + opts.cbPostRender({ + comp, + $iptStr, + propValue, + }); + } + + const [isDataEntered] = await pGetResolved(); + + if (typeof isDataEntered === "symbol") + return isDataEntered; + if (!isDataEntered) + return null; + const raw = $iptStr.val(); + return raw; + } + + static async pGetUserText(opts) { + opts = opts || {}; + + const $iptStr = $(``).val(opts.default); + if (opts.isCode) + $iptStr.addClass("code"); + + const {$modalInner, doClose, pGetResolved, doAutoResize: doAutoResizeModal} = await InputUiUtil._pGetShowModal({ + title: opts.title || "Enter Text", + isMinHeight0: true, + }); + + const $btnOk = this._$getBtnOk({ + opts, + doClose + }); + const $btnCancel = this._$getBtnCancel({ + opts, + doClose + }); + const $btnSkip = this._$getBtnSkip({ + opts, + doClose + }); + + $iptStr.appendTo($modalInner); + $$`
    ${$btnOk}${$btnCancel}${$btnSkip}
    `.appendTo($modalInner); + + if (doAutoResizeModal) + doAutoResizeModal(); + + $iptStr.focus(); + $iptStr.select(); + + const [isDataEntered] = await pGetResolved(); + + if (typeof isDataEntered === "symbol") + return isDataEntered; + if (!isDataEntered) + return null; + const raw = $iptStr.val(); + if (!raw.trim()) + return null; + else + return raw; + } + + static async pGetUserColor(opts) { + opts = opts || {}; + + const $iptRgb = $(``); + + const {$modalInner, doClose, pGetResolved, doAutoResize: doAutoResizeModal} = await InputUiUtil._pGetShowModal({ + title: opts.title || "Choose Color", + isMinHeight0: true, + }); + + const $btnOk = this._$getBtnOk({ + opts, + doClose + }); + const $btnCancel = this._$getBtnCancel({ + opts, + doClose + }); + const $btnSkip = this._$getBtnSkip({ + opts, + doClose + }); + + $iptRgb.appendTo($modalInner); + $$`
    ${$btnOk}${$btnCancel}${$btnSkip}
    `.appendTo($modalInner); + + if (doAutoResizeModal) + doAutoResizeModal(); + + $iptRgb.focus(); + $iptRgb.select(); + + const [isDataEntered] = await pGetResolved(); + + if (typeof isDataEntered === "symbol") + return isDataEntered; + if (!isDataEntered) + return null; + const raw = $iptRgb.val(); + if (!raw.trim()) + return null; + else + return raw; + } + + static async pGetUserDirection(opts) { + const X = 0; + const Y = 1; + const DEG_CIRCLE = 360; + + opts = opts || {}; + const step = Math.max(2, Math.min(DEG_CIRCLE, opts.step || DEG_CIRCLE)); + const stepDeg = DEG_CIRCLE / step; + + function getAngle(p1, p2) { + return Math.atan2(p2[Y] - p1[Y], p2[X] - p1[X]) * 180 / Math.PI; + } + + let active = false; + let curAngle = Math.min(DEG_CIRCLE, opts.default) || 0; + + const $arm = $(`
    `); + const handleAngle = ()=>$arm.css({ + transform: `rotate(${curAngle + 180}deg)` + }); + handleAngle(); + + const $pad = $$`
    ${$arm}
    `.on("mousedown touchstart", evt=>{ + active = true; + handleEvent(evt); + } + ); + + const $document = $(document); + const evtId = `ui_user_dir_${CryptUtil.uid()}`; + $document.on(`mousemove.${evtId} touchmove${evtId}`, evt=>{ + handleEvent(evt); + } + ).on(`mouseup.${evtId} touchend${evtId} touchcancel${evtId}`, evt=>{ + evt.preventDefault(); + evt.stopPropagation(); + active = false; + } + ); + const handleEvent = (evt)=>{ + if (!active) + return; + + const coords = [EventUtil.getClientX(evt), EventUtil.getClientY(evt)]; + + const {top, left} = $pad.offset(); + const center = [left + ($pad.width() / 2), top + ($pad.height() / 2)]; + curAngle = getAngle(center, coords) + 90; + if (step !== DEG_CIRCLE) + curAngle = Math.round(curAngle / stepDeg) * stepDeg; + else + curAngle = Math.round(curAngle); + handleAngle(); + } + ; + + const BTN_STEP_SIZE = 26; + const BORDER_PAD = 16; + const CONTROLS_RADIUS = (92 + BTN_STEP_SIZE + BORDER_PAD) / 2; + const $padOuter = opts.stepButtons ? (()=>{ + const steps = opts.stepButtons; + const SEG_ANGLE = 360 / steps.length; + + const $btns = []; + + for (let i = 0; i < steps.length; ++i) { + const theta = (SEG_ANGLE * i * (Math.PI / 180)) - (1.5708); + const x = CONTROLS_RADIUS * Math.cos(theta); + const y = CONTROLS_RADIUS * Math.sin(theta); + $btns.push($(``).css({ + top: y + CONTROLS_RADIUS - (BTN_STEP_SIZE / 2), + left: x + CONTROLS_RADIUS - (BTN_STEP_SIZE / 2), + width: BTN_STEP_SIZE, + height: BTN_STEP_SIZE, + zIndex: 1002, + }).click(()=>{ + curAngle = SEG_ANGLE * i; + handleAngle(); + } + ), ); + } + + const $wrpInner = $$`
    ${$btns}${$pad}
    `.css({ + width: CONTROLS_RADIUS * 2, + height: CONTROLS_RADIUS * 2, + }); + + return $$`
    ${$wrpInner}
    `.css({ + width: (CONTROLS_RADIUS * 2) + BTN_STEP_SIZE + BORDER_PAD, + height: (CONTROLS_RADIUS * 2) + BTN_STEP_SIZE + BORDER_PAD, + }); + } + )() : null; + + const {$modalInner, doClose, pGetResolved, doAutoResize: doAutoResizeModal} = await InputUiUtil._pGetShowModal({ + title: opts.title || "Select Direction", + isMinHeight0: true, + }); + + const $btnOk = this._$getBtnOk({ + opts, + doClose + }); + const $btnCancel = this._$getBtnCancel({ + opts, + doClose + }); + const $btnSkip = this._$getBtnSkip({ + opts, + doClose + }); + + $$`
    + ${$padOuter || $pad} +
    `.appendTo($modalInner); + $$`
    ${$btnOk}${$btnCancel}${$btnSkip}
    `.appendTo($modalInner); + + if (doAutoResizeModal) + doAutoResizeModal(); + + const [isDataEntered] = await pGetResolved(); + + if (typeof isDataEntered === "symbol") + return isDataEntered; + $document.off(`mousemove.${evtId} touchmove${evtId} mouseup.${evtId} touchend${evtId} touchcancel${evtId}`); + if (!isDataEntered) + return null; + if (curAngle < 0) + curAngle += 360; + return curAngle; + } + + static async pGetUserDice(opts) { + opts = opts || {}; + + const comp = BaseComponent$1.fromObject({ + num: (opts.default && opts.default.num) || 1, + faces: (opts.default && opts.default.faces) || 6, + bonus: (opts.default && opts.default.bonus) || null, + }); + + comp.render = function($parent) { + $parent.empty(); + + const $iptNum = ComponentUiUtil$1.$getIptInt(this, "num", 0, { + $ele: $(``) + }).appendTo($parent).keydown(evt=>{ + if (evt.key === "Escape") { + $iptNum.blur(); + return; + } + if (evt.which === 13) + doClose(true); + evt.stopPropagation(); + } + ); + const $selFaces = ComponentUiUtil$1.$getSelEnum(this, "faces", { + values: Renderer.dice.DICE + }).addClass("mr-2").addClass("ve-text-center").css("textAlignLast", "center"); + + const $iptBonus = $(``).change(()=>this._state.bonus = UiUtil$1.strToInt($iptBonus.val(), null, { + fallbackOnNaN: null + })).keydown(evt=>{ + if (evt.key === "Escape") { + $iptBonus.blur(); + return; + } + if (evt.which === 13) + doClose(true); + evt.stopPropagation(); + } + ); + const hook = ()=>$iptBonus.val(this._state.bonus != null ? UiUtil$1.intToBonus(this._state.bonus) : this._state.bonus); + comp._addHookBase("bonus", hook); + hook(); + + $$`
    ${$iptNum}
    d
    ${$selFaces}${$iptBonus}
    `.appendTo($parent); + } + ; + + comp.getAsString = function() { + return `${this._state.num}d${this._state.faces}${this._state.bonus ? UiUtil$1.intToBonus(this._state.bonus) : ""}`; + } + ; + + const {$modalInner, doClose, pGetResolved, doAutoResize: doAutoResizeModal} = await InputUiUtil._pGetShowModal({ + title: opts.title || "Enter Dice", + isMinHeight0: true, + }); + + const $btnOk = this._$getBtnOk({ + opts, + doClose + }); + const $btnCancel = this._$getBtnCancel({ + opts, + doClose + }); + const $btnSkip = this._$getBtnSkip({ + opts, + doClose + }); + + comp.render($modalInner); + + $$`
    ${$btnOk}${$btnCancel}${$btnSkip}
    `.appendTo($modalInner); + + if (doAutoResizeModal) + doAutoResizeModal(); + + const [isDataEntered] = await pGetResolved(); + + if (typeof isDataEntered === "symbol") + return isDataEntered; + if (!isDataEntered) + return null; + return comp.getAsString(); + } + + static async pGetUserScaleCr(opts={}) { + const crDefault = opts.default || "1"; + + let slider; + + const {$modalInner, doClose, pGetResolved, doAutoResize: doAutoResizeModal} = await InputUiUtil._pGetShowModal({ + title: opts.title || "Select Challenge Rating", + isMinHeight0: true, + cbClose: ()=>{ + slider.destroy(); + } + , + }); + + const cur = Parser.CRS.indexOf(crDefault); + if (!~cur) + throw new Error(`Initial CR ${crDefault} was not valid!`); + + const comp = BaseComponent$1.fromObject({ + min: 0, + max: Parser.CRS.length - 1, + cur, + }); + slider = new ComponentUiUtil$1.RangeSlider({ + comp, + propMin: "min", + propMax: "max", + propCurMin: "cur", + fnDisplay: ix=>Parser.CRS[ix], + }); + $$`
    ${slider.$get()}
    `.appendTo($modalInner); + + const $btnOk = this._$getBtnOk({ + opts, + doClose + }); + const $btnCancel = this._$getBtnCancel({ + opts, + doClose + }); + const $btnSkip = this._$getBtnSkip({ + opts, + doClose + }); + + $$`
    ${$btnOk}${$btnCancel}${$btnSkip}
    `.appendTo($modalInner); + + if (doAutoResizeModal) + doAutoResizeModal(); + + const [isDataEntered] = await pGetResolved(); + + if (typeof isDataEntered === "symbol") + return isDataEntered; + if (!isDataEntered) + return null; + + return Parser.CRS[comp._state.cur]; + } +} +//#endregion + +//#region Hist +let Hist = class Hist { + static hashChange({isForceLoad, isBlankFilterLoad=false}={}) { + if (Hist.isHistorySuppressed) { + Hist.setSuppressHistory(false); + return; + } + + const [link,...sub] = Hist.getHashParts(); + + if (link !== Hist.lastLoadedLink || sub.length === 0 || isForceLoad) { + Hist.lastLoadedLink = link; + if (link === HASH_BLANK) { + isBlankFilterLoad = true; + } else { + const listItem = Hist.getActiveListItem(link); + + if (listItem == null) { + if (typeof pHandleUnknownHash === "function" && window.location.hash.length && Hist._lastUnknownLink !== link) { + Hist._lastUnknownLink = link; + pHandleUnknownHash(link, sub); + return; + } else { + Hist._freshLoad(); + return; + } + } + + const toLoad = listItem.ix; + if (toLoad === undefined) + Hist._freshLoad(); + else { + Hist.lastLoadedId = listItem.ix; + loadHash(listItem.ix); + document.title = `${listItem.name ? `${listItem.name} - ` : ""}5etools`; + } + } + } + + if (typeof loadSubHash === "function" && (sub.length > 0 || isForceLoad)) + loadSubHash(sub); + if (isBlankFilterLoad) + Hist._freshLoad(); + } + + static init(initialLoadComplete) { + window.onhashchange = ()=>Hist.hashChange({ + isForceLoad: true + }); + if (window.location.hash.length) { + Hist.hashChange(); + } else { + Hist._freshLoad(); + } + if (initialLoadComplete) + Hist.initialLoad = false; + } + + static setSuppressHistory(val) { + Hist.isHistorySuppressed = val; + } + + static _listPage = null; + + static setListPage(listPage) { + this._listPage = listPage; + } + + static getSelectedListItem() { + const [link] = Hist.getHashParts(); + return Hist.getActiveListItem(link); + } + + static getSelectedListElementWithLocation() { + const [link] = Hist.getHashParts(); + return Hist.getActiveListItem(link, true); + } + + static getHashParts() { + return Hist.util.getHashParts(window.location.hash); + } + + static getActiveListItem(link, getIndex) { + const primaryLists = this._listPage.primaryLists; + if (primaryLists && primaryLists.length) { + for (let x = 0; x < primaryLists.length; ++x) { + const list = primaryLists[x]; + + const foundItemIx = list.items.findIndex(it=>it.values.hash === link); + if (~foundItemIx) { + if (getIndex) + return { + item: list.items[foundItemIx], + x: x, + y: foundItemIx, + list + }; + return list.items[foundItemIx]; + } + } + } + } + + static _freshLoad() { + setTimeout(()=>{ + const goTo = $("#listcontainer").find(".list a").attr("href"); + if (goTo) { + const parts = location.hash.split(HASH_PART_SEP); + const fullHash = `${goTo}${parts.length > 1 ? `${HASH_PART_SEP}${parts.slice(1).join(HASH_PART_SEP)}` : ""}`; + location.replace(fullHash); + } + } + , 1); + } + + static cleanSetHash(toSet) { + window.location.hash = Hist.util.getCleanHash(toSet); + } + + static getHashSource() { + const [link] = Hist.getHashParts(); + return link ? link.split(HASH_LIST_SEP).last() : null; + } + + static getSubHash(key) { + return Hist.util.getSubHash(window.location.hash, key); + } + + static setSubhash(key, val) { + const nxtHash = Hist.util.setSubhash(window.location.hash, key, val); + Hist.cleanSetHash(nxtHash); + } + + static setMainHash(hash) { + const subHashPart = Hist.util.getHashParts(window.location.hash, key, val).slice(1).join(HASH_PART_SEP); + Hist.cleanSetHash([hash, subHashPart].filter(Boolean).join(HASH_PART_SEP)); + } + + static replaceHistoryHash(hash) { + window.history.replaceState({}, document.title, `${location.origin}${location.pathname}${hash ? `#${hash}` : ""}`, ); + } +} +; +Hist.lastLoadedLink = null; +Hist._lastUnknownLink = null; +Hist.lastLoadedId = null; +Hist.initialLoad = true; +Hist.isHistorySuppressed = false; + +Hist.util = class { + static getCleanHash(hash) { + return hash.replace(/,+/g, ",").replace(/,$/, "").toLowerCase(); + } + + static _SYMS_NO_ENCODE = [/(,)/g, /(:)/g, /(=)/g]; + + static getHashParts(location, {isReturnEncoded=false}={}) { + if (location[0] === "#") + location = location.slice(1); + + if (location === "google_vignette") + location = ""; + + if (isReturnEncoded) { + return location.split(HASH_PART_SEP); + } + + let pts = [location]; + this._SYMS_NO_ENCODE.forEach(re=>{ + pts = pts.map(pt=>pt.split(re)).flat(); + } + ); + pts = pts.map(pt=>{ + if (this._SYMS_NO_ENCODE.some(re=>re.test(pt))) + return pt; + return decodeURIComponent(pt).toUrlified(); + } + ); + location = pts.join(""); + + return location.split(HASH_PART_SEP); + } + + static getSubHash(location, key) { + const [link,...sub] = Hist.util.getHashParts(location); + const hKey = `${key}${HASH_SUB_KV_SEP}`; + const part = sub.find(it=>it.startsWith(hKey)); + if (part) + return part.slice(hKey.length); + return null; + } + + static setSubhash(location, key, val) { + if (key.endsWith(HASH_SUB_KV_SEP)) + key = key.slice(0, -1); + + const [link,...sub] = Hist.util.getHashParts(location); + if (!link) + return ""; + + const hKey = `${key}${HASH_SUB_KV_SEP}`; + const out = [link]; + if (sub.length) + sub.filter(it=>!it.startsWith(hKey)).forEach(it=>out.push(it)); + if (val != null) + out.push(`${hKey}${val}`); + + return Hist.util.getCleanHash(out.join(HASH_PART_SEP)); + } +}; + +//#endregion \ No newline at end of file diff --git a/charbuilder/js/plutonium/vetools.js b/charbuilder/js/plutonium/vetools.js new file mode 100644 index 0000000..17714b1 --- /dev/null +++ b/charbuilder/js/plutonium/vetools.js @@ -0,0 +1,1271 @@ +globalThis.VeLock = function({name=null, isDbg=false}={}) { + this._name = name; + this._isDbg = isDbg; + this._lockMeta = null; + + this._getCaller = ()=>{ + return (new Error()).stack.split("\n")[3].trim(); + } + ; + + this.pLock = async({token=null}={})=>{ + if (token != null && this._lockMeta?.token === token) { + ++this._lockMeta.depth; + if (this._isDbg) + console.warn(`Lock "${this._name || "(unnamed)"}" add (now ${this._lockMeta.depth}) at ${this._getCaller()}`); + return token; + } + + while (this._lockMeta) + await this._lockMeta.lock; + + if (this._isDbg) + console.warn(`Lock "${this._name || "(unnamed)"}" acquired at ${this._getCaller()}`); + + let unlock = null; + const lock = new Promise(resolve=>unlock = resolve); + this._lockMeta = { + lock, + unlock, + token: CryptUtil.uid(), + depth: 0, + }; + + return this._lockMeta.token; + } + ; + + this.unlock = ()=>{ + if (!this._lockMeta) + return; + + if (this._lockMeta.depth > 0) { + if (this._isDbg) + console.warn(`Lock "${this._name || "(unnamed)"}" sub (now ${this._lockMeta.depth - 1}) at ${this._getCaller()}`); + return --this._lockMeta.depth; + } + + if (this._isDbg) + console.warn(`Lock "${this._name || "(unnamed)"}" released at ${this._getCaller()}`); + + const lockMeta = this._lockMeta; + this._lockMeta = null; + lockMeta.unlock(); + } + ; +} +; +class Vetools { + static PRERELEASE_INDEX__SOURCE = {}; + static PRERELEASE_INDEX__PROP = {}; + static PRERELEASE_INDEX__META = {}; + + static BREW_INDEX__SOURCE = {}; + static BREW_INDEX__PROP = {}; + static BREW_INDEX__META = {}; + + static async pDoPreload() { + if (Config.get("dataSources", "isNoPrereleaseBrewIndexes")){ + console.error(`Failed to get urlRoot from 'dataSources' with key 'isNoPrereleaseBrewIndexes'. Is config uninitialized?`); + return; + } + + await Vetools._pGetPrereleaseBrewIndices().then(({propPrerelease, sourcePrerelease, metaPrerelease, sourceBrew, propBrew, metaBrew})=>{ + Vetools.PRERELEASE_INDEX__PROP = propPrerelease; + Vetools.PRERELEASE_INDEX__SOURCE = sourcePrerelease; + Vetools.PRERELEASE_INDEX__META = metaPrerelease; + + Vetools.BREW_INDEX__PROP = propBrew; + Vetools.BREW_INDEX__SOURCE = sourceBrew; + Vetools.BREW_INDEX__META = metaBrew; + + console.log(...LGT, "Loaded prerelease/homebrew indexes."); + } + ).catch(e=>{ + Vetools.PRERELEASE_INDEX__SOURCE = {}; + Vetools.PRERELEASE_INDEX__PROP = {}; + Vetools.PRERELEASE_INDEX__META = {}; + + Vetools.BREW_INDEX__PROP = {}; + Vetools.BREW_INDEX__SOURCE = {}; + Vetools.BREW_INDEX__META = {}; + + //ui.notifications + console.error(`Failed to load prerelease/homebrew indexes! ${VeCt.STR_SEE_CONSOLE}`); + setTimeout(()=>{ throw e; }); + }); + } + + static withUnpatchedDiceRendering(fn) { + Renderer.getRollableEntryDice = Vetools._CACHED_GET_ROLLABLE_ENTRY_DICE; + const out = fn(); + Renderer.getRollableEntryDice = Vetools._PATCHED_GET_ROLLABLE_ENTRY_DICE; + return out; + } + + static withCustomDiceRenderingPatch(fn, fnRender) { + Renderer.getRollableEntryDice = fnRender; + const out = fn(); + Renderer.getRollableEntryDice = Vetools._PATCHED_GET_ROLLABLE_ENTRY_DICE; + return out; + } + + static getCleanDiceString(diceString) { + return diceString.replace(/ร—/g, "*").replace(/รท/g, "/").replace(/#\$.*?\$#/g, "0"); + } + + static doMonkeyPatchPreConfig() { + VeCt.STR_SEE_CONSOLE = "See the console (F12 or CTRL+SHIFT+J) for details."; + + //TEMPFIX + /* StorageUtil.pSet = GameStorage.pSetClient.bind(GameStorage); + StorageUtil.pGet = GameStorage.pGetClient.bind(GameStorage); + StorageUtil.pRemove = GameStorage.pRemoveClient.bind(GameStorage); */ + + ["monster", "vehicle", "object", "trap", "race", "background"].forEach(prop=>{ + const propFullName = `${prop}Name`; + const propFullSource = `${prop}Source`; + (Renderer[prop].CHILD_PROPS_EXTENDED || Renderer[prop].CHILD_PROPS || ["feature"]).forEach(propChild=>{ + const propChildFull = `${prop}${propChild.uppercaseFirst()}`; + if (UrlUtil.URL_TO_HASH_BUILDER[propChildFull]) + return; + UrlUtil.URL_TO_HASH_BUILDER[propChildFull] = it=>UrlUtil.encodeForHash([it.name, it[propFullName], it[propFullSource], it.source]); + }); + }); + } + + static _CACHED_DATA_UTIL_LOAD_JSON = null; + static _CACHED_DATA_UTIL_LOAD_RAW_JSON = null; + + static doMonkeyPatchPostConfig() { + JqueryExtension.init(); + this._initSourceLookup(); + + //TEMPFIX UtilsChangelog._RELEASE_URL = "https://github.com/TheGiddyLimit/plutonium-next/tags"; + + const hkSetRendererUrls = ()=>{ + Renderer.get().setBaseUrl(Vetools.BASE_SITE_URL); + + if (Config.get("import", "isUseLocalImages")) { + const localImageDirPath = `${Config.get("import", "localImageDirectoryPath")}/`.replace(/\/+$/, "/"); + Renderer.get().setBaseMediaUrl("img", localImageDirPath); + return; + } + + if (this._isCustomBaseSiteUrl()) { + Renderer.get().setBaseMediaUrl("img", Vetools.BASE_SITE_URL); + return; + } + + Renderer.get().setBaseMediaUrl("img", null); + } + ; + hkSetRendererUrls(); + + if(SETTINGS.USE_FVTT){UtilHooks.on(UtilHooks.HK_CONFIG_UPDATE, hkSetRendererUrls);} + + Renderer.hover.MIN_Z_INDEX = Consts.Z_INDEX_MAX_FOUNDRY + 1; + Renderer.hover._MAX_Z_INDEX = Renderer.hover.MIN_Z_INDEX + 10; + + Vetools._CACHED_GET_ROLLABLE_ENTRY_DICE = Renderer.getRollableEntryDice; + Vetools._PATCHED_GET_ROLLABLE_ENTRY_DICE = (entry,name,toDisplay,{isAddHandlers=true, pluginResults=null, }={},)=>{ + const cpy = MiscUtil.copy(entry); + + if (typeof cpy.toRoll !== "string") { + cpy.toRoll = Renderer.legacyDiceToString(cpy.toRoll); + } + + if (cpy.prompt) { + const minAdditionalDiceLevel = Math.min(...Object.keys(cpy.prompt.options).map(it=>Number(it)).filter(it=>cpy.prompt.options[it])); + cpy.toRoll = cpy.prompt.options[minAdditionalDiceLevel]; + } + + const toRollClean = this.getCleanDiceString(cpy.toRoll); + + if (Config.get("import", "isRendererDiceDisabled")) + return toDisplay || toRollClean; + + const ptDisplay = toRollClean.toLowerCase().trim() !== toDisplay.toLowerCase().trim() ? `{${toDisplay}}` : ""; + + if (cpy.autoRoll) + return `[[${toRollClean}]]${ptDisplay}`; + + if (Config.get("import", "isRenderCustomDiceEnrichers") && entry.subtype === "damage") { + return `[[/damage ${toRollClean} ${cpy.damageType ? `type=${cpy.damageType}` : ""}]]${ptDisplay}`; + } + + return `[[/r ${toRollClean}]]${ptDisplay}`; + } + ; + + Renderer.getRollableEntryDice = Vetools._PATCHED_GET_ROLLABLE_ENTRY_DICE; + + const cachedRenderHoverMethods = {}; + const renderHoverMethods = ["$getHoverContent_stats", "$getHoverContent_fluff", "$getHoverContent_statsCode", "$getHoverContent_miscCode", "$getHoverContent_generic", ]; + renderHoverMethods.forEach(methodName=>{ + cachedRenderHoverMethods[methodName] = Renderer.hover[methodName]; + Renderer.hover[methodName] = (...args)=>{ + Renderer.getRollableEntryDice = Vetools._CACHED_GET_ROLLABLE_ENTRY_DICE; + const out = cachedRenderHoverMethods[methodName](...args); + Renderer.getRollableEntryDice = Vetools._PATCHED_GET_ROLLABLE_ENTRY_DICE; + return out; + } + ; + } + ); + + const cachedGetMakePredefinedHover = Renderer.hover.getMakePredefinedHover.bind(Renderer.hover); + Renderer.hover.getMakePredefinedHover = (entry,opts)=>{ + const out = cachedGetMakePredefinedHover(entry, opts); + out.html = `data-plut-hover="${true}" data-plut-hover-preload="${true}" data-plut-hover-preload-id="${out.id}" ${opts ? `data-plut-hover-preload-options="${JSON.stringify(opts).qq()}"` : ""}`; + return out; + } + ; + + const cachedGetInlineHover = Renderer.hover.getInlineHover.bind(Renderer.hover); + Renderer.hover.getInlineHover = (entry,opts)=>{ + const out = cachedGetInlineHover(entry, opts); + out.html = `data-plut-hover="${true}" data-plut-hover-inline="${true}" data-plut-hover-inline-entry="${JSON.stringify(entry).qq()}" ${opts ? `data-plut-hover-inline-options="${JSON.stringify(opts).qq()}"` : ""}`; + return out; + } + ; + + Renderer.dice.rollerClick = (evtMock,ele,packed,name)=>{ + const entry = JSON.parse(packed); + if (entry.toRoll) + (new Roll(entry.toRoll)).toMessage(); + } + ; + + Renderer.dice.pRollEntry = (entry,rolledBy,opts)=>{ + if (entry.toRoll) + (new Roll(entry.toRoll)).toMessage(); + } + ; + + Renderer.dice.pRoll2 = async(str,rolledBy,opts)=>{ + const roll = new Roll(str); + await roll.evaluate({ + async: true + }); + await roll.toMessage(); + return roll.total; + } + ; + + if(SETTINGS.USE_FVTT){ + Vetools._CACHED_MONSTER_DO_BIND_COMPACT_CONTENT_HANDLERS = Renderer.monster.doBindCompactContentHandlers; + Renderer.monster.doBindCompactContentHandlers = (opts)=>{ + const nxtOpts = { + ...opts + }; + nxtOpts.fnRender = (...args)=>Vetools.withUnpatchedDiceRendering(()=>opts.fnRender(...args)); + return Vetools._CACHED_MONSTER_DO_BIND_COMPACT_CONTENT_HANDLERS(nxtOpts); + }; + } + + + JqueryUtil.doToast = (options)=>{ + if (typeof options === "string") { + options = { + content: options, + type: "info", + }; + } + options.type = options.type || "info"; + + switch (options.type) { + case "warning": + return ui.notifications.warn(options.content); + case "danger": + return ui.notifications.error(options.content); + default: + return ui.notifications.info(options.content); + } + }; + + if(SETTINGS.USE_FVTT){ + //Switches pGetShowModal to instead use the FoundryVTT Application class + UiUtil.pGetShowModal = opts=>UtilApplications.pGetShowApplicationModal(opts); + InputUiUtil._pGetShowModal = opts=>UtilApplications.pGetShowApplicationModal(opts); + } + + + this._CACHED_DATA_UTIL_LOAD_JSON = DataUtil.loadJSON.bind(DataUtil); + this._CACHED_DATA_UTIL_LOAD_RAW_JSON = DataUtil.loadRawJSON.bind(DataUtil); + + DataUtil.loadJSON = async(url,...rest)=>Vetools._CACHED_DATA_UTIL_LOAD_JSON(this._getMaybeLocalUrl(url), ...rest); + DataUtil.loadRawJSON = async(url,...rest)=>Vetools._CACHED_DATA_UTIL_LOAD_RAW_JSON(this._getMaybeLocalUrl(url), ...rest); + + Vetools._CACHED_RENDERER_HOVER_CACHE_AND_GET = DataLoader.pCacheAndGet.bind(DataLoader); + DataLoader.pCacheAndGet = async function(page, source, ...others) { + const sourceLower = `${source}`.toLowerCase(); + if (!Vetools._VET_SOURCE_LOOKUP[sourceLower]) { + Vetools._pCachingLocalPrerelease = Vetools._pCachingLocalPrerelease || Vetools._pDoCacheLocalPrerelease(); + Vetools._pCachingLocalBrew = Vetools._pCachingLocalBrew || Vetools._pDoCacheLocalBrew(); + + await Promise.all([Vetools._pCachingLocalPrerelease, Vetools._pCachingLocalBrew, ]); + } + + return Vetools._CACHED_RENDERER_HOVER_CACHE_AND_GET(page, source, ...others); + } + ; + + PrereleaseUtil._storage = new StorageUtilMemory(); + BrewUtil2._storage = new StorageUtilMemory(); + } + + static _initSourceLookup() { + Object.keys(Parser.SOURCE_JSON_TO_FULL).forEach(source=>Vetools._VET_SOURCE_LOOKUP[source.toLowerCase()] = true); + } + + static _pCachingLocalPrerelease = null; + static _pCachingLocalBrew = null; + + static async _pDoCacheLocalPrerelease() { + await this.pGetLocalPrereleaseSources(); + } + static async _pDoCacheLocalBrew() { + await this.pGetLocalBrewSources(); + } + + static _getMaybeLocalUrl(url) { + if (!url.includes("?")) + url = `${url}?t=${Consts.RUN_TIME}`; + + const parts = url.split(Vetools._RE_HTTP_URL).filter(Boolean); + parts[parts.length - 1] = parts.last().replace(/\/+/g, "/"); + url = parts.join(""); + + if (!Config.get("dataSources", "isNoLocalData") && (url.startsWith(`${Vetools.BASE_SITE_URL}data/`) || url.startsWith(`${Vetools.BASE_SITE_URL}search/`)) && url !== this._getChangelogUrl()) { + const urlPart = url.split(Vetools.BASE_SITE_URL).slice(1).join(Vetools.BASE_SITE_URL); + if(SETTINGS.LOCALPATH_REDIRECT){return urlPart;} + return `modules/${SharedConsts.MODULE_ID}/${urlPart}`; + } else { + return url; + } + } + + static _CACHE_IMPORTER_SOURCE_SPECIAL = {}; + + static async pLoadImporterSourceSpecial(source) { + if (!source.special.cacheKey) + return source.special.pGet(); + + this._CACHE_IMPORTER_SOURCE_SPECIAL[source.special.cacheKey] = this._CACHE_IMPORTER_SOURCE_SPECIAL[source.special.cacheKey] || source.special.pGet(); + + return this._CACHE_IMPORTER_SOURCE_SPECIAL[source.special.cacheKey]; + } + + static _getChangelogUrl() { + return `${Vetools.BASE_SITE_URL}data/changelog.json`; + } + static async pGetChangelog() { + return DataUtil.loadJSON(this._getChangelogUrl()); + } + + static async pGetPackageIndex() { + return DataUtil.loadJSON(Config.get("importAdventure", "indexUrl")); + } + + static async pGetItems() { + return { + item: (await Renderer.item.pBuildList()).filter(it=>!it._isItemGroup) + }; + } + + static async pGetPrereleaseItems(data) { + return this._pGetPrereleaseBrewItems({ + data, + pFnGetItems: Renderer.item.pGetItemsFromPrerelease.bind(Renderer.item) + }); + } + + static async pGetBrewItems(data) { + return this._pGetPrereleaseBrewItems({ + data, + pFnGetItems: Renderer.item.pGetItemsFromBrew.bind(Renderer.item) + }); + } + + static async _pGetPrereleaseBrewItems({data, pFnGetItems}) { + const sources = new Set(); + ["item", "magicvariant", "baseitem"].forEach(prop=>{ + if (!data[prop]) + return; + data[prop].forEach(ent=>sources.add(SourceUtil.getEntitySource(ent))); + } + ); + return (await pFnGetItems()).filter(ent=>sources.has(SourceUtil.getEntitySource(ent))); + } + + static async pGetRaces(opts) { + return DataUtil.race.loadJSON(opts); + } + + static async pGetClasses() { + return DataUtil.class.loadRawJSON(); + } + + static async pGetClassSubclassFeatures() { + return DataUtil.class.loadRawJSON(); + } + + static async pGetRollableTables() { + return DataUtil.table.loadJSON(); + } + + static async pGetDecks() { + return DataUtil.deck.loadJSON(); + } + + static async _pGetAdventureBookIndex(filename, {prop, fnGetUrl}) { + const url = `${Vetools.BASE_SITE_URL}data/${filename}`; + const index = await DataUtil.loadJSON(url); + index[prop].forEach(it=>{ + it._pubDate = new Date(it.published || "1970-01-01"); + it._url = fnGetUrl(it.id); + } + ); + return index; + } + + static async pGetAdventureIndex() { + return this._pGetAdventureBookIndex("adventures.json", { + prop: "adventure", + fnGetUrl: Vetools.getAdventureUrl.bind(Vetools) + }); + } + + static async pGetBookIndex() { + return this._pGetAdventureBookIndex("books.json", { + prop: "book", + fnGetUrl: Vetools.getBookUrl.bind(Vetools) + }); + } + + static _getAdventureBookUrl(type, id) { + return `${Vetools.BASE_SITE_URL}data/${type}/${type}-${id.toLowerCase()}.json`; + } + + static getAdventureUrl(id) { + return this._getAdventureBookUrl("adventure", id); + } + + static getBookUrl(id) { + return this._getAdventureBookUrl("book", id); + } + + static pGetImageUrlFromFluff(fluff) { + if (!fluff?.images?.length) + return; + + const imgEntry = fluff.images[0]; + if (!imgEntry?.href) + return; + + const urlsWarn = []; + const out = fluff.images.first(imgEntry=>{ + const url = this._pGetImageUrlFromFluff_getUrlFromEntry({ + imgEntry + }); + if (!this._isValidImageUrl({ + url + })) { + urlsWarn.push(url); + return null; + } + return url; + } + ); + + if (urlsWarn.length) + ui.notifications.warn(`Image URL${urlsWarn.length === 1 ? "" : "s"} did not have valid extensions: ${urlsWarn.map(it=>`"${it}"`).join(", ")}`); + + return out; + } + + static _pGetImageUrlFromFluff_getUrlFromEntry({imgEntry}) { + if (imgEntry.href.type === "internal") { + return imgEntry.href.path ? `${Vetools.getInternalImageUrl(imgEntry.href.path)}` : null; + } + + if (imgEntry.href.type === "external") { + return imgEntry.href.url ? imgEntry.href.url : null; + } + } + + static _isValidImageUrl({url}) { + return foundry.data.validators.hasFileExtension(url, Object.keys(CONST.IMAGE_FILE_EXTENSIONS)); + } + + static async pHasTokenUrl(entityType, it, opts) { + return (await Vetools._pGetTokenUrl(entityType, it, opts))?.hasToken; + } + + static async pGetTokenUrl(entityType, it, opts) { + return (await Vetools._pGetTokenUrl(entityType, it, opts))?.url; + } + + static _isSaveableToServerUrl(originalUrl) { + return originalUrl && typeof originalUrl === "string" && Vetools._RE_HTTP_URL.test(originalUrl); + } + static _isSaveTypedImagesToServer({imageType="image"}={}) { + switch (imageType) { + case "image": + return Config.get("import", "isSaveImagesToServer"); + case "token": + return Config.get("import", "isSaveTokensToServer"); + default: + throw new Error(`Unhandled type "${imageType}"!`); + } + } + + static async _pGetTokenUrl(entityType, it, {isSilent=false}={}) { + if (it.tokenUrl) + return { + url: it.tokenUrl, + hasToken: true + }; + + const fallbackMeta = { + url: this.getBlankTokenUrl(), + hasToken: false, + }; + + switch (entityType) { + case "monster": + case "vehicle": + case "object": + { + const fnGets = { + "monster": Renderer.monster.getTokenUrl, + "vehicle": Renderer.vehicle.getTokenUrl, + "object": Renderer.object.getTokenUrl, + }; + const fnGet = fnGets[entityType]; + if (!fnGet) + throw new Error(`Missing getter!`); + + if (it.hasToken) + return { + url: fnGet(it), + hasToken: true + }; + if (it._versionBase_hasToken) + return { + url: fnGet({ + name: it._versionBase_name, + source: it._versionBase_source + }), + hasToken: true + }; + + return fallbackMeta; + } + case "trap": + return fallbackMeta; + default: + { + if (isSilent) + return null; + throw new Error(`Unhandled entity type "${entityType}"`); + } + } + } + + static getBlankTokenUrl() { + return UrlUtil.link(`${Renderer.get().baseMediaUrls["img"] || Renderer.get().baseUrl}img/blank.png`); + } + + static getImageUrl(entry) { + if (entry?.href.type === "internal") + return Vetools.getInternalImageUrl(entry.href.path, { + isSkipEncode: true + }); + return entry.href?.url; + } + + static getInternalImageUrl(path, {isSkipEncode=false}={}) { + if (!path) + return null; + const fnEncode = isSkipEncode ? it=>it : encodeURI; + + const out = `${fnEncode(Renderer.get().baseMediaUrls["img"] || Renderer.get().baseUrl)}img/${fnEncode(path)}`; + + if (isSkipEncode) + return out; + return out.replace(/'/g, "%27"); + } + + static async pOptionallySaveImageToServerAndGetUrl(originalUrl, {imageType="image"}={}) { + if (this._isLocalUrl({ + originalUrl + })) + return originalUrl; + if (!this._isSaveTypedImagesToServer({ + imageType + })) + return originalUrl; + return this.pSaveImageToServerAndGetUrl({ + originalUrl + }); + } + + static _isLocalUrl({originalUrl}) { + return new URL(document.baseURI).origin === new URL(originalUrl,document.baseURI).origin; + } + + static getImageSavedToServerUrl({originalUrl=null, path, isSaveToRoot=false}={}) { + if (!path && !this._isSaveableToServerUrl(originalUrl)) + return originalUrl; + + const pathPart = (new URL(path ? `https://example.com/${path}` : originalUrl)).pathname; + return `${isSaveToRoot ? "" : `${Config.get("import", "localImageDirectoryPath")}/`}${decodeURI(pathPart)}`.replace(/\/+/g, "/"); + } + + static _getImageSavedToServerUrlMeta({originalUrl=null, path, isSaveToRoot=false}) { + const cleanOutPath = this.getImageSavedToServerUrl({ + originalUrl, + path, + isSaveToRoot + }); + const serverUrlPathParts = cleanOutPath.split("/"); + const serverUrlDirParts = serverUrlPathParts.slice(0, -1); + const serverUrlDir = serverUrlDirParts.join("/"); + + return { + serverUrl: cleanOutPath, + serverUrlPathParts, + serverUrlDir, + serverUrlDirParts, + }; + } + + static async pSaveImageToServerAndGetUrl({originalUrl=null, blob, force=false, path=null, isSaveToRoot=false}={}) { + if (blob && originalUrl) + throw new Error(`"blob" and "originalUrl" arguments are mutually exclusive!`); + + if (!blob && !this._isSaveableToServerUrl(originalUrl)) + return originalUrl; + + let out; + try { + await Vetools._LOCK_DOWNLOAD_IMAGE.pLock(); + out = await this._pSaveImageToServerAndGetUrl_({ + originalUrl, + blob, + force, + path, + isSaveToRoot + }); + } finally { + Vetools._LOCK_DOWNLOAD_IMAGE.unlock(); + } + return out; + } + + static async _pSaveImageToServerAndGetUrl_({originalUrl=null, blob, force=false, path=null, isSaveToRoot=false}={}) { + if (blob && originalUrl) + throw new Error(`"blob" and "originalUrl" arguments are mutually exclusive!`); + + const {serverUrl, serverUrlPathParts, serverUrlDir, serverUrlDirParts, } = this._getImageSavedToServerUrlMeta({ + originalUrl, + path, + isSaveToRoot + }); + + const {dirListing, isDirExists, isError: isErrorDirListing, } = await this.pGetDirectoryListing({ + originalUrl, + path, + isSaveToRoot + }); + + if (isErrorDirListing) { + const msgStart = `Could not check for existing files when saving imported images to server!`; + if (!force && blob) + throw new Error(msgStart); + + const msg = `${msgStart}${force ? "" : ` The original image URL will be used instead.`}`; + UtilNotifications.notifyOnce({ + type: "warn", + message: msg + }); + return force ? serverUrl : originalUrl; + } + + if (dirListing?.files && dirListing?.files.map(it=>UtilFileBrowser.decodeUrl(it)).includes(serverUrl)) + return serverUrl; + + if (!this._canUploadFiles()) { + if (!force && blob) + throw new Error(`Your permission levels do not allow you to upload files!`); + + const msg = `You have the "Save Imported Images to Server" config option enabled, but your permission levels do not allow you to upload files!${force ? "" : ` The original image URL will be used instead.`}`; + UtilNotifications.notifyOnce({ + type: "warn", + message: msg + }); + return force ? serverUrl : originalUrl; + } + + if (!isDirExists) { + try { + await this._pSaveImageToServerAndGetUrl_pCreateDirectories(serverUrlDirParts); + } catch (e) { + const msgStart = `Could not create required directories when saving imported images to server!`; + if (!force && blob) + throw new Error(msgStart); + + const msg = `${msgStart}${force ? "" : ` The original image URL will be used instead.`}`; + UtilNotifications.notifyOnce({ + type: "warn", + message: msg + }); + return force ? serverUrl : originalUrl; + } + } + + try { + blob = blob || await this._pSaveImageToServerAndGetUrl_pGetBlob(originalUrl); + } catch (e) { + const msg = `Failed to download image "${originalUrl}" when saving imported images to server!${force ? "" : ` The original image URL will be used instead.`} ${VeCt.STR_SEE_CONSOLE}`; + UtilNotifications.notifyOnce({ + type: "warn", + message: msg + }); + console.error(...LGT, e); + return force ? serverUrl : originalUrl; + } + + const name = serverUrlPathParts.last(); + let mimeType = `image/${(name.split(".").last() || "").trim().toLowerCase()}`; + if (mimeType === "image/jpg") + mimeType = "image/jpeg"; + + const resp = await FilePicker.upload("data", serverUrlDir, new File([blob],name,{ + lastModified: Date.now(), + type: mimeType, + },), {}, { + notify: false, + }, ); + if (resp?.path) + return UtilFileBrowser.decodeUrl(resp.path); + + return force ? serverUrl : originalUrl; + } + + static async _pSaveImageToServerAndGetUrl_pGetBlob(originalUrl) { + const isBackend = await UtilBackend.pGetBackendVersion(); + + try { + const blobResp = await fetch(originalUrl); + return blobResp.blob(); + } catch (e) { + if (!isBackend) + throw e; + console.warn(...LGT, `Could not directly load image from ${originalUrl}\u2014falling back on alternate loader (backend mod).`); + } + + const blobResp = await fetch(Config.backendEndpoint, { + method: "post", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + type: "getBinaryData", + url: originalUrl, + }), + }, ); + return blobResp.blob(); + } + + static async _pSaveImageToServerAndGetUrl_pCreateDirectories(serverUrlDirParts) { + if (!serverUrlDirParts.length) + return; + for (let i = 0; i < serverUrlDirParts.length; ++i) { + const dirPartSlice = serverUrlDirParts.slice(0, i + 1); + try { + await FilePicker.createDirectory("data", dirPartSlice.join("/")); + } catch (e) { + if (/EEXIST/.test(`${e}`)) + continue; + throw new Error(e); + } + } + } + + static _canUploadFiles() { + return game.isAdmin || (game.user && game.user.can("FILES_UPLOAD")); + } + + static async pGetDirectoryListing({originalUrl=null, path=null, isSaveToRoot=false, isDirPath=false}) { + if (originalUrl && isDirPath) + throw new Error(`Arguments "originalUrl" and "isDirPath" are mutually exclusive`); + if (!path && isDirPath) + throw new Error(`Argument "isDirPath" requires the "path" argument to be passed!`); + + const {serverUrlDir} = this._getImageSavedToServerUrlMeta({ + originalUrl, + path: path && isDirPath ? `${path}/stub` : path, + isSaveToRoot, + }); + + let dirListing = null; + let isDirExists = false; + let isError = false; + try { + dirListing = await FilePicker.browse("data", serverUrlDir); + if (dirListing?.target) + isDirExists = true; + } catch (e) { + isError = !/Directory .*? does not exist/.test(`${e}`); + } + + return { + dirListing, + isDirExists, + isError, + }; + } + + static async pGetAllSpells({isFilterNonStandard=false, additionalSourcesPrerelease=[], additionalSourcesBrew=[], isIncludeLoadedBrew=false, isIncludeLoadedPrerelease=false, isApplyBlocklist=false, }={}, ) { + let spells = MiscUtil.copyFast(await DataUtil.spell.pLoadAll()); + if (isFilterNonStandard) + spells = spells.filter(sp=>!SourceUtil.isNonstandardSource(sp.source)); + + if (isIncludeLoadedPrerelease) { + const prerelease = await PrereleaseUtil.pGetBrewProcessed(); + if (prerelease.spell?.length) + spells = spells.concat(prerelease.spell); + } + + if (isIncludeLoadedBrew) { + const brew = await BrewUtil2.pGetBrewProcessed(); + if (brew.spell?.length) + spells = spells.concat(brew.spell); + } + + const pHandleAdditionalSources = async({additionalSources, pFnLoad})=>{ + for (const src of additionalSources) { + const json = await pFnLoad(src); + if (!json) + continue; + if (json.spell?.length) + spells = spells.concat(json.spell); + } + } + ; + + if (additionalSourcesPrerelease?.length) + await pHandleAdditionalSources({ + additionalSources: additionalSourcesPrerelease, + pFnLoad: DataUtil.pLoadPrereleaseBySource.bind(DataUtil) + }); + if (additionalSourcesBrew?.length) + await pHandleAdditionalSources({ + additionalSources: additionalSourcesBrew, + pFnLoad: DataUtil.pLoadBrewBySource.bind(DataUtil) + }); + + if (isApplyBlocklist) { + spells = spells.filter(sp=>!ExcludeUtil.isExcluded(UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_SPELLS](sp), "spell", sp.source, { + isNoCount: true + }, ), ); + } + + spells.forEach(sp=>Renderer.spell.initBrewSources(sp)); + + return { + spell: spells + }; + } + + static async pGetAllCreatures(isFilterNonStandard=false) { + let creatures = await DataUtil.monster.pLoadAll(); + + if (isFilterNonStandard) + creatures = creatures.filter(mon=>!SourceUtil.isNonstandardSource(mon.source)); + + return { + monster: creatures + }; + } + + static async _pGetPrereleaseBrewIndices() { + const out = { + sourcePrerelease: {}, + propPrerelease: {}, + metaPrerelease: {}, + + sourceBrew: {}, + propBrew: {}, + metaBrew: {}, + }; + + try { + const [sourceIndexPrerelease,propIndexPrerelease,metaIndexPrerelease, + sourceIndexBrew,propIndexBrew,metaIndexBrew,] = await Promise.all([ + DataUtil.prerelease.pLoadSourceIndex(Config.get("dataSources", "basePrereleaseUrl")), + DataUtil.prerelease.pLoadPropIndex(Config.get("dataSources", "basePrereleaseUrl")), + DataUtil.prerelease.pLoadMetaIndex(Config.get("dataSources", "basePrereleaseUrl")), + DataUtil.brew.pLoadSourceIndex(Config.get("dataSources", "baseBrewUrl")), + DataUtil.brew.pLoadPropIndex(Config.get("dataSources", "baseBrewUrl")), + DataUtil.brew.pLoadMetaIndex(Config.get("dataSources", "baseBrewUrl")), ]); + + out.sourcePrerelease = sourceIndexPrerelease; + out.propPrerelease = propIndexPrerelease; + out.metaPrerelease = metaIndexPrerelease; + + out.sourceBrew = sourceIndexBrew; + out.propBrew = propIndexBrew; + out.metaBrew = metaIndexBrew; + } catch (e) { + ui.notifications.error(`Failed to load prerelease/homebrew index! ${VeCt.STR_SEE_CONSOLE}`); + setTimeout(()=>{ throw e; } ); + } + + return out; + } + + static async pGetPrereleaseSources(...dirs) { + return this._pGetPrereleaseBrewSources({ + dirs, + brewUtil: PrereleaseUtil, + indexProp: Vetools.PRERELEASE_INDEX__PROP, + indexMeta: Vetools.PRERELEASE_INDEX__META, + configKey: "basePrereleaseUrl", + }); + } + + /** + * @param {string[]} ...dirs + * @returns {Promise<{name:string, url:string, abbreviations:string[]}>} + */ + static async pGetBrewSources(...dirs) { + return this._pGetPrereleaseBrewSources({ + dirs, + brewUtil: BrewUtil2, + indexProp: Vetools.BREW_INDEX__PROP, + indexMeta: Vetools.BREW_INDEX__META, + configKey: "baseBrewUrl", + }); + } + + /** + * @param {{dirs:string[], brewUtil:any, indexProp:any, indexMeta:any, configKey:string}} + * @returns {Promise} + */ + static async _pGetPrereleaseBrewSources({dirs, brewUtil, indexProp, indexMeta, configKey}) { + const urlRoot = Config.get("dataSources", configKey); + + let paths; + if (dirs.includes("*")) { + paths = Object.values(indexProp).map(obj=>Object.keys(obj)).flat().unique(); + } else { + paths = dirs.map(dir=>Object.keys(indexProp[brewUtil.getDirProp(dir)] || {})).flat().unique(); + } + + return paths.map((path)=>{ + const metaName = UrlUtil.getFilename(path); + //if(!urlRoot){console.error(`Failed to get urlRoot from 'dataSources' with key '${configKey}'. Is config uninitialized?`);} + return ({ + url: brewUtil.getFileUrl(path, urlRoot), + name: this._getPrereleaseBrewName(path), + abbreviations: indexMeta[metaName]?.a || [], + }); + } + ).sort((a,b)=>SortUtil.ascSortLower(a.name, b.name)); + } + + static _getPrereleaseBrewName(brewPath) { + return brewPath.split("/").slice(-1).join("").replace(/\.json$/i, ""); + } + + static _LOCAL_PRERELEASE_SOURCE_SEEN_URLS = new Set(); + static async pGetLocalPrereleaseSources(...dirs) { + return this._pGetLocalPrereleaseBrewSources({ + brewUtil: PrereleaseUtil, + dirs, + displayName: "prerelease", + configKeyLocal: "localPrerelease", + configKeyIsLoadIndex: "isLoadLocalPrereleaseIndex", + configKeyIsUseIndex: "isUseLocalPrereleaseIndexJson", + configKeyDirectoryPath: "localPrereleaseDirectoryPath", + setSeenUrls: this._LOCAL_PRERELEASE_SOURCE_SEEN_URLS, + }); + } + + static _LOCAL_BREW_SOURCE_SEEN_URLS = new Set(); + static async pGetLocalBrewSources(...dirs) { + return this._pGetLocalPrereleaseBrewSources({ + brewUtil: BrewUtil2, + dirs, + displayName: "homebrew", + configKeyLocal: "localHomebrew", + configKeyIsLoadIndex: "isLoadLocalHomebrewIndex", + configKeyIsUseIndex: "isUseLocalHomebrewIndexJson", + configKeyDirectoryPath: "localHomebrewDirectoryPath", + setSeenUrls: this._LOCAL_BREW_SOURCE_SEEN_URLS, + }); + } + static async _pGetLocalPrereleaseBrewSources({brewUtil, dirs, displayName, configKeyLocal, configKeyIsLoadIndex, configKeyIsUseIndex, configKeyDirectoryPath, setSeenUrls}) { + try { + const listLocal = await this._pGetLocalPrereleaseBrewList({ + displayName, + configKeyIsLoadIndex, + configKeyIsUseIndex, + configKeyDirectoryPath, + }); + + const allFilenames = [...(listLocal || []), ...(Config.get("dataSources", configKeyLocal) || []), ]; + + if (!allFilenames.length) + return []; + + await allFilenames.pSerialAwaitMap(async name=>{ + if (setSeenUrls.has(name)) + return; + setSeenUrls.add(name); + await brewUtil.pAddBrewFromUrl(name, { + isLazy: true + }); + } + ); + await brewUtil.pAddBrewsLazyFinalize(); + + const brews = await allFilenames.pSerialAwaitMap(async name=>({ + url: name, + data: await DataUtil.loadJSON(name), + name: this._getPrereleaseBrewName(name), + })); + + const desiredProps = new Set(dirs.map(dir=>brewUtil.getDirProp(dir))); + + return brews.filter(({data})=>{ + if (desiredProps.has("*")) + return true; + + const propsInBrew = new Set([...Object.keys(data || {}).filter(it=>!it.startsWith("_")), ...Object.keys(data?._meta?.includes || {}), ]); + + return [...desiredProps].some(it=>propsInBrew.has(it)); + } + ).map(it=>{ + it.abbreviations = (it.data?._meta?.sources || []).map(it=>it.abbreviation).filter(Boolean); + return it; + } + ).map(({name, url, abbreviations})=>({ + name, + url, + abbreviations + })); + } catch (e) { + const msg = `Failed to load local homebrew index!`; + console.error(...LGT, msg, e); + ui.notifications.error(`${msg} ${VeCt.STR_SEE_CONSOLE}`); + } + return []; + } + + static async _pGetLocalPrereleaseBrewList({displayName, configKeyIsLoadIndex, configKeyIsUseIndex, configKeyDirectoryPath}) { + if (!Config.get("dataSources", configKeyIsLoadIndex)) + return null; + + const isUseIndexJson = Config.get("dataSources", configKeyIsUseIndex); + + if (isUseIndexJson) { + const indexUrl = `${Config.get("dataSources", configKeyDirectoryPath)}/index.json`.replace(/\/+/g, "/"); + const index = await DataUtil.loadJSON(indexUrl); + if (!index?.toImport) + return []; + return index.toImport.map(it=>{ + if (Vetools._RE_HTTP_URL.test(it)) + return it; + + return [...indexUrl.split("/").slice(0, -1), it].join("/"); + } + ); + } + + try { + const existingFiles = await FilePicker.browse("data", Config.get("dataSources", configKeyDirectoryPath)); + if (!existingFiles?.files?.length) + return null; + + return existingFiles.files.map(it=>decodeURIComponent(it)); + } catch (e) { + const ptReason = /You do not have permission to browse the host file system/i.test(e.message) ? `You do not have "Use File Browser" permissions!` : `Does the ${isUseIndexJson ? "file" : "directory"} "/${Config.get("dataSources", configKeyDirectoryPath)}${isUseIndexJson ? "/index.json" : ""}" exist?`; + const msg = `Failed to load local ${displayName}${isUseIndexJson ? " index" : ""}! ${ptReason}`; + console.error(...LGT, msg, e); + ui.notifications.error(`${msg} ${VeCt.STR_SEE_CONSOLE}`); + return null; + } + } + + static async pGetSpellSideData() { + return DataUtil.loadJSON(`${Vetools.BASE_SITE_URL}data/spells/foundry.json`); + } + static async pGetOptionalFeatureSideData() { + return DataUtil.loadJSON(`${Vetools.BASE_SITE_URL}data/foundry-optionalfeatures.json`); + } + static async pGetClassSubclassSideData() { + return DataUtil.loadJSON(`${Vetools.BASE_SITE_URL}data/class/foundry.json`); + } + static async pGetRaceSideData() { + return DataUtil.loadJSON(`${Vetools.BASE_SITE_URL}data/foundry-races.json`); + } + static async pGetItemSideData() { + return DataUtil.loadJSON(`${Vetools.BASE_SITE_URL}data/foundry-items.json`); + } + static async pGetFeatSideData() { + return DataUtil.loadJSON(`${Vetools.BASE_SITE_URL}data/foundry-feats.json`); + } + static async pGetRewardSideData() { + return DataUtil.loadJSON(`${Vetools.BASE_SITE_URL}data/foundry-rewards.json`); + } + static async pGetActionSideData() { + return DataUtil.loadJSON(`${Vetools.BASE_SITE_URL}data/foundry-actions.json`); + } + static async pGetVehicleUpgradeSideData() { + return DataUtil.loadJSON(`${Vetools.BASE_SITE_URL}data/foundry-vehicles.json`); + } + static async pGetCreatureSideData() { + return DataUtil.loadJSON(`${Vetools.BASE_SITE_URL}data/bestiary/foundry.json`); + } + static async pGeBackgroundSideData() { + return DataUtil.loadJSON(`${Vetools.BASE_SITE_URL}data/foundry-backgrounds.json`); + } + static async pGetPsionicsSideData() { + return DataUtil.loadJSON(`${Vetools.BASE_SITE_URL}data/foundry-psionics.json`); + } + + static async pGetConditionDiseaseSideData() { + return {} || DataUtil.loadJSON(`${Vetools.BASE_SITE_URL}data/foundry-conditionsdiseases.json`); + } + static async pGetObjectSideData() { + return {} || DataUtil.loadJSON(`${Vetools.BASE_SITE_URL}data/foundry-objects.json`); + } + static async pGetVehicleSideData() { + return {} || DataUtil.loadJSON(`${Vetools.BASE_SITE_URL}data/foundry-vehicles.json`); + } + static async pGetCharCreationOptionSideData() { + return {} || DataUtil.loadJSON(`${Vetools.BASE_SITE_URL}data/foundry-charcreationoptions.json`); + } + static async pGetCultBoonSideData() { + return {} || DataUtil.loadJSON(`${Vetools.BASE_SITE_URL}data/foundry-cultsboons.json`); + } + static async pGetTrapHazardSideData() { + return {} || DataUtil.loadJSON(`${Vetools.BASE_SITE_URL}data/foundry-trapshazards.json`); + } + static async pGetDeckSideData() { + return {} || DataUtil.loadJSON(`${Vetools.BASE_SITE_URL}data/foundry-decks.json`); + } + static async pGetDeitySideData() { + return {} || DataUtil.loadJSON(`${Vetools.BASE_SITE_URL}data/foundry-deities.json`); + } + static async pGetTableSideData() { + return {} || DataUtil.loadJSON(`${Vetools.BASE_SITE_URL}data/foundry-tables.json`); + } + static async pGetLanguageSideData() { + return {} || DataUtil.loadJSON(`${Vetools.BASE_SITE_URL}data/foundry-languages.json`); + } + static async pGetRecipeSideData() { + return {} || DataUtil.loadJSON(`${Vetools.BASE_SITE_URL}data/foundry-recipes.json`); + } + static async pGetVariantruleSideData() { + return {} || DataUtil.loadJSON(`${Vetools.BASE_SITE_URL}data/foundry-variantrules.json`); + } + + static async pGetCreatureFeatureSideData() { + return {} || DataUtil.loadJSON(`${Vetools.BASE_SITE_URL}data/foundry-todo.json`); + } + static async pGetObjectFeatureSideData() { + return {} || DataUtil.loadJSON(`${Vetools.BASE_SITE_URL}data/foundry-todo.json`); + } + static async pGetVehicleFeatureSideData() { + return {} || DataUtil.loadJSON(`${Vetools.BASE_SITE_URL}data/foundry-todo.json`); + } + static async pGetTrapFeatureSideData() { + return {} || DataUtil.loadJSON(`${Vetools.BASE_SITE_URL}data/foundry-todo.json`); + } + + static getModuleDataUrl(filename) { + if(SETTINGS.LOCALPATH_REDIRECT){return `data/${filename}`;} + return `modules/${SharedConsts.MODULE_ID}/data/${filename}`; + } + + static async pGetIconLookup(entityType) { + return DataUtil.loadJSON(this.getModuleDataUrl(`icon-${entityType}s.json`)); + } + + static get BASE_SITE_URL() { + if (this._isCustomBaseSiteUrl()) { + return Util.getCleanServerUrl(Config.get("dataSources", "baseSiteUrl")); + } + return Vetools._BASE_SITE_URL; + } + + static _isCustomBaseSiteUrl() { + const val = Config.get("dataSources", "baseSiteUrl"); + return !!(val && val.trim()); + } + + static get DATA_URL_FEATS() { + return `${Vetools.BASE_SITE_URL}data/feats.json`; + } + static get DATA_URL_BACKGROUNDS() { + return `${Vetools.BASE_SITE_URL}data/backgrounds.json`; + } + static get DATA_URL_VARIANTRULES() { + return `${Vetools.BASE_SITE_URL}data/variantrules.json`; + } + static get DATA_URL_PSIONICS() { + return `${Vetools.BASE_SITE_URL}data/psionics.json`; + } + static get DATA_URL_OPTIONALFEATURES() { + return `${Vetools.BASE_SITE_URL}data/optionalfeatures.json`; + } + static get DATA_URL_CONDITIONSDISEASES() { + return `${Vetools.BASE_SITE_URL}data/conditionsdiseases.json`; + } + static get DATA_URL_VEHICLES() { + return `${Vetools.BASE_SITE_URL}data/vehicles.json`; + } + static get DATA_URL_REWARDS() { + return `${Vetools.BASE_SITE_URL}data/rewards.json`; + } + static get DATA_URL_OBJECTS() { + return `${Vetools.BASE_SITE_URL}data/objects.json`; + } + static get DATA_URL_DEITIES() { + return `${Vetools.BASE_SITE_URL}data/deities.json`; + } + static get DATA_URL_RECIPES() { + return `${Vetools.BASE_SITE_URL}data/recipes.json`; + } + static get DATA_URL_CHAR_CREATION_OPTIONS() { + return `${Vetools.BASE_SITE_URL}data/charcreationoptions.json`; + } + static get DATA_URL_CULTSBOONS() { + return `${Vetools.BASE_SITE_URL}data/cultsboons.json`; + } + static get DATA_URL_ACTIONS() { + return `${Vetools.BASE_SITE_URL}data/actions.json`; + } + static get DATA_URL_LANGUAGES() { + return `${Vetools.BASE_SITE_URL}data/languages.json`; + } + static get DATA_URL_TRAPS_HAZARDS() { + return `${Vetools.BASE_SITE_URL}data/trapshazards.json`; + } +} +Vetools._RE_HTTP_URL = /(^https?:\/\/)/; +Vetools._BASE_SITE_URL = "https://5etools-mirror-2.github.io/"; +Vetools.BESTIARY_FLUFF_INDEX = null; +Vetools.BESTIARY_TOKEN_LOOKUP = null; +Vetools._CACHED_GET_ROLLABLE_ENTRY_DICE = null; +Vetools._PATCHED_GET_ROLLABLE_ENTRY_DICE = null; +Vetools._CACHED_MONSTER_DO_BIND_COMPACT_CONTENT_HANDLERS = null; +Vetools._CACHED_RENDERER_HOVER_CACHE_AND_GET = null; +Vetools._LOCK_DOWNLOAD_IMAGE = new VeLock(); +Vetools._VET_SOURCE_LOOKUP = {}; \ No newline at end of file diff --git a/charbuilder/js/roll.js b/charbuilder/js/roll.js new file mode 100644 index 0000000..ca4a90c --- /dev/null +++ b/charbuilder/js/roll.js @@ -0,0 +1,29 @@ +class Roll{ + + constructor(formula){ + this._formula = formula; + this._roller = new rpgDiceRoller.DiceRoller(); + } + + /** + * @param {{async:boolean}} options right now async does nothing, we always run sync + * @returns {any} + */ + evaluate(options){ + let roll = this._roller.roll(this._formula); + let total = roll.total; + let terms = []; + let ex = roll.export(rpgDiceRoller.exportFormats.OBJECT); + for(let r of ex.rolls){ + let results = []; + for(let r2 of r.rolls){ + results.push({result: r2.initialValue}); + } + terms.push({results:results}); + } + this._roller.clearLog(); + this.total = total; + this.terms = terms; + } + +} \ No newline at end of file diff --git a/charbuilder/js/sheet.js b/charbuilder/js/sheet.js new file mode 100644 index 0000000..7fb594a --- /dev/null +++ b/charbuilder/js/sheet.js @@ -0,0 +1,1567 @@ +class ActorCharactermancerSheet extends ActorCharactermancerBaseComponent{ + + /** + * What to display on the sheet: + * + * Character name + * Class + * Race + * Size + * Background - Just the name, right? So custom background is just "Custom Background", nothing else + * Ability scores (and modifiers) - Not sure if we can get ability score improvements from items, feats (and maybe subclass features?) to work + * Hit points - This needs CON modifier. Not sure if items, feats (and maybe subclass features?) can be considered + * Speed - Should be pulled from race. Not sure if we can consider items, class features, feats etc + * AC - Not sure if we can consider armor, features, items, etc. Unarmored defense? + * Darkvision + * Proficiency bonus + * Inititative bonus - Not sure if we can consider items, class features, feats + * Proficiencies - armor, weapons, tools, saves, skills + * Passive proficiency + * Languages + * Skills (and their modifiers, with * for proficient, and ** for expertise) (what about half proficiency?) + * Saves (modifiers, with * for proficient) + * Encumberance (lift & carry) + * Item list - (armor has AC in parenthesis) - then total weight + * Coins + total weight + * Spell list (known cantrips, lvl1, lvl2, etc. each category shows number of spell slots) + * + * Things we probably can't display on the sheet: + * + * Race features (too long, not sure what would be useful info) + * Class features (too long, not sure what would be useful info) + * Subclass features (too long, not sure what would be useful info) + * Warlock pact magic + * Battle master manouvers + * Resistances (comes from items and subclass features, needs serious parsing) + * Immunities + * Vulnerabilities + * Ranger pets + * Druid shapes + * Cleric channel divinity + * Rogue sneak attack + * Any mystic related stuff + * Any artificer related stuff + * Extra attacks + * Ki points + * + */ + + /** + * @param {{parent:CharacterBuilder}} parentInfo + */ + constructor(parentInfo) { + parentInfo = parentInfo || {}; + super(); + this._actor = parentInfo.actor; + this._data = parentInfo.data; //data is an object containing information about all classes, subclasses, feats, etc + this._parent = parentInfo.parent; + this._tabSheet = parentInfo.tabSheet; + + } + render({charInfo}){ + ActorCharactermancerSheet.characterName = null; + if(!!charInfo?.character?.about?.name?.length){ActorCharactermancerSheet.characterName = charInfo.character.about.name;} + const tabSheet = this._tabSheet?.$wrpTab; + if (!tabSheet) { return; } + const wrapper = $$`
    `; + //const noFeatsWarningLbl = $("
    No feats are available for your current build.
    ").appendTo(wrapper); + + const sheet = $$`
    `.appendTo(wrapper); + + const $form = $$`
    `; + const $lblClass = $$``; + const $lblRace = $$``; + const $lblBackground = $$``; + const $inputName = $$``; + const $inputAlignment = $$``; + + + const headerSection = $$`
    + ${$inputName} +
    +
    +
      +
    • + ${$lblClass} +
    • +
    • + ${$lblBackground} +
    • +
    • + ${$lblRace} +
    • +
    • + ${$inputAlignment} +
    • +
    +
    +
    `.appendTo($form); + + const $sectionAttributeScores = $$`
    `; + const $lblProfBonus = $$``; + const $lblPassivePerception = $$``; + const $sectionSkills = $$`
      `; + const $lblArmorClass = $$``; + const $lblInitiative = $$``; + const $sectionSaves = $$`
        `; + const $lblSpeed = $$``; + const $armorWornText = $$``; + const $spanWeaponProf = $$``; + const $spanArmorProf = $$``; + const $spanToolsProf = $$``; + const $spanLanguages = $$``; + const $divFeatures = $$`
        `; + const $divClassFeatures = $$`
        `; + const $divSubclassFeatures = $$`
        `; + const $divSpellAttackMod = $$`
        `; + const $divSpellDC = $$`
        `; + const $divSpells = $$`
        ${$divSpellAttackMod}${$divSpellDC}
        `; + const $divFeatFeatures = $$`
        `; + const $divBackgroundFeatures = $$`
        `; + const $divEquipment = $$`
        `; + const $attacksTextArea = $$`
        `; + const $lblMaxHP = $$``; + const $lblHitDice = $$``; + const $lblCoinage = $$``; + const $divCarry = $$`
        `; + + const mainSection = $$`
        +
        +
        + ${$sectionAttributeScores} +
        +
        +
        + +
        + ${$lblProfBonus} +
        +
        + ${$sectionSaves} +
        + Saving Throws +
        +
        +
        + ${$sectionSkills} +
        Skills
        +
        +
        +
        +
        +
        + +
        + ${$lblPassivePerception} +
        +
        + +
        +
        Armor: ${$spanArmorProf}
        +
        Weapons: ${$spanWeaponProf}
        +
        Tools: ${$spanToolsProf}
        +
        Languages: ${$spanLanguages}
        +
        +
        +
        +
        +
        +
        +
        + ${$lblArmorClass} +
        +
        +
        +
        + ${$lblInitiative} +
        +
        +
        +
        + ${$lblSpeed} +
        +
        +
        Armor worn: ${$armorWornText}
        +
        +
        + ${$lblMaxHP} +
        +
        +
        +
        + ${$lblHitDice} +
        +
        +
        +
        +
        + + ${$attacksTextArea} +
        +
        +
        +
        + + ${$divEquipment} +
        Currency: ${$lblCoinage}
        +
        +
        + + ${$divCarry} +
        +
        +
        +
        + +
        + ${$divBackgroundFeatures} + ${$divClassFeatures} + ${$divSubclassFeatures} + ${$divFeatFeatures} +
        +
        +
        +
        +
        + +
        + ${$divSpells} +
        +
        +
        +
        +
        `; + mainSection.appendTo($form); + + $form.appendTo(sheet); + + + const $wrpDisplay = $(`
        `).appendTo(wrapper); + + //#region Class + //When class changes, redraw the elements + const hkClass = () => { + $divClassFeatures.empty(); + $divSubclassFeatures.empty(); + $lblProfBonus.text("+2"); //Default, even for lvl 0 characters + let classData = ActorCharactermancerSheet.getClassData(this._parent.compClass); + //If there are no classes selected, just print none and return + let textOut = ""; + if(!classData?.length){ $lblClass.html(textOut); return; } + for(let i = 0; i < classData.length; ++i){ + const d = classData[i]; + if(d.isDeleted){continue;} + textOut += `${textOut.length > 0? " / " : ""}${d.cls.name} ${d.targetLevel}${d.sc? ` (${d.sc.name})` : ""}`; + //Try to get features from class + let classFeaturesText = ""; + + const tryPrintFeature = (feature, text, bannedFeatureNames=[], bannedLoadedsNames=[]) => { + if(feature.level > d.targetLevel){return text;} + let drawParentFeature = true; + for(let l of feature.loadeds){ + if(l.type=="optionalfeature" && !l.isRequiredOption){continue;} + drawParentFeature = false; + if(bannedLoadedsNames.includes(l.entity.name)){continue;} + if(l.entity.level > d.targetLevel){continue;} //Must not be from a higher level than we are + text += `${text.length > 0? ", " : ""}${l.entity.name}`; + } + if(!drawParentFeature || bannedFeatureNames.includes(feature.name)){return text;} + text += `${text.length > 0? ", " : ""}${feature.name}`; + return text; + } + + for(let f of d.cls.classFeatures){ + classFeaturesText = tryPrintFeature(f, classFeaturesText); + } + if(classFeaturesText.length > 0){ + $$`
        ${d.cls.name} Class Features:
        `.appendTo($divClassFeatures); + $$`
        ${classFeaturesText}
        `.appendTo($divClassFeatures); + } + + if(d.sc){ + let subclassFeaturesText = ""; + for(let f of d.sc.subclassFeatures){ + //Do not print subclass features named after the subclass (normally the first feature) + + subclassFeaturesText = tryPrintFeature(f, subclassFeaturesText, [d.sc.name], [d.sc.name]); + } + if(subclassFeaturesText.length > 0){ + $$`
        ${d.sc.name} Subclass Features:
        `.appendTo($divSubclassFeatures); + $$`
        ${subclassFeaturesText}
        `.appendTo($divSubclassFeatures); + } + + } + } + $lblClass.html(textOut); + + //Calculate proficiency bonus + const profBonus = this._getProfBonus(this._parent.compClass); + $lblProfBonus.text(`${(profBonus > 0? "+":"")}${profBonus}`); + }; + //We need some hooks to redraw class info + this._parent.compClass.addHookBase("class_ixPrimaryClass", hkClass); + this._parent.compClass.addHookBase("class_ixMax", hkClass); + this._parent.compClass.addHookBase("class_totalLevels", hkClass); + this._parent.compClass.addHookBase("class_pulseChange", hkClass); //This also senses when subclass is changed + hkClass(); + //#endregion + + //#region Race + //When race version changes, redraw the elements + const hkRace = () => { + let curRace = this.getRace_(); + const n = curRace? curRace.name : "None"; + $lblRace.text(n); + }; + this._parent.compRace.addHookBase("race_ixRace_version", hkRace); + hkRace(); + //#endregion + + //#region Background + const hkBackground = () => { + $divBackgroundFeatures.empty(); + let curBackground = this.getBackground(); + const n = curBackground? curBackground.name : "None"; + $lblBackground.text(n); + + //Lets also do some info for the personalities, ideals, bonds and flaws + if(!curBackground){return;} + $$`
        ${curBackground.name} Background:
        `.appendTo($divBackgroundFeatures); + //Get feature from background + let foundFeature = ""; + for(let i = 0; i < curBackground.entries.length && foundFeature.length < 1; ++i){ + let e = curBackground.entries[i]; + if(e.type=="entries" && e.name.startsWith("Feature: ")){ + foundFeature = e.name.substr(("Feature: ").length); + } + } + let list = $$`
          `; + if(foundFeature){ + $$`
        • Feature: ${foundFeature}
        • `.appendTo(list); + } + list.appendTo($divBackgroundFeatures); + let characteristics = this.getBackgroundChoices(); + let bonds = []; + let flaws = []; + let ideals = []; + let personalityTraits = []; + for(let key of Object.keys(characteristics)){ + const val = characteristics[key]; + if(!val){continue;} + key = key.toLowerCase(); + if(key.startsWith("bond")){bonds.push(val);} + else if(key.startsWith("flaw")){flaws.push(val);} + else if(key.startsWith("ideal")){ideals.push(val);} + else if(key.startsWith("personalitytrait")){personalityTraits.push(val);} + } + + const createSubElements = (strings, title) => { + let firstString = ""; + let subElements = []; + for(let s of strings){ + if(firstString.length<1){ + firstString = s; + } + else{ + subElements.push($$`
          ${s}
          `); + } + } + + const mainElement = $$`
          ${title}${firstString}
          `; + for(let sub of subElements){mainElement.append(sub);} + return $$`
        • ${mainElement}
        • `; + } + if(personalityTraits.length>0){ + createSubElements(personalityTraits, `Trait${personalityTraits.length>1?"s":""}: `).appendTo(list); + } + if(ideals.length > 0){ + createSubElements(ideals, `Ideal${ideals.length>1?"s":""}: `).appendTo(list); + } + if(bonds.length > 0){ + createSubElements(bonds, `Bond${bonds.length>1?"s":""}: `).appendTo(list); + } + if(flaws.length > 0){ + createSubElements(flaws, `Flaw${flaws.length>1?"s":""}: `).appendTo(list); + } + }; + this._parent.compBackground.addHookBase("background_pulseBackground", hkBackground); + hkBackground(); + //#endregion + + //#region Ability Scores + const hkAbilities = () => { + let totals = this.test_grabAbilityScoreTotals(this._parent.compAbility); + + + //NEW UI STUFF + const createAbilityScoreElement = (label, score) => { + const modifier = Math.floor((score-10) / 2); + return $$`
        • +
          + +
          +
          + +
          +
        • `; + } + $sectionAttributeScores.empty(); + const ul = $$`
            `; + ul.append(createAbilityScoreElement("Strength", totals.values.str)); + ul.append(createAbilityScoreElement("Dexterity", totals.values.dex)); + ul.append(createAbilityScoreElement("Constitution", totals.values.con)); + ul.append(createAbilityScoreElement("Wisdom", totals.values.wis,)); + ul.append(createAbilityScoreElement("Intelligence", totals.values.int)); + ul.append(createAbilityScoreElement("Charisma", totals.values.cha)); + ul.appendTo($sectionAttributeScores); + + //Calculate saving throws + let saves = this._grabSavingThrowProficiencies(); + const createSavingThrowElement = (label, attr, totals, proficiencies, profBonus) => { + + const isProficient = !!proficiencies[attr]; + const modifier = this._getAbilityModifier(attr, totals.values[attr]) + (isProficient? profBonus : 0); + const checkbox = $$``; + + let iconClass = "fa-regular fa-circle"; + if(isProficient){iconClass = "fas fa-fw fa-check";} + //else if(isExpertised){iconClass = "fas fa-fw fa-check-double";} + const icon = $$``; + + return $$`
          • + + + ${icon} +
          • `; + } + + const profBonus = this._getProfBonus(this._parent.compClass); + $sectionSaves.empty(); + $sectionSaves.append(createSavingThrowElement("Strength", "str", totals, saves, profBonus)); + $sectionSaves.append(createSavingThrowElement("Dexterity", "dex", totals, saves, profBonus)); + $sectionSaves.append(createSavingThrowElement("Constitution", "con", totals, saves, profBonus)); + $sectionSaves.append(createSavingThrowElement("Wisdom", "wis", totals, saves, profBonus)); + $sectionSaves.append(createSavingThrowElement("Intelligence", "int", totals, saves, profBonus)); + $sectionSaves.append(createSavingThrowElement("Charisma", "cha", totals, saves, profBonus)); + }; + this._parent.compAbility.compStatgen.addHookBase("common_export_str", hkAbilities); + this._parent.compAbility.compStatgen.addHookBase("common_export_dex", hkAbilities); + this._parent.compAbility.compStatgen.addHookBase("common_export_con", hkAbilities); + this._parent.compAbility.compStatgen.addHookBase("common_export_int", hkAbilities); + this._parent.compAbility.compStatgen.addHookBase("common_export_wis", hkAbilities); + this._parent.compAbility.compStatgen.addHookBase("common_export_cha", hkAbilities); + this._parent.compAbility.compStatgen.addHookBase("common_pulseAsi", hkAbilities); + hkAbilities(); + //#endregion + + //#region HP, Speed, Initiative + const hkHpSpeed = () => { + //Let's try to estimate HP + //Grab constitution score + const conMod = this._getAbilityModifier("con"); + const dexMod = this._getAbilityModifier("dex"); + //Grab HP increase mode from class component (from each of the classes!) + const classList = ActorCharactermancerSheet.getClassData(this._parent.compClass); + let hpTotal = 0; //Calculate max + let levelTotal = 0; + let hitDiceInfo = {}; + for(let ix = 0; ix < classList.length; ++ix){ + const data = classList[ix]; + if(!data.cls){continue;} + //A problem is we dont know what hp increase mode the class is using when a class is first picked (and this hook fires) + //This is because the components that handle that choice arent built yet + //So we will need a backup + let hpMode = -1; let customFormula = ""; + const hpModeComp = this._parent.compClass._compsClassHpIncreaseMode[ix]; + const hpInfoComp = this._parent.compClass._compsClassHpInfo[ix]; + const targetLevel = data.targetLevel || 1; + + if(hpModeComp && hpInfoComp){ + const formData = hpModeComp.pGetFormData(); + if(formData.isFormComplete){ + hpMode = formData.data.mode; + customFormula = formData.data.formula; + } + } + if(hpMode<0){ //Fallback + hpMode = Config.get("importClass", "hpIncreaseMode") ?? ConfigConsts.C_IMPORT_CLASS_HP_INCREASE_MODE__TAKE_AVERAGE; + customFormula = Config.get("importClass", "hpIncreaseModeCustomRollFormula") ?? "(2 * @hd.number)d(@hd.faces / 2)"; + } + + const hp = ActorCharactermancerSheet.calcHitPointsAtLevel(data.cls.hd.number, data.cls.hd.faces, targetLevel, hpMode, customFormula); + hpTotal += hp; + levelTotal += targetLevel; + + if(!hitDiceInfo[data.cls.hd.faces]){ + hitDiceInfo[data.cls.hd.faces] = data.cls.hd.number; + } + else{ + hitDiceInfo[data.cls.hd.faces] += data.cls.hd.number; + } + } + + hpTotal += (conMod * levelTotal); + + $lblMaxHP.text(hpTotal); + $lblHitDice.empty(); + for(let diceSize of Object.keys(hitDiceInfo)){ + let str = hitDiceInfo[diceSize]+"d"+diceSize; + $$`
            ${str}
            `.appendTo($lblHitDice); + } + + + const scoreInitiative = dexMod; + $lblInitiative.html(`${scoreInitiative>=0?"+"+scoreInitiative : scoreInitiative}`); + + const speedFt = 30; + $lblSpeed.html(`${speedFt}`); + }; + this._parent.compClass.addHookBase("class_ixMax", hkHpSpeed); + this._parent.compClass.addHookBase("class_totalLevels", hkHpSpeed); + this._parent.compAbility.compStatgen.addHookBase("common_export_con", hkHpSpeed); + this._parent.compAbility.compStatgen.addHookBase("common_export_dex", hkHpSpeed); + this._parent.compClass.addHookBase("class_pulseChange", hkHpSpeed); + //needs a hook here in case any of the classes change their HP mode + hkHpSpeed(); + //#endregion + + //#region Proficiencies + //#region Skills + const hkSkills = () => { + $sectionSkills.empty(); + //We need to get the proficiency bonus, which is based upon combined class levels + const profBonus = this._getProfBonus(this._parent.compClass); + //We now need to get the names of all skill proficiencies + const proficientSkills = this._grabSkillProficiencies(); + const allSkillNames = Parser.SKILL_TO_ATB_ABV; + for(let skillName of Object.keys(allSkillNames)){ + //Get the modifier for the ability score + let score = this._getAbilityModifier(Parser.SKILL_TO_ATB_ABV[skillName]); + //Get proficiency / expertise if we are proficient in the skill + let iconClass = "fa-regular fa-circle"; + if(proficientSkills[skillName] == 1){score += profBonus; iconClass = "fas fa-fw fa-check";} + else if(proficientSkills[skillName] == 2){score += (profBonus * 2); iconClass = "fas fa-fw fa-check-double";} + const icon = $$``; + + $$`
          • + + ${icon} +
          • `.appendTo($sectionSkills); + + if(skillName.toLowerCase() == "perception"){ + $lblPassivePerception.text(`${(10 + score)}`); + } + } + } + this._parent.compAbility.compStatgen.addHookBase("common_export_str", hkSkills); + this._parent.compAbility.compStatgen.addHookBase("common_export_dex", hkSkills); + this._parent.compAbility.compStatgen.addHookBase("common_export_con", hkSkills); + this._parent.compAbility.compStatgen.addHookBase("common_export_int", hkSkills); + this._parent.compAbility.compStatgen.addHookBase("common_export_wis", hkSkills); + this._parent.compAbility.compStatgen.addHookBase("common_export_cha", hkSkills); + //We need a hook here to understand when proficiencies are lost/gained, and when we level up + //We can listen to feature source tracker for a pulse regarding skill proficiencies + this._parent.featureSourceTracker_._addHookBase("pulseSkillProficiencies", hkSkills); + this._parent.compClass.addHookBase("class_totalLevels", hkSkills); + hkSkills(); + //#endregion + + //#region Tools + const hkTools = () => { + $spanToolsProf.text(""); + //We now need to get the names of all tool proficiencies + const proficientTools = this._grabToolProficiencies(); + let outStr = ""; + for(let toolName of Object.keys(proficientTools)){ + outStr += outStr.length>0? ", " : ""; + outStr += toolName; + } + $spanToolsProf.text(outStr); + } + //We need a hook here to understand when proficiencies are lost/gained, and when we level up + //We can listen to feature source tracker for a pulse regarding skill proficiencies + this._parent.featureSourceTracker_._addHookBase("pulseToolsProficiencies", hkTools); + this._parent.compClass.addHookBase("class_totalLevels", hkTools); + hkTools(); + //#endregion + //#region Weapons + const hkWeaponsArmor = () => { + $spanWeaponProf.text(""); + $spanArmorProf.text(""); + let outStrWep = ""; + let outStrArm = ""; + //We now need to get the names of all tool proficiencies + const weapons = this._grabWeaponProficiencies(); + for(let name of Object.keys(weapons)){ + outStrWep += outStrWep.length>0? ", " : ""; + outStrWep += name; + } + const armors = this._grabArmorProficiencies(); + for(let name of Object.keys(armors)){ + outStrArm += outStrArm.length>0? ", " : ""; + outStrArm += name; + } + $spanWeaponProf.text(outStrWep); + $spanArmorProf.text(outStrArm); + } + //We need a hook here to understand when proficiencies are lost/gained, and when we level up + //We can listen to feature source tracker for a pulse regarding skill proficiencies + this._parent.featureSourceTracker_._addHookBase("pulseToolsProficiencies", hkWeaponsArmor); + this._parent.compClass.addHookBase("class_totalLevels", hkWeaponsArmor); + hkWeaponsArmor(); + //#endregion + //#region Language + const hkLanguages = () => { + $spanLanguages.text(""); + let outStr = ""; + const languages = this._grabLanguageProficiencies(); + for(let name of Object.keys(languages)){ + outStr += outStr.length>0? ", " : ""; + outStr += name; + } + $spanLanguages.text(outStr); + } + this._parent.featureSourceTracker_._addHookBase("pulseLanguageProficiencies", hkLanguages); + this._parent.compClass.addHookBase("class_totalLevels", hkLanguages); + hkLanguages(); + //#endregion + //#region Saving Throws + //#endregion + //#endregion + + //#region Attacks + const hkCalcAttacks = () => { + this._getOurItems().then(result => { + $attacksTextArea.empty(); + //Try to fill in weapon attacks + const weaponProfs = this._grabWeaponProficiencies(); + const strMod = this._getAbilityModifier("str"); + const dexMod = this._getAbilityModifier("dex"); + const profBonus = this._getProfBonus(); + const calcMeleeAttack = (it, strMod, dexMod, weaponProfs) => { + const isProficient = !!weaponProfs[it.item.weaponCategory.toLowerCase()]; + const attr = (it.item.property?.includes("F") && dexMod > strMod)? dexMod : strMod; //If weapon is finesse and our dex is better, use dex + const toHit = attr + (isProficient? profBonus : 0); + const dmg = it.item.dmg1 + (attr>=0? "+" : "") + attr.toString(); + return {toHit:(toHit>=0? "+" : "")+toHit.toString(), dmg:dmg, dmgType:it.item.dmgType}; + } + const calcRangedAttack = (it, strMod, dexMod, weaponProfs) => { + const isProficient = !!weaponProfs[it.item.weaponCategory.toLowerCase()]; + const attr = dexMod; //For now, always assume all ranged weapons use dex + const toHit = attr + (isProficient? profBonus : 0); + const dmg = it.item.dmg1 + (attr>=0? "+" : "") + attr.toString(); + return {toHit:(toHit>=0? "+" : "")+toHit.toString(), dmg:dmg, dmgType:it.item.dmgType}; + } + const printWeaponAttack = (it) => { + const isMeleeWeapon = it.item._typeListText.includes("melee weapon"); + const isRangedWeapon = it.item._typeListText.includes("ranged weapon"); + const isMeleeAndThrown = isMeleeWeapon && it.item.property?.includes("T"); + if(!isMeleeWeapon && !isRangedWeapon){console.error("weapon type not recognized:", it.item, it.item._typeListText);} + + if(isMeleeWeapon){ + const result = calcMeleeAttack(it, strMod, dexMod, weaponProfs); + let str = `Melee Weapon Attack, ${result.toHit} to hit, ${result.dmg} ${Parser.dmgTypeToFull(result.dmgType)}.`; + $$`
            ${it.item.name}. ${str}
            `.appendTo($attacksTextArea); + } + else if(isRangedWeapon){ + const result = calcRangedAttack(it, strMod, dexMod, weaponProfs); + let str = `Ranged Weapon Attack, Range ${it.item.range} ft, ${result.toHit} to hit, ${result.dmg} ${Parser.dmgTypeToFull(result.dmgType)}.`; + $$`
            ${it.item.name}. ${str}
            `.appendTo($attacksTextArea); + } + if(isMeleeAndThrown && !isRangedWeapon){ + const result = calcRangedAttack(it, strMod, dexMod, weaponProfs); + let str = `Ranged Weapon Attack, Range ${it.item.range} ft, ${result.toHit} to hit, ${result.dmg} ${Parser.dmgTypeToFull(result.dmgType)}.`; + $$`
            ${it.item.name} (Thrown). ${str}
            `.appendTo($attacksTextArea); + } + } + + for(let it of result.startingItems){ + if(!it.item.weapon){continue;} + printWeaponAttack(it); + } + for(let it of result.boughtItems){ + if(!it.item.weapon){continue;} + printWeaponAttack(it); + + } + + //TODO: cantrip attacks + //Get cantrips + //const cantrips = ActorCharactermancerSheet.getAllSpellsKnown(this._parent.compSpell)[0]; + }); + } + this._parent.compEquipment._compEquipmentShopGold._addHookBase("itemPurchases", hkCalcAttacks); + this._parent.compEquipment._compEquipmentCurrency._addHookBase("cpRolled", hkCalcAttacks); + this._parent.compEquipment._compEquipmentStartingDefault._addHookBase("defaultItemPulse", hkCalcAttacks); + this._parent.compAbility.compStatgen.addHookBase("common_export_str", hkCalcAttacks); + this._parent.compAbility.compStatgen.addHookBase("common_export_dex", hkCalcAttacks); + + hkCalcAttacks(); + //#endregion + + //#region Spells + + const hkSpells = () => { + $divSpells.empty(); + $divSpells.append($divSpellAttackMod); + $divSpells.append($divSpellDC); + + const spellsListStr = (spells) => { + let spellsStr = ""; + for(let i = 0; i < spells.length; ++i){ + let uid = spells[i].name.toLowerCase() + "|" + spells[i].source.toLowerCase(); + //spellsStr += spells[i].name; + spellsStr += Renderer.get().render(`{@spell ${uid}}`); + if(i+1 < spells.length){spellsStr += ", ";} + } + return spellsStr; + }; + + + + const spellsKnownByLvl = ActorCharactermancerSheet.getAllSpellsKnown(this._parent.compSpell); + //List cantrips known (these never change) + $$`
            Cantrips Known: ${spellsListStr(spellsKnownByLvl[0])}
            `.appendTo($divSpells); + //Add a bit of a spacing here + + $$`
            Prepared Spells
            `.appendTo($divSpells); + for(let lvl = 1; lvl < spellsKnownByLvl.length; ++lvl){ + const knownSpellsAtLvl = spellsKnownByLvl[lvl] || null; + if(!knownSpellsAtLvl || !knownSpellsAtLvl.length){continue;} + let str = "Cantrips"; + switch(lvl){ + case 0: str = "Cantrips"; break; + case 1: str = "1st Level"; break; + case 2: str = "2nd Level"; break; + case 3: str = "3rd Level"; break; + case 4: str = "4th Level"; break; + case 5: str = "5th Level"; break; + case 6: str = "6th Level"; break; + case 7: str = "7th Level"; break; + case 8: str = "8th Level"; break; + case 9: str = "9th Level"; break; + default: throw new Error("Unimplemented!"); break; + } + const slots = ActorCharactermancerSheet.getSpellSlotsAtLvl(lvl, this._parent.compClass); + $$`
            ${str} (${slots} slots): ${spellsListStr(knownSpellsAtLvl)}
            `.appendTo($divSpells); + } + + hkCalcAttacks(); //Calculate attacks as well, since it displays cantrip attacks + }; + this._parent.compSpell.addHookBase("pulsePreparedLearned", hkSpells); + this._parent.compSpell.addHookBase("pulseAlwaysPrepared", hkSpells); + this._parent.compSpell.addHookBase("pulseAlwaysKnown", hkSpells); + this._parent.compSpell.addHookBase("pulseExpandedSpells", hkSpells); //Not sure if this one is needed + hkSpells(); + + const hkSpellDC = () => { + $divSpellAttackMod.empty(); + $divSpellDC.empty(); + //Show spellcasting modifier (prof bonus + ability modifier (differs between classes)) + //Look through each class + let classData = ActorCharactermancerSheet.getClassData(this._parent.compClass); + let bestAbilityAbv = ""; + let bestAbilityScore = -100; + const profBonus = this._getProfBonus(); + for(let d of classData){; + if(d?.cls?.spellcastingAbility){ + let score = this._getAbilityModifier(d.cls.spellcastingAbility); + if(score > bestAbilityScore){bestAbilityScore = score; bestAbilityAbv = d.cls.spellcastingAbility;} + } + } + if(bestAbilityAbv.length<1){bestAbilityScore = 0;} + bestAbilityScore += profBonus; + $$`Spell Attack Modifier: ${(bestAbilityScore>=0?"+":"")}${bestAbilityScore}`.appendTo($divSpellAttackMod); + //Show spell save DC + bestAbilityScore += 8; + $$`
            Spell Save DC ${bestAbilityScore}`.appendTo($divSpellDC); + } + this._parent.compAbility.compStatgen.addHookBase("common_export_str", hkSpellDC); + this._parent.compAbility.compStatgen.addHookBase("common_export_dex", hkSpellDC); + this._parent.compAbility.compStatgen.addHookBase("common_export_con", hkSpellDC); + this._parent.compAbility.compStatgen.addHookBase("common_export_int", hkSpellDC); + this._parent.compAbility.compStatgen.addHookBase("common_export_wis", hkSpellDC); + this._parent.compAbility.compStatgen.addHookBase("common_export_cha", hkSpellDC); + this._parent.compAbility.compStatgen.addHookBase("common_pulseAsi", hkSpellDC); + this._parent.compClass.addHookBase("class_totalLevels", hkSpellDC); + //#endregion + + //#region Equipment + const hkEquipment = () => { + + const strScore = this._getAbilityScore("str"); + + this._calcArmorClass().then(result=>{ + const str = `AC: ${result.ac} (${result.name})`; + $lblArmorClass.text(result.ac); + $armorWornText.text(result.name); + }); + + //Fill UI list of items + $divEquipment.empty(); + $divCarry.empty(); + + //Calculate currency + const currency = this._getRemainingCoinage(); + $lblCoinage.text(`${currency.gold}gp, ${currency.silver}sp, ${currency.copper}cp`); + + + let coinWeight = (currency.gold + currency.silver + currency.copper) * (1/50); //50 coins weigh 1 lbs + let weightLbs = 0; + + this._getOurItems().then(result => { + $divEquipment.empty(); + $divCarry.empty(); + let outStr = ""; + for(let it of result.startingItems){ + outStr += (outStr.length>0? ", " : "") + (it.quantity>1? it.quantity+"x " : "") + it.item.name; + if(!it.item.weight){continue;} + weightLbs += (it.item.weight * it.quantity); + } + for(let it of result.boughtItems){ + outStr += (outStr.length>0? ", " : "") + (it.quantity>1? it.quantity+"x " : "") + it.item.name; + if(!it.item.weight){continue;} + weightLbs += (it.item.weight * it.quantity); + } + const span = $$`${outStr}`; + $$``.appendTo($divEquipment); + + //Print carrying capacity info + const USE_COIN_WEIGHT = true; + const USE_VARIANT_ENCUMBERANCE = true; + if(USE_COIN_WEIGHT){weightLbs += coinWeight;} + const size = this.getCharacterSize().toUpperCase(); + const sizeModifier = size == "T"? 1/4 : size == "S"? 1/2 : size == "M"? 1 : + size == "L"? 2 : size == "H"? 4 : size == "G"? 8 : 1; //fallback is 1 + const pushDragLiftCapacity = strScore * 30 * sizeModifier; + const carryCapacityMax = strScore * 15 * sizeModifier; + const carryCapacityEncumberance = strScore * 5 * sizeModifier; + const carryCapacityHeavyEncumberance = strScore * 10 * sizeModifier; + let encumberanceText = ""; + if(USE_VARIANT_ENCUMBERANCE){ + if(weightLbs >= carryCapacityHeavyEncumberance){encumberanceText = + " (heavily encumbered: -20ft speed, disadv. on ability checks, attacks, and saves that use STR, DEX or CON)";} + if(weightLbs >= carryCapacityEncumberance){encumberanceText = + " (heavily encumbered: -5ft speed)";} + $$``.appendTo($divCarry); + } + else{ + $$`
            `.appendTo($divCarry); + } + $$`
            `.appendTo($divCarry); + $$`
            `.appendTo($divCarry); + }); + + + } + this._parent.compRace.addHookBase("race_ixRace_version", hkEquipment); //needed to refresh race size, which impacts carrying capacity + this._parent.compRace.addHookBase("pulseSize", hkEquipment); //needed to refresh race size, which impacts carrying capacity + this._parent.compEquipment._compEquipmentCurrency._addHookBase("cpRolled", hkEquipment); + this._parent.compEquipment._compEquipmentShopGold._addHookBase("itemPurchases", hkEquipment); + this._parent.compEquipment._compEquipmentStartingDefault._addHookBase("defaultItemPulse", hkEquipment); + this._parent.compAbility.compStatgen.addHookBase("common_export_str", hkEquipment); + this._parent.compAbility.compStatgen.addHookBase("common_export_dex", hkEquipment); + this._parent.compAbility.compStatgen.addHookBase("common_export_cha", hkEquipment); + hkEquipment(); + //#endregion + + //#region Feats + const hkFeats = () => { + $divFeatFeatures.empty(); + //We need to get all our feats somehow + let featInfo = this._getFeats(); + + let featsText = ""; + for(let asiFeat of featInfo.asiFeats){ + featsText += (featsText.length>0? ", " : "") + asiFeat.feat.name; + } + + const race = this.getRace_(); + if(race != null){ + for(let raceFeat of featInfo.raceFeats){ + featsText += (featsText.length>0? ", " : "") + raceFeat.feat.name; + // + ` (${race.name})`; + } + } + + const bk = this.getBackground(); + for(let bkFeat of featInfo.backgroundFeats){ + featsText += (featsText.length>0? ", " : "") + bkFeat.feat.name; + // + ` (${bk.name})`; + } + + for(let customFeat of featInfo.customFeats){ + featsText += (featsText.length>0? ", " : "") + customFeat.feat.name; + // + ` (${bk.name})`; + } + if(featsText.length<1){return;} + $$`
            Feats:
            `.appendTo($divFeatFeatures); + $$`
            ${featsText}
            `.appendTo($divFeatFeatures); + } + this._parent.compClass.addHookBase("class_ixPrimaryClass", hkFeats); + this._parent.compClass.addHookBase("class_ixMax", hkFeats); + this._parent.compClass.addHookBase("class_totalLevels", hkFeats); + this._parent.compClass.addHookBase("class_pulseChange", hkFeats); //This also senses when subclass is changed + this._parent.compAbility.compStatgen.addHookBase("common_pulseAsi", hkFeats); //This gets fired like all the time feats get added/removed/altered + //#endregion + + //#region Character Description + const hkName = () => { + let name = this._parent.compDescription.__state["description_name"] || " "; + if(name.length < 1){name = " ";} //Make this at least a blankspace so the label isnt empty (and dissaspears) + $inputName.text(name); + } + this._parent.compDescription.addHookBase("description_name", hkName); + hkName(); + const hkAlignment = () => { + let al = this._parent.compDescription.__state["description_alignment"] || ""; + if(al.length === 0){al = "None";} //Make this at least a blankspace so the label isnt empty (and dissaspears) + $inputAlignment.text(al); + } + this._parent.compDescription.addHookBase("description_alignment", hkAlignment); + hkAlignment(); + //#endregion + + wrapper.appendTo(tabSheet); + } + + getRace_() { return this._parent.compRace.getRace_(); } + getCharacterSize(fallback="M"){ + const race = this.getRace_(); + if(!race){return fallback;} + const compSize = this._parent.compRace.compRaceSize; + if(!compSize){return fallback;} + const form = compSize.pGetFormData(); + if(!form || !form.isFormComplete){return fallback;} + return form.data; + } + static getClassData(compClass) { + const primaryClassIndex = compClass._state.class_ixPrimaryClass; + //If we have 2 classes, this will be 1 + const highestClassIndex = compClass._state.class_ixMax; + + const classList = []; + for(let i = 0; i <= highestClassIndex; ++i){ + const isPrimary = i == primaryClassIndex; + //Get a string property that will help us grab actual class data + const { propIxClass: propIxClass, propIxSubclass: propIxSubclass, propCurLevel:propCurLevel, propTargetLevel: propTargetLevel } = + ActorCharactermancerBaseComponent.class_getProps(i); + //Grab actual class data + const cls = compClass.getClass_({propIxClass: propIxClass}); + if(!cls){continue;} + const targetLevel = compClass._state[propTargetLevel]; + const block = { + cls: cls, + isPrimary: isPrimary, + propIxClass: propIxClass, + propIxSubclass:propIxSubclass, + targetLevel:targetLevel, + isDeleted:ActorCharactermancerBaseComponent.class_isDeleted(i), + } + //Now we want to ask compClass if there is a subclass selected for this index + const sc = compClass.getSubclass_({cls:cls, propIxSubclass:propIxSubclass}); + if(sc != null) { block.sc = sc; } + classList.push(block); + } + return classList; + } + /** + * Description + * @param {ActorCharactermancerClass} compClass + * @param {number} ix + * @param {number} conMod + * @returns {number} + */ + getClassHpInfo(compClass, ix, conMod){ + const hpModeComp = compClass._compsClassHpIncreaseMode[ix]; + const hpInfoComp = compClass._compsClassHpInfo[ix]; + + //HP at level 1 + const lvl1Hp = (((hpInfoComp._hitDice.faces / 2) + 1) * hpInfoComp._hitDice.number) + conMod; + let totalHp = lvl1Hp; + } + getBackground(){ + return this._parent.compBackground.getBackground_(); + } + getBackgroundChoices(){ + let compBk = this._parent.compBackground; + let form = compBk.compBackgroundCharacteristics.pGetFormData(); + return form.data; + } + + test_gatherExportInfo() { + //Get class(es) selected + const p = this._parent; + //Grab class data + const classNames = this.test_grabClassNames(p.compClass); + + //Time to grab race data + const raceName = this.test_grabRaceName(p.compRace); + + //Grab background name + const bkName = this.test_grabBackgroundName(p.compBackground); + + //Grab Ability scores + const abs = this.test_grabAbilityScoreTotals(p.compAbility); + + //Grab spells + const spells = this.test_grabSpells(p.compSpell); + } + test_grabClassNames(compClass){ + const primaryClassIndex = compClass._state.class_ixPrimaryClass; + //If we have 2 classes, this will be 1 + const highestClassIndex = compClass._state.class_ixMax; + + const classList = []; //lets make this an array of names + for(let i = 0; i <= highestClassIndex; ++i){ + const isPrimary = i == primaryClassIndex; + //Get a string property that will help us grab actual class data + const { propIxClass: propIxClass } = + ActorCharactermancerBaseComponent.class_getProps(i); + //Grab actual class data + const cls = compClass.getClass_({propIxClass: propIxClass}); + if(!cls){continue;} + classList.push(cls.name + (isPrimary? " (Primary)" : "")); + } + return classList; + } + test_grabRaceName(compRace){ + const race = compRace.getRace_(); + if(race==null){return "no race";} + return race.name; + } + test_grabBackgroundName(compBackground){ + const b = compBackground.getBackground_(); + if(b==null){return "no background";} + return b.name; + } + test_grabAbilityScoreTotals(compAbility) { + const info = compAbility.getTotals(); + if(info.mode == "none"){return {mode: info.mode, values: {str:0,dex:0, con:0, int:0, wis:0, cha:0}};} + const result = info.totals[info.mode]; + return {mode: info.mode, values: result}; + } + _getFeats(){ + const s = this._parent.compAbility.compStatgen.__state; + let asiData = {}; + //We can get feats from ASIs, Races, and... what else? Backgrounds? + //TODO: Add background support here + for(let prop of Object.keys(s)){ + if(prop.startsWith("common_asi_") || prop.startsWith("common_additionalFeats_")){ + asiData[prop] = s[prop]; + } + //Some races provide ASI choices, include them here while we are at it + else if(prop.startsWith("common_raceChoice")){ + asiData[prop] = s[prop]; + } + //Keep track of number of custom feats + //TODO: only include custom feats with index lower than this number + else if (prop.startsWith("common_cntFeatsCustom")){ + asiData[prop] = s[prop]; + } + } + + //Figure out feats gained through ASI's + const numASIFeatOpportunities = Object.keys(asiData).filter((prop, val) => prop.startsWith("common_asi_ability_") && prop.endsWith("_mode")).length; + const asiFeats = []; + for(let i = 0; i < numASIFeatOpportunities; ++i){ + const mode = asiData[(`common_asi_ability_${i}_mode`)]; + if(mode != "feat"){continue;} //Mode must be feat, else it means the user choise an ASI instead + const ixFeat = asiData[(`common_asi_ability_${i}_ixFeat`)]; + if(ixFeat==null || ixFeat<0){continue;} //No feat selected yet + const feat = this._parent.compAbility._data.feat[ixFeat]; + asiFeats.push({asiIx:i, ixFeat:ixFeat, feat:feat}); + } + + //Figure out feats gained through race + const numRaceFeats = Object.keys(asiData).filter((prop, val) => prop.startsWith("common_additionalFeats_race_fromFilter_") && prop.endsWith("_ixFeat")).length; + const raceFeats = []; + for(let i = 0; i < numRaceFeats; ++i){ + const ixFeat = asiData[(`common_additionalFeats_race_fromFilter_${i}_ixFeat`)]; + if(ixFeat==null || ixFeat<0){continue;} //No feat selected yet + const feat = this._parent.compAbility._data.feat[ixFeat]; + raceFeats.push({raceFeatIx:i, ixFeat:ixFeat, feat:feat}); + } + + + //Get feats from backgrounds + //For now, assume we get ALL feats from the background (i dont know of a background that lets you choose between feats) + const bkFeats = []; + const bk = this.getBackground(); + if(bk && bk.feats){ + for(let f of bk.feats){ + //find feat by uid + //TODO: improve this + const featUid = Object.keys(f)[0]; + let parts = featUid.split("|"); + if(parts.length!=2){console.error("Feat UID does not seem to be structured correctly:", featUid);} + let name = parts[0].toLowerCase(); + let source = parts[1].toLowerCase(); + const feat = this._parent.compAbility._data.feat.filter(f => f.name.toLowerCase() == name && f.source.toLowerCase() == source)[0]; + bkFeats.push({feat:feat}); + } + } + + //Add custom added feats + const numCustomASIFeatOpportunities = Object.keys(asiData).filter((prop, val) => prop.startsWith("common_asi_custom_") && prop.endsWith("_mode")).length; + const customFeats = []; + for(let i = 0; i < numCustomASIFeatOpportunities; ++i){ + const mode = asiData[(`common_asi_custom_${i}_mode`)]; + if(mode != "feat"){continue;} //Mode must be feat, else it means the user choise an ASI instead + const ixFeat = asiData[(`common_asi_custom_${i}_ixFeat`)]; + if(ixFeat==null || ixFeat<0){continue;} //No feat selected yet + const feat = this._parent.compAbility._data.feat[ixFeat]; + customFeats.push({asiIx:i, ixFeat:ixFeat, feat:feat}); + } + + return {asiFeats: asiFeats, raceFeats: raceFeats, backgroundFeats:bkFeats, customFeats:customFeats}; + } + + _pullProficienciesFromComponentForm(component, output, prop, isStringArray = false){ + const pasteVals = (fromSkills, isStringArray) => { + if(fromSkills){ + if(isStringArray){ + for(let str of fromSkills){ + output[str] = 1; + } + } + else{ + for(let skillName of Object.keys(fromSkills)){ + skillName = skillName.toLowerCase(); //enforce lower case, just for safety + let skillVal = fromSkills[skillName]; + let existingSkillVal = output[skillName] || 0; + //Only replace values if higher + //1 means proficiency, 2 means expertise + if(skillVal > existingSkillVal){output[skillName] = skillVal;} + } + } + } + } + + if(!component){return;} + const form = component.pGetFormData(); + pasteVals(form.data[prop], isStringArray); + } + async _getOurItems() { + let boughtItems = []; + let startingItems = []; + //Try to get bought items first + const compEquipShop = this._parent.compEquipment._compEquipmentShopGold; + //Go through bought items + const itemKeys = compEquipShop.__state.itemPurchases; + const itemDatas = compEquipShop.__state.itemDatas.item; + for(let item of itemKeys){ + //cant be trusted to not be null + const foundItem = ActorCharactermancerEquipment.findItemByUID(item.data.uid, itemDatas); + if(!foundItem){continue;} + boughtItems.push({item:foundItem, quantity:item.data.quantity}); + } + + //We also need to go through starting items, but only if we didnt roll for gold instead + const rolledForGold = !!this._parent.compEquipment._compEquipmentCurrency._state.cpRolled; + if(!rolledForGold) + { + //If we rolled for gold, it means we dont get any default starting equipment + const compEquipDefault = this._parent.compEquipment._compEquipmentStartingDefault; + const form = await compEquipDefault.pGetFormData(); + const items = form.data.equipmentItemEntries; + for(let it of items){ startingItems.push(it); } + } + + return {boughtItems: boughtItems, startingItems:startingItems}; + } + _getRemainingCoinage(){ + const compGold = this._parent.compEquipment._compEquipmentCurrency; + const state = compGold.__state; + let startingCp = state.cpRolled != null? state.cpRolled : state.cpFromDefault; + let spentCp = state.cpSpent; + let remainingCp = startingCp - spentCp; + const totalCp = remainingCp; + + //const ratio_pp = Parser.COIN_CONVERSIONS[Parser.COIN_ABVS.indexOf("pp")]; + const ratio_gp = Parser.COIN_CONVERSIONS[Parser.COIN_ABVS.indexOf("gp")]; + //const ratio_el = Parser.COIN_CONVERSIONS[Parser.COIN_ABVS.indexOf("ep")]; + const ratio_sp = Parser.COIN_CONVERSIONS[Parser.COIN_ABVS.indexOf("sp")]; + //let platinum = Math.floor(remainingCp / ratio_pp); remainingCp -= (platinum * ratio_pp); + let gold = Math.floor(remainingCp / ratio_gp); remainingCp -= (gold * ratio_gp); + let silver = Math.floor(remainingCp / ratio_sp); remainingCp -= (silver * ratio_sp); + + + return {copper:remainingCp, silver:silver, gold:gold}; + } + + /** + * @param {ActorCharactermancerClass} compClass + * @param {ActorCharactermancerBackground} compBackground + * @param {ActorCharactermancerRace} compRace + * @returns {{acrobatics:number, athletics:number}} Returned object has a bunch of parameters named after skills, their values are either 1 (proficient) or 2 (expertise) + */ + _grabSkillProficiencies(){ + //What can give us proficiencies? + //Classes + //Subclasses + //Backgrounds + //Races + //Feats + + const compClass = this._parent.compClass; + const compBackground = this._parent.compBackground; + const compRace = this._parent.compRace; + const dataProp = "skillProficiencies"; + + let out = {}; + + //Get number of classes + const highestClassIndex = compClass._state.class_ixMax; + //Then paste skills gained from each class + for(let ix = 0; ix <= highestClassIndex; ++ix){ + this._pullProficienciesFromComponentForm(compClass.compsClassSkillProficiencies[ix], out, dataProp); + for(let fos of compClass.compsClassFeatureOptionsSelect[ix]){ + if(fos._subCompsExpertise){for(let subcomp of fos._subCompsExpertise){this._pullProficienciesFromComponentForm(subcomp, out, dataProp);}} + if(fos._subCompsExpertise){for(let subcomp of fos._subCompsSkillProficiencies){this._pullProficienciesFromComponentForm(subcomp, out, dataProp);}} + if(fos._subCompsExpertise){for(let subcomp of fos._subCompsSkillToolLanguageProficiencies){this._pullProficienciesFromComponentForm(subcomp, out, dataProp);}} + } + } + this._pullProficienciesFromComponentForm(compRace.compRaceSkillProficiencies, out, dataProp); + this._pullProficienciesFromComponentForm(compBackground.compBackgroundSkillProficiencies, out, dataProp); + + return out; + } + /** + * @param {ActorCharactermancerClass} compClass + * @param {ActorCharactermancerBackground} compBackground + * @param {ActorCharactermancerRace} compRace + * @returns {{"disguise kit":number, "musical instrument":number}} Returned object has a bunch of parameters named after skills, their values are either 1 (proficient) or 2 (expertise) + */ + _grabToolProficiencies(){ + //What can give us proficiencies? + //Classes + //Subclasses + //Backgrounds + //Races + //Feats + const compClass = this._parent.compClass; + const compBackground = this._parent.compBackground; + const compRace = this._parent.compRace; + const dataProp = "toolProficiencies"; + + let out = {}; + //Get number of classes + const highestClassIndex = compClass._state.class_ixMax; + //Then paste skills gained from each class + for(let ix = 0; ix <= highestClassIndex; ++ix){ + this._pullProficienciesFromComponentForm(compClass.compsClassToolProficiencies[ix], out, dataProp); + } + this._pullProficienciesFromComponentForm(compRace.compRaceToolProficiencies, out, dataProp); + this._pullProficienciesFromComponentForm(compBackground.compBackgroundToolProficiencies, out, dataProp); + + return out; + } + + /** + * @returns {{"simple":number, "dagger":number}} Returned object has a bunch of parameters named after skills, their values are either 1 (proficient) or 2 (expertise) + */ + _grabWeaponProficiencies(){ + //What can give us proficiencies? + //Classes + //Feats? + const compClass = this._parent.compClass; + const dataProp = "weapons"; + + let out = {}; + //Get number of classes + const highestClassIndex = compClass._state.class_ixMax; + //Then paste skills gained from each class + for(let ix = 0; ix <= highestClassIndex; ++ix){ + this._pullProficienciesFromComponentForm(compClass.compsClassStartingProficiencies[ix], out, dataProp, true); + } + + //We need to do some additional parsing (we will get stuff like "shortsword|phb", "dart|phb"); + for(let prop of Object.keys(out)){ + if(!prop.includes("|")){continue;} + out[prop.split("|")[0]] = out[prop]; + delete out[prop]; + } + + return out; + } + /** + * @returns {{"light":number, "medium":number}} Returned object has a bunch of parameters named after skills, their values are either 1 (proficient) or 2 (expertise) + */ + _grabArmorProficiencies(){ + //What can give us proficiencies? + //Classes + //Feats? + const compClass = this._parent.compClass; + const dataProp = "armor"; + + let out = {}; + //Get number of classes + const highestClassIndex = compClass._state.class_ixMax; + //Then paste skills gained from each class + for(let ix = 0; ix <= highestClassIndex; ++ix){ + this._pullProficienciesFromComponentForm(compClass.compsClassStartingProficiencies[ix], out, dataProp, true); + } + + //We need to do some additional parsing (we will get stuff like "light|phb", "medium|phb"); + for(let prop of Object.keys(out)){ + if(!prop.includes("|")){continue;} + out[prop.split("|")[0]] = out[prop]; + delete out[prop]; + } + + return out; + } + /** + * @returns {{"common":number, "dwarvish":number}} Returned object has a bunch of parameters named after skills, their values are either 1 (proficient) or 2 (expertise) + */ + _grabLanguageProficiencies(){ + //What can give us proficiencies? + //Races + //Backgrounds + //Classes + const compClass = this._parent.compClass; + const compRace = this._parent.compRace; + const dataProp = "languageProficiencies"; + + let out = {}; + //Get number of classes + const highestClassIndex = compClass._state.class_ixMax; + //Then paste languages gained from each class + for(let ix = 0; ix <= highestClassIndex; ++ix){ + for(let fos of compClass.compsClassFeatureOptionsSelect[ix]){ + for(let subcomp of fos._subCompsLanguageProficiencies){ + this._pullProficienciesFromComponentForm(subcomp, out, dataProp); + } + for(let subcomp of fos._subCompsSkillToolLanguageProficiencies){ + this._pullProficienciesFromComponentForm(subcomp, out, dataProp); + } + + } + + } + + this._pullProficienciesFromComponentForm(compRace._compRaceSkillToolLanguageProficiencies, out, dataProp); + this._pullProficienciesFromComponentForm(compRace._compRaceLanguageProficiencies, out, dataProp); + + //We need to do some additional parsing (we will get stuff like "light|phb", "medium|phb"); + /* for(let prop of Object.keys(out)){ + if(!prop.includes("|")){continue;} + out[prop.split("|")[0]] = out[prop]; + delete out[prop]; + } */ + + return out; + } + /** + * @returns {{"str":number, "dex":number}} Returned object has a bunch of parameters named after skills, their values are either 1 (proficient) or 2 (expertise) + */ + _grabSavingThrowProficiencies(){ + //What can give us proficiencies? + //Races + //Backgrounds + //Classes + const compClass = this._parent.compClass; + const compRace = this._parent.compRace; + const dataProp = "savingThrows"; + + let out = {}; + //Get number of classes + const highestClassIndex = compClass._state.class_ixMax; + //Then paste languages gained from each class + for(let ix = 0; ix <= highestClassIndex; ++ix){ + this._pullProficienciesFromComponentForm(compClass.compsClassStartingProficiencies[ix], out, dataProp, true); + } + + this._pullProficienciesFromComponentForm(compRace._compRaceSkillToolLanguageProficiencies, out, dataProp); + this._pullProficienciesFromComponentForm(compRace._compRaceLanguageProficiencies, out, dataProp); + + return out; + } + + /** + * Get the proficiency bonus of our character (depends on character level) + * @returns {number} + */ + _getProfBonus(){ + const classList = ActorCharactermancerSheet.getClassData(this._parent.compClass); + let levelTotal = 0; + for(let ix = 0; ix < classList.length; ++ix){ + const data = classList[ix]; + if(!data.cls){continue;} + const targetLevel = data.targetLevel || 1; + levelTotal += targetLevel; + } + + return Parser.levelToPb(levelTotal); + } + _getAbilityModifier(abl_abbreviation, total=null){ + if(total == null){ + const totals = this.test_grabAbilityScoreTotals(this._parent.compAbility); + total = totals.values[abl_abbreviation]; + } + return Math.floor((total-10) / 2); + } + _getAbilityScore(abl_abbreviation){ + const totals = this.test_grabAbilityScoreTotals(this._parent.compAbility); + return totals.values[abl_abbreviation]; + } + + async _calcArmorClass(){ + const dexModifier = this._getAbilityModifier("dex"); + //Try to get items from bought items (we will do starting items later) + const compEquipShop = this._parent.compEquipment._compEquipmentShopGold; + + let bestArmorAC = Number.MIN_VALUE; + let bestArmor = null; + + const tryUseArmor = (item) => { + //Account for if proficient in armor? nah not yet + //check if strength requirement is met? + const armorAC = item.ac; + const armorType = item.type.toUpperCase(); //LA, MA, HA + //Light armor has no dex cap. Medium and heavy has +2 as an upper cap + const dexBonus = armorType == "LA"? dexModifier : Math.min(dexModifier, 2); + const finalAC = armorAC + dexBonus; + if(finalAC > bestArmorAC){bestArmor = {ac:armorAC, dexBonus:dexBonus, name:item.name}; bestArmorAC = finalAC; } + } + + const tryGetArmors = async () => { + //Go through bought items + const itemKeys = compEquipShop.__state.itemPurchases; + const itemDatas = compEquipShop.__state.itemDatas.item; + for(let item of itemKeys){ + //cant be trusted to not be null + const foundItem = ActorCharactermancerEquipment.findItemByUID(item.data.uid, itemDatas); + if(!foundItem){continue;} + if(foundItem.armor == true){tryUseArmor(foundItem);} + } + + //We also need to go through starting items + const rolledForGold = !!this._parent.compEquipment._compEquipmentCurrency._state.cpRolled; + if(!rolledForGold) + { + //If we rolled for gold, it means we dont get any default starting equipment + const compEquipDefault = this._parent.compEquipment._compEquipmentStartingDefault; + const form = await compEquipDefault.pGetFormData(); + const items = form.data.equipmentItemEntries; + for(let it of items){if(it.armor == true){tryUseArmor(it);}} + } + } + + await tryGetArmors(); + //TODO: unarmored defense? + //TODO: shield? + + const naturalAC = 10 + dexModifier; //unarmored defense here? + if(bestArmorAC > naturalAC){ + return {ac:bestArmorAC, name:bestArmor.name}; + } + else{ + return {ac:naturalAC, name:"Natural Armor"}; + } + } + + /** + * @param {ActorCharactermancerSpell} compSpells + */ + test_grabSpells(compSpells){ + let spellsKnown = new Array(10); + for(let j = 0; j < compSpells.compsSpellSpells.length; ++j) + { + const comp1 = compSpells.compsSpellSpells[j]; + for(let spellLevelIx = 0; spellLevelIx < comp1._compsLevel.length; ++spellLevelIx) + { + const comp2 = comp1._compsLevel[spellLevelIx]; + const known = comp2.getSpellsKnown(); + for(let arrayEntry of known){ + spellsKnown[spellLevelIx].push(arrayEntry.spell.name); + } + } + } + } + + /** + * * @param {number} hitDiceNumber + * @param {number} hitDiceFaces + * @param {number} level + * @param {number} mode + * @param {string} customFormula + * @returns {number} + */ + static calcHitPointsAtLevel(hitDiceNumber, hitDiceFaces, level, mode, customFormula){ + switch(mode){ + case 0: //Take Average + return (hitDiceFaces * hitDiceNumber) + ((((hitDiceFaces * hitDiceNumber) / 2)+1) * (level-1)); + case 1: //Minimum Value + return (hitDiceFaces * hitDiceNumber) + ((1) * (level-1)); + case 2: //Maximum Value + return (hitDiceFaces * hitDiceNumber) + (((hitDiceFaces * hitDiceNumber)) * (level-1)); + case 3: //Roll + console.error("Roll mode not yet implemented!"); return; + case 4: //Custom Formula + console.error("Custom Formula mode not yet implemented!"); return; + case 5: //Do Not Increase HP + return (hitDiceFaces * hitDiceNumber); + default: console.error("Unimplemented!"); return 0; + } + } + + /** + * @param {ActorCharactermancerSpell} compSpells + * @returns {any[][]} + */ + static getAllSpellsKnown(compSpells){ + let spellsBylevel = [[],[],[],[],[],[],[],[],[],[]]; + //Go through each component that can add spells + for(let j = 0; j < compSpells.compsSpellSpells.length; ++j) + { + //console.log(compSpells.compsSpellSpells); + const comp1 = compSpells.compsSpellSpells[j]; + if(comp1 == null){continue;} //Switching classes can make components here be null + //Go through each level for the component + for(let spellLevelIx = 0; spellLevelIx < comp1._compsLevel.length; ++spellLevelIx) + { + //Grab the subcomponent that handles that specific level + const subcomponent = comp1._compsLevel[spellLevelIx]; + const known = subcomponent.getSpellsKnown(true); //Get the spells known by that subcomponent + for(let i = 0; i < known.length; ++i){ + spellsBylevel[spellLevelIx].push(known[i].spell); + } + } + } + + return spellsBylevel; + } + static getSpellSlotsAtLvl(spellLevel, compClass){ + + let total = 0; + let classData = ActorCharactermancerSheet.getClassData(compClass); + for(let d of classData){ + + //Ask class for spellslots + if(d.cls?.classTableGroups){ + //What is the level we have achieved for this class? + let classLevel = d.targetLevel; //this is base 1, so value 1 = level 1 + let foundSpellSlotsTable = false; + for(let i = 0; i < d.cls.classTableGroups.length && !foundSpellSlotsTable; ++i){ + const t = d.cls.classTableGroups[i]; + if(!t.rowsSpellProgression){continue;} + foundSpellSlotsTable = true; + total += t.rowsSpellProgression[classLevel-1][spellLevel-1]; //0 is level 1, 1 is level 2, etc (this applies for both) + } + } + //TODO: Ask subclass for spells lots + /* if(d.sc?.classTableGroups){ + let foundSpellSlotsTable = false; + for(let i = 0; i < d.cls.classTableGroups.length && !foundSpellSlotsTable; ++i){ + const t = d.cls.classTableGroups[i]; + if(!t.rowsSpellProgression){continue;} + foundSpellSlotsTable = true; + total += t.rowsSpellProgression[level-1]; //0 is level 1, 1 is level 2, etc + } + } */ + } + return total; + } +} \ No newline at end of file diff --git a/charbuilder/js/stringextensions.js b/charbuilder/js/stringextensions.js new file mode 100644 index 0000000..262e4c5 --- /dev/null +++ b/charbuilder/js/stringextensions.js @@ -0,0 +1,222 @@ +String.prototype.uppercaseFirst = String.prototype.uppercaseFirst || function() { + const str = this.toString(); + if (str.length === 0) + return str; + if (str.length === 1) + return str.charAt(0).toUpperCase(); + return str.charAt(0).toUpperCase() + str.slice(1); +} +; + +String.prototype.lowercaseFirst = String.prototype.lowercaseFirst || function() { + const str = this.toString(); + if (str.length === 0) + return str; + if (str.length === 1) + return str.charAt(0).toLowerCase(); + return str.charAt(0).toLowerCase() + str.slice(1); +} +; + +String.prototype.toTitleCase = String.prototype.toTitleCase || function() { + let str = this.replace(/([^\W_]+[^-\u2014\s/]*) */g, m0=>m0.charAt(0).toUpperCase() + m0.substring(1).toLowerCase()); + + StrUtil._TITLE_LOWER_WORDS_RE = StrUtil._TITLE_LOWER_WORDS_RE || StrUtil.TITLE_LOWER_WORDS.map(it=>new RegExp(`\\s${it}\\s`,"gi")); + StrUtil._TITLE_UPPER_WORDS_RE = StrUtil._TITLE_UPPER_WORDS_RE || StrUtil.TITLE_UPPER_WORDS.map(it=>new RegExp(`\\b${it}\\b`,"g")); + StrUtil._TITLE_UPPER_WORDS_PLURAL_RE = StrUtil._TITLE_UPPER_WORDS_PLURAL_RE || StrUtil.TITLE_UPPER_WORDS_PLURAL.map(it=>new RegExp(`\\b${it}\\b`,"g")); + + const len = StrUtil.TITLE_LOWER_WORDS.length; + for (let i = 0; i < len; i++) { + str = str.replace(StrUtil._TITLE_LOWER_WORDS_RE[i], txt=>txt.toLowerCase(), ); + } + + const len1 = StrUtil.TITLE_UPPER_WORDS.length; + for (let i = 0; i < len1; i++) { + str = str.replace(StrUtil._TITLE_UPPER_WORDS_RE[i], StrUtil.TITLE_UPPER_WORDS[i].toUpperCase(), ); + } + + for (let i = 0; i < len1; i++) { + str = str.replace(StrUtil._TITLE_UPPER_WORDS_PLURAL_RE[i], `${StrUtil.TITLE_UPPER_WORDS_PLURAL[i].slice(0, -1).toUpperCase()}${StrUtil.TITLE_UPPER_WORDS_PLURAL[i].slice(-1).toLowerCase()}`, ); + } + + str = str.split(/([;:?!.])/g).map(pt=>pt.replace(/^(\s*)([^\s])/, (...m)=>`${m[1]}${m[2].toUpperCase()}`)).join(""); + + return str; +} +; + +String.prototype.toSentenceCase = String.prototype.toSentenceCase || function() { + const out = []; + const re = /([^.!?]+)([.!?]\s*|$)/gi; + let m; + do { + m = re.exec(this); + if (m) { + out.push(m[0].toLowerCase().uppercaseFirst()); + } + } while (m); + return out.join(""); +} +; + +String.prototype.toSpellCase = String.prototype.toSpellCase || function() { + return this.toLowerCase().replace(/(^|of )(bigby|otiluke|mordenkainen|evard|hadar|agathys|abi-dalzim|aganazzar|drawmij|leomund|maximilian|melf|nystul|otto|rary|snilloc|tasha|tenser|jim)('s|$| )/g, (...m)=>`${m[1]}${m[2].toTitleCase()}${m[3]}`); +} +; + +String.prototype.toCamelCase = String.prototype.toCamelCase || function() { + return this.split(" ").map((word,index)=>{ + if (index === 0) + return word.toLowerCase(); + return `${word.charAt(0).toUpperCase()}${word.slice(1).toLowerCase()}`; + } + ).join(""); +} +; + +String.prototype.toPlural = String.prototype.toPlural || function() { + let plural; + if (StrUtil.IRREGULAR_PLURAL_WORDS[this.toLowerCase()]) + plural = StrUtil.IRREGULAR_PLURAL_WORDS[this.toLowerCase()]; + else if (/(s|x|z|ch|sh)$/i.test(this)) + plural = `${this}es`; + else if (/[bcdfghjklmnpqrstvwxyz]y$/i.test(this)) + plural = this.replace(/y$/i, "ies"); + else + plural = `${this}s`; + + if (this.toLowerCase() === this) + return plural; + if (this.toUpperCase() === this) + return plural.toUpperCase(); + if (this.toTitleCase() === this) + return plural.toTitleCase(); + return plural; +} +; + +String.prototype.escapeQuotes = String.prototype.escapeQuotes || function() { + return this.replace(/'/g, `'`).replace(/"/g, `"`).replace(//g, `>`); +} +; + +String.prototype.qq = String.prototype.qq || function() { + return this.escapeQuotes(); +} +; + +String.prototype.unescapeQuotes = String.prototype.unescapeQuotes || function() { + return this.replace(/'/g, `'`).replace(/"/g, `"`).replace(/</g, `<`).replace(/>/g, `>`); +} +; + +String.prototype.uq = String.prototype.uq || function() { + return this.unescapeQuotes(); +} +; + +String.prototype.encodeApos = String.prototype.encodeApos || function() { + return this.replace(/'/g, `%27`); +} +; + +String.prototype.distance = String.prototype.distance || function(target) { + let source = this; + let i; + let j; + if (!source) + return target ? target.length : 0; + else if (!target) + return source.length; + + const m = source.length; + const n = target.length; + const INF = m + n; + const score = new Array(m + 2); + const sd = {}; + for (i = 0; i < m + 2; i++) + score[i] = new Array(n + 2); + score[0][0] = INF; + for (i = 0; i <= m; i++) { + score[i + 1][1] = i; + score[i + 1][0] = INF; + sd[source[i]] = 0; + } + for (j = 0; j <= n; j++) { + score[1][j + 1] = j; + score[0][j + 1] = INF; + sd[target[j]] = 0; + } + + for (i = 1; i <= m; i++) { + let DB = 0; + for (j = 1; j <= n; j++) { + const i1 = sd[target[j - 1]]; + const j1 = DB; + if (source[i - 1] === target[j - 1]) { + score[i + 1][j + 1] = score[i][j]; + DB = j; + } else { + score[i + 1][j + 1] = Math.min(score[i][j], Math.min(score[i + 1][j], score[i][j + 1])) + 1; + } + score[i + 1][j + 1] = Math.min(score[i + 1][j + 1], score[i1] ? score[i1][j1] + (i - i1 - 1) + 1 + (j - j1 - 1) : Infinity); + } + sd[source[i - 1]] = i; + } + return score[m + 1][n + 1]; +} +; + +String.prototype.isNumeric = String.prototype.isNumeric || function() { + return !isNaN(parseFloat(this)) && isFinite(this); +} +; + +String.prototype.last = String.prototype.last || function() { + return this[this.length - 1]; +}; + +String.prototype.escapeRegexp = String.prototype.escapeRegexp || function() { + return this.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&"); +}; + +String.prototype.toUrlified = String.prototype.toUrlified || function() { + return encodeURIComponent(this.toLowerCase()).toLowerCase(); +} +; + +String.prototype.toChunks = String.prototype.toChunks || function(size) { + const numChunks = Math.ceil(this.length / size); + const chunks = new Array(numChunks); + for (let i = 0, o = 0; i < numChunks; ++i, + o += size) + chunks[i] = this.substr(o, size); + return chunks; +} +; + +String.prototype.toAscii = String.prototype.toAscii || function() { + return this.normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/ร†/g, "AE").replace(/รฆ/g, "ae"); +} +; + +String.prototype.trimChar = String.prototype.trimChar || function(ch) { + let start = 0; + let end = this.length; + while (start < end && this[start] === ch) + ++start; + while (end > start && this[end - 1] === ch) + --end; + return (start > 0 || end < this.length) ? this.substring(start, end) : this; +} +; + +String.prototype.trimAnyChar = String.prototype.trimAnyChar || function(chars) { + let start = 0; + let end = this.length; + while (start < end && chars.indexOf(this[start]) >= 0) + ++start; + while (end > start && chars.indexOf(this[end - 1]) >= 0) + --end; + return (start > 0 || end < this.length) ? this.substring(start, end) : this; +}; \ No newline at end of file diff --git a/charbuilder/js/vetools/parser.js b/charbuilder/js/vetools/parser.js new file mode 100644 index 0000000..c7b0659 --- /dev/null +++ b/charbuilder/js/vetools/parser.js @@ -0,0 +1,3693 @@ +"use strict"; + +// PARSING ============================================================================================================= +globalThis.Parser = {}; + +Parser._parse_aToB = function (abMap, a, fallback) { + if (a === undefined || a === null) throw new TypeError("undefined or null object passed to parser"); + if (typeof a === "string") a = a.trim(); + if (abMap[a] !== undefined) return abMap[a]; + return fallback !== undefined ? fallback : a; +}; + +Parser._parse_bToA = function (abMap, b, fallback) { + if (b === undefined || b === null) throw new TypeError("undefined or null object passed to parser"); + if (typeof b === "string") b = b.trim(); + for (const v in abMap) { + if (!abMap.hasOwnProperty(v)) continue; + if (abMap[v] === b) return v; + } + return fallback !== undefined ? fallback : b; +}; + +Parser.attrChooseToFull = function (attList) { + if (attList.length === 1) return `${Parser.attAbvToFull(attList[0])} modifier`; + else { + const attsTemp = []; + for (let i = 0; i < attList.length; ++i) { + attsTemp.push(Parser.attAbvToFull(attList[i])); + } + return `${attsTemp.join(" or ")} modifier (your choice)`; + } +}; + +Parser.numberToText = function (number) { + if (number == null) throw new TypeError(`undefined or null object passed to parser`); + if (Math.abs(number) >= 100) return `${number}`; + + return `${number < 0 ? "negative " : ""}${Parser.numberToText._getPositiveNumberAsText(Math.abs(number))}`; +}; + +Parser.numberToText._getPositiveNumberAsText = num => { + const [preDotRaw, postDotRaw] = `${num}`.split("."); + + if (!postDotRaw) return Parser.numberToText._getPositiveIntegerAsText(num); + + let preDot = preDotRaw === "0" ? "" : `${Parser.numberToText._getPositiveIntegerAsText(Math.trunc(num))} and `; + + // See also: `Parser.numberToVulgar` + switch (postDotRaw) { + case "125": return `${preDot}one-eighth`; + case "2": return `${preDot}one-fifth`; + case "25": return `${preDot}one-quarter`; + case "375": return `${preDot}three-eighths`; + case "4": return `${preDot}two-fifths`; + case "5": return `${preDot}one-half`; + case "6": return `${preDot}three-fifths`; + case "625": return `${preDot}five-eighths`; + case "75": return `${preDot}three-quarters`; + case "8": return `${preDot}four-fifths`; + case "875": return `${preDot}seven-eighths`; + + default: { + // Handle recursive + const asNum = Number(`0.${postDotRaw}`); + + if (asNum.toFixed(2) === (1 / 3).toFixed(2)) return `${preDot}one-third`; + if (asNum.toFixed(2) === (2 / 3).toFixed(2)) return `${preDot}two-thirds`; + + if (asNum.toFixed(2) === (1 / 6).toFixed(2)) return `${preDot}one-sixth`; + if (asNum.toFixed(2) === (5 / 6).toFixed(2)) return `${preDot}five-sixths`; + } + } +}; + +Parser.numberToText._getPositiveIntegerAsText = num => { + switch (num) { + case 0: return "zero"; + case 1: return "one"; + case 2: return "two"; + case 3: return "three"; + case 4: return "four"; + case 5: return "five"; + case 6: return "six"; + case 7: return "seven"; + case 8: return "eight"; + case 9: return "nine"; + case 10: return "ten"; + case 11: return "eleven"; + case 12: return "twelve"; + case 13: return "thirteen"; + case 14: return "fourteen"; + case 15: return "fifteen"; + case 16: return "sixteen"; + case 17: return "seventeen"; + case 18: return "eighteen"; + case 19: return "nineteen"; + case 20: return "twenty"; + case 30: return "thirty"; + case 40: return "forty"; + case 50: return "fifty"; + case 60: return "sixty"; + case 70: return "seventy"; + case 80: return "eighty"; + case 90: return "ninety"; + default: { + const str = String(num); + return `${Parser.numberToText._getPositiveIntegerAsText(Number(`${str[0]}0`))}-${Parser.numberToText._getPositiveIntegerAsText(Number(str[1]))}`; + } + } +}; + +Parser.textToNumber = function (str) { + str = str.trim().toLowerCase(); + if (!isNaN(str)) return Number(str); + switch (str) { + case "zero": return 0; + case "one": case "a": case "an": return 1; + case "two": case "double": return 2; + case "three": case "triple": return 3; + case "four": case "quadruple": return 4; + case "five": return 5; + case "six": return 6; + case "seven": return 7; + case "eight": return 8; + case "nine": return 9; + case "ten": return 10; + case "eleven": return 11; + case "twelve": return 12; + case "thirteen": return 13; + case "fourteen": return 14; + case "fifteen": return 15; + case "sixteen": return 16; + case "seventeen": return 17; + case "eighteen": return 18; + case "nineteen": return 19; + case "twenty": return 20; + case "thirty": return 30; + case "forty": return 40; + case "fifty": return 50; + case "sixty": return 60; + case "seventy": return 70; + case "eighty": return 80; + case "ninety": return 90; + } + return NaN; +}; + +Parser.numberToVulgar = function (number, {isFallbackOnFractional = true} = {}) { + const isNeg = number < 0; + const spl = `${number}`.replace(/^-/, "").split("."); + if (spl.length === 1) return number; + + let preDot = spl[0] === "0" ? "" : spl[0]; + if (isNeg) preDot = `-${preDot}`; + + // See also: `Parser.numberToText._getPositiveNumberAsText` + switch (spl[1]) { + case "125": return `${preDot}โ…›`; + case "2": return `${preDot}โ…•`; + case "25": return `${preDot}ยผ`; + case "375": return `${preDot}โ…œ`; + case "4": return `${preDot}โ…–`; + case "5": return `${preDot}ยฝ`; + case "6": return `${preDot}โ…—`; + case "625": return `${preDot}โ…`; + case "75": return `${preDot}ยพ`; + case "8": return `${preDot}โ…˜`; + case "875": return `${preDot}โ…ž`; + + default: { + // Handle recursive + const asNum = Number(`0.${spl[1]}`); + + if (asNum.toFixed(2) === (1 / 3).toFixed(2)) return `${preDot}โ…“`; + if (asNum.toFixed(2) === (2 / 3).toFixed(2)) return `${preDot}โ…”`; + + if (asNum.toFixed(2) === (1 / 6).toFixed(2)) return `${preDot}โ…™`; + if (asNum.toFixed(2) === (5 / 6).toFixed(2)) return `${preDot}โ…š`; + } + } + + return isFallbackOnFractional ? Parser.numberToFractional(number) : null; +}; + +Parser.vulgarToNumber = function (str) { + const [, leading = "0", vulgar = ""] = /^(\d+)?([โ…›ยผโ…œยฝโ…ยพโ…žโ…“โ…”โ…™โ…š])?$/.exec(str) || []; + let out = Number(leading); + switch (vulgar) { + case "โ…›": out += 0.125; break; + case "ยผ": out += 0.25; break; + case "โ…œ": out += 0.375; break; + case "ยฝ": out += 0.5; break; + case "โ…": out += 0.625; break; + case "ยพ": out += 0.75; break; + case "โ…ž": out += 0.875; break; + case "โ…“": out += 1 / 3; break; + case "โ…”": out += 2 / 3; break; + case "โ…™": out += 1 / 6; break; + case "โ…š": out += 5 / 6; break; + case "": break; + default: throw new Error(`Unhandled vulgar part "${vulgar}"`); + } + return out; +}; + +Parser.numberToSuperscript = function (number) { + return `${number}`.split("").map(c => isNaN(c) ? c : Parser._NUMBERS_SUPERSCRIPT[Number(c)]).join(""); +}; +Parser._NUMBERS_SUPERSCRIPT = "โฐยนยฒยณโดโตโถโทโธโน"; + +Parser.numberToSubscript = function (number) { + return `${number}`.split("").map(c => isNaN(c) ? c : Parser._NUMBERS_SUBSCRIPT[Number(c)]).join(""); +}; +Parser._NUMBERS_SUBSCRIPT = "โ‚€โ‚โ‚‚โ‚ƒโ‚„โ‚…โ‚†โ‚‡โ‚ˆโ‚‰"; + +Parser._greatestCommonDivisor = function (a, b) { + if (b < Number.EPSILON) return a; + return Parser._greatestCommonDivisor(b, Math.floor(a % b)); +}; +Parser.numberToFractional = function (number) { + const len = number.toString().length - 2; + let denominator = 10 ** len; + let numerator = number * denominator; + const divisor = Parser._greatestCommonDivisor(numerator, denominator); + numerator = Math.floor(numerator / divisor); + denominator = Math.floor(denominator / divisor); + + return denominator === 1 ? String(numerator) : `${Math.floor(numerator)}/${Math.floor(denominator)}`; +}; + +Parser.ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + +Parser.attAbvToFull = function (abv) { + return Parser._parse_aToB(Parser.ATB_ABV_TO_FULL, abv); +}; + +Parser.attFullToAbv = function (full) { + return Parser._parse_bToA(Parser.ATB_ABV_TO_FULL, full); +}; + +Parser.sizeAbvToFull = function (abv) { + return Parser._parse_aToB(Parser.SIZE_ABV_TO_FULL, abv); +}; + +Parser.getAbilityModNumber = function (abilityScore) { + return Math.floor((abilityScore - 10) / 2); +}; + +Parser.getAbilityModifier = function (abilityScore) { + let modifier = Parser.getAbilityModNumber(abilityScore); + if (modifier >= 0) modifier = `+${modifier}`; + return `${modifier}`; +}; + +Parser.getSpeedString = (ent, {isMetric = false, isSkipZeroWalk = false} = {}) => { + if (ent.speed == null) return "\u2014"; + + const unit = isMetric ? Parser.metric.getMetricUnit({originalUnit: "ft.", isShortForm: true}) : "ft."; + if (typeof ent.speed === "object") { + const stack = []; + let joiner = ", "; + + Parser.SPEED_MODES + .filter(mode => !ent.speed.hidden?.includes(mode)) + .forEach(mode => Parser._getSpeedString_addSpeedMode({ent, prop: mode, stack, isMetric, isSkipZeroWalk, unit})); + + if (ent.speed.choose && !ent.speed.hidden?.includes("choose")) { + joiner = "; "; + stack.push(`${ent.speed.choose.from.sort().joinConjunct(", ", " or ")} ${ent.speed.choose.amount} ${unit}${ent.speed.choose.note ? ` ${ent.speed.choose.note}` : ""}`); + } + + return stack.join(joiner) + (ent.speed.note ? ` ${ent.speed.note}` : ""); + } + + return (isMetric ? Parser.metric.getMetricNumber({originalValue: ent.speed, originalUnit: Parser.UNT_FEET}) : ent.speed) + + (ent.speed === "Varies" ? "" : ` ${unit} `); +}; +Parser._getSpeedString_addSpeedMode = ({ent, prop, stack, isMetric, isSkipZeroWalk, unit}) => { + if (ent.speed[prop] || (!isSkipZeroWalk && prop === "walk")) Parser._getSpeedString_addSpeed({prop, speed: ent.speed[prop] || 0, isMetric, unit, stack}); + if (ent.speed.alternate && ent.speed.alternate[prop]) ent.speed.alternate[prop].forEach(speed => Parser._getSpeedString_addSpeed({prop, speed, isMetric, unit, stack})); +}; +Parser._getSpeedString_addSpeed = ({prop, speed, isMetric, unit, stack}) => { + const ptName = prop === "walk" ? "" : `${prop} `; + const ptValue = Parser._getSpeedString_getVal({prop, speed, isMetric}); + const ptUnit = speed === true ? "" : ` ${unit}`; + const ptCondition = Parser._getSpeedString_getCondition({speed}); + stack.push([ptName, ptValue, ptUnit, ptCondition].join("")); +}; +Parser._getSpeedString_getVal = ({prop, speed, isMetric}) => { + if (speed === true && prop !== "walk") return "equal to your walking speed"; + + const num = speed === true + ? 0 + : speed.number != null ? speed.number : speed; + + return isMetric ? Parser.metric.getMetricNumber({originalValue: num, originalUnit: Parser.UNT_FEET}) : num; +}; +Parser._getSpeedString_getCondition = ({speed}) => speed.condition ? ` ${Renderer.get().render(speed.condition)}` : ""; + +Parser.SPEED_MODES = ["walk", "burrow", "climb", "fly", "swim"]; + +Parser.SPEED_TO_PROGRESSIVE = { + "walk": "walking", + "burrow": "burrowing", + "climb": "climbing", + "fly": "flying", + "swim": "swimming", +}; + +Parser.speedToProgressive = function (prop) { + return Parser._parse_aToB(Parser.SPEED_TO_PROGRESSIVE, prop); +}; + +Parser._addCommas = function (intNum) { + return `${intNum}`.replace(/(\d)(?=(\d{3})+$)/g, "$1,"); +}; + +Parser.raceCreatureTypesToFull = function (creatureTypes) { + const hasSubOptions = creatureTypes.some(it => it.choose); + return creatureTypes + .map(it => { + if (!it.choose) return Parser.monTypeToFullObj(it).asText; + return [...it.choose] + .sort(SortUtil.ascSortLower) + .map(sub => Parser.monTypeToFullObj(sub).asText) + .joinConjunct(", ", " or "); + }) + .joinConjunct(hasSubOptions ? "; " : ", ", " and "); +}; + +Parser.crToXp = function (cr, {isDouble = false} = {}) { + if (cr != null && cr.xp) return Parser._addCommas(`${isDouble ? cr.xp * 2 : cr.xp}`); + + const toConvert = cr ? (cr.cr || cr) : null; + if (toConvert === "Unknown" || toConvert == null || !Parser.XP_CHART_ALT[toConvert]) return "Unknown"; + // CR 0 creatures can be 0 or 10 XP, but 10 XP is used in almost every case. + // Exceptions, such as MM's Frog and Sea Horse, have their XP set to 0 on the creature + if (toConvert === "0") return "10"; + const xp = Parser.XP_CHART_ALT[toConvert]; + return Parser._addCommas(`${isDouble ? 2 * xp : xp}`); +}; + +Parser.crToXpNumber = function (cr) { + if (cr != null && cr.xp) return cr.xp; + const toConvert = cr ? (cr.cr || cr) : cr; + if (toConvert === "Unknown" || toConvert == null) return null; + return Parser.XP_CHART_ALT[toConvert] ?? null; +}; + +Parser.LEVEL_TO_XP_EASY = [0, 25, 50, 75, 125, 250, 300, 350, 450, 550, 600, 800, 1000, 1100, 1250, 1400, 1600, 2000, 2100, 2400, 2800]; +Parser.LEVEL_TO_XP_MEDIUM = [0, 50, 100, 150, 250, 500, 600, 750, 900, 1100, 1200, 1600, 2000, 2200, 2500, 2800, 3200, 3900, 4100, 4900, 5700]; +Parser.LEVEL_TO_XP_HARD = [0, 75, 150, 225, 375, 750, 900, 1100, 1400, 1600, 1900, 2400, 3000, 3400, 3800, 4300, 4800, 5900, 6300, 7300, 8500]; +Parser.LEVEL_TO_XP_DEADLY = [0, 100, 200, 400, 500, 1100, 1400, 1700, 2100, 2400, 2800, 3600, 4500, 5100, 5700, 6400, 7200, 8800, 9500, 10900, 12700]; +Parser.LEVEL_TO_XP_DAILY = [0, 300, 600, 1200, 1700, 3500, 4000, 5000, 6000, 7500, 9000, 10500, 11500, 13500, 15000, 18000, 20000, 25000, 27000, 30000, 40000]; + +Parser.LEVEL_XP_REQUIRED = [0, 300, 900, 2700, 6500, 14000, 23000, 34000, 48000, 64000, 85000, 100000, 120000, 140000, 165000, 195000, 225000, 265000, 305000, 355000]; + +Parser.CRS = ["0", "1/8", "1/4", "1/2", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30"]; + +Parser.levelToXpThreshold = function (level) { + return [Parser.LEVEL_TO_XP_EASY[level], Parser.LEVEL_TO_XP_MEDIUM[level], Parser.LEVEL_TO_XP_HARD[level], Parser.LEVEL_TO_XP_DEADLY[level]]; +}; + +Parser.isValidCr = function (cr) { + return Parser.CRS.includes(cr); +}; + +Parser.crToNumber = function (cr, opts = {}) { + const {isDefaultNull = false} = opts; + + if (cr === "Unknown" || cr === "\u2014" || cr == null) return isDefaultNull ? null : VeCt.CR_UNKNOWN; + if (cr.cr) return Parser.crToNumber(cr.cr, opts); + + const parts = cr.trim().split("/"); + if (!parts.length || parts.length >= 3) return isDefaultNull ? null : VeCt.CR_CUSTOM; + if (isNaN(parts[0])) return isDefaultNull ? null : VeCt.CR_CUSTOM; + + if (parts.length === 2) { + if (isNaN(Number(parts[1]))) return isDefaultNull ? null : VeCt.CR_CUSTOM; + return Number(parts[0]) / Number(parts[1]); + } + + return Number(parts[0]); +}; + +Parser.numberToCr = function (number, safe) { + // avoid dying if already-converted number is passed in + if (safe && typeof number === "string" && Parser.CRS.includes(number)) return number; + + if (number == null) return "Unknown"; + + return Parser.numberToFractional(number); +}; + +Parser.crToPb = function (cr) { + if (cr === "Unknown" || cr == null) return 0; + cr = cr.cr || cr; + if (Parser.crToNumber(cr) < 5) return 2; + return Math.ceil(cr / 4) + 1; +}; + +Parser.levelToPb = function (level) { + if (!level) return 2; + return Math.ceil(level / 4) + 1; +}; + +Parser.SKILL_TO_ATB_ABV = { + "athletics": "str", + "acrobatics": "dex", + "sleight of hand": "dex", + "stealth": "dex", + "arcana": "int", + "history": "int", + "investigation": "int", + "nature": "int", + "religion": "int", + "animal handling": "wis", + "insight": "wis", + "medicine": "wis", + "perception": "wis", + "survival": "wis", + "deception": "cha", + "intimidation": "cha", + "performance": "cha", + "persuasion": "cha", +}; + +Parser.skillToAbilityAbv = function (skill) { + return Parser._parse_aToB(Parser.SKILL_TO_ATB_ABV, skill); +}; + +Parser.SKILL_TO_SHORT = { + "athletics": "ath", + "acrobatics": "acro", + "sleight of hand": "soh", + "stealth": "slth", + "arcana": "arc", + "history": "hist", + "investigation": "invn", + "nature": "natr", + "religion": "reli", + "animal handling": "hndl", + "insight": "ins", + "medicine": "med", + "perception": "perp", + "survival": "surv", + "deception": "decp", + "intimidation": "intm", + "performance": "perf", + "persuasion": "pers", +}; + +Parser.skillToShort = function (skill) { + return Parser._parse_aToB(Parser.SKILL_TO_SHORT, skill); +}; + +Parser.LANGUAGES_STANDARD = [ + "Common", + "Dwarvish", + "Elvish", + "Giant", + "Gnomish", + "Goblin", + "Halfling", + "Orc", +]; + +Parser.LANGUAGES_EXOTIC = [ + "Abyssal", + "Aquan", + "Auran", + "Celestial", + "Draconic", + "Deep Speech", + "Ignan", + "Infernal", + "Primordial", + "Sylvan", + "Terran", + "Undercommon", +]; + +Parser.LANGUAGES_SECRET = [ + "Druidic", + "Thieves' cant", +]; + +Parser.LANGUAGES_ALL = [ + ...Parser.LANGUAGES_STANDARD, + ...Parser.LANGUAGES_EXOTIC, + ...Parser.LANGUAGES_SECRET, +].sort(); + +Parser.acToFull = function (ac, renderer) { + if (typeof ac === "string") return ac; // handle classic format + + renderer = renderer || Renderer.get(); + + let stack = ""; + let inBraces = false; + for (let i = 0; i < ac.length; ++i) { + const cur = ac[i]; + const nxt = ac[i + 1]; + + if (cur.special != null) { + if (inBraces) inBraces = false; + + stack += cur.special; + } else if (cur.ac) { + const isNxtBraces = nxt && nxt.braces; + + if (!inBraces && cur.braces) { + stack += "("; + inBraces = true; + } + + stack += cur.ac; + + if (cur.from) { + // always brace nested braces + if (cur.braces) { + stack += " ("; + } else { + stack += inBraces ? "; " : " ("; + } + + inBraces = true; + + stack += cur.from.map(it => renderer.render(it)).join(", "); + + if (cur.braces) { + stack += ")"; + } else if (!isNxtBraces) { + stack += ")"; + inBraces = false; + } + } + + if (cur.condition) stack += ` ${renderer.render(cur.condition)}`; + + if (inBraces && !isNxtBraces) { + stack += ")"; + inBraces = false; + } + } else { + stack += cur; + } + + if (nxt) { + if (nxt.braces) { + stack += inBraces ? "; " : " ("; + inBraces = true; + } else stack += ", "; + } + } + if (inBraces) stack += ")"; + + return stack.trim(); +}; + +Parser.MONSTER_COUNT_TO_XP_MULTIPLIER = [1, 1.5, 2, 2, 2, 2, 2.5, 2.5, 2.5, 2.5, 3, 3, 3, 3, 4]; +Parser.numMonstersToXpMult = function (num, playerCount = 3) { + const baseVal = (() => { + if (num >= Parser.MONSTER_COUNT_TO_XP_MULTIPLIER.length) return 4; + return Parser.MONSTER_COUNT_TO_XP_MULTIPLIER[num - 1]; + })(); + + if (playerCount < 3) return baseVal >= 3 ? baseVal + 1 : baseVal + 0.5; + else if (playerCount > 5) { + return baseVal === 4 ? 3 : baseVal - 0.5; + } else return baseVal; +}; + +Parser.armorFullToAbv = function (armor) { + return Parser._parse_bToA(Parser.ARMOR_ABV_TO_FULL, armor); +}; + +Parser.weaponFullToAbv = function (weapon) { + return Parser._parse_bToA(Parser.WEAPON_ABV_TO_FULL, weapon); +}; + +Parser._getSourceStringFromSource = function (source) { + if (source && source.source) return source.source; + return source; +}; +Parser._buildSourceCache = function (dict) { + const out = {}; + Object.entries(dict).forEach(([k, v]) => out[k.toLowerCase()] = v); + return out; +}; +Parser._sourceJsonCache = null; +Parser.hasSourceJson = function (source) { + Parser._sourceJsonCache = Parser._sourceJsonCache || Parser._buildSourceCache(Object.keys(Parser.SOURCE_JSON_TO_FULL).mergeMap(k => ({[k]: k}))); + return !!Parser._sourceJsonCache[source.toLowerCase()]; +}; +Parser._sourceFullCache = null; +Parser.hasSourceFull = function (source) { + Parser._sourceFullCache = Parser._sourceFullCache || Parser._buildSourceCache(Parser.SOURCE_JSON_TO_FULL); + return !!Parser._sourceFullCache[source.toLowerCase()]; +}; +Parser._sourceAbvCache = null; +Parser.hasSourceAbv = function (source) { + Parser._sourceAbvCache = Parser._sourceAbvCache || Parser._buildSourceCache(Parser.SOURCE_JSON_TO_ABV); + return !!Parser._sourceAbvCache[source.toLowerCase()]; +}; +Parser._sourceDateCache = null; +Parser.hasSourceDate = function (source) { + Parser._sourceDateCache = Parser._sourceDateCache || Parser._buildSourceCache(Parser.SOURCE_JSON_TO_DATE); + return !!Parser._sourceDateCache[source.toLowerCase()]; +}; +Parser.sourceJsonToJson = function (source) { + source = Parser._getSourceStringFromSource(source); + if (Parser.hasSourceJson(source)) return Parser._sourceJsonCache[source.toLowerCase()]; + if (typeof PrereleaseUtil !== "undefined" && PrereleaseUtil.hasSourceJson(source)) return PrereleaseUtil.sourceJsonToSource(source).json; + if (typeof BrewUtil2 !== "undefined" && BrewUtil2.hasSourceJson(source)) return BrewUtil2.sourceJsonToSource(source).json; + return source; +}; +Parser.sourceJsonToFull = function (source) { + source = Parser._getSourceStringFromSource(source); + if (Parser.hasSourceFull(source)) return Parser._sourceFullCache[source.toLowerCase()].replace(/'/g, "\u2019"); + if (typeof PrereleaseUtil !== "undefined" && PrereleaseUtil.hasSourceJson(source)) return PrereleaseUtil.sourceJsonToFull(source).replace(/'/g, "\u2019"); + if (typeof BrewUtil2 !== "undefined" && BrewUtil2.hasSourceJson(source)) return BrewUtil2.sourceJsonToFull(source).replace(/'/g, "\u2019"); + return Parser._parse_aToB(Parser.SOURCE_JSON_TO_FULL, source).replace(/'/g, "\u2019"); +}; +Parser.sourceJsonToFullCompactPrefix = function (source) { + return Parser.sourceJsonToFull(source) + .replace(Parser.UA_PREFIX, Parser.UA_PREFIX_SHORT) + .replace(/^Unearthed Arcana (\d+): /, "UA$1: ") + .replace(Parser.AL_PREFIX, Parser.AL_PREFIX_SHORT) + .replace(Parser.PS_PREFIX, Parser.PS_PREFIX_SHORT); +}; +Parser.sourceJsonToAbv = function (source) { + source = Parser._getSourceStringFromSource(source); + if (Parser.hasSourceAbv(source)) return Parser._sourceAbvCache[source.toLowerCase()]; + if (typeof PrereleaseUtil !== "undefined" && PrereleaseUtil.hasSourceJson(source)) return PrereleaseUtil.sourceJsonToAbv(source); + if (typeof BrewUtil2 !== "undefined" && BrewUtil2.hasSourceJson(source)) return BrewUtil2.sourceJsonToAbv(source); + return Parser._parse_aToB(Parser.SOURCE_JSON_TO_ABV, source); +}; +Parser.sourceJsonToDate = function (source) { + source = Parser._getSourceStringFromSource(source); + if (Parser.hasSourceDate(source)) return Parser._sourceDateCache[source.toLowerCase()]; + if (typeof PrereleaseUtil !== "undefined" && PrereleaseUtil.hasSourceJson(source)) return PrereleaseUtil.sourceJsonToDate(source); + if (typeof BrewUtil2 !== "undefined" && BrewUtil2.hasSourceJson(source)) return BrewUtil2.sourceJsonToDate(source); + return Parser._parse_aToB(Parser.SOURCE_JSON_TO_DATE, source, null); +}; + +Parser.sourceJsonToColor = function (source) { + return `source${Parser.sourceJsonToAbv(source)}`; +}; + +Parser.sourceJsonToStyle = function (source) { + source = Parser._getSourceStringFromSource(source); + if (Parser.hasSourceJson(source)) return ""; + if (typeof PrereleaseUtil !== "undefined" && PrereleaseUtil.hasSourceJson(source)) return PrereleaseUtil.sourceJsonToStyle(source); + if (typeof BrewUtil2 !== "undefined" && BrewUtil2.hasSourceJson(source)) return BrewUtil2.sourceJsonToStyle(source); + return ""; +}; + +Parser.sourceJsonToStylePart = function (source) { + source = Parser._getSourceStringFromSource(source); + if (Parser.hasSourceJson(source)) return ""; + if (typeof PrereleaseUtil !== "undefined" && PrereleaseUtil.hasSourceJson(source)) return PrereleaseUtil.sourceJsonToStylePart(source); + if (typeof BrewUtil2 !== "undefined" && BrewUtil2.hasSourceJson(source)) return BrewUtil2.sourceJsonToStylePart(source); + return ""; +}; + +Parser.sourceJsonToMarkerHtml = function (source, {isList = true, additionalStyles = ""} = {}) { + source = Parser._getSourceStringFromSource(source); + // TODO(Future) consider enabling this + // if (SourceUtil.isPartneredSourceWotc(source)) return `${isList ? "" : "["}โœฆ${isList ? "" : "]"}`; + if (SourceUtil.isLegacySourceWotc(source)) return `${isList ? "" : "["}สŸ${isList ? "" : "]"}`; + return ""; +}; + +Parser.stringToSlug = function (str) { + return str.trim().toLowerCase().toAscii().replace(/[^\w ]+/g, "").replace(/ +/g, "-"); +}; + +Parser.stringToCasedSlug = function (str) { + return str.toAscii().replace(/[^\w ]+/g, "").replace(/ +/g, "-"); +}; + +Parser.ITEM_SPELLCASTING_FOCUS_CLASSES = ["Artificer", "Bard", "Cleric", "Druid", "Paladin", "Ranger", "Sorcerer", "Warlock", "Wizard"]; + +Parser.itemValueToFull = function (item, opts = {isShortForm: false, isSmallUnits: false}) { + return Parser._moneyToFull(item, "value", "valueMult", opts); +}; + +Parser.itemValueToFullMultiCurrency = function (item, opts = {isShortForm: false, isSmallUnits: false}) { + return Parser._moneyToFullMultiCurrency(item, "value", "valueMult", opts); +}; + +Parser.itemVehicleCostsToFull = function (item, isShortForm) { + return { + travelCostFull: Parser._moneyToFull(item, "travelCost", "travelCostMult", {isShortForm}), + shippingCostFull: Parser._moneyToFull(item, "shippingCost", "shippingCostMult", {isShortForm}), + }; +}; + +Parser.spellComponentCostToFull = function (item, isShortForm) { + return Parser._moneyToFull(item, "cost", "costMult", {isShortForm}); +}; + +Parser.vehicleCostToFull = function (item, isShortForm) { + return Parser._moneyToFull(item, "cost", "costMult", {isShortForm}); +}; + +Parser._moneyToFull = function (it, prop, propMult, opts = {isShortForm: false, isSmallUnits: false}) { + if (it[prop] == null && it[propMult] == null) return ""; + if (it[prop] != null) { + const {coin, mult} = Parser.getCurrencyAndMultiplier(it[prop], it.currencyConversion); + return `${(it[prop] * mult).toLocaleString(undefined, {maximumFractionDigits: 5})}${opts.isSmallUnits ? `${coin}` : ` ${coin}`}`; + } else if (it[propMult] != null) return opts.isShortForm ? `ร—${it[propMult]}` : `base value ร—${it[propMult]}`; + return ""; +}; + +Parser._moneyToFullMultiCurrency = function (it, prop, propMult, {isShortForm, multiplier} = {}) { + if (it[prop]) { + const conversionTable = Parser.getCurrencyConversionTable(it.currencyConversion); + + const simplified = it.currencyConversion + ? CurrencyUtil.doSimplifyCoins( + { + // Assume the e.g. item's value is in the lowest available denomination + [conversionTable[0]?.coin || "cp"]: it[prop] * (multiplier ?? conversionTable[0]?.mult ?? 1), + }, + { + currencyConversionId: it.currencyConversion, + }, + ) + : CurrencyUtil.doSimplifyCoins({ + cp: it[prop] * (multiplier ?? 1), + }); + + return [...conversionTable] + .reverse() + .filter(meta => simplified[meta.coin]) + .map(meta => `${simplified[meta.coin].toLocaleString(undefined, {maximumFractionDigits: 5})} ${meta.coin}`) + .join(", "); + } + + if (it[propMult]) return isShortForm ? `ร—${it[propMult]}` : `base value ร—${it[propMult]}`; + + return ""; +}; + +Parser.DEFAULT_CURRENCY_CONVERSION_TABLE = [ + { + coin: "cp", + mult: 1, + }, + { + coin: "sp", + mult: 0.1, + }, + { + coin: "gp", + mult: 0.01, + isFallback: true, + }, +]; +Parser.FULL_CURRENCY_CONVERSION_TABLE = [ + { + coin: "cp", + mult: 1, + }, + { + coin: "sp", + mult: 0.1, + }, + { + coin: "ep", + mult: 0.02, + }, + { + coin: "gp", + mult: 0.01, + isFallback: true, + }, + { + coin: "pp", + mult: 0.001, + }, +]; +Parser.getCurrencyConversionTable = function (currencyConversionId) { + const fromPrerelease = currencyConversionId ? PrereleaseUtil.getMetaLookup("currencyConversions")?.[currencyConversionId] : null; + const fromBrew = currencyConversionId ? BrewUtil2.getMetaLookup("currencyConversions")?.[currencyConversionId] : null; + const conversionTable = fromPrerelease?.length + ? fromPrerelease + : fromBrew?.length + ? fromBrew + : Parser.DEFAULT_CURRENCY_CONVERSION_TABLE; + if (conversionTable !== Parser.DEFAULT_CURRENCY_CONVERSION_TABLE) conversionTable.sort((a, b) => SortUtil.ascSort(b.mult, a.mult)); + return conversionTable; +}; +Parser.getCurrencyAndMultiplier = function (value, currencyConversionId) { + const conversionTable = Parser.getCurrencyConversionTable(currencyConversionId); + + if (!value) return conversionTable.find(it => it.isFallback) || conversionTable[0]; + if (conversionTable.length === 1) return conversionTable[0]; + if (!Number.isInteger(value) && value < conversionTable[0].mult) return conversionTable[0]; + + for (let i = conversionTable.length - 1; i >= 0; --i) { + if (Number.isInteger(value * conversionTable[i].mult)) return conversionTable[i]; + } + + return conversionTable.last(); +}; + +Parser.COIN_ABVS = ["cp", "sp", "ep", "gp", "pp"]; +Parser.COIN_ABV_TO_FULL = { + "cp": "copper pieces", + "sp": "silver pieces", + "ep": "electrum pieces", + "gp": "gold pieces", + "pp": "platinum pieces", +}; +Parser.COIN_CONVERSIONS = [1, 10, 50, 100, 1000]; + +Parser.coinAbvToFull = function (coin) { + return Parser._parse_aToB(Parser.COIN_ABV_TO_FULL, coin); +}; + +/** + * @param currency Object of the form `{pp: , gp: , ... }`. + * @param isDisplayEmpty If "empty" values (i.e., those which are 0) should be displayed. + */ +Parser.getDisplayCurrency = function (currency, {isDisplayEmpty = false} = {}) { + return [...Parser.COIN_ABVS] + .reverse() + .filter(abv => isDisplayEmpty ? currency[abv] != null : currency[abv]) + .map(abv => `${currency[abv].toLocaleString()} ${abv}`) + .join(", "); +}; + +Parser.itemWeightToFull = function (item, isShortForm) { + if (item.weight) { + // Handle pure integers + if (Math.round(item.weight) === item.weight) return `${item.weight} lb.${(item.weightNote ? ` ${item.weightNote}` : "")}`; + + const integerPart = Math.floor(item.weight); + + // Attempt to render the amount as (a number +) a vulgar + const vulgarGlyph = Parser.numberToVulgar(item.weight - integerPart, {isFallbackOnFractional: false}); + if (vulgarGlyph) return `${integerPart || ""}${vulgarGlyph} lb.${(item.weightNote ? ` ${item.weightNote}` : "")}`; + + // Fall back on decimal pounds or ounces + return `${(item.weight < 1 ? item.weight * 16 : item.weight).toLocaleString(undefined, {maximumFractionDigits: 5})} ${item.weight < 1 ? "oz" : "lb"}.${(item.weightNote ? ` ${item.weightNote}` : "")}`; + } + if (item.weightMult) return isShortForm ? `ร—${item.weightMult}` : `base weight ร—${item.weightMult}`; + return ""; +}; + +Parser.ITEM_RECHARGE_TO_FULL = { + round: "Every Round", + restShort: "Short Rest", + restLong: "Long Rest", + dawn: "Dawn", + dusk: "Dusk", + midnight: "Midnight", + week: "Week", + month: "Month", + year: "Year", + decade: "Decade", + century: "Century", + special: "Special", +}; +Parser.itemRechargeToFull = function (recharge) { + return Parser._parse_aToB(Parser.ITEM_RECHARGE_TO_FULL, recharge); +}; + +Parser.ITEM_MISC_TAG_TO_FULL = { + "CF/W": "Creates Food/Water", + "TT": "Trinket Table", +}; +Parser.itemMiscTagToFull = function (type) { + return Parser._parse_aToB(Parser.ITEM_MISC_TAG_TO_FULL, type); +}; + +Parser._decimalSeparator = (0.1).toLocaleString().substring(1, 2); +Parser._numberCleanRegexp = Parser._decimalSeparator === "." ? new RegExp(/[\s,]*/g, "g") : new RegExp(/[\s.]*/g, "g"); +Parser._costSplitRegexp = Parser._decimalSeparator === "." ? new RegExp(/(\d+(\.\d+)?)([csegp]p)/) : new RegExp(/(\d+(,\d+)?)([csegp]p)/); + +/** input e.g. "25 gp", "1,000pp" */ +Parser.coinValueToNumber = function (value) { + if (!value) return 0; + // handle oddities + if (value === "Varies") return 0; + + value = value + .replace(/\s*/, "") + .replace(Parser._numberCleanRegexp, "") + .toLowerCase(); + const m = Parser._costSplitRegexp.exec(value); + if (!m) throw new Error(`Badly formatted value "${value}"`); + const ixCoin = Parser.COIN_ABVS.indexOf(m[3]); + if (!~ixCoin) throw new Error(`Unknown coin type "${m[3]}"`); + return Number(m[1]) * Parser.COIN_CONVERSIONS[ixCoin]; +}; + +Parser.weightValueToNumber = function (value) { + if (!value) return 0; + + if (Number(value)) return Number(value); + else throw new Error(`Badly formatted value ${value}`); +}; + +Parser.dmgTypeToFull = function (dmgType) { + return Parser._parse_aToB(Parser.DMGTYPE_JSON_TO_FULL, dmgType); +}; + +Parser.skillProficienciesToFull = function (skillProficiencies) { + function renderSingle (skProf) { + if (skProf.any) { + skProf = MiscUtil.copyFast(skProf); + skProf.choose = {"from": Object.keys(Parser.SKILL_TO_ATB_ABV), "count": skProf.any}; + delete skProf.any; + } + + const keys = Object.keys(skProf).sort(SortUtil.ascSortLower); + + const ixChoose = keys.indexOf("choose"); + if (~ixChoose) keys.splice(ixChoose, 1); + + const baseStack = []; + keys.filter(k => skProf[k]).forEach(k => baseStack.push(Renderer.get().render(`{@skill ${k.toTitleCase()}}`))); + + const chooseStack = []; + if (~ixChoose) { + const chObj = skProf.choose; + if (chObj.from.length === 18) { + chooseStack.push(`choose any ${!chObj.count || chObj.count === 1 ? "skill" : chObj.count}`); + } else { + chooseStack.push(`choose ${chObj.count || 1} from ${chObj.from.map(it => Renderer.get().render(`{@skill ${it.toTitleCase()}}`)).joinConjunct(", ", " and ")}`); + } + } + + const base = baseStack.joinConjunct(", ", " and "); + const choose = chooseStack.join(""); // this should currently only ever be 1-length + + if (baseStack.length && chooseStack.length) return `${base}; and ${choose}`; + else if (baseStack.length) return base; + else if (chooseStack.length) return choose; + } + + return skillProficiencies.map(renderSingle).join(" or "); +}; + +// sp-prefix functions are for parsing spell data, and shared with the roll20 script +Parser.spSchoolAndSubschoolsAbvsToFull = function (school, subschools) { + if (!subschools || !subschools.length) return Parser.spSchoolAbvToFull(school); + else return `${Parser.spSchoolAbvToFull(school)} (${subschools.map(sub => Parser.spSchoolAbvToFull(sub)).join(", ")})`; +}; + +Parser.spSchoolAbvToFull = function (schoolOrSubschool) { + const out = Parser._parse_aToB(Parser.SP_SCHOOL_ABV_TO_FULL, schoolOrSubschool); + if (Parser.SP_SCHOOL_ABV_TO_FULL[schoolOrSubschool]) return out; + if (PrereleaseUtil.getMetaLookup("spellSchools")?.[schoolOrSubschool]) return PrereleaseUtil.getMetaLookup("spellSchools")?.[schoolOrSubschool].full; + if (BrewUtil2.getMetaLookup("spellSchools")?.[schoolOrSubschool]) return BrewUtil2.getMetaLookup("spellSchools")?.[schoolOrSubschool].full; + return out; +}; + +Parser.spSchoolAndSubschoolsAbvsShort = function (school, subschools) { + if (!subschools || !subschools.length) return Parser.spSchoolAbvToShort(school); + else return `${Parser.spSchoolAbvToShort(school)} (${subschools.map(sub => Parser.spSchoolAbvToShort(sub)).join(", ")})`; +}; + +Parser.spSchoolAbvToShort = function (school) { + const out = Parser._parse_aToB(Parser.SP_SCHOOL_ABV_TO_SHORT, school); + if (Parser.SP_SCHOOL_ABV_TO_SHORT[school]) return out; + if (PrereleaseUtil.getMetaLookup("spellSchools")?.[school]) return PrereleaseUtil.getMetaLookup("spellSchools")?.[school].short; + if (BrewUtil2.getMetaLookup("spellSchools")?.[school]) return BrewUtil2.getMetaLookup("spellSchools")?.[school].short; + if (out.length <= 4) return out; + return `${out.slice(0, 3)}.`; +}; + +Parser.spSchoolAbvToStyle = function (school) { // For prerelease/homebrew + const stylePart = Parser.spSchoolAbvToStylePart(school); + if (!stylePart) return stylePart; + return `style="${stylePart}"`; +}; + +Parser.spSchoolAbvToStylePart = function (school) { // For prerelease/homebrew + return Parser._spSchoolAbvToStylePart_prereleaseBrew({school, brewUtil: PrereleaseUtil}) + || Parser._spSchoolAbvToStylePart_prereleaseBrew({school, brewUtil: BrewUtil2}) + || ""; +}; + +Parser._spSchoolAbvToStylePart_prereleaseBrew = function ({school, brewUtil}) { + const rawColor = brewUtil.getMetaLookup("spellSchools")?.[school]?.color; + if (!rawColor || !rawColor.trim()) return ""; + const validColor = BrewUtilShared.getValidColor(rawColor); + if (validColor.length) return `color: #${validColor};`; +}; + +Parser.getOrdinalForm = function (i) { + i = Number(i); + if (isNaN(i)) return ""; + const j = i % 10; const k = i % 100; + if (j === 1 && k !== 11) return `${i}st`; + if (j === 2 && k !== 12) return `${i}nd`; + if (j === 3 && k !== 13) return `${i}rd`; + return `${i}th`; +}; + +Parser.spLevelToFull = function (level) { + if (level === 0) return "Cantrip"; + else return Parser.getOrdinalForm(level); +}; + +Parser.getArticle = function (str) { + str = `${str}`; + str = str.replace(/\d+/g, (...m) => Parser.numberToText(m[0])); + return /^[aeiou]/i.test(str) ? "an" : "a"; +}; + +Parser.spLevelToFullLevelText = function (level, {isDash = false, isPluralCantrips = true} = {}) { + return `${Parser.spLevelToFull(level)}${(level === 0 ? (isPluralCantrips ? "s" : "") : `${isDash ? "-" : " "}level`)}`; +}; + +Parser.spLevelToSpellPoints = function (lvl) { + lvl = Number(lvl); + if (isNaN(lvl) || lvl === 0) return 0; + return Math.ceil(1.34 * lvl); +}; + +Parser.spMetaToArr = function (meta) { + if (!meta) return []; + return Object.entries(meta) + .filter(([_, v]) => v) + .sort(SortUtil.ascSort) + .map(([k]) => k); +}; + +Parser.spMetaToFull = function (meta) { + if (!meta) return ""; + const metaTags = Parser.spMetaToArr(meta); + if (metaTags.length) return ` (${metaTags.join(", ")})`; + return ""; +}; + +Parser.spLevelSchoolMetaToFull = function (level, school, meta, subschools) { + const levelPart = level === 0 ? Parser.spLevelToFull(level).toLowerCase() : `${Parser.spLevelToFull(level)}-level`; + const levelSchoolStr = level === 0 ? `${Parser.spSchoolAbvToFull(school)} ${levelPart}` : `${levelPart} ${Parser.spSchoolAbvToFull(school).toLowerCase()}`; + + const metaArr = Parser.spMetaToArr(meta); + if (metaArr.length || (subschools && subschools.length)) { + const metaAndSubschoolPart = [ + (subschools || []).map(sub => Parser.spSchoolAbvToFull(sub)).join(", "), + metaArr.join(", "), + ].filter(Boolean).join("; ").toLowerCase(); + return `${levelSchoolStr} (${metaAndSubschoolPart})`; + } + return levelSchoolStr; +}; + +Parser.spTimeListToFull = function (times, isStripTags) { + return times.map(t => `${Parser.getTimeToFull(t)}${t.condition ? `, ${isStripTags ? Renderer.stripTags(t.condition) : Renderer.get().render(t.condition)}` : ""}`).join(" or "); +}; + +Parser.getTimeToFull = function (time) { + return `${time.number ? `${time.number} ` : ""}${time.unit === "bonus" ? "bonus action" : time.unit}${time.number > 1 ? "s" : ""}`; +}; + +Parser.getMinutesToFull = function (mins) { + const days = Math.floor(mins / (24 * 60)); + mins = mins % (24 * 60); + + const hours = Math.floor(mins / 60); + mins = mins % 60; + + return [ + days ? `${days} day${days > 1 ? "s" : ""}` : null, + hours ? `${hours} hour${hours > 1 ? "s" : ""}` : null, + mins ? `${mins} minute${mins > 1 ? "s" : ""}` : null, + ].filter(Boolean) + .join(" "); +}; + +Parser.RNG_SPECIAL = "special"; +Parser.RNG_POINT = "point"; +Parser.RNG_LINE = "line"; +Parser.RNG_CUBE = "cube"; +Parser.RNG_CONE = "cone"; +Parser.RNG_RADIUS = "radius"; +Parser.RNG_SPHERE = "sphere"; +Parser.RNG_HEMISPHERE = "hemisphere"; +Parser.RNG_CYLINDER = "cylinder"; // homebrew only +Parser.RNG_SELF = "self"; +Parser.RNG_SIGHT = "sight"; +Parser.RNG_UNLIMITED = "unlimited"; +Parser.RNG_UNLIMITED_SAME_PLANE = "plane"; +Parser.RNG_TOUCH = "touch"; +Parser.SP_RANGE_TYPE_TO_FULL = { + [Parser.RNG_SPECIAL]: "Special", + [Parser.RNG_POINT]: "Point", + [Parser.RNG_LINE]: "Line", + [Parser.RNG_CUBE]: "Cube", + [Parser.RNG_CONE]: "Cone", + [Parser.RNG_RADIUS]: "Radius", + [Parser.RNG_SPHERE]: "Sphere", + [Parser.RNG_HEMISPHERE]: "Hemisphere", + [Parser.RNG_CYLINDER]: "Cylinder", + [Parser.RNG_SELF]: "Self", + [Parser.RNG_SIGHT]: "Sight", + [Parser.RNG_UNLIMITED]: "Unlimited", + [Parser.RNG_UNLIMITED_SAME_PLANE]: "Unlimited on the same plane", + [Parser.RNG_TOUCH]: "Touch", +}; + +Parser.spRangeTypeToFull = function (range) { + return Parser._parse_aToB(Parser.SP_RANGE_TYPE_TO_FULL, range); +}; + +Parser.UNT_FEET = "feet"; +Parser.UNT_YARDS = "yards"; +Parser.UNT_MILES = "miles"; +Parser.SP_DIST_TYPE_TO_FULL = { + [Parser.UNT_FEET]: "Feet", + [Parser.UNT_YARDS]: "Yards", + [Parser.UNT_MILES]: "Miles", + [Parser.RNG_SELF]: Parser.SP_RANGE_TYPE_TO_FULL[Parser.RNG_SELF], + [Parser.RNG_TOUCH]: Parser.SP_RANGE_TYPE_TO_FULL[Parser.RNG_TOUCH], + [Parser.RNG_SIGHT]: Parser.SP_RANGE_TYPE_TO_FULL[Parser.RNG_SIGHT], + [Parser.RNG_UNLIMITED]: Parser.SP_RANGE_TYPE_TO_FULL[Parser.RNG_UNLIMITED], + [Parser.RNG_UNLIMITED_SAME_PLANE]: Parser.SP_RANGE_TYPE_TO_FULL[Parser.RNG_UNLIMITED_SAME_PLANE], +}; + +Parser.spDistanceTypeToFull = function (range) { + return Parser._parse_aToB(Parser.SP_DIST_TYPE_TO_FULL, range); +}; + +Parser.SP_RANGE_TO_ICON = { + [Parser.RNG_SPECIAL]: "fa-star", + [Parser.RNG_POINT]: "", + [Parser.RNG_LINE]: "fa-grip-lines-vertical", + [Parser.RNG_CUBE]: "fa-cube", + [Parser.RNG_CONE]: "fa-traffic-cone", + [Parser.RNG_RADIUS]: "fa-hockey-puck", + [Parser.RNG_SPHERE]: "fa-globe", + [Parser.RNG_HEMISPHERE]: "fa-globe", + [Parser.RNG_CYLINDER]: "fa-database", + [Parser.RNG_SELF]: "fa-street-view", + [Parser.RNG_SIGHT]: "fa-eye", + [Parser.RNG_UNLIMITED_SAME_PLANE]: "fa-globe-americas", + [Parser.RNG_UNLIMITED]: "fa-infinity", + [Parser.RNG_TOUCH]: "fa-hand-paper", +}; + +Parser.spRangeTypeToIcon = function (range) { + return Parser._parse_aToB(Parser.SP_RANGE_TO_ICON, range); +}; + +Parser.spRangeToShortHtml = function (range) { + switch (range.type) { + case Parser.RNG_SPECIAL: return ``; + case Parser.RNG_POINT: return Parser.spRangeToShortHtml._renderPoint(range); + case Parser.RNG_LINE: + case Parser.RNG_CUBE: + case Parser.RNG_CONE: + case Parser.RNG_RADIUS: + case Parser.RNG_SPHERE: + case Parser.RNG_HEMISPHERE: + case Parser.RNG_CYLINDER: + return Parser.spRangeToShortHtml._renderArea(range); + } +}; +Parser.spRangeToShortHtml._renderPoint = function (range) { + const dist = range.distance; + switch (dist.type) { + case Parser.RNG_SELF: + case Parser.RNG_SIGHT: + case Parser.RNG_UNLIMITED: + case Parser.RNG_UNLIMITED_SAME_PLANE: + case Parser.RNG_SPECIAL: + case Parser.RNG_TOUCH: return ``; + case Parser.UNT_FEET: + case Parser.UNT_YARDS: + case Parser.UNT_MILES: + default: + return `${dist.amount} ${Parser.getSingletonUnit(dist.type, true)}`; + } +}; +Parser.spRangeToShortHtml._renderArea = function (range) { + const size = range.distance; + return ` ${size.amount}-${Parser.getSingletonUnit(size.type, true)} ${Parser.spRangeToShortHtml._getAreaStyleString(range)}`; +}; +Parser.spRangeToShortHtml._getAreaStyleString = function (range) { + return ``; +}; + +Parser.spRangeToFull = function (range) { + switch (range.type) { + case Parser.RNG_SPECIAL: return Parser.spRangeTypeToFull(range.type); + case Parser.RNG_POINT: return Parser.spRangeToFull._renderPoint(range); + case Parser.RNG_LINE: + case Parser.RNG_CUBE: + case Parser.RNG_CONE: + case Parser.RNG_RADIUS: + case Parser.RNG_SPHERE: + case Parser.RNG_HEMISPHERE: + case Parser.RNG_CYLINDER: + return Parser.spRangeToFull._renderArea(range); + } +}; +Parser.spRangeToFull._renderPoint = function (range) { + const dist = range.distance; + switch (dist.type) { + case Parser.RNG_SELF: + case Parser.RNG_SIGHT: + case Parser.RNG_UNLIMITED: + case Parser.RNG_UNLIMITED_SAME_PLANE: + case Parser.RNG_SPECIAL: + case Parser.RNG_TOUCH: return Parser.spRangeTypeToFull(dist.type); + case Parser.UNT_FEET: + case Parser.UNT_YARDS: + case Parser.UNT_MILES: + default: + return `${dist.amount} ${dist.amount === 1 ? Parser.getSingletonUnit(dist.type) : dist.type}`; + } +}; +Parser.spRangeToFull._renderArea = function (range) { + const size = range.distance; + return `Self (${size.amount}-${Parser.getSingletonUnit(size.type)}${Parser.spRangeToFull._getAreaStyleString(range)}${range.type === Parser.RNG_CYLINDER ? `${size.amountSecondary != null && size.typeSecondary != null ? `, ${size.amountSecondary}-${Parser.getSingletonUnit(size.typeSecondary)}-high` : ""} cylinder` : ""})`; +}; +Parser.spRangeToFull._getAreaStyleString = function (range) { + switch (range.type) { + case Parser.RNG_SPHERE: return " radius"; + case Parser.RNG_HEMISPHERE: return `-radius ${range.type}`; + case Parser.RNG_CYLINDER: return "-radius"; + default: return ` ${range.type}`; + } +}; + +Parser.getSingletonUnit = function (unit, isShort) { + switch (unit) { + case Parser.UNT_FEET: + return isShort ? "ft." : "foot"; + case Parser.UNT_YARDS: + return isShort ? "yd." : "yard"; + case Parser.UNT_MILES: + return isShort ? "mi." : "mile"; + default: { + const fromPrerelease = Parser._getSingletonUnit_prereleaseBrew({unit, isShort, brewUtil: PrereleaseUtil}); + if (fromPrerelease) return fromPrerelease; + + const fromBrew = Parser._getSingletonUnit_prereleaseBrew({unit, isShort, brewUtil: BrewUtil2}); + if (fromBrew) return fromBrew; + + if (unit.charAt(unit.length - 1) === "s") return unit.slice(0, -1); + return unit; + } + } +}; + +Parser._getSingletonUnit_prereleaseBrew = function ({unit, isShort, brewUtil}) { + const fromBrew = brewUtil.getMetaLookup("spellDistanceUnits")?.[unit]?.["singular"]; + if (fromBrew) return fromBrew; +}; + +Parser.RANGE_TYPES = [ + {type: Parser.RNG_POINT, hasDistance: true, isRequireAmount: false}, + + {type: Parser.RNG_LINE, hasDistance: true, isRequireAmount: true}, + {type: Parser.RNG_CUBE, hasDistance: true, isRequireAmount: true}, + {type: Parser.RNG_CONE, hasDistance: true, isRequireAmount: true}, + {type: Parser.RNG_RADIUS, hasDistance: true, isRequireAmount: true}, + {type: Parser.RNG_SPHERE, hasDistance: true, isRequireAmount: true}, + {type: Parser.RNG_HEMISPHERE, hasDistance: true, isRequireAmount: true}, + {type: Parser.RNG_CYLINDER, hasDistance: true, isRequireAmount: true}, + + {type: Parser.RNG_SPECIAL, hasDistance: false, isRequireAmount: false}, +]; + +Parser.DIST_TYPES = [ + {type: Parser.RNG_SELF, hasAmount: false}, + {type: Parser.RNG_TOUCH, hasAmount: false}, + + {type: Parser.UNT_FEET, hasAmount: true}, + {type: Parser.UNT_YARDS, hasAmount: true}, + {type: Parser.UNT_MILES, hasAmount: true}, + + {type: Parser.RNG_SIGHT, hasAmount: false}, + {type: Parser.RNG_UNLIMITED_SAME_PLANE, hasAmount: false}, + {type: Parser.RNG_UNLIMITED, hasAmount: false}, +]; + +Parser.spComponentsToFull = function (comp, level, {isPlainText = false} = {}) { + if (!comp) return "None"; + const out = []; + if (comp.v) out.push("V"); + if (comp.s) out.push("S"); + if (comp.m != null) { + const fnRender = isPlainText ? Renderer.stripTags.bind(Renderer) : Renderer.get().render.bind(Renderer.get()); + out.push(`M${comp.m !== true ? ` (${fnRender(comp.m.text != null ? comp.m.text : comp.m)})` : ""}`); + } + if (comp.r) out.push(`R (${level} gp)`); + return out.join(", ") || "None"; +}; + +Parser.SP_END_TYPE_TO_FULL = { + "dispel": "dispelled", + "trigger": "triggered", + "discharge": "discharged", +}; +Parser.spEndTypeToFull = function (type) { + return Parser._parse_aToB(Parser.SP_END_TYPE_TO_FULL, type); +}; + +Parser.spDurationToFull = function (dur) { + let hasSubOr = false; + const outParts = dur.map(d => { + switch (d.type) { + case "special": + return "Special"; + case "instant": + return `Instantaneous${d.condition ? ` (${d.condition})` : ""}`; + case "timed": + return `${d.concentration ? "Concentration, " : ""}${d.concentration ? "u" : d.duration.upTo ? "U" : ""}${d.concentration || d.duration.upTo ? "p to " : ""}${d.duration.amount} ${d.duration.amount === 1 ? d.duration.type : `${d.duration.type}s`}`; + case "permanent": { + if (d.ends) { + const endsToJoin = d.ends.map(m => Parser.spEndTypeToFull(m)); + hasSubOr = hasSubOr || endsToJoin.length > 1; + return `Until ${endsToJoin.joinConjunct(", ", " or ")}`; + } else { + return "Permanent"; + } + } + } + }); + return `${outParts.joinConjunct(hasSubOr ? "; " : ", ", " or ")}${dur.length > 1 ? " (see below)" : ""}`; +}; + +Parser.DURATION_TYPES = [ + {type: "instant", full: "Instantaneous"}, + {type: "timed", hasAmount: true}, + {type: "permanent", hasEnds: true}, + {type: "special"}, +]; + +Parser.DURATION_AMOUNT_TYPES = [ + "turn", + "round", + "minute", + "hour", + "day", + "week", + "month", + "year", +]; + +Parser.spClassesToFull = function (sp, {isTextOnly = false, subclassLookup = {}} = {}) { + const fromSubclassList = Renderer.spell.getCombinedClasses(sp, "fromSubclass"); + const fromSubclasses = Parser.spSubclassesToFull(fromSubclassList, {isTextOnly, subclassLookup}); + const fromClassList = Renderer.spell.getCombinedClasses(sp, "fromClassList"); + return `${Parser.spMainClassesToFull(fromClassList, {isTextOnly})}${fromSubclasses ? `, ${fromSubclasses}` : ""}`; +}; + +Parser.spMainClassesToFull = function (fromClassList, {isTextOnly = false} = {}) { + return fromClassList + .map(c => ({hash: UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_CLASSES](c), c})) + .filter(it => !ExcludeUtil.isInitialised || !ExcludeUtil.isExcluded(it.hash, "class", it.c.source)) + .sort((a, b) => SortUtil.ascSort(a.c.name, b.c.name)) + .map(it => { + if (isTextOnly) return it.c.name; + + return `${Renderer.get().render(`{@class ${it.c.name}|${it.c.source}}`)}`; + }) + .join(", ") || ""; +}; + +Parser.spSubclassesToFull = function (fromSubclassList, {isTextOnly = false, subclassLookup = {}} = {}) { + return fromSubclassList + .filter(mt => { + if (!ExcludeUtil.isInitialised) return true; + const excludeClass = ExcludeUtil.isExcluded(UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_CLASSES](mt.class), "class", mt.class.source); + if (excludeClass) return false; + + return !ExcludeUtil.isExcluded( + UrlUtil.URL_TO_HASH_BUILDER["subclass"]({ + shortName: mt.subclass.name, + source: mt.subclass.source, + className: mt.class.name, + classSource: mt.class.source, + }), + "subclass", + mt.subclass.source, + {isNoCount: true}, + ); + }) + .sort((a, b) => { + const byName = SortUtil.ascSort(a.class.name, b.class.name); + return byName || SortUtil.ascSort(a.subclass.name, b.subclass.name); + }) + .map(c => Parser._spSubclassItem({fromSubclass: c, isTextOnly})) + .join(", ") || ""; +}; + +Parser._spSubclassItem = function ({fromSubclass, isTextOnly}) { + const c = fromSubclass.class; + const sc = fromSubclass.subclass; + const text = `${sc.shortName}${sc.subSubclass ? ` (${sc.subSubclass})` : ""}`; + if (isTextOnly) return text; + + const classPart = `${Renderer.get().render(`{@class ${c.name}|${c.source}}`)}`; + + return `${Renderer.get().render(`{@class ${c.name}|${c.source}|${text}|${sc.shortName}|${sc.source}}`)} ${classPart}`; +}; + +Parser.SPELL_ATTACK_TYPE_TO_FULL = {}; +Parser.SPELL_ATTACK_TYPE_TO_FULL["M"] = "Melee"; +Parser.SPELL_ATTACK_TYPE_TO_FULL["R"] = "Ranged"; +Parser.SPELL_ATTACK_TYPE_TO_FULL["O"] = "Other/Unknown"; + +Parser.spAttackTypeToFull = function (type) { + return Parser._parse_aToB(Parser.SPELL_ATTACK_TYPE_TO_FULL, type); +}; + +Parser.SPELL_AREA_TYPE_TO_FULL = { + ST: "Single Target", + MT: "Multiple Targets", + C: "Cube", + N: "Cone", + Y: "Cylinder", + S: "Sphere", + R: "Circle", + Q: "Square", + L: "Line", + H: "Hemisphere", + W: "Wall", +}; +Parser.spAreaTypeToFull = function (type) { + return Parser._parse_aToB(Parser.SPELL_AREA_TYPE_TO_FULL, type); +}; + +Parser.SP_MISC_TAG_TO_FULL = { + HL: "Healing", + THP: "Grants Temporary Hit Points", + SGT: "Requires Sight", + PRM: "Permanent Effects", + SCL: "Scaling Effects", + SMN: "Summons Creature", + MAC: "Modifies AC", + TP: "Teleportation", + FMV: "Forced Movement", + RO: "Rollable Effects", + LGTS: "Creates Sunlight", + LGT: "Creates Light", + UBA: "Uses Bonus Action", + PS: "Plane Shifting", + OBS: "Obscures Vision", + DFT: "Difficult Terrain", + AAD: "Additional Attack Damage", + OBJ: "Affects Objects", + ADV: "Grants Advantage", +}; +Parser.spMiscTagToFull = function (type) { + return Parser._parse_aToB(Parser.SP_MISC_TAG_TO_FULL, type); +}; + +Parser.SP_CASTER_PROGRESSION_TO_FULL = { + full: "Full", + "1/2": "Half", + "1/3": "One-Third", + "pact": "Pact Magic", +}; +Parser.spCasterProgressionToFull = function (type) { + return Parser._parse_aToB(Parser.SP_CASTER_PROGRESSION_TO_FULL, type); +}; + +// mon-prefix functions are for parsing monster data, and shared with the roll20 script +Parser.monTypeToFullObj = function (type) { + const out = { + types: [], + tags: [], + asText: "", + asTextShort: "", + + typeSidekick: null, + tagsSidekick: [], + asTextSidekick: null, + }; + + // handles e.g. "fey" + if (typeof type === "string") { + out.types = [type]; + out.asText = type.toTitleCase(); + out.asTextShort = out.asText; + return out; + } + + if (type.type?.choose) { + out.types = type.type.choose; + } else { + out.types = [type.type]; + } + + if (type.swarmSize) { + out.tags.push("swarm"); + out.asText = `swarm of ${Parser.sizeAbvToFull(type.swarmSize)} ${out.types.map(typ => Parser.monTypeToPlural(typ).toTitleCase()).joinConjunct(", ", " or ")}`; + out.asTextShort = out.asText; + out.swarmSize = type.swarmSize; + } else { + out.asText = out.types.map(typ => typ.toTitleCase()).joinConjunct(", ", " or "); + out.asTextShort = out.asText; + } + + const tagMetas = Parser.monTypeToFullObj._getTagMetas(type.tags); + if (tagMetas.length) { + out.tags.push(...tagMetas.map(({filterTag}) => filterTag)); + const ptTags = ` (${tagMetas.map(({displayTag}) => displayTag).join(", ")})`; + out.asText += ptTags; + out.asTextShort += ptTags; + } + + if (type.note) out.asText += ` ${type.note}`; + + // region Sidekick + if (type.sidekickType) { + out.typeSidekick = type.sidekickType; + if (!type.sidekickHidden) out.asTextSidekick = `${type.sidekickType}`; + + const tagMetas = Parser.monTypeToFullObj._getTagMetas(type.sidekickTags); + if (tagMetas.length) { + out.tagsSidekick.push(...tagMetas.map(({filterTag}) => filterTag)); + if (!type.sidekickHidden) out.asTextSidekick += ` (${tagMetas.map(({displayTag}) => displayTag).join(", ")})`; + } + } + // endregion + + return out; +}; + +Parser.monTypeToFullObj._getTagMetas = (tags) => { + return tags + ? tags.map(tag => { + if (typeof tag === "string") { // handles e.g. "Fiend (Devil)" + return { + filterTag: tag.toLowerCase(), + displayTag: tag.toTitleCase(), + }; + } else { // handles e.g. "Humanoid (Chondathan Human)" + return { + filterTag: tag.tag.toLowerCase(), + displayTag: `${tag.prefix} ${tag.tag}`.toTitleCase(), + }; + } + }) + : []; +}; + +Parser.monTypeToPlural = function (type) { + return Parser._parse_aToB(Parser.MON_TYPE_TO_PLURAL, type); +}; + +Parser.monTypeFromPlural = function (type) { + return Parser._parse_bToA(Parser.MON_TYPE_TO_PLURAL, type); +}; + +Parser.monCrToFull = function (cr, {xp = null, isMythic = false} = {}) { + if (cr == null) return ""; + + if (typeof cr === "string") { + if (Parser.crToNumber(cr) >= VeCt.CR_CUSTOM) return `${cr}${xp != null ? ` (${xp} XP)` : ""}`; + + xp = xp != null ? Parser._addCommas(xp) : Parser.crToXp(cr); + return `${cr} (${xp} XP${isMythic ? `, or ${Parser.crToXp(cr, {isDouble: true})} XP as a mythic encounter` : ""})`; + } else { + const stack = [Parser.monCrToFull(cr.cr, {xp: cr.xp, isMythic})]; + if (cr.lair) stack.push(`${Parser.monCrToFull(cr.lair)} when encountered in lair`); + if (cr.coven) stack.push(`${Parser.monCrToFull(cr.coven)} when part of a coven`); + return stack.joinConjunct(", ", " or "); + } +}; + +Parser.getFullImmRes = function (toParse) { + if (!toParse?.length) return ""; + + let maxDepth = 0; + + function toString (it, depth = 0) { + maxDepth = Math.max(maxDepth, depth); + if (typeof it === "string") { + return it; + } else if (it.special) { + return it.special; + } else { + const stack = []; + + if (it.preNote) stack.push(it.preNote); + + const prop = it.immune ? "immune" : it.resist ? "resist" : it.vulnerable ? "vulnerable" : null; + if (prop) { + const toJoin = it[prop].length === Parser.DMG_TYPES.length && CollectionUtil.deepEquals(Parser.DMG_TYPES, it[prop]) + ? ["all damage"] + : it[prop].map(nxt => toString(nxt, depth + 1)); + stack.push(depth ? toJoin.join(maxDepth ? "; " : ", ") : toJoin.joinConjunct(", ", " and ")); + } + + if (it.note) stack.push(it.note); + + return stack.join(" "); + } + } + + const arr = toParse.map(it => toString(it)); + + if (arr.length <= 1) return arr.join(""); + + let out = ""; + for (let i = 0; i < arr.length - 1; ++i) { + const it = arr[i]; + const nxt = arr[i + 1]; + + const orig = toParse[i]; + const origNxt = toParse[i + 1]; + + out += it; + out += (it.includes(",") || nxt.includes(",") || (orig && orig.cond) || (origNxt && origNxt.cond)) ? "; " : ", "; + } + out += arr.last(); + return out; +}; + +Parser.getFullCondImm = function (condImm, {isPlainText = false, isEntry = false} = {}) { + if (isPlainText && isEntry) throw new Error(`Options "isPlainText" and "isEntry" are mutually exclusive!`); + + if (!condImm?.length) return ""; + + const render = condition => { + if (isPlainText) return condition; + const ent = `{@condition ${condition}}`; + if (isEntry) return ent; + return Renderer.get().render(ent); + }; + + return condImm + .map(it => { + if (it.special) return it.special; + if (it.conditionImmune) return `${it.preNote ? `${it.preNote} ` : ""}${it.conditionImmune.map(render).join(", ")}${it.note ? ` ${it.note}` : ""}`; + return render(it); + }) + .sort(SortUtil.ascSortLower).join(", "); +}; + +Parser.MON_SENSE_TAG_TO_FULL = { + "B": "blindsight", + "D": "darkvision", + "SD": "superior darkvision", + "T": "tremorsense", + "U": "truesight", +}; +Parser.monSenseTagToFull = function (tag) { + return Parser._parse_aToB(Parser.MON_SENSE_TAG_TO_FULL, tag); +}; + +Parser.MON_SPELLCASTING_TAG_TO_FULL = { + "P": "Psionics", + "I": "Innate", + "F": "Form Only", + "S": "Shared", + "O": "Other", + "CA": "Class, Artificer", + "CB": "Class, Bard", + "CC": "Class, Cleric", + "CD": "Class, Druid", + "CP": "Class, Paladin", + "CR": "Class, Ranger", + "CS": "Class, Sorcerer", + "CL": "Class, Warlock", + "CW": "Class, Wizard", +}; +Parser.monSpellcastingTagToFull = function (tag) { + return Parser._parse_aToB(Parser.MON_SPELLCASTING_TAG_TO_FULL, tag); +}; + +Parser.MON_MISC_TAG_TO_FULL = { + "AOE": "Has Areas of Effect", + "HPR": "Has HP Reduction", + "MW": "Has Weapon Attacks, Melee", + "RW": "Has Weapon Attacks, Ranged", + "MLW": "Has Melee Weapons", + "RNG": "Has Ranged Weapons", + "RCH": "Has Reach Attacks", + "THW": "Has Thrown Weapons", +}; +Parser.monMiscTagToFull = function (tag) { + return Parser._parse_aToB(Parser.MON_MISC_TAG_TO_FULL, tag); +}; + +Parser.MON_LANGUAGE_TAG_TO_FULL = { + "AB": "Abyssal", + "AQ": "Aquan", + "AU": "Auran", + "C": "Common", + "CE": "Celestial", + "CS": "Can't Speak Known Languages", + "D": "Dwarvish", + "DR": "Draconic", + "DS": "Deep Speech", + "DU": "Druidic", + "E": "Elvish", + "G": "Gnomish", + "GI": "Giant", + "GO": "Goblin", + "GTH": "Gith", + "H": "Halfling", + "I": "Infernal", + "IG": "Ignan", + "LF": "Languages Known in Life", + "O": "Orc", + "OTH": "Other", + "P": "Primordial", + "S": "Sylvan", + "T": "Terran", + "TC": "Thieves' cant", + "TP": "Telepathy", + "U": "Undercommon", + "X": "Any (Choose)", + "XX": "All", +}; +Parser.monLanguageTagToFull = function (tag) { + return Parser._parse_aToB(Parser.MON_LANGUAGE_TAG_TO_FULL, tag); +}; + +Parser.ENVIRONMENTS = ["arctic", "coastal", "desert", "forest", "grassland", "hill", "mountain", "swamp", "underdark", "underwater", "urban"]; + +// psi-prefix functions are for parsing psionic data, and shared with the roll20 script +Parser.PSI_ABV_TYPE_TALENT = "T"; +Parser.PSI_ABV_TYPE_DISCIPLINE = "D"; +Parser.PSI_ORDER_NONE = "None"; +Parser.psiTypeToFull = type => Parser.psiTypeToMeta(type).full; + +Parser.psiTypeToMeta = type => { + let out = {}; + if (type === Parser.PSI_ABV_TYPE_TALENT) out = {hasOrder: false, full: "Talent"}; + else if (type === Parser.PSI_ABV_TYPE_DISCIPLINE) out = {hasOrder: true, full: "Discipline"}; + else if (PrereleaseUtil.getMetaLookup("psionicTypes")?.[type]) out = MiscUtil.copyFast(PrereleaseUtil.getMetaLookup("psionicTypes")[type]); + else if (BrewUtil2.getMetaLookup("psionicTypes")?.[type]) out = MiscUtil.copyFast(BrewUtil2.getMetaLookup("psionicTypes")[type]); + out.full = out.full || "Unknown"; + out.short = out.short || out.full; + return out; +}; + +Parser.psiOrderToFull = (order) => { + return order === undefined ? Parser.PSI_ORDER_NONE : order; +}; + +Parser.prereqSpellToFull = function (spell, {isTextOnly = false} = {}) { + if (spell) { + const [text, suffix] = spell.split("#"); + if (!suffix) return isTextOnly ? spell : Renderer.get().render(`{@spell ${spell}}`); + else if (suffix === "c") return (isTextOnly ? Renderer.stripTags : Renderer.get().render.bind(Renderer.get()))(`{@spell ${text}} cantrip`); + else if (suffix === "x") return (isTextOnly ? Renderer.stripTags : Renderer.get().render.bind(Renderer.get()))("{@spell hex} spell or a warlock feature that curses"); + } else return VeCt.STR_NONE; +}; + +Parser.prereqPactToFull = function (pact) { + if (pact === "Chain") return "Pact of the Chain"; + if (pact === "Tome") return "Pact of the Tome"; + if (pact === "Blade") return "Pact of the Blade"; + if (pact === "Talisman") return "Pact of the Talisman"; + return pact; +}; + +Parser.prereqPatronToShort = function (patron) { + if (patron === "Any") return patron; + const mThe = /^The (.*?)$/.exec(patron); + if (mThe) return mThe[1]; + return patron; +}; + +// NOTE: These need to be reflected in omnidexer.js to be indexed +Parser.OPT_FEATURE_TYPE_TO_FULL = { + AI: "Artificer Infusion", + ED: "Elemental Discipline", + EI: "Eldritch Invocation", + MM: "Metamagic", + "MV": "Maneuver", + "MV:B": "Maneuver, Battle Master", + "MV:C2-UA": "Maneuver, Cavalier V2 (UA)", + "AS:V1-UA": "Arcane Shot, V1 (UA)", + "AS:V2-UA": "Arcane Shot, V2 (UA)", + "AS": "Arcane Shot", + OTH: "Other", + "FS:F": "Fighting Style; Fighter", + "FS:B": "Fighting Style; Bard", + "FS:P": "Fighting Style; Paladin", + "FS:R": "Fighting Style; Ranger", + "PB": "Pact Boon", + "OR": "Onomancy Resonant", + "RN": "Rune Knight Rune", + "AF": "Alchemical Formula", +}; + +Parser.optFeatureTypeToFull = function (type) { + if (Parser.OPT_FEATURE_TYPE_TO_FULL[type]) return Parser.OPT_FEATURE_TYPE_TO_FULL[type]; + if (PrereleaseUtil.getMetaLookup("optionalFeatureTypes")?.[type]) return PrereleaseUtil.getMetaLookup("optionalFeatureTypes")[type]; + if (BrewUtil2.getMetaLookup("optionalFeatureTypes")?.[type]) return BrewUtil2.getMetaLookup("optionalFeatureTypes")[type]; + return type; +}; + +Parser.CHAR_OPTIONAL_FEATURE_TYPE_TO_FULL = { + "SG": "Supernatural Gift", + "OF": "Optional Feature", + "DG": "Dark Gift", + "RF:B": "Replacement Feature: Background", + "CS": "Character Secret", // Specific to IDRotF (rules on page 14) +}; + +Parser.charCreationOptionTypeToFull = function (type) { + if (Parser.CHAR_OPTIONAL_FEATURE_TYPE_TO_FULL[type]) return Parser.CHAR_OPTIONAL_FEATURE_TYPE_TO_FULL[type]; + if (PrereleaseUtil.getMetaLookup("charOption")?.[type]) return PrereleaseUtil.getMetaLookup("charOption")[type]; + if (BrewUtil2.getMetaLookup("charOption")?.[type]) return BrewUtil2.getMetaLookup("charOption")[type]; + return type; +}; + +Parser.alignmentAbvToFull = function (alignment) { + if (!alignment) return null; // used in sidekicks + if (typeof alignment === "object") { + if (alignment.special != null) { + // use in MTF Sacred Statue + return alignment.special; + } else { + // e.g. `{alignment: ["N", "G"], chance: 50}` or `{alignment: ["N", "G"]}` + return `${alignment.alignment.map(a => Parser.alignmentAbvToFull(a)).join(" ")}${alignment.chance ? ` (${alignment.chance}%)` : ""}${alignment.note ? ` (${alignment.note})` : ""}`; + } + } else { + alignment = alignment.toUpperCase(); + switch (alignment) { + case "L": + return "lawful"; + case "N": + return "neutral"; + case "NX": + return "neutral (law/chaos axis)"; + case "NY": + return "neutral (good/evil axis)"; + case "C": + return "chaotic"; + case "G": + return "good"; + case "E": + return "evil"; + // "special" values + case "U": + return "unaligned"; + case "A": + return "any alignment"; + } + return alignment; + } +}; + +Parser.alignmentListToFull = function (alignList) { + if (!alignList) return ""; + if (alignList.some(it => typeof it !== "string")) { + if (alignList.some(it => typeof it === "string")) throw new Error(`Mixed alignment types: ${JSON.stringify(alignList)}`); + // filter out any nonexistent alignments, as we don't care about "alignment does not exist" if there are other alignments + alignList = alignList.filter(it => it.alignment === undefined || it.alignment != null); + return alignList.map(it => it.special != null || it.chance != null || it.note != null ? Parser.alignmentAbvToFull(it) : Parser.alignmentListToFull(it.alignment)).join(" or "); + } else { + // assume all single-length arrays can be simply parsed + if (alignList.length === 1) return Parser.alignmentAbvToFull(alignList[0]); + // a pair of abv's, e.g. "L" "G" + if (alignList.length === 2) { + return alignList.map(a => Parser.alignmentAbvToFull(a)).join(" "); + } + if (alignList.length === 3) { + if (alignList.includes("NX") && alignList.includes("NY") && alignList.includes("N")) return "any neutral alignment"; + } + // longer arrays should have a custom mapping + if (alignList.length === 5) { + if (!alignList.includes("G")) return "any non-good alignment"; + if (!alignList.includes("E")) return "any non-evil alignment"; + if (!alignList.includes("L")) return "any non-lawful alignment"; + if (!alignList.includes("C")) return "any non-chaotic alignment"; + } + if (alignList.length === 4) { + if (!alignList.includes("L") && !alignList.includes("NX")) return "any chaotic alignment"; + if (!alignList.includes("G") && !alignList.includes("NY")) return "any evil alignment"; + if (!alignList.includes("C") && !alignList.includes("NX")) return "any lawful alignment"; + if (!alignList.includes("E") && !alignList.includes("NY")) return "any good alignment"; + } + throw new Error(`Unmapped alignment: ${JSON.stringify(alignList)}`); + } +}; + +Parser.weightToFull = function (lbs, isSmallUnit) { + const tons = Math.floor(lbs / 2000); + lbs = lbs - (2000 * tons); + return [ + tons ? `${tons}${isSmallUnit ? `` : " "}ton${tons === 1 ? "" : "s"}${isSmallUnit ? `` : ""}` : null, + lbs ? `${lbs}${isSmallUnit ? `` : " "}lb.${isSmallUnit ? `` : ""}` : null, + ].filter(Boolean).join(", "); +}; + +Parser.RARITIES = ["common", "uncommon", "rare", "very rare", "legendary", "artifact"]; +Parser.ITEM_RARITIES = ["none", ...Parser.RARITIES, "unknown", "unknown (magic)", "other"]; + +Parser.CAT_ID_CREATURE = 1; +Parser.CAT_ID_SPELL = 2; +Parser.CAT_ID_BACKGROUND = 3; +Parser.CAT_ID_ITEM = 4; +Parser.CAT_ID_CLASS = 5; +Parser.CAT_ID_CONDITION = 6; +Parser.CAT_ID_FEAT = 7; +Parser.CAT_ID_ELDRITCH_INVOCATION = 8; +Parser.CAT_ID_PSIONIC = 9; +Parser.CAT_ID_RACE = 10; +Parser.CAT_ID_OTHER_REWARD = 11; +Parser.CAT_ID_VARIANT_OPTIONAL_RULE = 12; +Parser.CAT_ID_ADVENTURE = 13; +Parser.CAT_ID_DEITY = 14; +Parser.CAT_ID_OBJECT = 15; +Parser.CAT_ID_TRAP = 16; +Parser.CAT_ID_HAZARD = 17; +Parser.CAT_ID_QUICKREF = 18; +Parser.CAT_ID_CULT = 19; +Parser.CAT_ID_BOON = 20; +Parser.CAT_ID_DISEASE = 21; +Parser.CAT_ID_METAMAGIC = 22; +Parser.CAT_ID_MANEUVER_BATTLEMASTER = 23; +Parser.CAT_ID_TABLE = 24; +Parser.CAT_ID_TABLE_GROUP = 25; +Parser.CAT_ID_MANEUVER_CAVALIER = 26; +Parser.CAT_ID_ARCANE_SHOT = 27; +Parser.CAT_ID_OPTIONAL_FEATURE_OTHER = 28; +Parser.CAT_ID_FIGHTING_STYLE = 29; +Parser.CAT_ID_CLASS_FEATURE = 30; +Parser.CAT_ID_VEHICLE = 31; +Parser.CAT_ID_PACT_BOON = 32; +Parser.CAT_ID_ELEMENTAL_DISCIPLINE = 33; +Parser.CAT_ID_ARTIFICER_INFUSION = 34; +Parser.CAT_ID_SHIP_UPGRADE = 35; +Parser.CAT_ID_INFERNAL_WAR_MACHINE_UPGRADE = 36; +Parser.CAT_ID_ONOMANCY_RESONANT = 37; +Parser.CAT_ID_RUNE_KNIGHT_RUNE = 37; +Parser.CAT_ID_ALCHEMICAL_FORMULA = 38; +Parser.CAT_ID_MANEUVER = 39; +Parser.CAT_ID_SUBCLASS = 40; +Parser.CAT_ID_SUBCLASS_FEATURE = 41; +Parser.CAT_ID_ACTION = 42; +Parser.CAT_ID_LANGUAGE = 43; +Parser.CAT_ID_BOOK = 44; +Parser.CAT_ID_PAGE = 45; +Parser.CAT_ID_LEGENDARY_GROUP = 46; +Parser.CAT_ID_CHAR_CREATION_OPTIONS = 47; +Parser.CAT_ID_RECIPES = 48; +Parser.CAT_ID_STATUS = 49; +Parser.CAT_ID_SKILLS = 50; +Parser.CAT_ID_SENSES = 51; +Parser.CAT_ID_DECK = 52; +Parser.CAT_ID_CARD = 53; + +Parser.CAT_ID_TO_FULL = {}; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_CREATURE] = "Bestiary"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_SPELL] = "Spell"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_BACKGROUND] = "Background"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_ITEM] = "Item"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_CLASS] = "Class"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_CONDITION] = "Condition"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_FEAT] = "Feat"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_ELDRITCH_INVOCATION] = "Eldritch Invocation"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_PSIONIC] = "Psionic"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_RACE] = "Race"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_OTHER_REWARD] = "Other Reward"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_VARIANT_OPTIONAL_RULE] = "Variant/Optional Rule"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_ADVENTURE] = "Adventure"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_DEITY] = "Deity"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_OBJECT] = "Object"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_TRAP] = "Trap"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_HAZARD] = "Hazard"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_QUICKREF] = "Quick Reference"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_CULT] = "Cult"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_BOON] = "Boon"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_DISEASE] = "Disease"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_METAMAGIC] = "Metamagic"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_MANEUVER_BATTLEMASTER] = "Maneuver; Battlemaster"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_TABLE] = "Table"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_TABLE_GROUP] = "Table"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_MANEUVER_CAVALIER] = "Maneuver; Cavalier"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_ARCANE_SHOT] = "Arcane Shot"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_OPTIONAL_FEATURE_OTHER] = "Optional Feature"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_FIGHTING_STYLE] = "Fighting Style"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_CLASS_FEATURE] = "Class Feature"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_VEHICLE] = "Vehicle"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_PACT_BOON] = "Pact Boon"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_ELEMENTAL_DISCIPLINE] = "Elemental Discipline"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_ARTIFICER_INFUSION] = "Infusion"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_SHIP_UPGRADE] = "Ship Upgrade"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_INFERNAL_WAR_MACHINE_UPGRADE] = "Infernal War Machine Upgrade"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_ONOMANCY_RESONANT] = "Onomancy Resonant"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_RUNE_KNIGHT_RUNE] = "Rune Knight Rune"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_ALCHEMICAL_FORMULA] = "Alchemical Formula"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_MANEUVER] = "Maneuver"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_SUBCLASS] = "Subclass"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_SUBCLASS_FEATURE] = "Subclass Feature"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_ACTION] = "Action"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_LANGUAGE] = "Language"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_BOOK] = "Book"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_PAGE] = "Page"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_LEGENDARY_GROUP] = "Legendary Group"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_CHAR_CREATION_OPTIONS] = "Character Creation Option"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_RECIPES] = "Recipe"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_STATUS] = "Status"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_DECK] = "Deck"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_CARD] = "Card"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_SKILLS] = "Skill"; +Parser.CAT_ID_TO_FULL[Parser.CAT_ID_SENSES] = "Sense"; + +Parser.pageCategoryToFull = function (catId) { + return Parser._parse_aToB(Parser.CAT_ID_TO_FULL, catId); +}; + +Parser.CAT_ID_TO_PROP = {}; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_CREATURE] = "monster"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_SPELL] = "spell"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_BACKGROUND] = "background"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_ITEM] = "item"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_CLASS] = "class"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_CONDITION] = "condition"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_FEAT] = "feat"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_PSIONIC] = "psionic"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_RACE] = "race"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_OTHER_REWARD] = "reward"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_VARIANT_OPTIONAL_RULE] = "variantrule"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_ADVENTURE] = "adventure"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_DEITY] = "deity"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_OBJECT] = "object"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_TRAP] = "trap"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_HAZARD] = "hazard"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_CULT] = "cult"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_BOON] = "boon"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_DISEASE] = "condition"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_TABLE] = "table"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_TABLE_GROUP] = "tableGroup"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_VEHICLE] = "vehicle"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_ELDRITCH_INVOCATION] = "optionalfeature"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_MANEUVER_CAVALIER] = "optionalfeature"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_ARCANE_SHOT] = "optionalfeature"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_OPTIONAL_FEATURE_OTHER] = "optionalfeature"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_FIGHTING_STYLE] = "optionalfeature"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_METAMAGIC] = "optionalfeature"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_MANEUVER_BATTLEMASTER] = "optionalfeature"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_PACT_BOON] = "optionalfeature"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_ELEMENTAL_DISCIPLINE] = "optionalfeature"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_ARTIFICER_INFUSION] = "optionalfeature"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_SHIP_UPGRADE] = "vehicleUpgrade"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_INFERNAL_WAR_MACHINE_UPGRADE] = "vehicleUpgrade"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_ONOMANCY_RESONANT] = "optionalfeature"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_RUNE_KNIGHT_RUNE] = "optionalfeature"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_ALCHEMICAL_FORMULA] = "optionalfeature"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_MANEUVER] = "optionalfeature"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_QUICKREF] = null; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_CLASS_FEATURE] = "classFeature"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_SUBCLASS] = "subclass"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_SUBCLASS_FEATURE] = "subclassFeature"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_ACTION] = "action"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_LANGUAGE] = "language"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_BOOK] = "book"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_PAGE] = null; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_LEGENDARY_GROUP] = "legendaryGroup"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_CHAR_CREATION_OPTIONS] = "charoption"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_RECIPES] = "recipe"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_STATUS] = "status"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_DECK] = "deck"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_CARD] = "card"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_SKILLS] = "skill"; +Parser.CAT_ID_TO_PROP[Parser.CAT_ID_SENSES] = "sense"; + +Parser.pageCategoryToProp = function (catId) { + return Parser._parse_aToB(Parser.CAT_ID_TO_PROP, catId); +}; + +Parser.ABIL_ABVS = ["str", "dex", "con", "int", "wis", "cha"]; + +Parser.spClassesToCurrentAndLegacy = function (fromClassList) { + const current = []; + const legacy = []; + fromClassList.forEach(cls => { + if ((cls.name === "Artificer" && cls.source === "UAArtificer") || (cls.name === "Artificer (Revisited)" && cls.source === "UAArtificerRevisited")) legacy.push(cls); + else current.push(cls); + }); + return [current, legacy]; +}; + +/** + * Build a pair of strings; one with all current subclasses, one with all legacy subclasses + * + * @param sp a spell + * @param subclassLookup Data loaded from `generated/gendata-subclass-lookup.json`. Of the form: `{PHB: {Barbarian: {PHB: {Berserker: "Path of the Berserker"}}}}` + * @returns {*[]} A two-element array. First item is a string of all the current subclasses, second item a string of + * all the legacy/superseded subclasses + */ +Parser.spSubclassesToCurrentAndLegacyFull = function (sp, subclassLookup) { + return Parser._spSubclassesToCurrentAndLegacyFull({sp, subclassLookup, prop: "fromSubclass"}); +}; + +Parser.spVariantSubclassesToCurrentAndLegacyFull = function (sp, subclassLookup) { + return Parser._spSubclassesToCurrentAndLegacyFull({sp, subclassLookup, prop: "fromSubclassVariant"}); +}; + +Parser._spSubclassesToCurrentAndLegacyFull = ({sp, subclassLookup, prop}) => { + const fromSubclass = Renderer.spell.getCombinedClasses(sp, prop); + if (!fromSubclass.length) return ["", ""]; + + const current = []; + const legacy = []; + const curNames = new Set(); + const toCheck = []; + fromSubclass + .filter(c => { + const excludeClass = ExcludeUtil.isExcluded( + UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_CLASSES]({name: c.class.name, source: c.class.source}), + "class", + c.class.source, + {isNoCount: true}, + ); + if (excludeClass) return false; + + const excludeSubclass = ExcludeUtil.isExcluded( + UrlUtil.URL_TO_HASH_BUILDER["subclass"]({ + shortName: c.subclass.shortName, + source: c.subclass.source, + className: c.class.name, + classSource: c.class.source, + }), + "subclass", + c.subclass.source, + {isNoCount: true}, + ); + if (excludeSubclass) return false; + + return !Renderer.spell.isExcludedSubclassVariantSource({classDefinedInSource: c.class.definedInSource}); + }) + .sort((a, b) => { + const byName = SortUtil.ascSort(a.subclass.name, b.subclass.name); + return byName || SortUtil.ascSort(a.class.name, b.class.name); + }) + .forEach(c => { + const nm = c.subclass.name; + const src = c.subclass.source; + + const toAdd = Parser._spSubclassItem({fromSubclass: c, isTextOnly: false}); + + const fromLookup = MiscUtil.get( + subclassLookup, + c.class.source, + c.class.name, + c.subclass.source, + c.subclass.name, + ); + + if (fromLookup && fromLookup.isReprinted) { + legacy.push(toAdd); + } else if (SourceUtil.isNonstandardSource(src)) { + const cleanName = Parser._spSubclassesToCurrentAndLegacyFull.mapClassShortNameToMostRecent( + nm.split("(")[0].trim().split(/v\d+/)[0].trim(), + ); + toCheck.push({"name": cleanName, "ele": toAdd}); + } else { + current.push(toAdd); + curNames.add(nm); + } + }); + + toCheck.forEach(n => { + if (curNames.has(n.name)) { + legacy.push(n.ele); + } else { + current.push(n.ele); + } + }); + + return [current.join(", "), legacy.join(", ")]; +}; + +/** + * Get the most recent iteration of a subclass name. + */ +Parser._spSubclassesToCurrentAndLegacyFull.mapClassShortNameToMostRecent = (shortName) => { + switch (shortName) { + case "Favored Soul": return "Divine Soul"; + case "Undying Light": return "Celestial"; + case "Deep Stalker": return "Gloom Stalker"; + } + return shortName; +}; + +Parser.spVariantClassesToCurrentAndLegacy = function (fromVariantClassList) { + const current = []; + const legacy = []; + fromVariantClassList.forEach(cls => { + if (SourceUtil.isPrereleaseSource(cls.definedInSource)) legacy.push(cls); + else current.push(cls); + }); + return [current, legacy]; +}; + +Parser.attackTypeToFull = function (attackType) { + return Parser._parse_aToB(Parser.ATK_TYPE_TO_FULL, attackType); +}; + +Parser.trapHazTypeToFull = function (type) { + return Parser._parse_aToB(Parser.TRAP_HAZARD_TYPE_TO_FULL, type); +}; + +Parser.TRAP_HAZARD_TYPE_TO_FULL = { + MECH: "Mechanical Trap", + MAG: "Magical Trap", + SMPL: "Simple Trap", + CMPX: "Complex Trap", + HAZ: "Hazard", + WTH: "Weather", + ENV: "Environmental Hazard", + WLD: "Wilderness Hazard", + GEN: "Generic", + EST: "Eldritch Storm", +}; + +Parser.tierToFullLevel = function (tier) { + return Parser._parse_aToB(Parser.TIER_TO_FULL_LEVEL, tier); +}; + +Parser.TIER_TO_FULL_LEVEL = {}; +Parser.TIER_TO_FULL_LEVEL[1] = "1st\u20134th Level"; +Parser.TIER_TO_FULL_LEVEL[2] = "5th\u201310th Level"; +Parser.TIER_TO_FULL_LEVEL[3] = "11th\u201316th Level"; +Parser.TIER_TO_FULL_LEVEL[4] = "17th\u201320th Level"; + +Parser.trapInitToFull = function (init) { + return Parser._parse_aToB(Parser.TRAP_INIT_TO_FULL, init); +}; + +Parser.TRAP_INIT_TO_FULL = {}; +Parser.TRAP_INIT_TO_FULL[1] = "initiative count 10"; +Parser.TRAP_INIT_TO_FULL[2] = "initiative count 20"; +Parser.TRAP_INIT_TO_FULL[3] = "initiative count 20 and initiative count 10"; + +Parser.ATK_TYPE_TO_FULL = {}; +Parser.ATK_TYPE_TO_FULL["MW"] = "Melee Weapon Attack"; +Parser.ATK_TYPE_TO_FULL["RW"] = "Ranged Weapon Attack"; + +Parser.bookOrdinalToAbv = (ordinal, preNoSuff) => { + if (ordinal === undefined) return ""; + switch (ordinal.type) { + case "part": return `${preNoSuff ? " " : ""}Part ${ordinal.identifier}${preNoSuff ? "" : " \u2014 "}`; + case "chapter": return `${preNoSuff ? " " : ""}Ch. ${ordinal.identifier}${preNoSuff ? "" : ": "}`; + case "episode": return `${preNoSuff ? " " : ""}Ep. ${ordinal.identifier}${preNoSuff ? "" : ": "}`; + case "appendix": return `${preNoSuff ? " " : ""}App.${ordinal.identifier != null ? ` ${ordinal.identifier}` : ""}${preNoSuff ? "" : ": "}`; + case "level": return `${preNoSuff ? " " : ""}Level ${ordinal.identifier}${preNoSuff ? "" : ": "}`; + default: throw new Error(`Unhandled ordinal type "${ordinal.type}"`); + } +}; + +Parser.IMAGE_TYPE_TO_FULL = { + "map": "Map", + "mapPlayer": "Map (Player)", +}; +Parser.imageTypeToFull = function (imageType) { + return Parser._parse_aToB(Parser.IMAGE_TYPE_TO_FULL, imageType, "Other"); +}; + +Parser.nameToTokenName = function (name) { + return name + .toAscii() + .replace(/"/g, ""); +}; + +Parser.bytesToHumanReadable = function (bytes, {fixedDigits = 2} = {}) { + if (bytes == null) return ""; + if (!bytes) return "0 B"; + const e = Math.floor(Math.log(bytes) / Math.log(1024)); + return `${(bytes / Math.pow(1024, e)).toFixed(fixedDigits)} ${`\u200bKMGTP`.charAt(e)}B`; +}; + +Parser.SKL_ABV_ABJ = "A"; +Parser.SKL_ABV_EVO = "V"; +Parser.SKL_ABV_ENC = "E"; +Parser.SKL_ABV_ILL = "I"; +Parser.SKL_ABV_DIV = "D"; +Parser.SKL_ABV_NEC = "N"; +Parser.SKL_ABV_TRA = "T"; +Parser.SKL_ABV_CON = "C"; +Parser.SKL_ABV_PSI = "P"; +Parser.SKL_ABVS = [ + Parser.SKL_ABV_ABJ, + Parser.SKL_ABV_CON, + Parser.SKL_ABV_DIV, + Parser.SKL_ABV_ENC, + Parser.SKL_ABV_EVO, + Parser.SKL_ABV_ILL, + Parser.SKL_ABV_NEC, + Parser.SKL_ABV_PSI, + Parser.SKL_ABV_TRA, +]; + +Parser.SP_TM_ACTION = "action"; +Parser.SP_TM_B_ACTION = "bonus"; +Parser.SP_TM_REACTION = "reaction"; +Parser.SP_TM_ROUND = "round"; +Parser.SP_TM_MINS = "minute"; +Parser.SP_TM_HRS = "hour"; +Parser.SP_TIME_SINGLETONS = [Parser.SP_TM_ACTION, Parser.SP_TM_B_ACTION, Parser.SP_TM_REACTION, Parser.SP_TM_ROUND]; +Parser.SP_TIME_TO_FULL = { + [Parser.SP_TM_ACTION]: "Action", + [Parser.SP_TM_B_ACTION]: "Bonus Action", + [Parser.SP_TM_REACTION]: "Reaction", + [Parser.SP_TM_ROUND]: "Rounds", + [Parser.SP_TM_MINS]: "Minutes", + [Parser.SP_TM_HRS]: "Hours", +}; +Parser.spTimeUnitToFull = function (timeUnit) { + return Parser._parse_aToB(Parser.SP_TIME_TO_FULL, timeUnit); +}; + +Parser.SP_TIME_TO_SHORT = { + [Parser.SP_TM_ROUND]: "Rnd.", + [Parser.SP_TM_MINS]: "Min.", + [Parser.SP_TM_HRS]: "Hr.", +}; +Parser.spTimeUnitToShort = function (timeUnit) { + return Parser._parse_aToB(Parser.SP_TIME_TO_SHORT, timeUnit); +}; + +Parser.SP_TIME_TO_ABV = { + [Parser.SP_TM_ACTION]: "A", + [Parser.SP_TM_B_ACTION]: "BA", + [Parser.SP_TM_REACTION]: "R", + [Parser.SP_TM_ROUND]: "rnd", + [Parser.SP_TM_MINS]: "min", + [Parser.SP_TM_HRS]: "hr", +}; +Parser.spTimeUnitToAbv = function (timeUnit) { + return Parser._parse_aToB(Parser.SP_TIME_TO_ABV, timeUnit); +}; + +Parser.spTimeToShort = function (time, isHtml) { + if (!time) return ""; + return (time.number === 1 && Parser.SP_TIME_SINGLETONS.includes(time.unit)) + ? `${Parser.spTimeUnitToAbv(time.unit).uppercaseFirst()}${time.condition ? "*" : ""}` + : `${time.number} ${isHtml ? `` : ""}${Parser.spTimeUnitToAbv(time.unit)}${isHtml ? `` : ""}${time.condition ? "*" : ""}`; +}; + +Parser.SKL_ABJ = "Abjuration"; +Parser.SKL_EVO = "Evocation"; +Parser.SKL_ENC = "Enchantment"; +Parser.SKL_ILL = "Illusion"; +Parser.SKL_DIV = "Divination"; +Parser.SKL_NEC = "Necromancy"; +Parser.SKL_TRA = "Transmutation"; +Parser.SKL_CON = "Conjuration"; +Parser.SKL_PSI = "Psionic"; + +Parser.SP_SCHOOL_ABV_TO_FULL = {}; +Parser.SP_SCHOOL_ABV_TO_FULL[Parser.SKL_ABV_ABJ] = Parser.SKL_ABJ; +Parser.SP_SCHOOL_ABV_TO_FULL[Parser.SKL_ABV_EVO] = Parser.SKL_EVO; +Parser.SP_SCHOOL_ABV_TO_FULL[Parser.SKL_ABV_ENC] = Parser.SKL_ENC; +Parser.SP_SCHOOL_ABV_TO_FULL[Parser.SKL_ABV_ILL] = Parser.SKL_ILL; +Parser.SP_SCHOOL_ABV_TO_FULL[Parser.SKL_ABV_DIV] = Parser.SKL_DIV; +Parser.SP_SCHOOL_ABV_TO_FULL[Parser.SKL_ABV_NEC] = Parser.SKL_NEC; +Parser.SP_SCHOOL_ABV_TO_FULL[Parser.SKL_ABV_TRA] = Parser.SKL_TRA; +Parser.SP_SCHOOL_ABV_TO_FULL[Parser.SKL_ABV_CON] = Parser.SKL_CON; +Parser.SP_SCHOOL_ABV_TO_FULL[Parser.SKL_ABV_PSI] = Parser.SKL_PSI; + +Parser.SP_SCHOOL_ABV_TO_SHORT = {}; +Parser.SP_SCHOOL_ABV_TO_SHORT[Parser.SKL_ABV_ABJ] = "Abj."; +Parser.SP_SCHOOL_ABV_TO_SHORT[Parser.SKL_ABV_EVO] = "Evoc."; +Parser.SP_SCHOOL_ABV_TO_SHORT[Parser.SKL_ABV_ENC] = "Ench."; +Parser.SP_SCHOOL_ABV_TO_SHORT[Parser.SKL_ABV_ILL] = "Illu."; +Parser.SP_SCHOOL_ABV_TO_SHORT[Parser.SKL_ABV_DIV] = "Divin."; +Parser.SP_SCHOOL_ABV_TO_SHORT[Parser.SKL_ABV_NEC] = "Necro."; +Parser.SP_SCHOOL_ABV_TO_SHORT[Parser.SKL_ABV_TRA] = "Trans."; +Parser.SP_SCHOOL_ABV_TO_SHORT[Parser.SKL_ABV_CON] = "Conj."; +Parser.SP_SCHOOL_ABV_TO_SHORT[Parser.SKL_ABV_PSI] = "Psi."; + +Parser.ATB_ABV_TO_FULL = { + "str": "Strength", + "dex": "Dexterity", + "con": "Constitution", + "int": "Intelligence", + "wis": "Wisdom", + "cha": "Charisma", +}; + +Parser.TP_ABERRATION = "aberration"; +Parser.TP_BEAST = "beast"; +Parser.TP_CELESTIAL = "celestial"; +Parser.TP_CONSTRUCT = "construct"; +Parser.TP_DRAGON = "dragon"; +Parser.TP_ELEMENTAL = "elemental"; +Parser.TP_FEY = "fey"; +Parser.TP_FIEND = "fiend"; +Parser.TP_GIANT = "giant"; +Parser.TP_HUMANOID = "humanoid"; +Parser.TP_MONSTROSITY = "monstrosity"; +Parser.TP_OOZE = "ooze"; +Parser.TP_PLANT = "plant"; +Parser.TP_UNDEAD = "undead"; +Parser.MON_TYPES = [Parser.TP_ABERRATION, Parser.TP_BEAST, Parser.TP_CELESTIAL, Parser.TP_CONSTRUCT, Parser.TP_DRAGON, Parser.TP_ELEMENTAL, Parser.TP_FEY, Parser.TP_FIEND, Parser.TP_GIANT, Parser.TP_HUMANOID, Parser.TP_MONSTROSITY, Parser.TP_OOZE, Parser.TP_PLANT, Parser.TP_UNDEAD]; +Parser.MON_TYPE_TO_PLURAL = {}; +Parser.MON_TYPE_TO_PLURAL[Parser.TP_ABERRATION] = "aberrations"; +Parser.MON_TYPE_TO_PLURAL[Parser.TP_BEAST] = "beasts"; +Parser.MON_TYPE_TO_PLURAL[Parser.TP_CELESTIAL] = "celestials"; +Parser.MON_TYPE_TO_PLURAL[Parser.TP_CONSTRUCT] = "constructs"; +Parser.MON_TYPE_TO_PLURAL[Parser.TP_DRAGON] = "dragons"; +Parser.MON_TYPE_TO_PLURAL[Parser.TP_ELEMENTAL] = "elementals"; +Parser.MON_TYPE_TO_PLURAL[Parser.TP_FEY] = "fey"; +Parser.MON_TYPE_TO_PLURAL[Parser.TP_FIEND] = "fiends"; +Parser.MON_TYPE_TO_PLURAL[Parser.TP_GIANT] = "giants"; +Parser.MON_TYPE_TO_PLURAL[Parser.TP_HUMANOID] = "humanoids"; +Parser.MON_TYPE_TO_PLURAL[Parser.TP_MONSTROSITY] = "monstrosities"; +Parser.MON_TYPE_TO_PLURAL[Parser.TP_OOZE] = "oozes"; +Parser.MON_TYPE_TO_PLURAL[Parser.TP_PLANT] = "plants"; +Parser.MON_TYPE_TO_PLURAL[Parser.TP_UNDEAD] = "undead"; + +Parser.SZ_FINE = "F"; +Parser.SZ_DIMINUTIVE = "D"; +Parser.SZ_TINY = "T"; +Parser.SZ_SMALL = "S"; +Parser.SZ_MEDIUM = "M"; +Parser.SZ_LARGE = "L"; +Parser.SZ_HUGE = "H"; +Parser.SZ_GARGANTUAN = "G"; +Parser.SZ_COLOSSAL = "C"; +Parser.SZ_VARIES = "V"; +Parser.SIZE_ABVS = [Parser.SZ_TINY, Parser.SZ_SMALL, Parser.SZ_MEDIUM, Parser.SZ_LARGE, Parser.SZ_HUGE, Parser.SZ_GARGANTUAN, Parser.SZ_VARIES]; +Parser.SIZE_ABV_TO_FULL = {}; +Parser.SIZE_ABV_TO_FULL[Parser.SZ_FINE] = "Fine"; +Parser.SIZE_ABV_TO_FULL[Parser.SZ_DIMINUTIVE] = "Diminutive"; +Parser.SIZE_ABV_TO_FULL[Parser.SZ_TINY] = "Tiny"; +Parser.SIZE_ABV_TO_FULL[Parser.SZ_SMALL] = "Small"; +Parser.SIZE_ABV_TO_FULL[Parser.SZ_MEDIUM] = "Medium"; +Parser.SIZE_ABV_TO_FULL[Parser.SZ_LARGE] = "Large"; +Parser.SIZE_ABV_TO_FULL[Parser.SZ_HUGE] = "Huge"; +Parser.SIZE_ABV_TO_FULL[Parser.SZ_GARGANTUAN] = "Gargantuan"; +Parser.SIZE_ABV_TO_FULL[Parser.SZ_COLOSSAL] = "Colossal"; +Parser.SIZE_ABV_TO_FULL[Parser.SZ_VARIES] = "Varies"; + +Parser.XP_CHART_ALT = { + "0": 10, + "1/8": 25, + "1/4": 50, + "1/2": 100, + "1": 200, + "2": 450, + "3": 700, + "4": 1100, + "5": 1800, + "6": 2300, + "7": 2900, + "8": 3900, + "9": 5000, + "10": 5900, + "11": 7200, + "12": 8400, + "13": 10000, + "14": 11500, + "15": 13000, + "16": 15000, + "17": 18000, + "18": 20000, + "19": 22000, + "20": 25000, + "21": 33000, + "22": 41000, + "23": 50000, + "24": 62000, + "25": 75000, + "26": 90000, + "27": 105000, + "28": 120000, + "29": 135000, + "30": 155000, +}; + +Parser.ARMOR_ABV_TO_FULL = { + "l.": "light", + "m.": "medium", + "h.": "heavy", +}; + +Parser.WEAPON_ABV_TO_FULL = { + "s.": "simple", + "m.": "martial", +}; + +Parser.CONDITION_TO_COLOR = { + "Blinded": "#525252", + "Charmed": "#f01789", + "Deafened": "#ababab", + "Exhausted": "#947a47", + "Frightened": "#c9ca18", + "Grappled": "#8784a0", + "Incapacitated": "#3165a0", + "Invisible": "#7ad2d6", + "Paralyzed": "#c00900", + "Petrified": "#a0a0a0", + "Poisoned": "#4dc200", + "Prone": "#5e60a0", + "Restrained": "#d98000", + "Stunned": "#a23bcb", + "Unconscious": "#3a40ad", + + "Concentration": "#009f7a", +}; + +Parser.RULE_TYPE_TO_FULL = { + "O": "Optional", + "P": "Prerelease", + "V": "Variant", + "VO": "Variant Optional", + "VV": "Variant Variant", + "U": "Unknown", +}; + +Parser.ruleTypeToFull = function (ruleType) { + return Parser._parse_aToB(Parser.RULE_TYPE_TO_FULL, ruleType); +}; + +Parser.VEHICLE_TYPE_TO_FULL = { + "SHIP": "Ship", + "SPELLJAMMER": "Spelljammer Ship", + "INFWAR": "Infernal War Machine", + "CREATURE": "Creature", + "OBJECT": "Object", + "SHP:H": "Ship Upgrade, Hull", + "SHP:M": "Ship Upgrade, Movement", + "SHP:W": "Ship Upgrade, Weapon", + "SHP:F": "Ship Upgrade, Figurehead", + "SHP:O": "Ship Upgrade, Miscellaneous", + "IWM:W": "Infernal War Machine Variant, Weapon", + "IWM:A": "Infernal War Machine Upgrade, Armor", + "IWM:G": "Infernal War Machine Upgrade, Gadget", +}; + +Parser.vehicleTypeToFull = function (vehicleType) { + return Parser._parse_aToB(Parser.VEHICLE_TYPE_TO_FULL, vehicleType); +}; + +// SOURCES ============================================================================================================= + +Parser.SRC_5ETOOLS_TMP = "Parser.SRC_5ETOOLS_TMP"; // Temp source, used as a placeholder value + +Parser.SRC_CoS = "CoS"; +Parser.SRC_DMG = "DMG"; +Parser.SRC_EEPC = "EEPC"; +Parser.SRC_EET = "EET"; +Parser.SRC_HotDQ = "HotDQ"; +Parser.SRC_LMoP = "LMoP"; +Parser.SRC_MM = "MM"; +Parser.SRC_OotA = "OotA"; +Parser.SRC_PHB = "PHB"; +Parser.SRC_PotA = "PotA"; +Parser.SRC_RoT = "RoT"; +Parser.SRC_RoTOS = "RoTOS"; +Parser.SRC_SCAG = "SCAG"; +Parser.SRC_SKT = "SKT"; +Parser.SRC_ToA = "ToA"; +Parser.SRC_TLK = "TLK"; +Parser.SRC_ToD = "ToD"; +Parser.SRC_TTP = "TTP"; +Parser.SRC_TYP = "TftYP"; +Parser.SRC_TYP_AtG = "TftYP-AtG"; +Parser.SRC_TYP_DiT = "TftYP-DiT"; +Parser.SRC_TYP_TFoF = "TftYP-TFoF"; +Parser.SRC_TYP_THSoT = "TftYP-THSoT"; +Parser.SRC_TYP_TSC = "TftYP-TSC"; +Parser.SRC_TYP_ToH = "TftYP-ToH"; +Parser.SRC_TYP_WPM = "TftYP-WPM"; +Parser.SRC_VGM = "VGM"; +Parser.SRC_XGE = "XGE"; +Parser.SRC_OGA = "OGA"; +Parser.SRC_MTF = "MTF"; +Parser.SRC_WDH = "WDH"; +Parser.SRC_WDMM = "WDMM"; +Parser.SRC_GGR = "GGR"; +Parser.SRC_KKW = "KKW"; +Parser.SRC_LLK = "LLK"; +Parser.SRC_AZfyT = "AZfyT"; +Parser.SRC_GoS = "GoS"; +Parser.SRC_AI = "AI"; +Parser.SRC_OoW = "OoW"; +Parser.SRC_ESK = "ESK"; +Parser.SRC_DIP = "DIP"; +Parser.SRC_HftT = "HftT"; +Parser.SRC_DC = "DC"; +Parser.SRC_SLW = "SLW"; +Parser.SRC_SDW = "SDW"; +Parser.SRC_BGDIA = "BGDIA"; +Parser.SRC_LR = "LR"; +Parser.SRC_AL = "AL"; +Parser.SRC_SAC = "SAC"; +Parser.SRC_ERLW = "ERLW"; +Parser.SRC_EFR = "EFR"; +Parser.SRC_RMBRE = "RMBRE"; +Parser.SRC_RMR = "RMR"; +Parser.SRC_MFF = "MFF"; +Parser.SRC_AWM = "AWM"; +Parser.SRC_IMR = "IMR"; +Parser.SRC_SADS = "SADS"; +Parser.SRC_EGW = "EGW"; +Parser.SRC_EGW_ToR = "ToR"; +Parser.SRC_EGW_DD = "DD"; +Parser.SRC_EGW_FS = "FS"; +Parser.SRC_EGW_US = "US"; +Parser.SRC_MOT = "MOT"; +Parser.SRC_IDRotF = "IDRotF"; +Parser.SRC_TCE = "TCE"; +Parser.SRC_VRGR = "VRGR"; +Parser.SRC_HoL = "HoL"; +Parser.SRC_XMtS = "XMtS"; +Parser.SRC_RtG = "RtG"; +Parser.SRC_AitFR = "AitFR"; +Parser.SRC_AitFR_ISF = "AitFR-ISF"; +Parser.SRC_AitFR_THP = "AitFR-THP"; +Parser.SRC_AitFR_AVT = "AitFR-AVT"; +Parser.SRC_AitFR_DN = "AitFR-DN"; +Parser.SRC_AitFR_FCD = "AitFR-FCD"; +Parser.SRC_WBtW = "WBtW"; +Parser.SRC_DoD = "DoD"; +Parser.SRC_MaBJoV = "MaBJoV"; +Parser.SRC_FTD = "FTD"; +Parser.SRC_SCC = "SCC"; +Parser.SRC_SCC_CK = "SCC-CK"; +Parser.SRC_SCC_HfMT = "SCC-HfMT"; +Parser.SRC_SCC_TMM = "SCC-TMM"; +Parser.SRC_SCC_ARiR = "SCC-ARiR"; +Parser.SRC_MPMM = "MPMM"; +Parser.SRC_CRCotN = "CRCotN"; +Parser.SRC_JttRC = "JttRC"; +Parser.SRC_SAiS = "SAiS"; +Parser.SRC_AAG = "AAG"; +Parser.SRC_BAM = "BAM"; +Parser.SRC_LoX = "LoX"; +Parser.SRC_DoSI = "DoSI"; +Parser.SRC_DSotDQ = "DSotDQ"; +Parser.SRC_KftGV = "KftGV"; +Parser.SRC_BGG = "BGG"; +Parser.SRC_TDCSR = "TDCSR"; +Parser.SRC_PaBTSO = "PaBTSO"; +Parser.SRC_PAitM = "PAitM"; +Parser.SRC_SatO = "SatO"; +Parser.SRC_ToFW = "ToFW"; +Parser.SRC_MPP = "MPP"; +Parser.SRC_BMT = "BMT"; +Parser.SRC_GHLoE = "GHLoE"; +Parser.SRC_DoDk = "DoDk"; +Parser.SRC_SCREEN = "Screen"; +Parser.SRC_SCREEN_WILDERNESS_KIT = "ScreenWildernessKit"; +Parser.SRC_SCREEN_DUNGEON_KIT = "ScreenDungeonKit"; +Parser.SRC_SCREEN_SPELLJAMMER = "ScreenSpelljammer"; +Parser.SRC_HF = "HF"; +Parser.SRC_HFFotM = "HFFotM"; +Parser.SRC_HFStCM = "HFStCM"; +Parser.SRC_CM = "CM"; +Parser.SRC_NRH = "NRH"; +Parser.SRC_NRH_TCMC = "NRH-TCMC"; +Parser.SRC_NRH_AVitW = "NRH-AVitW"; +Parser.SRC_NRH_ASS = "NRH-ASS"; // lmao +Parser.SRC_NRH_CoI = "NRH-CoI"; +Parser.SRC_NRH_TLT = "NRH-TLT"; +Parser.SRC_NRH_AWoL = "NRH-AWoL"; +Parser.SRC_NRH_AT = "NRH-AT"; +Parser.SRC_MGELFT = "MGELFT"; +Parser.SRC_VD = "VD"; +Parser.SRC_SjA = "SjA"; +Parser.SRC_HAT_TG = "HAT-TG"; +Parser.SRC_HAT_LMI = "HAT-LMI"; +Parser.SRC_GotSF = "GotSF"; +Parser.SRC_LK = "LK"; +Parser.SRC_CoA = "CoA"; +Parser.SRC_PiP = "PiP"; + +Parser.SRC_AL_PREFIX = "AL"; + +Parser.SRC_ALCoS = `${Parser.SRC_AL_PREFIX}CurseOfStrahd`; +Parser.SRC_ALEE = `${Parser.SRC_AL_PREFIX}ElementalEvil`; +Parser.SRC_ALRoD = `${Parser.SRC_AL_PREFIX}RageOfDemons`; + +Parser.SRC_PS_PREFIX = "PS"; + +Parser.SRC_PSA = `${Parser.SRC_PS_PREFIX}A`; +Parser.SRC_PSI = `${Parser.SRC_PS_PREFIX}I`; +Parser.SRC_PSK = `${Parser.SRC_PS_PREFIX}K`; +Parser.SRC_PSZ = `${Parser.SRC_PS_PREFIX}Z`; +Parser.SRC_PSX = `${Parser.SRC_PS_PREFIX}X`; +Parser.SRC_PSD = `${Parser.SRC_PS_PREFIX}D`; + +Parser.SRC_UA_PREFIX = "UA"; +Parser.SRC_UA_ONE_PREFIX = "XUA"; +Parser.SRC_MCVX_PREFIX = "MCV"; +Parser.SRC_MisMVX_PREFIX = "MisMV"; +Parser.SRC_AA_PREFIX = "AA"; + +Parser.SRC_UATMC = `${Parser.SRC_UA_PREFIX}TheMysticClass`; +Parser.SRC_MCV1SC = `${Parser.SRC_MCVX_PREFIX}1SC`; +Parser.SRC_MCV2DC = `${Parser.SRC_MCVX_PREFIX}2DC`; +Parser.SRC_MCV3MC = `${Parser.SRC_MCVX_PREFIX}3MC`; +Parser.SRC_MCV4EC = `${Parser.SRC_MCVX_PREFIX}4EC`; +Parser.SRC_MisMV1 = `${Parser.SRC_MisMVX_PREFIX}1`; +Parser.SRC_AATM = `${Parser.SRC_AA_PREFIX}TM`; + +Parser.AL_PREFIX = "Adventurers League: "; +Parser.AL_PREFIX_SHORT = "AL: "; +Parser.PS_PREFIX = "Plane Shift: "; +Parser.PS_PREFIX_SHORT = "PS: "; +Parser.UA_PREFIX = "Unearthed Arcana: "; +Parser.UA_PREFIX_SHORT = "UA: "; +Parser.TftYP_NAME = "Tales from the Yawning Portal"; +Parser.AitFR_NAME = "Adventures in the Forgotten Realms"; +Parser.NRH_NAME = "NERDS Restoring Harmony"; +Parser.MCVX_PREFIX = "Monstrous Compendium Volume "; +Parser.MisMVX_PREFIX = "Misplaced Monsters: Volume "; +Parser.AA_PREFIX = "Adventure Atlas: "; + +Parser.SOURCE_JSON_TO_FULL = {}; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_CoS] = "Curse of Strahd"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_DMG] = "Dungeon Master's Guide"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_EEPC] = "Elemental Evil Player's Companion"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_EET] = "Elemental Evil: Trinkets"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_HotDQ] = "Hoard of the Dragon Queen"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_LMoP] = "Lost Mine of Phandelver"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_MM] = "Monster Manual"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_OotA] = "Out of the Abyss"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_PHB] = "Player's Handbook"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_PotA] = "Princes of the Apocalypse"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_RoT] = "The Rise of Tiamat"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_RoTOS] = "The Rise of Tiamat Online Supplement"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_SCAG] = "Sword Coast Adventurer's Guide"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_SKT] = "Storm King's Thunder"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_ToA] = "Tomb of Annihilation"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_TLK] = "The Lost Kenku"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_ToD] = "Tyranny of Dragons"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_TTP] = "The Tortle Package"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_TYP] = Parser.TftYP_NAME; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_TYP_AtG] = `${Parser.TftYP_NAME}: Against the Giants`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_TYP_DiT] = `${Parser.TftYP_NAME}: Dead in Thay`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_TYP_TFoF] = `${Parser.TftYP_NAME}: The Forge of Fury`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_TYP_THSoT] = `${Parser.TftYP_NAME}: The Hidden Shrine of Tamoachan`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_TYP_TSC] = `${Parser.TftYP_NAME}: The Sunless Citadel`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_TYP_ToH] = `${Parser.TftYP_NAME}: Tomb of Horrors`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_TYP_WPM] = `${Parser.TftYP_NAME}: White Plume Mountain`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_VGM] = "Volo's Guide to Monsters"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_XGE] = "Xanathar's Guide to Everything"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_OGA] = "One Grung Above"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_MTF] = "Mordenkainen's Tome of Foes"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_WDH] = "Waterdeep: Dragon Heist"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_WDMM] = "Waterdeep: Dungeon of the Mad Mage"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_GGR] = "Guildmasters' Guide to Ravnica"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_KKW] = "Krenko's Way"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_LLK] = "Lost Laboratory of Kwalish"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_AZfyT] = "A Zib for your Thoughts"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_GoS] = "Ghosts of Saltmarsh"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_AI] = "Acquisitions Incorporated"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_OoW] = "The Orrery of the Wanderer"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_ESK] = "Essentials Kit"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_DIP] = "Dragon of Icespire Peak"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_HftT] = "Hunt for the Thessalhydra"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_DC] = "Divine Contention"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_SLW] = "Storm Lord's Wrath"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_SDW] = "Sleeping Dragon's Wake"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_BGDIA] = "Baldur's Gate: Descent Into Avernus"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_LR] = "Locathah Rising"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_AL] = "Adventurers' League"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_SAC] = "Sage Advice Compendium"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_ERLW] = "Eberron: Rising from the Last War"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_EFR] = "Eberron: Forgotten Relics"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_RMBRE] = "The Lost Dungeon of Rickedness: Big Rick Energy"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_RMR] = "Dungeons & Dragons vs. Rick and Morty: Basic Rules"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_MFF] = "Mordenkainen's Fiendish Folio"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_AWM] = "Adventure with Muk"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_IMR] = "Infernal Machine Rebuild"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_SADS] = "Sapphire Anniversary Dice Set"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_EGW] = "Explorer's Guide to Wildemount"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_EGW_ToR] = "Tide of Retribution"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_EGW_DD] = "Dangerous Designs"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_EGW_FS] = "Frozen Sick"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_EGW_US] = "Unwelcome Spirits"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_MOT] = "Mythic Odysseys of Theros"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_IDRotF] = "Icewind Dale: Rime of the Frostmaiden"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_TCE] = "Tasha's Cauldron of Everything"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_VRGR] = "Van Richten's Guide to Ravenloft"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_HoL] = "The House of Lament"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_RtG] = "Return to Glory"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_AitFR] = Parser.AitFR_NAME; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_AitFR_ISF] = `${Parser.AitFR_NAME}: In Scarlet Flames`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_AitFR_THP] = `${Parser.AitFR_NAME}: The Hidden Page`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_AitFR_AVT] = `${Parser.AitFR_NAME}: A Verdant Tomb`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_AitFR_DN] = `${Parser.AitFR_NAME}: Deepest Night`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_AitFR_FCD] = `${Parser.AitFR_NAME}: From Cyan Depths`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_WBtW] = "The Wild Beyond the Witchlight"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_DoD] = "Domains of Delight"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_MaBJoV] = "Minsc and Boo's Journal of Villainy"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_FTD] = "Fizban's Treasury of Dragons"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_SCC] = "Strixhaven: A Curriculum of Chaos"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_SCC_CK] = "Campus Kerfuffle"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_SCC_HfMT] = "Hunt for Mage Tower"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_SCC_TMM] = "The Magister's Masquerade"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_SCC_ARiR] = "A Reckoning in Ruins"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_MPMM] = "Mordenkainen Presents: Monsters of the Multiverse"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_CRCotN] = "Critical Role: Call of the Netherdeep"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_JttRC] = "Journeys through the Radiant Citadel"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_SAiS] = "Spelljammer: Adventures in Space"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_AAG] = "Astral Adventurer's Guide"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_BAM] = "Boo's Astral Menagerie"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_LoX] = "Light of Xaryxis"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_DoSI] = "Dragons of Stormwreck Isle"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_DSotDQ] = "Dragonlance: Shadow of the Dragon Queen"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_KftGV] = "Keys from the Golden Vault"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_BGG] = "Bigby Presents: Glory of the Giants"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_TDCSR] = "Tal'Dorei Campaign Setting Reborn"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_PaBTSO] = "Phandelver and Below: The Shattered Obelisk"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_PAitM] = "Planescape: Adventures in the Multiverse"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_SatO] = "Sigil and the Outlands"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_ToFW] = "Turn of Fortune's Wheel"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_MPP] = "Morte's Planar Parade"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_BMT] = "The Book of Many Things"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_GHLoE] = "Grim Hollow: Lairs of Etharis"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_DoDk] = "Dungeons of Drakkenheim"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_SCREEN] = "Dungeon Master's Screen"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_SCREEN_WILDERNESS_KIT] = "Dungeon Master's Screen: Wilderness Kit"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_SCREEN_DUNGEON_KIT] = "Dungeon Master's Screen: Dungeon Kit"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_SCREEN_SPELLJAMMER] = "Dungeon Master's Screen: Spelljammer"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_HF] = "Heroes' Feast"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_HFFotM] = "Heroes' Feast: Flavors of the Multiverse"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_HFStCM] = "Heroes' Feast: Saving the Childrens Menu"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_CM] = "Candlekeep Mysteries"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_NRH] = Parser.NRH_NAME; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_NRH_TCMC] = `${Parser.NRH_NAME}: The Candy Mountain Caper`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_NRH_AVitW] = `${Parser.NRH_NAME}: A Voice in the Wilderness`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_NRH_ASS] = `${Parser.NRH_NAME}: A Sticky Situation`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_NRH_CoI] = `${Parser.NRH_NAME}: Circus of Illusions`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_NRH_TLT] = `${Parser.NRH_NAME}: The Lost Tomb`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_NRH_AWoL] = `${Parser.NRH_NAME}: A Web of Lies`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_NRH_AT] = `${Parser.NRH_NAME}: Adventure Together`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_MGELFT] = "Muk's Guide To Everything He Learned From Tasha"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_VD] = "Vecna Dossier"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_SjA] = "Spelljammer Academy"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_HAT_TG] = "Honor Among Thieves: Thieves' Gallery"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_HAT_LMI] = "Honor Among Thieves: Legendary Magic Items"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_GotSF] = "Giants of the Star Forge"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_LK] = "Lightning Keep"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_CoA] = "Chains of Asmodeus"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_PiP] = "Peril in Pinebrook"; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_ALCoS] = `${Parser.AL_PREFIX}Curse of Strahd`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_ALEE] = `${Parser.AL_PREFIX}Elemental Evil`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_ALRoD] = `${Parser.AL_PREFIX}Rage of Demons`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_PSA] = `${Parser.PS_PREFIX}Amonkhet`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_PSI] = `${Parser.PS_PREFIX}Innistrad`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_PSK] = `${Parser.PS_PREFIX}Kaladesh`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_PSZ] = `${Parser.PS_PREFIX}Zendikar`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_PSX] = `${Parser.PS_PREFIX}Ixalan`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_PSD] = `${Parser.PS_PREFIX}Dominaria`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_XMtS] = `X Marks the Spot`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_UATMC] = `${Parser.UA_PREFIX}The Mystic Class`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_MCV1SC] = `${Parser.MCVX_PREFIX}1: Spelljammer Creatures`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_MCV2DC] = `${Parser.MCVX_PREFIX}2: Dragonlance Creatures`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_MCV3MC] = `${Parser.MCVX_PREFIX}3: Minecraft Creatures`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_MCV4EC] = `${Parser.MCVX_PREFIX}4: Eldraine Creatures`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_MisMV1] = `${Parser.MisMVX_PREFIX}1`; +Parser.SOURCE_JSON_TO_FULL[Parser.SRC_AATM] = `${Parser.AA_PREFIX}The Mortuary`; + +Parser.SOURCE_JSON_TO_ABV = {}; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_CoS] = "CoS"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_DMG] = "DMG"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_EEPC] = "EEPC"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_EET] = "EET"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_HotDQ] = "HotDQ"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_LMoP] = "LMoP"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_MM] = "MM"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_OotA] = "OotA"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_PHB] = "PHB"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_PotA] = "PotA"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_RoT] = "RoT"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_RoTOS] = "RoTOS"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_SCAG] = "SCAG"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_SKT] = "SKT"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_ToA] = "ToA"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_TLK] = "TLK"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_ToD] = "ToD"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_TTP] = "TTP"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_TYP] = "TftYP"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_TYP_AtG] = "TftYP"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_TYP_DiT] = "TftYP"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_TYP_TFoF] = "TftYP"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_TYP_THSoT] = "TftYP"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_TYP_TSC] = "TftYP"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_TYP_ToH] = "TftYP"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_TYP_WPM] = "TftYP"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_VGM] = "VGM"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_XGE] = "XGE"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_OGA] = "OGA"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_MTF] = "MTF"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_WDH] = "WDH"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_WDMM] = "WDMM"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_GGR] = "GGR"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_KKW] = "KKW"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_LLK] = "LLK"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_AZfyT] = "AZfyT"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_GoS] = "GoS"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_AI] = "AI"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_OoW] = "OoW"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_ESK] = "ESK"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_DIP] = "DIP"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_HftT] = "HftT"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_DC] = "DC"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_SLW] = "SLW"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_SDW] = "SDW"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_BGDIA] = "BGDIA"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_LR] = "LR"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_AL] = "AL"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_SAC] = "SAC"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_ERLW] = "ERLW"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_EFR] = "EFR"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_RMBRE] = "RMBRE"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_RMR] = "RMR"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_MFF] = "MFF"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_AWM] = "AWM"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_IMR] = "IMR"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_SADS] = "SADS"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_EGW] = "EGW"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_EGW_ToR] = "ToR"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_EGW_DD] = "DD"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_EGW_FS] = "FS"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_EGW_US] = "US"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_MOT] = "MOT"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_IDRotF] = "IDRotF"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_TCE] = "TCE"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_VRGR] = "VRGR"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_HoL] = "HoL"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_RtG] = "RtG"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_AitFR] = "AitFR"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_AitFR_ISF] = "AitFR-ISF"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_AitFR_THP] = "AitFR-THP"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_AitFR_AVT] = "AitFR-AVT"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_AitFR_DN] = "AitFR-DN"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_AitFR_FCD] = "AitFR-FCD"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_WBtW] = "WBtW"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_DoD] = "DoD"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_MaBJoV] = "MaBJoV"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_FTD] = "FTD"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_SCC] = "SCC"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_SCC_CK] = "SCC-CK"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_SCC_HfMT] = "SCC-HfMT"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_SCC_TMM] = "SCC-TMM"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_SCC_ARiR] = "SCC-ARiR"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_MPMM] = "MPMM"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_CRCotN] = "CRCotN"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_JttRC] = "JttRC"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_SAiS] = "SAiS"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_AAG] = "AAG"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_BAM] = "BAM"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_LoX] = "LoX"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_DoSI] = "DoSI"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_DSotDQ] = "DSotDQ"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_KftGV] = "KftGV"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_BGG] = "BGG"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_TDCSR] = "TDCSR"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_PaBTSO] = "PaBTSO"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_PAitM] = "PAitM"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_SatO] = "SatO"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_ToFW] = "ToFW"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_MPP] = "MPP"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_BMT] = "BMT"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_GHLoE] = "GHLoE"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_DoDk] = "DoDk"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_SCREEN] = "Screen"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_SCREEN_WILDERNESS_KIT] = "ScWild"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_SCREEN_DUNGEON_KIT] = "ScDun"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_SCREEN_SPELLJAMMER] = "ScSJ"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_HF] = "HF"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_HFFotM] = "HFFotM"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_HFStCM] = "HFStCM"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_CM] = "CM"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_NRH] = "NRH"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_NRH_TCMC] = "NRH-TCMC"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_NRH_AVitW] = "NRH-AVitW"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_NRH_ASS] = "NRH-ASS"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_NRH_CoI] = "NRH-CoI"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_NRH_TLT] = "NRH-TLT"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_NRH_AWoL] = "NRH-AWoL"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_NRH_AT] = "NRH-AT"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_MGELFT] = "MGELFT"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_VD] = "VD"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_SjA] = "SjA"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_HAT_TG] = "HAT-TG"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_HAT_LMI] = "HAT-LMI"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_GotSF] = "GotSF"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_LK] = "LK"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_CoA] = "CoA"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_PiP] = "PiP"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_ALCoS] = "ALCoS"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_ALEE] = "ALEE"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_ALRoD] = "ALRoD"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_PSA] = "PSA"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_PSI] = "PSI"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_PSK] = "PSK"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_PSZ] = "PSZ"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_PSX] = "PSX"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_PSD] = "PSD"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_XMtS] = "XMtS"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_UATMC] = "UAMy"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_MCV1SC] = "MCV1SC"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_MCV2DC] = "MCV2DC"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_MCV3MC] = "MCV3MC"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_MCV4EC] = "MCV4EC"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_MisMV1] = "MisMV1"; +Parser.SOURCE_JSON_TO_ABV[Parser.SRC_AATM] = "AATM"; + +Parser.SOURCE_JSON_TO_DATE = {}; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_CoS] = "2016-03-15"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_DMG] = "2014-12-09"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_EEPC] = "2015-03-10"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_EET] = "2015-03-10"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_HotDQ] = "2014-08-19"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_LMoP] = "2014-07-15"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_MM] = "2014-09-30"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_OotA] = "2015-09-15"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_PHB] = "2014-08-19"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_PotA] = "2015-04-07"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_RoT] = "2014-11-04"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_RoTOS] = "2014-11-04"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_SCAG] = "2015-11-03"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_SKT] = "2016-09-06"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_ToA] = "2017-09-19"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_TLK] = "2017-11-28"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_ToD] = "2019-10-22"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_TTP] = "2017-09-19"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_TYP] = "2017-04-04"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_TYP_AtG] = "2017-04-04"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_TYP_DiT] = "2017-04-04"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_TYP_TFoF] = "2017-04-04"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_TYP_THSoT] = "2017-04-04"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_TYP_TSC] = "2017-04-04"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_TYP_ToH] = "2017-04-04"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_TYP_WPM] = "2017-04-04"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_VGM] = "2016-11-15"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_XGE] = "2017-11-21"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_OGA] = "2017-10-11"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_MTF] = "2018-05-29"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_WDH] = "2018-09-18"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_WDMM] = "2018-11-20"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_GGR] = "2018-11-20"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_KKW] = "2018-11-20"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_LLK] = "2018-11-10"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_AZfyT] = "2019-03-05"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_GoS] = "2019-05-21"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_AI] = "2019-06-18"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_OoW] = "2019-06-18"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_ESK] = "2019-06-24"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_DIP] = "2019-06-24"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_HftT] = "2019-05-01"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_DC] = "2019-06-24"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_SLW] = "2019-06-24"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_SDW] = "2019-06-24"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_BGDIA] = "2019-09-17"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_LR] = "2019-09-19"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_SAC] = "2019-01-31"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_ERLW] = "2019-11-19"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_EFR] = "2019-11-19"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_RMBRE] = "2019-11-19"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_RMR] = "2019-11-19"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_MFF] = "2019-11-12"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_AWM] = "2019-11-12"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_IMR] = "2019-11-12"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_SADS] = "2019-12-12"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_EGW] = "2020-03-17"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_EGW_ToR] = "2020-03-17"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_EGW_DD] = "2020-03-17"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_EGW_FS] = "2020-03-17"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_EGW_US] = "2020-03-17"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_MOT] = "2020-06-02"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_IDRotF] = "2020-09-15"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_TCE] = "2020-11-17"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_VRGR] = "2021-05-18"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_HoL] = "2021-05-18"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_RtG] = "2021-05-21"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_AitFR] = "2021-06-30"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_AitFR_ISF] = "2021-06-30"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_AitFR_THP] = "2021-07-07"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_AitFR_AVT] = "2021-07-14"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_AitFR_DN] = "2021-07-21"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_AitFR_FCD] = "2021-07-28"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_WBtW] = "2021-09-21"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_DoD] = "2021-09-21"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_MaBJoV] = "2021-10-05"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_FTD] = "2021-11-26"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_SCC] = "2021-12-07"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_SCC_CK] = "2021-12-07"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_SCC_HfMT] = "2021-12-07"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_SCC_TMM] = "2021-12-07"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_SCC_ARiR] = "2021-12-07"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_MPMM] = "2022-01-25"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_CRCotN] = "2022-03-15"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_JttRC] = "2022-07-19"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_SAiS] = "2022-08-16"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_AAG] = "2022-08-16"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_BAM] = "2022-08-16"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_LoX] = "2022-08-16"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_DoSI] = "2022-07-31"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_DSotDQ] = "2022-11-22"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_KftGV] = "2023-02-21"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_BGG] = "2023-08-15"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_TDCSR] = "2022-01-18"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_PaBTSO] = "2023-09-19"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_PAitM] = "2023-10-17"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_SatO] = "2023-10-17"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_ToFW] = "2023-10-17"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_MPP] = "2023-10-17"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_BMT] = "2023-11-14"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_GHLoE] = "2023-11-30"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_DoDk] = "2023-12-21"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_SCREEN] = "2015-01-20"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_SCREEN_WILDERNESS_KIT] = "2020-11-17"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_SCREEN_DUNGEON_KIT] = "2020-09-21"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_SCREEN_SPELLJAMMER] = "2022-08-16"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_HF] = "2020-10-27"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_HFFotM] = "2023-11-07"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_HFStCM] = "2023-11-21"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_CM] = "2021-03-16"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_NRH] = "2021-09-01"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_NRH_TCMC] = "2021-09-01"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_NRH_AVitW] = "2021-09-01"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_NRH_ASS] = "2021-09-01"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_NRH_CoI] = "2021-09-01"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_NRH_TLT] = "2021-09-01"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_NRH_AWoL] = "2021-09-01"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_NRH_AT] = "2021-09-01"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_MGELFT] = "2020-12-01"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_VD] = "2022-06-09"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_SjA] = "2022-07-11"; // pt1; pt2 2022-07-18; pt3 2022-07-25; pt4 2022-08-01 +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_HAT_TG] = "2023-03-06"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_HAT_LMI] = "2023-03-31"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_GotSF] = "2023-08-01"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_LK] = "2023-09-26"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_CoA] = "2023-10-30"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_PiP] = "2023-11-20"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_ALCoS] = "2016-03-15"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_ALEE] = "2015-04-07"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_ALRoD] = "2015-09-15"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_PSA] = "2017-07-06"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_PSI] = "2016-07-12"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_PSK] = "2017-02-16"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_PSZ] = "2016-04-27"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_PSX] = "2018-01-09"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_PSD] = "2018-07-31"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_XMtS] = "2017-12-11"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_UATMC] = "2017-03-13"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_MCV1SC] = "2022-04-21"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_MCV2DC] = "2022-12-05"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_MCV3MC] = "2023-03-28"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_MCV4EC] = "2023-09-21"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_MisMV1] = "2023-05-03"; +Parser.SOURCE_JSON_TO_DATE[Parser.SRC_AATM] = "2023-10-17"; + +Parser.SOURCES_ADVENTURES = new Set([ + Parser.SRC_LMoP, + Parser.SRC_HotDQ, + Parser.SRC_RoT, + Parser.SRC_RoTOS, + Parser.SRC_PotA, + Parser.SRC_OotA, + Parser.SRC_CoS, + Parser.SRC_SKT, + Parser.SRC_TYP, + Parser.SRC_TYP_AtG, + Parser.SRC_TYP_DiT, + Parser.SRC_TYP_TFoF, + Parser.SRC_TYP_THSoT, + Parser.SRC_TYP_TSC, + Parser.SRC_TYP_ToH, + Parser.SRC_TYP_WPM, + Parser.SRC_ToA, + Parser.SRC_TLK, + Parser.SRC_TTP, + Parser.SRC_WDH, + Parser.SRC_LLK, + Parser.SRC_WDMM, + Parser.SRC_KKW, + Parser.SRC_AZfyT, + Parser.SRC_GoS, + Parser.SRC_HftT, + Parser.SRC_OoW, + Parser.SRC_DIP, + Parser.SRC_SLW, + Parser.SRC_SDW, + Parser.SRC_DC, + Parser.SRC_BGDIA, + Parser.SRC_LR, + Parser.SRC_EFR, + Parser.SRC_RMBRE, + Parser.SRC_IMR, + Parser.SRC_EGW_ToR, + Parser.SRC_EGW_DD, + Parser.SRC_EGW_FS, + Parser.SRC_EGW_US, + Parser.SRC_IDRotF, + Parser.SRC_CM, + Parser.SRC_HoL, + Parser.SRC_XMtS, + Parser.SRC_RtG, + Parser.SRC_AitFR, + Parser.SRC_AitFR_ISF, + Parser.SRC_AitFR_THP, + Parser.SRC_AitFR_AVT, + Parser.SRC_AitFR_DN, + Parser.SRC_AitFR_FCD, + Parser.SRC_WBtW, + Parser.SRC_NRH, + Parser.SRC_NRH_TCMC, + Parser.SRC_NRH_AVitW, + Parser.SRC_NRH_ASS, + Parser.SRC_NRH_CoI, + Parser.SRC_NRH_TLT, + Parser.SRC_NRH_AWoL, + Parser.SRC_NRH_AT, + Parser.SRC_SCC, + Parser.SRC_SCC_CK, + Parser.SRC_SCC_HfMT, + Parser.SRC_SCC_TMM, + Parser.SRC_SCC_ARiR, + Parser.SRC_CRCotN, + Parser.SRC_JttRC, + Parser.SRC_SjA, + Parser.SRC_LoX, + Parser.SRC_DoSI, + Parser.SRC_DSotDQ, + Parser.SRC_KftGV, + Parser.SRC_GotSF, + Parser.SRC_PaBTSO, + Parser.SRC_LK, + Parser.SRC_CoA, + Parser.SRC_PiP, + Parser.SRC_HFStCM, + Parser.SRC_GHLoE, + Parser.SRC_DoDk, + + Parser.SRC_AWM, +]); +Parser.SOURCES_CORE_SUPPLEMENTS = new Set(Object.keys(Parser.SOURCE_JSON_TO_FULL).filter(it => !Parser.SOURCES_ADVENTURES.has(it))); +Parser.SOURCES_NON_STANDARD_WOTC = new Set([ + Parser.SRC_OGA, + Parser.SRC_LLK, + Parser.SRC_AZfyT, + Parser.SRC_LR, + Parser.SRC_TLK, + Parser.SRC_TTP, + Parser.SRC_AWM, + Parser.SRC_IMR, + Parser.SRC_SADS, + Parser.SRC_MFF, + Parser.SRC_XMtS, + Parser.SRC_RtG, + Parser.SRC_AitFR, + Parser.SRC_AitFR_ISF, + Parser.SRC_AitFR_THP, + Parser.SRC_AitFR_AVT, + Parser.SRC_AitFR_DN, + Parser.SRC_AitFR_FCD, + Parser.SRC_DoD, + Parser.SRC_MaBJoV, + Parser.SRC_NRH, + Parser.SRC_NRH_TCMC, + Parser.SRC_NRH_AVitW, + Parser.SRC_NRH_ASS, + Parser.SRC_NRH_CoI, + Parser.SRC_NRH_TLT, + Parser.SRC_NRH_AWoL, + Parser.SRC_NRH_AT, + Parser.SRC_MGELFT, + Parser.SRC_VD, + Parser.SRC_SjA, + Parser.SRC_HAT_TG, + Parser.SRC_HAT_LMI, + Parser.SRC_GotSF, + Parser.SRC_MCV3MC, + Parser.SRC_MCV4EC, + Parser.SRC_MisMV1, + Parser.SRC_LK, + Parser.SRC_AATM, + Parser.SRC_CoA, + Parser.SRC_PiP, + Parser.SRC_HFStCM, +]); +Parser.SOURCES_PARTNERED_WOTC = new Set([ + Parser.SRC_RMBRE, + Parser.SRC_RMR, + Parser.SRC_EGW, + Parser.SRC_EGW_ToR, + Parser.SRC_EGW_DD, + Parser.SRC_EGW_FS, + Parser.SRC_EGW_US, + Parser.SRC_CRCotN, + Parser.SRC_TDCSR, + Parser.SRC_HftT, + Parser.SRC_GHLoE, + Parser.SRC_DoDk, +]); +// region Source categories + +// An opinionated set of source that could be considered "core-core" +Parser.SOURCES_VANILLA = new Set([ + Parser.SRC_DMG, + Parser.SRC_MM, + Parser.SRC_PHB, + Parser.SRC_SCAG, + // Parser.SRC_TTP, // "Legacy" source, removed in favor of MPMM + // Parser.SRC_VGM, // "Legacy" source, removed in favor of MPMM + Parser.SRC_XGE, + // Parser.SRC_MTF, // "Legacy" source, removed in favor of MPMM + Parser.SRC_SAC, + Parser.SRC_MFF, + Parser.SRC_SADS, + Parser.SRC_TCE, + Parser.SRC_FTD, + Parser.SRC_MPMM, + Parser.SRC_SCREEN, + Parser.SRC_SCREEN_WILDERNESS_KIT, + Parser.SRC_SCREEN_DUNGEON_KIT, + Parser.SRC_VD, + Parser.SRC_GotSF, + Parser.SRC_BGG, + Parser.SRC_MaBJoV, + Parser.SRC_CoA, + Parser.SRC_BMT, +]); + +// Any opinionated set of sources that are """hilarious, dude""" +Parser.SOURCES_COMEDY = new Set([ + Parser.SRC_AI, + Parser.SRC_OoW, + Parser.SRC_RMR, + Parser.SRC_RMBRE, + Parser.SRC_HftT, + Parser.SRC_AWM, + Parser.SRC_MGELFT, + Parser.SRC_HAT_TG, + Parser.SRC_HAT_LMI, + Parser.SRC_MCV3MC, + Parser.SRC_MisMV1, + Parser.SRC_LK, + Parser.SRC_PiP, +]); + +// Any opinionated set of sources that are "other settings" +Parser.SOURCES_NON_FR = new Set([ + Parser.SRC_GGR, + Parser.SRC_KKW, + Parser.SRC_ERLW, + Parser.SRC_EFR, + Parser.SRC_EGW, + Parser.SRC_EGW_ToR, + Parser.SRC_EGW_DD, + Parser.SRC_EGW_FS, + Parser.SRC_EGW_US, + Parser.SRC_MOT, + Parser.SRC_XMtS, + Parser.SRC_AZfyT, + Parser.SRC_SCC, + Parser.SRC_SCC_CK, + Parser.SRC_SCC_HfMT, + Parser.SRC_SCC_TMM, + Parser.SRC_SCC_ARiR, + Parser.SRC_CRCotN, + Parser.SRC_SjA, + Parser.SRC_SAiS, + Parser.SRC_AAG, + Parser.SRC_BAM, + Parser.SRC_LoX, + Parser.SRC_DSotDQ, + Parser.SRC_TDCSR, + Parser.SRC_PAitM, + Parser.SRC_SatO, + Parser.SRC_ToFW, + Parser.SRC_MPP, + Parser.SRC_MCV4EC, + Parser.SRC_LK, + Parser.SRC_GHLoE, + Parser.SRC_DoDk, +]); + +// endregion +Parser.SOURCES_AVAILABLE_DOCS_BOOK = {}; +[ + Parser.SRC_PHB, + Parser.SRC_MM, + Parser.SRC_DMG, + Parser.SRC_SCAG, + Parser.SRC_VGM, + Parser.SRC_OGA, + Parser.SRC_XGE, + Parser.SRC_MTF, + Parser.SRC_GGR, + Parser.SRC_AI, + Parser.SRC_ERLW, + Parser.SRC_RMR, + Parser.SRC_EGW, + Parser.SRC_MOT, + Parser.SRC_TCE, + Parser.SRC_VRGR, + Parser.SRC_DoD, + Parser.SRC_MaBJoV, + Parser.SRC_FTD, + Parser.SRC_SCC, + Parser.SRC_MPMM, + Parser.SRC_AAG, + Parser.SRC_BAM, + Parser.SRC_HAT_TG, + Parser.SRC_SCREEN, + Parser.SRC_SCREEN_WILDERNESS_KIT, + Parser.SRC_SCREEN_DUNGEON_KIT, + Parser.SRC_SCREEN_SPELLJAMMER, + Parser.SRC_BGG, + Parser.SRC_TDCSR, + Parser.SRC_SatO, + Parser.SRC_MPP, + Parser.SRC_HF, + Parser.SRC_HFFotM, + Parser.SRC_BMT, +].forEach(src => { + Parser.SOURCES_AVAILABLE_DOCS_BOOK[src] = src; + Parser.SOURCES_AVAILABLE_DOCS_BOOK[src.toLowerCase()] = src; +}); +[ + {src: Parser.SRC_PSA, id: "PS-A"}, + {src: Parser.SRC_PSI, id: "PS-I"}, + {src: Parser.SRC_PSK, id: "PS-K"}, + {src: Parser.SRC_PSZ, id: "PS-Z"}, + {src: Parser.SRC_PSX, id: "PS-X"}, + {src: Parser.SRC_PSD, id: "PS-D"}, +].forEach(({src, id}) => { + Parser.SOURCES_AVAILABLE_DOCS_BOOK[src] = id; + Parser.SOURCES_AVAILABLE_DOCS_BOOK[src.toLowerCase()] = id; +}); +Parser.SOURCES_AVAILABLE_DOCS_ADVENTURE = {}; +[ + Parser.SRC_LMoP, + Parser.SRC_HotDQ, + Parser.SRC_RoT, + Parser.SRC_PotA, + Parser.SRC_OotA, + Parser.SRC_CoS, + Parser.SRC_SKT, + Parser.SRC_TYP_AtG, + Parser.SRC_TYP_DiT, + Parser.SRC_TYP_TFoF, + Parser.SRC_TYP_THSoT, + Parser.SRC_TYP_TSC, + Parser.SRC_TYP_ToH, + Parser.SRC_TYP_WPM, + Parser.SRC_ToA, + Parser.SRC_TLK, + Parser.SRC_TTP, + Parser.SRC_WDH, + Parser.SRC_LLK, + Parser.SRC_WDMM, + Parser.SRC_KKW, + Parser.SRC_AZfyT, + Parser.SRC_GoS, + Parser.SRC_HftT, + Parser.SRC_OoW, + Parser.SRC_DIP, + Parser.SRC_SLW, + Parser.SRC_SDW, + Parser.SRC_DC, + Parser.SRC_BGDIA, + Parser.SRC_LR, + Parser.SRC_EFR, + Parser.SRC_RMBRE, + Parser.SRC_IMR, + Parser.SRC_EGW_ToR, + Parser.SRC_EGW_DD, + Parser.SRC_EGW_FS, + Parser.SRC_EGW_US, + Parser.SRC_IDRotF, + Parser.SRC_CM, + Parser.SRC_HoL, + Parser.SRC_XMtS, + Parser.SRC_RtG, + Parser.SRC_AitFR_ISF, + Parser.SRC_AitFR_THP, + Parser.SRC_AitFR_AVT, + Parser.SRC_AitFR_DN, + Parser.SRC_AitFR_FCD, + Parser.SRC_WBtW, + Parser.SRC_NRH, + Parser.SRC_NRH_TCMC, + Parser.SRC_NRH_AVitW, + Parser.SRC_NRH_ASS, + Parser.SRC_NRH_CoI, + Parser.SRC_NRH_TLT, + Parser.SRC_NRH_AWoL, + Parser.SRC_NRH_AT, + Parser.SRC_SCC_CK, + Parser.SRC_SCC_HfMT, + Parser.SRC_SCC_TMM, + Parser.SRC_SCC_ARiR, + Parser.SRC_CRCotN, + Parser.SRC_JttRC, + Parser.SRC_LoX, + Parser.SRC_DoSI, + Parser.SRC_DSotDQ, + Parser.SRC_KftGV, + Parser.SRC_GotSF, + Parser.SRC_PaBTSO, + Parser.SRC_ToFW, + Parser.SRC_LK, + Parser.SRC_CoA, + Parser.SRC_PiP, + Parser.SRC_HFStCM, + Parser.SRC_GHLoE, + Parser.SRC_DoDk, +].forEach(src => { + Parser.SOURCES_AVAILABLE_DOCS_ADVENTURE[src] = src; + Parser.SOURCES_AVAILABLE_DOCS_ADVENTURE[src.toLowerCase()] = src; +}); + +Parser.getTagSource = function (tag, source) { + if (source && source.trim()) return source; + + tag = tag.trim(); + + const tagMeta = Renderer.tag.TAG_LOOKUP[tag]; + + if (!tagMeta) throw new Error(`Unhandled tag "${tag}"`); + return tagMeta.defaultSource; +}; + +Parser.PROP_TO_TAG = { + "monster": "creature", + "optionalfeature": "optfeature", + "tableGroup": "table", + "vehicleUpgrade": "vehupgrade", + "baseitem": "item", + "itemGroup": "item", + "magicvariant": "item", +}; +Parser.getPropTag = function (prop) { + if (Parser.PROP_TO_TAG[prop]) return Parser.PROP_TO_TAG[prop]; + return prop; +}; + +Parser.PROP_TO_DISPLAY_NAME = { + "variantrule": "Variant Rule", + "optionalfeature": "Option/Feature", + "magicvariant": "Magic Item Variant", + "baseitem": "Item (Base)", + "item": "Item", + "adventure": "Adventure", + "adventureData": "Adventure Text", + "book": "Book", + "bookData": "Book Text", + "makebrewCreatureTrait": "Homebrew Builder Creature Trait", + "charoption": "Other Character Creation Option", + + "bonus": "Bonus Action", + "legendary": "Legendary Action", + "mythic": "Mythic Action", + "lairActions": "Lair Action", + "regionalEffects": "Regional Effect", +}; +Parser.getPropDisplayName = function (prop, {suffix = ""} = {}) { + if (Parser.PROP_TO_DISPLAY_NAME[prop]) return `${Parser.PROP_TO_DISPLAY_NAME[prop]}${suffix}`; + + const mFluff = /Fluff$/.exec(prop); + if (mFluff) return Parser.getPropDisplayName(prop.slice(0, -mFluff[0].length), {suffix: " Fluff"}); + + const mFoundry = /^foundry(?[A-Z].*)$/.exec(prop); + if (mFoundry) return Parser.getPropDisplayName(mFoundry.groups.prop.lowercaseFirst(), {suffix: " Foundry Data"}); + + return `${prop.split(/([A-Z][a-z]+)/g).filter(Boolean).join(" ").uppercaseFirst()}${suffix}`; +}; + +Parser.ITEM_TYPE_JSON_TO_ABV = { + "A": "ammunition", + "AF": "ammunition", + "AT": "artisan's tools", + "EM": "eldritch machine", + "EXP": "explosive", + "FD": "food and drink", + "G": "adventuring gear", + "GS": "gaming set", + "HA": "heavy armor", + "IDG": "illegal drug", + "INS": "instrument", + "LA": "light armor", + "M": "melee weapon", + "MA": "medium armor", + "MNT": "mount", + "MR": "master rune", + "GV": "generic variant", + "P": "potion", + "R": "ranged weapon", + "RD": "rod", + "RG": "ring", + "S": "shield", + "SC": "scroll", + "SCF": "spellcasting focus", + "OTH": "other", + "T": "tools", + "TAH": "tack and harness", + "TG": "trade good", + "$": "treasure", + "VEH": "vehicle (land)", + "SHP": "vehicle (water)", + "AIR": "vehicle (air)", + "SPC": "vehicle (space)", + "WD": "wand", +}; + +Parser.DMGTYPE_JSON_TO_FULL = { + "A": "acid", + "B": "bludgeoning", + "C": "cold", + "F": "fire", + "O": "force", + "L": "lightning", + "N": "necrotic", + "P": "piercing", + "I": "poison", + "Y": "psychic", + "R": "radiant", + "S": "slashing", + "T": "thunder", +}; + +Parser.DMG_TYPES = ["acid", "bludgeoning", "cold", "fire", "force", "lightning", "necrotic", "piercing", "poison", "psychic", "radiant", "slashing", "thunder"]; +Parser.CONDITIONS = ["blinded", "charmed", "deafened", "exhaustion", "frightened", "grappled", "incapacitated", "invisible", "paralyzed", "petrified", "poisoned", "prone", "restrained", "stunned", "unconscious"]; + +Parser.SENSES = [ + {"name": "blindsight", "source": Parser.SRC_PHB}, + {"name": "darkvision", "source": Parser.SRC_PHB}, + {"name": "tremorsense", "source": Parser.SRC_MM}, + {"name": "truesight", "source": Parser.SRC_PHB}, +]; + +Parser.NUMBERS_ONES = ["", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine"]; +Parser.NUMBERS_TENS = ["", "", "twenty", "thirty", "forty", "fifty", "sixty", "seventy", "eighty", "ninety"]; +Parser.NUMBERS_TEENS = ["ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeen", "eighteen", "nineteen"]; + +// region Metric conversion +Parser.metric = { + // See MPMB's breakdown: https://old.reddit.com/r/dndnext/comments/6gkuec + MILES_TO_KILOMETRES: 1.6, + FEET_TO_METRES: 0.3, // 5 ft = 1.5 m + YARDS_TO_METRES: 0.9, // (as above) + POUNDS_TO_KILOGRAMS: 0.5, // 2 lb = 1 kg + + getMetricNumber ({originalValue, originalUnit, toFixed = null}) { + if (originalValue == null || isNaN(originalValue)) return originalValue; + + originalValue = Number(originalValue); + if (!originalValue) return originalValue; + + let out = null; + switch (originalUnit) { + case "ft.": case "ft": case Parser.UNT_FEET: out = originalValue * Parser.metric.FEET_TO_METRES; break; + case "yd.": case "yd": case Parser.UNT_YARDS: out = originalValue * Parser.metric.YARDS_TO_METRES; break; + case "mi.": case "mi": case Parser.UNT_MILES: out = originalValue * Parser.metric.MILES_TO_KILOMETRES; break; + case "lb.": case "lb": case "lbs": out = originalValue * Parser.metric.POUNDS_TO_KILOGRAMS; break; + default: return originalValue; + } + if (toFixed != null) return NumberUtil.toFixedNumber(out, toFixed); + return out; + }, + + getMetricUnit ({originalUnit, isShortForm = false, isPlural = true}) { + switch (originalUnit) { + case "ft.": case "ft": case Parser.UNT_FEET: return isShortForm ? "m" : `meter`[isPlural ? "toPlural" : "toString"](); + case "yd.": case "yd": case Parser.UNT_YARDS: return isShortForm ? "m" : `meter`[isPlural ? "toPlural" : "toString"](); + case "mi.": case "mi": case Parser.UNT_MILES: return isShortForm ? "km" : `kilometre`[isPlural ? "toPlural" : "toString"](); + case "lb.": case "lb": case "lbs": return isShortForm ? "kg" : `kilogram`[isPlural ? "toPlural" : "toString"](); + default: return originalUnit; + } + }, +}; +// endregion +// region Map grids + +Parser.MAP_GRID_TYPE_TO_FULL = {}; +Parser.MAP_GRID_TYPE_TO_FULL["none"] = "None"; +Parser.MAP_GRID_TYPE_TO_FULL["square"] = "Square"; +Parser.MAP_GRID_TYPE_TO_FULL["hexRowsOdd"] = "Hex Rows (Odd)"; +Parser.MAP_GRID_TYPE_TO_FULL["hexRowsEven"] = "Hex Rows (Even)"; +Parser.MAP_GRID_TYPE_TO_FULL["hexColsOdd"] = "Hex Columns (Odd)"; +Parser.MAP_GRID_TYPE_TO_FULL["hexColsEven"] = "Hex Columns (Even)"; + +Parser.mapGridTypeToFull = function (gridType) { + return Parser._parse_aToB(Parser.MAP_GRID_TYPE_TO_FULL, gridType); +}; +// endregion diff --git a/charbuilder/js/vetools/render.js b/charbuilder/js/vetools/render.js new file mode 100644 index 0000000..5f8c7c7 --- /dev/null +++ b/charbuilder/js/vetools/render.js @@ -0,0 +1,16436 @@ +//#region Renderer +globalThis.Renderer = function() { + this.wrapperTag = "div"; + this.baseUrl = ""; + this.baseMediaUrls = {}; + + if (globalThis.DEPLOYED_IMG_ROOT) { + this.baseMediaUrls["img"] = globalThis.DEPLOYED_IMG_ROOT; + } + + this._lazyImages = false; + this._subVariant = false; + this._firstSection = true; + this._isAddHandlers = true; + this._headerIndex = 1; + this._tagExportDict = null; + this._roll20Ids = null; + this._trackTitles = { + enabled: false, + titles: {} + }; + this._enumerateTitlesRel = { + enabled: false, + titles: {} + }; + this._isHeaderIndexIncludeTableCaptions = false; + this._isHeaderIndexIncludeImageTitles = false; + this._plugins = {}; + this._fnPostProcess = null; + this._extraSourceClasses = null; + this._depthTracker = null; + this._depthTrackerAdditionalProps = []; + this._depthTrackerAdditionalPropsInherited = []; + this._lastDepthTrackerInheritedProps = {}; + this._isInternalLinksDisabled = false; + this._isPartPageExpandCollapseDisabled = false; + this._fnsGetStyleClasses = {}; + + this.setLazyImages = function(bool) { + if (typeof IntersectionObserver === "undefined") + this._lazyImages = false; + else + this._lazyImages = !!bool; + return this; + } + ; + + this.setWrapperTag = function(tag) { + this.wrapperTag = tag; + return this; + } + ; + + this.setBaseUrl = function(url) { + this.baseUrl = url; + return this; + } + ; + + this.setBaseMediaUrl = function(mediaDir, url) { + this.baseMediaUrls[mediaDir] = url; + return this; + } + ; + + this.setFirstSection = function(bool) { + this._firstSection = bool; + return this; + } + ; + + this.setAddHandlers = function(bool) { + this._isAddHandlers = bool; + return this; + } + ; + + this.setFnPostProcess = function(fn) { + this._fnPostProcess = fn; + return this; + } + ; + + this.setExtraSourceClasses = function(arr) { + this._extraSourceClasses = arr; + return this; + } + ; + + this.resetHeaderIndex = function() { + this._headerIndex = 1; + this._trackTitles.titles = {}; + this._enumerateTitlesRel.titles = {}; + return this; + } + ; + + this.getHeaderIndex = function() { + return this._headerIndex; + } + ; + + this.setHeaderIndexTableCaptions = function(bool) { + this._isHeaderIndexIncludeTableCaptions = bool; + return this; + } + ; + this.setHeaderIndexImageTitles = function(bool) { + this._isHeaderIndexIncludeImageTitles = bool; + return this; + } + ; + + this.doExportTags = function(toObj) { + this._tagExportDict = toObj; + return this; + } + ; + + this.resetExportTags = function() { + this._tagExportDict = null; + return this; + } + ; + + this.setRoll20Ids = function(roll20Ids) { + this._roll20Ids = roll20Ids; + return this; + } + ; + + this.resetRoll20Ids = function() { + this._roll20Ids = null; + return this; + } + ; + + this.setInternalLinksDisabled = function(val) { + this._isInternalLinksDisabled = !!val; + return this; + } + ; + this.isInternalLinksDisabled = function() { + return !!this._isInternalLinksDisabled; + } + ; + + this.setPartPageExpandCollapseDisabled = function(val) { + this._isPartPageExpandCollapseDisabled = !!val; + return this; + } + ; + + this.setFnGetStyleClasses = function(identifier, fn) { + if (fn == null) { + delete this._fnsGetStyleClasses[identifier]; + return this; + } + + this._fnsGetStyleClasses[identifier] = fn; + return this; + } + ; + + this.setEnumerateTitlesRel = function(bool) { + this._enumerateTitlesRel.enabled = bool; + return this; + } + ; + + this._getEnumeratedTitleRel = function(name) { + if (this._enumerateTitlesRel.enabled && name) { + const clean = name.toLowerCase(); + this._enumerateTitlesRel.titles[clean] = this._enumerateTitlesRel.titles[clean] || 0; + return `data-title-relative-index="${this._enumerateTitlesRel.titles[clean]++}"`; + } else + return ""; + } + ; + + this.setTrackTitles = function(bool) { + this._trackTitles.enabled = bool; + return this; + } + ; + + this.getTrackedTitles = function() { + return MiscUtil.copyFast(this._trackTitles.titles); + } + ; + + this.getTrackedTitlesInverted = function({isStripTags=false}={}) { + const trackedTitlesInverse = {}; + Object.entries(this._trackTitles.titles || {}).forEach(([titleIx,titleName])=>{ + if (isStripTags) + titleName = Renderer.stripTags(titleName); + titleName = titleName.toLowerCase().trim(); + (trackedTitlesInverse[titleName] = trackedTitlesInverse[titleName] || []).push(titleIx); + } + ); + return trackedTitlesInverse; + } + ; + + this._handleTrackTitles = function(name, {isTable=false, isImage=false}={}) { + if (!this._trackTitles.enabled) + return; + if (isTable && !this._isHeaderIndexIncludeTableCaptions) + return; + if (isImage && !this._isHeaderIndexIncludeImageTitles) + return; + this._trackTitles.titles[this._headerIndex] = name; + } + ; + + this._handleTrackDepth = function(entry, depth) { + if (!entry.name || !this._depthTracker) + return; + + this._lastDepthTrackerInheritedProps = MiscUtil.copyFast(this._lastDepthTrackerInheritedProps); + if (entry.source) + this._lastDepthTrackerInheritedProps.source = entry.source; + if (this._depthTrackerAdditionalPropsInherited?.length) { + this._depthTrackerAdditionalPropsInherited.forEach(prop=>this._lastDepthTrackerInheritedProps[prop] = entry[prop] || this._lastDepthTrackerInheritedProps[prop]); + } + + const additionalData = this._depthTrackerAdditionalProps.length ? this._depthTrackerAdditionalProps.mergeMap(it=>({ + [it]: entry[it] + })) : {}; + + this._depthTracker.push({ + ...this._lastDepthTrackerInheritedProps, + ...additionalData, + depth, + name: entry.name, + type: entry.type, + ixHeader: this._headerIndex, + source: this._lastDepthTrackerInheritedProps.source, + data: entry.data, + page: entry.page, + alias: entry.alias, + entry, + }); + } + ; + + this.addPlugin = function(pluginType, fnPlugin) { + MiscUtil.getOrSet(this._plugins, pluginType, []).push(fnPlugin); + } + ; + + this.removePlugin = function(pluginType, fnPlugin) { + if (!fnPlugin) + return; + const ix = (MiscUtil.get(this._plugins, pluginType) || []).indexOf(fnPlugin); + if (~ix) + this._plugins[pluginType].splice(ix, 1); + } + ; + + this.removePlugins = function(pluginType) { + MiscUtil.delete(this._plugins, pluginType); + } + ; + + this._getPlugins = function(pluginType) { + return this._plugins[pluginType] || []; + } + ; + + this.withPlugin = function({pluginTypes, fnPlugin, fn}) { + for (const pt of pluginTypes) + this.addPlugin(pt, fnPlugin); + try { + return fn(this); + } finally { + for (const pt of pluginTypes) + this.removePlugin(pt, fnPlugin); + } + } + ; + + this.pWithPlugin = async function({pluginTypes, fnPlugin, pFn}) { + for (const pt of pluginTypes) + this.addPlugin(pt, fnPlugin); + try { + const out = await pFn(this); + return out; + } finally { + for (const pt of pluginTypes) + this.removePlugin(pt, fnPlugin); + } + } + ; + + this.setDepthTracker = function(arr, {additionalProps, additionalPropsInherited}={}) { + this._depthTracker = arr; + this._depthTrackerAdditionalProps = additionalProps || []; + this._depthTrackerAdditionalPropsInherited = additionalPropsInherited || []; + return this; + } + ; + + this.getLineBreak = function() { + return "
            "; + } + ; + + this.recursiveRender = function(entry, textStack, meta, options) { + if (entry instanceof Array) { + entry.forEach(nxt=>this.recursiveRender(nxt, textStack, meta, options)); + setTimeout(()=>{ + throw new Error(`Array passed to renderer! The renderer only guarantees support for primitives and basic objects.`); + } + ); + return this; + } + + if (textStack.length === 0) + textStack[0] = ""; + else + textStack.reverse(); + + meta = meta || {}; + meta._typeStack = []; + meta.depth = meta.depth == null ? 0 : meta.depth; + + this._recursiveRender(entry, textStack, meta, options); + if (this._fnPostProcess) + textStack[0] = this._fnPostProcess(textStack[0]); + textStack.reverse(); + + return this; + } + ; + + this._recursiveRender = function(entry, textStack, meta, options) { + if (entry == null) + return; + if (!textStack) + throw new Error("Missing stack!"); + if (!meta) + throw new Error("Missing metadata!"); + if (entry.type === "section") + meta.depth = -1; + + options = options || {}; + + meta._didRenderPrefix = false; + meta._didRenderSuffix = false; + + if (typeof entry === "object") { + const type = entry.type == null || entry.type === "section" ? "entries" : entry.type; + + if (type === "wrapper") + return this._recursiveRender(entry.wrapped, textStack, meta, options); + + meta._typeStack.push(type); + + switch (type) { + case "entries": + this._renderEntries(entry, textStack, meta, options); + break; + case "options": + this._renderOptions(entry, textStack, meta, options); + break; + case "list": + this._renderList(entry, textStack, meta, options); + break; + case "table": + this._renderTable(entry, textStack, meta, options); + break; + case "tableGroup": + this._renderTableGroup(entry, textStack, meta, options); + break; + case "inset": + this._renderInset(entry, textStack, meta, options); + break; + case "insetReadaloud": + this._renderInsetReadaloud(entry, textStack, meta, options); + break; + case "variant": + this._renderVariant(entry, textStack, meta, options); + break; + case "variantInner": + this._renderVariantInner(entry, textStack, meta, options); + break; + case "variantSub": + this._renderVariantSub(entry, textStack, meta, options); + break; + case "spellcasting": + this._renderSpellcasting(entry, textStack, meta, options); + break; + case "quote": + this._renderQuote(entry, textStack, meta, options); + break; + case "optfeature": + this._renderOptfeature(entry, textStack, meta, options); + break; + case "patron": + this._renderPatron(entry, textStack, meta, options); + break; + + case "abilityDc": + this._renderAbilityDc(entry, textStack, meta, options); + break; + case "abilityAttackMod": + this._renderAbilityAttackMod(entry, textStack, meta, options); + break; + case "abilityGeneric": + this._renderAbilityGeneric(entry, textStack, meta, options); + break; + + case "inline": + this._renderInline(entry, textStack, meta, options); + break; + case "inlineBlock": + this._renderInlineBlock(entry, textStack, meta, options); + break; + case "bonus": + this._renderBonus(entry, textStack, meta, options); + break; + case "bonusSpeed": + this._renderBonusSpeed(entry, textStack, meta, options); + break; + case "dice": + this._renderDice(entry, textStack, meta, options); + break; + case "link": + this._renderLink(entry, textStack, meta, options); + break; + case "actions": + this._renderActions(entry, textStack, meta, options); + break; + case "attack": + this._renderAttack(entry, textStack, meta, options); + break; + case "ingredient": + this._renderIngredient(entry, textStack, meta, options); + break; + + case "item": + this._renderItem(entry, textStack, meta, options); + break; + case "itemSub": + this._renderItemSub(entry, textStack, meta, options); + break; + case "itemSpell": + this._renderItemSpell(entry, textStack, meta, options); + break; + + case "statblockInline": + this._renderStatblockInline(entry, textStack, meta, options); + break; + case "statblock": + this._renderStatblock(entry, textStack, meta, options); + break; + + case "image": + this._renderImage(entry, textStack, meta, options); + break; + case "gallery": + this._renderGallery(entry, textStack, meta, options); + break; + + case "flowchart": + this._renderFlowchart(entry, textStack, meta, options); + break; + case "flowBlock": + this._renderFlowBlock(entry, textStack, meta, options); + break; + + case "homebrew": + this._renderHomebrew(entry, textStack, meta, options); + break; + + case "code": + this._renderCode(entry, textStack, meta, options); + break; + case "hr": + this._renderHr(entry, textStack, meta, options); + break; + } + + meta._typeStack.pop(); + } else if (typeof entry === "string") { + this._renderPrefix(entry, textStack, meta, options); + this._renderString(entry, textStack, meta, options); + this._renderSuffix(entry, textStack, meta, options); + } else { + this._renderPrefix(entry, textStack, meta, options); + this._renderPrimitive(entry, textStack, meta, options); + this._renderSuffix(entry, textStack, meta, options); + } + } + ; + + this._RE_TEXT_CENTER = /\btext-center\b/; + + this._getMutatedStyleString = function(str) { + if (!str) + return str; + return str.replace(this._RE_TEXT_CENTER, "ve-text-center"); + } + ; + + this._adjustDepth = function(meta, dDepth) { + const cachedDepth = meta.depth; + meta.depth += dDepth; + meta.depth = Math.min(Math.max(-1, meta.depth), 2); + return cachedDepth; + } + ; + + this._renderPrefix = function(entry, textStack, meta, options) { + if (meta._didRenderPrefix) + return; + if (options.prefix != null) { + textStack[0] += options.prefix; + meta._didRenderPrefix = true; + } + } + ; + + this._renderSuffix = function(entry, textStack, meta, options) { + if (meta._didRenderSuffix) + return; + if (options.suffix != null) { + textStack[0] += options.suffix; + meta._didRenderSuffix = true; + } + } + ; + + this._renderImage = function(entry, textStack, meta, options) { + if (entry.title) + this._handleTrackTitles(entry.title, { + isImage: true + }); + + textStack[0] += `
            `; + + if (entry.imageType === "map" || entry.imageType === "mapPlayer") + textStack[0] += `
            `; + textStack[0] += `
            `; + + const href = this._renderImage_getUrl(entry); + const svg = this._lazyImages && entry.width != null && entry.height != null ? `data:image/svg+xml,${encodeURIComponent(``)}` : null; + const ptTitleCreditTooltip = this._renderImage_getTitleCreditTooltipText(entry); + const ptTitle = ptTitleCreditTooltip ? `title="${ptTitleCreditTooltip}"` : ""; + const pluginDataIsNoLink = this._getPlugins("image_isNoLink").map(plugin=>plugin(entry, textStack, meta, options)).some(Boolean); + + textStack[0] += `
            + ${pluginDataIsNoLink ? "" : ``} + + ${pluginDataIsNoLink ? "" : ``} +
            `; + + if (!this._renderImage_isComicStyling(entry) && (entry.title || entry.credit || entry.mapRegions)) { + const ptAdventureBookMeta = entry.mapRegions && meta.adventureBookPage && meta.adventureBookSource && meta.adventureBookHash ? `data-rd-adventure-book-map-page="${meta.adventureBookPage.qq()}" data-rd-adventure-book-map-source="${meta.adventureBookSource.qq()}" data-rd-adventure-book-map-hash="${meta.adventureBookHash.qq()}"` : ""; + + textStack[0] += `
            `; + + if (entry.title && !entry.mapRegions) + textStack[0] += `
            ${this.render(entry.title)}
            `; + + if (entry.mapRegions && !IS_VTT) { + textStack[0] += ``; + } + + if (entry.credit) + textStack[0] += `
            ${this.render(entry.credit)}
            `; + + textStack[0] += `
            `; + } + + if (entry._galleryTitlePad) + textStack[0] += `
             
            `; + if (entry._galleryCreditPad) + textStack[0] += `
             
            `; + + textStack[0] += `
            `; + if (entry.imageType === "map" || entry.imageType === "mapPlayer") + textStack[0] += `
            `; + } + ; + + this._renderImage_getTitleCreditTooltipText = function(entry) { + if (!entry.title && !entry.credit) + return null; + return Renderer.stripTags([entry.title, entry.credit ? `Art credit: ${entry.credit}` : null].filter(Boolean).join(". "), ).qq(); + } + ; + + this._renderImage_getStylePart = function(entry) { + const styles = [entry.maxWidth ? `max-width: min(100%, ${entry.maxWidth}${entry.maxWidthUnits || "px"})` : "", entry.maxHeight ? `max-height: min(60vh, ${entry.maxHeight}${entry.maxHeightUnits || "px"})` : "", ].filter(Boolean).join("; "); + return styles ? `style="${styles}"` : ""; + } + ; + + this._renderImage_getMapRegionData = function(entry) { + return JSON.stringify(this.getMapRegionData(entry)).escapeQuotes(); + } + ; + + this.getMapRegionData = function(entry) { + return { + regions: entry.mapRegions, + width: entry.width, + height: entry.height, + href: this._renderImage_getUrl(entry), + hrefThumbnail: this._renderImage_getUrlThumbnail(entry), + page: entry.page, + source: entry.source, + hash: entry.hash, + }; + } + ; + + this._renderImage_isComicStyling = function(entry) { + if (!entry.style) + return false; + return ["comic-speaker-left", "comic-speaker-right"].includes(entry.style); + } + ; + + this._renderImage_getWrapperClasses = function(entry) { + const out = ["rd__wrp-image", "relative"]; + if (entry.style) { + switch (entry.style) { + case "comic-speaker-left": + out.push("rd__comic-img-speaker", "rd__comic-img-speaker--left"); + break; + case "comic-speaker-right": + out.push("rd__comic-img-speaker", "rd__comic-img-speaker--right"); + break; + } + } + return out.join(" "); + } + ; + + this._renderImage_getImageClasses = function(entry) { + const out = ["rd__image"]; + if (entry.style) { + switch (entry.style) { + case "deity-symbol": + out.push("rd__img-small"); + break; + } + } + return out.join(" "); + } + ; + + this._renderImage_getUrl = function(entry) { + let url = Renderer.utils.getMediaUrl(entry, "href", "img"); + for (const plugin of this._getPlugins(`image_urlPostProcess`)) { + url = plugin(entry, url) || url; + } + return url; + } + ; + + this._renderImage_getUrlThumbnail = function(entry) { + let url = Renderer.utils.getMediaUrl(entry, "hrefThumbnail", "img"); + for (const plugin of this._getPlugins(`image_urlThumbnailPostProcess`)) { + url = plugin(entry, url) || url; + } + return url; + } + ; + + this._renderList_getListCssClasses = function(entry, textStack, meta, options) { + const out = [`rd__list`]; + if (entry.style || entry.columns) { + if (entry.style) + out.push(...entry.style.split(" ").map(it=>`rd__${it}`)); + if (entry.columns) + out.push(`columns-${entry.columns}`); + } + return out.join(" "); + } + ; + + this._renderTableGroup = function(entry, textStack, meta, options) { + const len = entry.tables.length; + for (let i = 0; i < len; ++i) + this._recursiveRender(entry.tables[i], textStack, meta); + } + ; + + this._renderTable = function(entry, textStack, meta, options) { + if (entry.intro) { + const len = entry.intro.length; + for (let i = 0; i < len; ++i) { + this._recursiveRender(entry.intro[i], textStack, meta, { + prefix: "

            ", + suffix: "

            " + }); + } + } + + textStack[0] += ``; + + const headerRowMetas = Renderer.table.getHeaderRowMetas(entry); + + const autoRollMode = Renderer.table.getAutoConvertedRollMode(entry, {headerRowMetas}); + + const toRenderLabel = autoRollMode ? RollerUtil.getFullRollCol(headerRowMetas.last()[0]) : null; + const isInfiniteResults = autoRollMode === RollerUtil.ROLL_COL_VARIABLE; + + if (entry.caption != null) { + this._handleTrackTitles(entry.caption, { + isTable: true + }); + textStack[0] += ``; + } + + const rollCols = []; + let bodyStack = [""]; + bodyStack[0] += ""; + const lenRows = entry.rows.length; + for (let ixRow = 0; ixRow < lenRows; ++ixRow) { + bodyStack[0] += ""; + const r = entry.rows[ixRow]; + let roRender = r.type === "row" ? r.row : r; + + const len = roRender.length; + for (let ixCell = 0; ixCell < len; ++ixCell) { + rollCols[ixCell] = rollCols[ixCell] || false; + + if (autoRollMode && ixCell === 0) { + roRender = Renderer.getRollableRow(roRender, { + isForceInfiniteResults: isInfiniteResults, + isFirstRow: ixRow === 0, + isLastRow: ixRow === lenRows - 1, + }, ); + rollCols[ixCell] = true; + } + + let toRenderCell; + if (roRender[ixCell].type === "cell") { + if (roRender[ixCell].roll) { + rollCols[ixCell] = true; + if (roRender[ixCell].entry) { + toRenderCell = roRender[ixCell].entry; + } else if (roRender[ixCell].roll.exact != null) { + toRenderCell = roRender[ixCell].roll.pad ? StrUtil.padNumber(roRender[ixCell].roll.exact, 2, "0") : roRender[ixCell].roll.exact; + } else { + + const dispMin = roRender[ixCell].roll.displayMin != null ? roRender[ixCell].roll.displayMin : roRender[ixCell].roll.min; + const dispMax = roRender[ixCell].roll.displayMax != null ? roRender[ixCell].roll.displayMax : roRender[ixCell].roll.max; + + if (dispMax === Renderer.dice.POS_INFINITE) { + toRenderCell = roRender[ixCell].roll.pad ? `${StrUtil.padNumber(dispMin, 2, "0")}+` : `${dispMin}+`; + } else { + toRenderCell = roRender[ixCell].roll.pad ? `${StrUtil.padNumber(dispMin, 2, "0")}-${StrUtil.padNumber(dispMax, 2, "0")}` : `${dispMin}-${dispMax}`; + } + } + } else if (roRender[ixCell].entry) { + toRenderCell = roRender[ixCell].entry; + } + } else { + toRenderCell = roRender[ixCell]; + } + bodyStack[0] += `"; + } + bodyStack[0] += ""; + } + bodyStack[0] += ""; + + if (headerRowMetas) { + textStack[0] += ""; + + for (let ixRow = 0, lenRows = headerRowMetas.length; ixRow < lenRows; ++ixRow) { + textStack[0] += ""; + + const headerRowMeta = headerRowMetas[ixRow]; + for (let ixCell = 0, lenCells = headerRowMeta.length; ixCell < lenCells; ++ixCell) { + const lbl = headerRowMeta[ixCell]; + textStack[0] += ``; + } + + textStack[0] += ""; + } + + textStack[0] += ""; + } + + textStack[0] += bodyStack[0]; + + if (entry.footnotes != null) { + textStack[0] += ""; + const len = entry.footnotes.length; + for (let i = 0; i < len; ++i) { + textStack[0] += `"; + } + textStack[0] += ""; + } + textStack[0] += "
            ${entry.caption}
            `; + if (r.style === "row-indent-first" && ixCell === 0) + bodyStack[0] += `
            `; + const cacheDepth = this._adjustDepth(meta, 1); + this._recursiveRender(toRenderCell, bodyStack, meta); + meta.depth = cacheDepth; + bodyStack[0] += "
            `; + this._recursiveRender(autoRollMode && ixCell === 0 ? RollerUtil.getFullRollCol(lbl) : lbl, textStack, meta); + textStack[0] += `
            `; + const cacheDepth = this._adjustDepth(meta, 1); + this._recursiveRender(entry.footnotes[i], textStack, meta); + meta.depth = cacheDepth; + textStack[0] += "
            "; + + if (entry.outro) { + const len = entry.outro.length; + for (let i = 0; i < len; ++i) { + this._recursiveRender(entry.outro[i], textStack, meta, { + prefix: "

            ", + suffix: "

            " + }); + } + } + } + ; + + this._renderTable_getCellDataStr = function(ent) { + function convertZeros(num) { + if (num === 0) + return 100; + return num; + } + + if (ent.roll) { + return `data-roll-min="${convertZeros(ent.roll.exact != null ? ent.roll.exact : ent.roll.min)}" data-roll-max="${convertZeros(ent.roll.exact != null ? ent.roll.exact : ent.roll.max)}"`; + } + + return ""; + } + ; + + this._renderTable_getTableThClassText = function(entry, i) { + return entry.colStyles == null || i >= entry.colStyles.length ? "" : `class="${this._getMutatedStyleString(entry.colStyles[i])}"`; + } + ; + + this._renderTable_makeTableTdClassText = function(entry, i) { + if (entry.rowStyles != null) + return i >= entry.rowStyles.length ? "" : `class="${this._getMutatedStyleString(entry.rowStyles[i])}"`; + else + return this._renderTable_getTableThClassText(entry, i); + } + ; + + this._renderEntries = function(entry, textStack, meta, options) { + this._renderEntriesSubtypes(entry, textStack, meta, options, true); + } + ; + + this._getPagePart = function(entry, isInset) { + if (!Renderer.utils.isDisplayPage(entry.page)) + return ""; + return ` ${entry.source ? `${Parser.sourceJsonToAbv(entry.source)} ` : ""}p${entry.page}`; + } + ; + + this._renderEntriesSubtypes = function(entry, textStack, meta, options, incDepth) { + const type = entry.type || "entries"; + const isInlineTitle = meta.depth >= 2; + const isAddPeriod = isInlineTitle && entry.name && !Renderer._INLINE_HEADER_TERMINATORS.has(entry.name[entry.name.length - 1]); + const pagePart = !this._isPartPageExpandCollapseDisabled && !isInlineTitle ? this._getPagePart(entry) : ""; + const partExpandCollapse = !this._isPartPageExpandCollapseDisabled && !isInlineTitle ? `[\u2013]` : ""; + const partPageExpandCollapse = !this._isPartPageExpandCollapseDisabled && (pagePart || partExpandCollapse) ? `${[pagePart, partExpandCollapse].filter(Boolean).join("")}` : ""; + const nextDepth = incDepth && meta.depth < 2 ? meta.depth + 1 : meta.depth; + const styleString = this._renderEntriesSubtypes_getStyleString(entry, meta, isInlineTitle); + const dataString = this._renderEntriesSubtypes_getDataString(entry); + if (entry.name != null && Renderer.ENTRIES_WITH_ENUMERATED_TITLES_LOOKUP[entry.type]) + this._handleTrackTitles(entry.name); + + const headerTag = isInlineTitle ? "span" : `h${Math.min(Math.max(meta.depth + 2, 1), 6)}`; + const headerClass = `rd__h--${meta.depth + 1}`; + const cachedLastDepthTrackerProps = MiscUtil.copyFast(this._lastDepthTrackerInheritedProps); + this._handleTrackDepth(entry, meta.depth); + + const pluginDataNamePrefix = this._getPlugins(`${type}_namePrefix`).map(plugin=>plugin(entry, textStack, meta, options)).filter(Boolean); + + const headerSpan = entry.name ? `<${headerTag} class="rd__h ${headerClass}" data-title-index="${this._headerIndex++}" ${this._getEnumeratedTitleRel(entry.name)}> ${pluginDataNamePrefix.join("")}${this.render({ + type: "inline", + entries: [entry.name] + })}${isAddPeriod ? "." : ""}${partPageExpandCollapse} ` : ""; + + if (meta.depth === -1) { + if (!this._firstSection) + textStack[0] += `
            `; + this._firstSection = false; + } + + if (entry.entries || entry.name) { + textStack[0] += `<${this.wrapperTag} ${dataString} ${styleString}>${headerSpan}`; + this._renderEntriesSubtypes_renderPreReqText(entry, textStack, meta); + if (entry.entries) { + const cacheDepth = meta.depth; + const len = entry.entries.length; + for (let i = 0; i < len; ++i) { + meta.depth = nextDepth; + this._recursiveRender(entry.entries[i], textStack, meta, { + prefix: "

            ", + suffix: "

            " + }); + if (i === 0 && cacheDepth >= 2) + textStack[0] += `
            `; + } + meta.depth = cacheDepth; + } + textStack[0] += ``; + } + + this._lastDepthTrackerInheritedProps = cachedLastDepthTrackerProps; + } + ; + + this._renderEntriesSubtypes_getDataString = function(entry) { + let dataString = ""; + if (entry.source) + dataString += `data-source="${entry.source}"`; + if (entry.data) { + for (const k in entry.data) { + if (!k.startsWith("rd-")) + continue; + dataString += ` data-${k}="${`${entry.data[k]}`.escapeQuotes()}"`; + } + } + return dataString; + } + ; + + this._renderEntriesSubtypes_renderPreReqText = function(entry, textStack, meta) { + if (entry.prerequisite) { + textStack[0] += `Prerequisite: `; + this._recursiveRender({ + type: "inline", + entries: [entry.prerequisite] + }, textStack, meta); + textStack[0] += ``; + } + } + ; + + this._renderEntriesSubtypes_getStyleString = function(entry, meta, isInlineTitle) { + const styleClasses = ["rd__b"]; + styleClasses.push(this._getStyleClass(entry.type || "entries", entry)); + if (isInlineTitle) { + if (this._subVariant) + styleClasses.push(Renderer.HEAD_2_SUB_VARIANT); + else + styleClasses.push(Renderer.HEAD_2); + } else + styleClasses.push(meta.depth === -1 ? Renderer.HEAD_NEG_1 : meta.depth === 0 ? Renderer.HEAD_0 : Renderer.HEAD_1); + return styleClasses.length > 0 ? `class="${styleClasses.join(" ")}"` : ""; + } + ; + + this._renderOptions = function(entry, textStack, meta, options) { + if (!entry.entries) + return; + entry.entries = entry.entries.sort((a,b)=>a.name && b.name ? SortUtil.ascSort(a.name, b.name) : a.name ? -1 : b.name ? 1 : 0); + + if (entry.style && entry.style === "list-hang-notitle") { + const fauxEntry = { + type: "list", + style: "list-hang-notitle", + items: entry.entries.map(ent=>{ + if (typeof ent === "string") + return ent; + if (ent.type === "item") + return ent; + + const out = { + ...ent, + type: "item" + }; + if (ent.name) + out.name = Renderer._INLINE_HEADER_TERMINATORS.has(ent.name[ent.name.length - 1]) ? out.name : `${out.name}.`; + return out; + } + ), + }; + this._renderList(fauxEntry, textStack, meta, options); + } else + this._renderEntriesSubtypes(entry, textStack, meta, options, false); + } + ; + + this._renderList = function(entry, textStack, meta, options) { + if (entry.items) { + const tag = entry.start ? "ol" : "ul"; + const cssClasses = this._renderList_getListCssClasses(entry, textStack, meta, options); + textStack[0] += `<${tag} ${cssClasses ? `class="${cssClasses}"` : ""} ${entry.start ? `start="${entry.start}"` : ""}>`; + if (entry.name) + textStack[0] += `
          • ${entry.name}
          • `; + const isListHang = entry.style && entry.style.split(" ").includes("list-hang"); + const len = entry.items.length; + for (let i = 0; i < len; ++i) { + const item = entry.items[i]; + if (item.type !== "list") { + const className = `${this._getStyleClass(entry.type, item)}${item.type === "itemSpell" ? " rd__li-spell" : ""}`; + textStack[0] += `
          • `; + } + if (isListHang && typeof item === "string") + textStack[0] += "
            "; + this._recursiveRender(item, textStack, meta); + if (isListHang && typeof item === "string") + textStack[0] += "
            "; + if (item.type !== "list") + textStack[0] += "
          • "; + } + textStack[0] += ``; + } + } + ; + + this._getPtExpandCollapseSpecial = function() { + return `[\u2013]`; + } + ; + + this._renderInset = function(entry, textStack, meta, options) { + const dataString = this._renderEntriesSubtypes_getDataString(entry); + textStack[0] += `<${this.wrapperTag} class="rd__b-special rd__b-inset ${this._getMutatedStyleString(entry.style || "")}" ${dataString}>`; + + const cachedLastDepthTrackerProps = MiscUtil.copyFast(this._lastDepthTrackerInheritedProps); + this._handleTrackDepth(entry, 1); + + const pagePart = this._getPagePart(entry, true); + const partExpandCollapse = this._getPtExpandCollapseSpecial(); + const partPageExpandCollapse = `${[pagePart, partExpandCollapse].filter(Boolean).join("")}`; + + if (entry.name != null) { + if (Renderer.ENTRIES_WITH_ENUMERATED_TITLES_LOOKUP[entry.type]) + this._handleTrackTitles(entry.name); + textStack[0] += `

            ${entry.name}

            ${partPageExpandCollapse}
            `; + } else { + textStack[0] += `${partPageExpandCollapse}`; + } + + if (entry.entries) { + const len = entry.entries.length; + for (let i = 0; i < len; ++i) { + const cacheDepth = meta.depth; + meta.depth = 2; + this._recursiveRender(entry.entries[i], textStack, meta, { + prefix: "

            ", + suffix: "

            " + }); + meta.depth = cacheDepth; + } + } + textStack[0] += `
            `; + textStack[0] += ``; + + this._lastDepthTrackerInheritedProps = cachedLastDepthTrackerProps; + } + ; + + this._renderInsetReadaloud = function(entry, textStack, meta, options) { + const dataString = this._renderEntriesSubtypes_getDataString(entry); + textStack[0] += `<${this.wrapperTag} class="rd__b-special rd__b-inset rd__b-inset--readaloud ${this._getMutatedStyleString(entry.style || "")}" ${dataString}>`; + + const cachedLastDepthTrackerProps = MiscUtil.copyFast(this._lastDepthTrackerInheritedProps); + this._handleTrackDepth(entry, 1); + + const pagePart = this._getPagePart(entry, true); + const partExpandCollapse = this._getPtExpandCollapseSpecial(); + const partPageExpandCollapse = `${[pagePart, partExpandCollapse].filter(Boolean).join("")}`; + + if (entry.name != null) { + if (Renderer.ENTRIES_WITH_ENUMERATED_TITLES_LOOKUP[entry.type]) + this._handleTrackTitles(entry.name); + textStack[0] += `

            ${entry.name}

            ${this._getPagePart(entry, true)}
            `; + } else { + textStack[0] += `${partPageExpandCollapse}`; + } + + const len = entry.entries.length; + for (let i = 0; i < len; ++i) { + const cacheDepth = meta.depth; + meta.depth = 2; + this._recursiveRender(entry.entries[i], textStack, meta, { + prefix: "

            ", + suffix: "

            " + }); + meta.depth = cacheDepth; + } + textStack[0] += `
            `; + textStack[0] += ``; + + this._lastDepthTrackerInheritedProps = cachedLastDepthTrackerProps; + } + ; + + this._renderVariant = function(entry, textStack, meta, options) { + const dataString = this._renderEntriesSubtypes_getDataString(entry); + + if (entry.name != null && Renderer.ENTRIES_WITH_ENUMERATED_TITLES_LOOKUP[entry.type]) + this._handleTrackTitles(entry.name); + const cachedLastDepthTrackerProps = MiscUtil.copyFast(this._lastDepthTrackerInheritedProps); + this._handleTrackDepth(entry, 1); + + const pagePart = this._getPagePart(entry, true); + const partExpandCollapse = this._getPtExpandCollapseSpecial(); + const partPageExpandCollapse = `${[pagePart, partExpandCollapse].filter(Boolean).join("")}`; + + textStack[0] += `<${this.wrapperTag} class="rd__b-special rd__b-inset" ${dataString}>`; + textStack[0] += `

            Variant: ${entry.name}

            ${partPageExpandCollapse}
            `; + const len = entry.entries.length; + for (let i = 0; i < len; ++i) { + const cacheDepth = meta.depth; + meta.depth = 2; + this._recursiveRender(entry.entries[i], textStack, meta, { + prefix: "

            ", + suffix: "

            " + }); + meta.depth = cacheDepth; + } + if (entry.source) + textStack[0] += Renderer.utils.getSourceAndPageTrHtml({ + source: entry.source, + page: entry.page + }); + textStack[0] += ``; + + this._lastDepthTrackerInheritedProps = cachedLastDepthTrackerProps; + } + ; + + this._renderVariantInner = function(entry, textStack, meta, options) { + const dataString = this._renderEntriesSubtypes_getDataString(entry); + + if (entry.name != null && Renderer.ENTRIES_WITH_ENUMERATED_TITLES_LOOKUP[entry.type]) + this._handleTrackTitles(entry.name); + const cachedLastDepthTrackerProps = MiscUtil.copyFast(this._lastDepthTrackerInheritedProps); + this._handleTrackDepth(entry, 1); + + textStack[0] += `<${this.wrapperTag} class="rd__b-inset-inner" ${dataString}>`; + textStack[0] += `

            ${entry.name}

            `; + const len = entry.entries.length; + for (let i = 0; i < len; ++i) { + const cacheDepth = meta.depth; + meta.depth = 2; + this._recursiveRender(entry.entries[i], textStack, meta, { + prefix: "

            ", + suffix: "

            " + }); + meta.depth = cacheDepth; + } + if (entry.source) + textStack[0] += Renderer.utils.getSourceAndPageTrHtml({ + source: entry.source, + page: entry.page + }); + textStack[0] += ``; + + this._lastDepthTrackerInheritedProps = cachedLastDepthTrackerProps; + } + ; + + this._renderVariantSub = function(entry, textStack, meta, options) { + this._subVariant = true; + const fauxEntry = entry; + fauxEntry.type = "entries"; + const cacheDepth = meta.depth; + meta.depth = 3; + this._recursiveRender(fauxEntry, textStack, meta, { + prefix: "

            ", + suffix: "

            " + }); + meta.depth = cacheDepth; + this._subVariant = false; + } + ; + + this._renderSpellcasting_getEntries = function(entry) { + const hidden = new Set(entry.hidden || []); + const toRender = [{ + type: "entries", + name: entry.name, + entries: entry.headerEntries ? MiscUtil.copyFast(entry.headerEntries) : [] + }]; + + if (entry.constant || entry.will || entry.recharge || entry.charges || entry.rest || entry.daily || entry.weekly || entry.yearly || entry.ritual) { + const tempList = { + type: "list", + style: "list-hang-notitle", + items: [], + data: { + isSpellList: true + } + }; + if (entry.constant && !hidden.has("constant")) + tempList.items.push({ + type: "itemSpell", + name: `Constant:`, + entry: this._renderSpellcasting_getRenderableList(entry.constant).join(", ") + }); + if (entry.will && !hidden.has("will")) + tempList.items.push({ + type: "itemSpell", + name: `At will:`, + entry: this._renderSpellcasting_getRenderableList(entry.will).join(", ") + }); + + this._renderSpellcasting_getEntries_procPerDuration({ + entry, + tempList, + hidden, + prop: "recharge", + fnGetDurationText: num=>`{@recharge ${num}|m}`, + isSkipPrefix: true + }); + this._renderSpellcasting_getEntries_procPerDuration({ + entry, + tempList, + hidden, + prop: "charges", + fnGetDurationText: num=>` charge${num === 1 ? "" : "s"}` + }); + this._renderSpellcasting_getEntries_procPerDuration({ + entry, + tempList, + hidden, + prop: "rest", + durationText: "/rest" + }); + this._renderSpellcasting_getEntries_procPerDuration({ + entry, + tempList, + hidden, + prop: "daily", + durationText: "/day" + }); + this._renderSpellcasting_getEntries_procPerDuration({ + entry, + tempList, + hidden, + prop: "weekly", + durationText: "/week" + }); + this._renderSpellcasting_getEntries_procPerDuration({ + entry, + tempList, + hidden, + prop: "yearly", + durationText: "/year" + }); + + if (entry.ritual && !hidden.has("ritual")) + tempList.items.push({ + type: "itemSpell", + name: `Rituals:`, + entry: this._renderSpellcasting_getRenderableList(entry.ritual).join(", ") + }); + tempList.items = tempList.items.filter(it=>it.entry !== ""); + if (tempList.items.length) + toRender[0].entries.push(tempList); + } + + if (entry.spells && !hidden.has("spells")) { + const tempList = { + type: "list", + style: "list-hang-notitle", + items: [], + data: { + isSpellList: true + } + }; + + const lvls = Object.keys(entry.spells).map(lvl=>Number(lvl)).sort(SortUtil.ascSort); + + for (const lvl of lvls) { + const spells = entry.spells[lvl]; + if (spells) { + let levelCantrip = `${Parser.spLevelToFull(lvl)}${(lvl === 0 ? "s" : " level")}`; + let slotsAtWill = ` (at will)`; + const slots = spells.slots; + if (slots >= 0) + slotsAtWill = slots > 0 ? ` (${slots} slot${slots > 1 ? "s" : ""})` : ``; + if (spells.lower && spells.lower !== lvl) { + levelCantrip = `${Parser.spLevelToFull(spells.lower)}-${levelCantrip}`; + if (slots >= 0) + slotsAtWill = slots > 0 ? ` (${slots} ${Parser.spLevelToFull(lvl)}-level slot${slots > 1 ? "s" : ""})` : ``; + } + tempList.items.push({ + type: "itemSpell", + name: `${levelCantrip}${slotsAtWill}:`, + entry: this._renderSpellcasting_getRenderableList(spells.spells).join(", ") || "\u2014" + }); + } + } + + toRender[0].entries.push(tempList); + } + + if (entry.footerEntries) + toRender.push({ + type: "entries", + entries: entry.footerEntries + }); + return toRender; + } + ; + + this._renderSpellcasting_getEntries_procPerDuration = function({entry, hidden, tempList, prop, durationText, fnGetDurationText, isSkipPrefix}) { + if (!entry[prop] || hidden.has(prop)) + return; + + for (let lvl = 9; lvl > 0; lvl--) { + const perDur = entry[prop]; + if (perDur[lvl]) { + tempList.items.push({ + type: "itemSpell", + name: `${isSkipPrefix ? "" : lvl}${fnGetDurationText ? fnGetDurationText(lvl) : durationText}:`, + entry: this._renderSpellcasting_getRenderableList(perDur[lvl]).join(", "), + }); + } + + const lvlEach = `${lvl}e`; + if (perDur[lvlEach]) { + const isHideEach = !perDur[lvl] && perDur[lvlEach].length === 1; + tempList.items.push({ + type: "itemSpell", + name: `${isSkipPrefix ? "" : lvl}${fnGetDurationText ? fnGetDurationText(lvl) : durationText}${isHideEach ? "" : ` each`}:`, + entry: this._renderSpellcasting_getRenderableList(perDur[lvlEach]).join(", "), + }); + } + } + } + ; + + this._renderSpellcasting_getRenderableList = function(spellList) { + return spellList.filter(it=>!it.hidden).map(it=>it.entry || it); + } + ; + + this._renderSpellcasting = function(entry, textStack, meta, options) { + const toRender = this._renderSpellcasting_getEntries(entry); + if (!toRender?.[0].entries?.length) + return; + this._recursiveRender({ + type: "entries", + entries: toRender + }, textStack, meta); + } + ; + + this._renderQuote = function(entry, textStack, meta, options) { + textStack[0] += `
            `; + + const len = entry.entries.length; + for (let i = 0; i < len; ++i) { + textStack[0] += `

            ${i === 0 && !entry.skipMarks ? "“" : ""}`; + this._recursiveRender(entry.entries[i], textStack, meta, { + prefix: entry.skipItalics ? "" : "", + suffix: entry.skipItalics ? "" : "" + }); + textStack[0] += `${i === len - 1 && !entry.skipMarks ? "”" : ""}

            `; + } + + if (entry.by || entry.from) { + textStack[0] += `

            `; + const tempStack = [""]; + const byArr = this._renderQuote_getBy(entry); + if (byArr) { + for (let i = 0, len = byArr.length; i < len; ++i) { + const by = byArr[i]; + this._recursiveRender(by, tempStack, meta); + if (i < len - 1) + tempStack[0] += "
            "; + } + } + textStack[0] += `\u2014 ${byArr ? tempStack.join("") : ""}${byArr && entry.from ? `, ` : ""}${entry.from ? `${entry.from}` : ""}`; + textStack[0] += `

            `; + } + + textStack[0] += `
            `; + } + ; + + this._renderList_getQuoteCssClasses = function(entry, textStack, meta, options) { + const out = [`rd__quote`]; + if (entry.style) { + if (entry.style) + out.push(...entry.style.split(" ").map(it=>`rd__${it}`)); + } + return out.join(" "); + } + ; + + this._renderQuote_getBy = function(entry) { + if (!entry.by?.length) + return null; + return entry.by instanceof Array ? entry.by : [entry.by]; + } + ; + + this._renderOptfeature = function(entry, textStack, meta, options) { + this._renderEntriesSubtypes(entry, textStack, meta, options, true); + } + ; + + this._renderPatron = function(entry, textStack, meta, options) { + this._renderEntriesSubtypes(entry, textStack, meta, options, false); + } + ; + + this._renderAbilityDc = function(entry, textStack, meta, options) { + this._renderPrefix(entry, textStack, meta, options); + textStack[0] += `
            `; + this._recursiveRender(entry.name, textStack, meta); + textStack[0] += ` save DC = 8 + your proficiency bonus + your ${Parser.attrChooseToFull(entry.attributes)}
            `; + this._renderSuffix(entry, textStack, meta, options); + } + ; + + this._renderAbilityAttackMod = function(entry, textStack, meta, options) { + this._renderPrefix(entry, textStack, meta, options); + textStack[0] += `
            `; + this._recursiveRender(entry.name, textStack, meta); + textStack[0] += ` attack modifier = your proficiency bonus + your ${Parser.attrChooseToFull(entry.attributes)}
            `; + this._renderSuffix(entry, textStack, meta, options); + } + ; + + this._renderAbilityGeneric = function(entry, textStack, meta, options) { + this._renderPrefix(entry, textStack, meta, options); + textStack[0] += `
            `; + if (entry.name) + this._recursiveRender(entry.name, textStack, meta, { + prefix: "", + suffix: " = " + }); + textStack[0] += `${entry.text}${entry.attributes ? ` ${Parser.attrChooseToFull(entry.attributes)}` : ""}
            `; + this._renderSuffix(entry, textStack, meta, options); + } + ; + + this._renderInline = function(entry, textStack, meta, options) { + if (entry.entries) { + const len = entry.entries.length; + for (let i = 0; i < len; ++i) + this._recursiveRender(entry.entries[i], textStack, meta); + } + } + ; + + this._renderInlineBlock = function(entry, textStack, meta, options) { + this._renderPrefix(entry, textStack, meta, options); + if (entry.entries) { + const len = entry.entries.length; + for (let i = 0; i < len; ++i) + this._recursiveRender(entry.entries[i], textStack, meta); + } + this._renderSuffix(entry, textStack, meta, options); + } + ; + + this._renderBonus = function(entry, textStack, meta, options) { + textStack[0] += (entry.value < 0 ? "" : "+") + entry.value; + } + ; + + this._renderBonusSpeed = function(entry, textStack, meta, options) { + textStack[0] += entry.value === 0 ? "\u2014" : `${entry.value < 0 ? "" : "+"}${entry.value} ft.`; + } + ; + + this._renderDice = function(entry, textStack, meta, options) { + const pluginResults = this._getPlugins("dice").map(plugin=>plugin(entry, textStack, meta, options)).filter(Boolean); + + //TEMPFIX + if(!SETTINGS.DO_RENDER_DICE){textStack[0] += entry.displayText; return;} + textStack[0] += Renderer.getEntryDice(entry, entry.name, { + isAddHandlers: this._isAddHandlers, pluginResults + }); + }; + + this._renderActions = function(entry, textStack, meta, options) { + const dataString = this._renderEntriesSubtypes_getDataString(entry); + + if (entry.name != null && Renderer.ENTRIES_WITH_ENUMERATED_TITLES_LOOKUP[entry.type]) + this._handleTrackTitles(entry.name); + const cachedLastDepthTrackerProps = MiscUtil.copyFast(this._lastDepthTrackerInheritedProps); + this._handleTrackDepth(entry, 2); + + textStack[0] += `<${this.wrapperTag} class="${Renderer.HEAD_2}" ${dataString}>${entry.name}. `; + const len = entry.entries.length; + for (let i = 0; i < len; ++i) + this._recursiveRender(entry.entries[i], textStack, meta, { + prefix: "

            ", + suffix: "

            " + }); + textStack[0] += ``; + + this._lastDepthTrackerInheritedProps = cachedLastDepthTrackerProps; + } + ; + + this._renderAttack = function(entry, textStack, meta, options) { + this._renderPrefix(entry, textStack, meta, options); + textStack[0] += `${Parser.attackTypeToFull(entry.attackType)}: `; + const len = entry.attackEntries.length; + for (let i = 0; i < len; ++i) + this._recursiveRender(entry.attackEntries[i], textStack, meta); + textStack[0] += ` Hit: `; + const len2 = entry.hitEntries.length; + for (let i = 0; i < len2; ++i) + this._recursiveRender(entry.hitEntries[i], textStack, meta); + this._renderSuffix(entry, textStack, meta, options); + } + ; + + this._renderIngredient = function(entry, textStack, meta, options) { + this._renderPrefix(entry, textStack, meta, options); + this._recursiveRender(entry.entry, textStack, meta); + this._renderSuffix(entry, textStack, meta, options); + } + ; + + this._renderItem = function(entry, textStack, meta, options) { + this._renderPrefix(entry, textStack, meta, options); + textStack[0] += `

            ${this.render(entry.name)}${this._renderItem_isAddPeriod(entry) ? "." : ""} `; + if (entry.entry) + this._recursiveRender(entry.entry, textStack, meta); + else if (entry.entries) { + const len = entry.entries.length; + for (let i = 0; i < len; ++i) + this._recursiveRender(entry.entries[i], textStack, meta, { + prefix: i > 0 ? `` : "", + suffix: i > 0 ? "" : "" + }); + } + textStack[0] += "

            "; + this._renderSuffix(entry, textStack, meta, options); + } + ; + + this._renderItem_isAddPeriod = function(entry) { + return entry.name && entry.nameDot !== false && !Renderer._INLINE_HEADER_TERMINATORS.has(entry.name[entry.name.length - 1]); + } + ; + + this._renderItemSub = function(entry, textStack, meta, options) { + this._renderPrefix(entry, textStack, meta, options); + const isAddPeriod = entry.name && entry.nameDot !== false && !Renderer._INLINE_HEADER_TERMINATORS.has(entry.name[entry.name.length - 1]); + this._recursiveRender(entry.entry, textStack, meta, { + prefix: `

            ${entry.name}${isAddPeriod ? "." : ""} `, + suffix: "

            " + }); + this._renderSuffix(entry, textStack, meta, options); + } + ; + + this._renderItemSpell = function(entry, textStack, meta, options) { + this._renderPrefix(entry, textStack, meta, options); + + const tempStack = [""]; + this._recursiveRender(entry.name || "", tempStack, meta); + + this._recursiveRender(entry.entry, textStack, meta, { + prefix: `

            ${tempStack.join("")} `, + suffix: "

            " + }); + this._renderSuffix(entry, textStack, meta, options); + } + ; + + this._InlineStatblockStrategy = function({pFnPreProcess, }, ) { + this.pFnPreProcess = pFnPreProcess; + } + ; + + this._INLINE_STATBLOCK_STRATEGIES = { + "item": new this._InlineStatblockStrategy({ + pFnPreProcess: async(ent)=>{ + await Renderer.item.pPopulatePropertyAndTypeReference(); + Renderer.item.enhanceItem(ent); + return ent; + } + , + }), + }; + + this._renderStatblockInline = function(entry, textStack, meta, options) { + const fnGetRenderCompact = Renderer.hover.getFnRenderCompact(entry.dataType); + + const headerName = entry.displayName || entry.data?.name; + const headerStyle = entry.style; + + if (!fnGetRenderCompact) { + this._renderPrefix(entry, textStack, meta, options); + this._renderDataHeader(textStack, headerName, headerStyle); + textStack[0] += ` + + Cannot render "${entry.type}"—unknown data type "${entry.dataType}"! + + `; + this._renderDataFooter(textStack); + this._renderSuffix(entry, textStack, meta, options); + return; + } + + const strategy = this._INLINE_STATBLOCK_STRATEGIES[entry.dataType]; + + if (!strategy?.pFnPreProcess && !entry.data?._copy) { + this._renderPrefix(entry, textStack, meta, options); + this._renderDataHeader(textStack, headerName, headerStyle, { + isCollapsed: entry.collapsed + }); + textStack[0] += fnGetRenderCompact(entry.data, { + isEmbeddedEntity: true + }); + this._renderDataFooter(textStack); + this._renderSuffix(entry, textStack, meta, options); + return; + } + + this._renderPrefix(entry, textStack, meta, options); + this._renderDataHeader(textStack, headerName, headerStyle, { + isCollapsed: entry.collapsed + }); + + const id = CryptUtil.uid(); + Renderer._cache.inlineStatblock[id] = { + pFn: async(ele)=>{ + const entLoaded = entry.data?._copy ? (await DataUtil.pDoMetaMergeSingle(entry.dataType, { + dependencies: { + [entry.dataType]: entry.dependencies + } + }, entry.data, )) : entry.data; + + const ent = strategy?.pFnPreProcess ? await strategy.pFnPreProcess(entLoaded) : entLoaded; + + const tbl = ele.closest("table"); + const nxt = e_({ + outer: Renderer.utils.getEmbeddedDataHeader(headerName, headerStyle, { + isCollapsed: entry.collapsed + }) + fnGetRenderCompact(ent, { + isEmbeddedEntity: true + }) + Renderer.utils.getEmbeddedDataFooter(), + }); + tbl.parentNode.replaceChild(nxt, tbl, ); + } + , + }; + + textStack[0] += ``; + this._renderDataFooter(textStack); + this._renderSuffix(entry, textStack, meta, options); + } + ; + + this._renderDataHeader = function(textStack, name, style, {isCollapsed=false}={}) { + textStack[0] += Renderer.utils.getEmbeddedDataHeader(name, style, { + isCollapsed + }); + } + ; + + this._renderDataFooter = function(textStack) { + textStack[0] += Renderer.utils.getEmbeddedDataFooter(); + } + ; + + this._renderStatblock = function(entry, textStack, meta, options) { + this._renderPrefix(entry, textStack, meta, options); + + const page = entry.prop || Renderer.tag.getPage(entry.tag); + const source = Parser.getTagSource(entry.tag, entry.source); + const hash = entry.hash || (UrlUtil.URL_TO_HASH_BUILDER[page] ? UrlUtil.URL_TO_HASH_BUILDER[page]({ + ...entry, + name: entry.name, + source + }) : null); + + const asTag = `{@${entry.tag} ${entry.name}|${source}${entry.displayName ? `|${entry.displayName}` : ""}}`; + + if (!page || !source || !hash) { + this._renderDataHeader(textStack, entry.name, entry.style); + textStack[0] += ` + + Cannot load ${entry.tag ? `"${asTag}"` : entry.displayName || entry.name}! An unknown tag/prop, source, or hash was provided. + + `; + this._renderDataFooter(textStack); + this._renderSuffix(entry, textStack, meta, options); + + return; + } + + this._renderDataHeader(textStack, entry.displayName || entry.name, entry.style, { + isCollapsed: entry.collapsed + }); + textStack[0] += ` + + Loading ${entry.tag ? `${Renderer.get().render(asTag)}` : entry.displayName || entry.name}... + + + `; + this._renderDataFooter(textStack); + this._renderSuffix(entry, textStack, meta, options); + } + ; + + this._renderGallery = function(entry, textStack, meta, options) { + if (entry.name) + textStack[0] += ``; + textStack[0] += ``; + } + ; + + this._renderFlowchart = function(entry, textStack, meta, options) { + textStack[0] += `
            `; + const len = entry.blocks.length; + for (let i = 0; i < len; ++i) { + this._recursiveRender(entry.blocks[i], textStack, meta, options); + if (i !== len - 1) { + textStack[0] += `
            `; + } + } + textStack[0] += `
            `; + } + ; + + this._renderFlowBlock = function(entry, textStack, meta, options) { + const dataString = this._renderEntriesSubtypes_getDataString(entry); + textStack[0] += `<${this.wrapperTag} class="rd__b-special rd__b-flow ve-text-center" ${dataString}>`; + + const cachedLastDepthTrackerProps = MiscUtil.copyFast(this._lastDepthTrackerInheritedProps); + this._handleTrackDepth(entry, 1); + + if (entry.name != null) { + if (Renderer.ENTRIES_WITH_ENUMERATED_TITLES_LOOKUP[entry.type]) + this._handleTrackTitles(entry.name); + textStack[0] += `

            ${this.render({ + type: "inline", + entries: [entry.name] + })}

            `; + } + if (entry.entries) { + const len = entry.entries.length; + for (let i = 0; i < len; ++i) { + const cacheDepth = meta.depth; + meta.depth = 2; + this._recursiveRender(entry.entries[i], textStack, meta, { + prefix: "

            ", + suffix: "

            " + }); + meta.depth = cacheDepth; + } + } + textStack[0] += `
            `; + textStack[0] += ``; + + this._lastDepthTrackerInheritedProps = cachedLastDepthTrackerProps; + } + ; + + this._renderHomebrew = function(entry, textStack, meta, options) { + this._renderPrefix(entry, textStack, meta, options); + textStack[0] += `
            `; + + if (entry.oldEntries) { + const hoverMeta = Renderer.hover.getInlineHover({ + type: "entries", + name: "Homebrew", + entries: entry.oldEntries + }); + let markerText; + if (entry.movedTo) { + markerText = "(See moved content)"; + } else if (entry.entries) { + markerText = "(See replaced content)"; + } else { + markerText = "(See removed content)"; + } + textStack[0] += `${markerText}`; + } + + textStack[0] += `
            `; + + if (entry.entries) { + const len = entry.entries.length; + for (let i = 0; i < len; ++i) + this._recursiveRender(entry.entries[i], textStack, meta, { + prefix: "

            ", + suffix: "

            " + }); + } else if (entry.movedTo) { + textStack[0] += `This content has been moved to ${entry.movedTo}.`; + } else { + textStack[0] += "This content has been deleted."; + } + + textStack[0] += `
            `; + this._renderSuffix(entry, textStack, meta, options); + } + ; + + this._renderCode = function(entry, textStack, meta, options) { + const isWrapped = !!StorageUtil.syncGet("rendererCodeWrap"); + textStack[0] += ` +
            +
            + + +
            +
            ${entry.preformatted}
            +
            + `; + } + ; + + this._renderHr = function(entry, textStack, meta, options) { + textStack[0] += `
            `; + } + ; + + this._getStyleClass = function(entryType, entry) { + const outList = []; + + const pluginResults = this._getPlugins(`${entryType}_styleClass_fromSource`).map(plugin=>plugin(entryType, entry)).filter(Boolean); + + if (!pluginResults.some(it=>it.isSkip)) { + if (SourceUtil.isNonstandardSource(entry.source) || (typeof PrereleaseUtil !== "undefined" && PrereleaseUtil.hasSourceJson(entry.source))) + outList.push("spicy-sauce"); + if (typeof BrewUtil2 !== "undefined" && BrewUtil2.hasSourceJson(entry.source)) + outList.push("refreshing-brew"); + } + + if (this._extraSourceClasses) + outList.push(...this._extraSourceClasses); + for (const k in this._fnsGetStyleClasses) { + const fromFn = this._fnsGetStyleClasses[k](entry); + if (fromFn) + outList.push(...fromFn); + } + if (entry.style) + outList.push(this._getMutatedStyleString(entry.style)); + return outList.join(" "); + } + ; + + this._renderString = function(entry, textStack, meta, options) { + const tagSplit = Renderer.splitByTags(entry); + const len = tagSplit.length; + for (let i = 0; i < len; ++i) { + const s = tagSplit[i]; + if (!s) + continue; + if (s.startsWith("{@")) { + const [tag,text] = Renderer.splitFirstSpace(s.slice(1, -1)); + this._renderString_renderTag(textStack, meta, options, tag, text); + } else + textStack[0] += s; + } + } + ; + + this._renderString_renderTag = function(textStack, meta, options, tag, text) { + for (const plugin of this._getPlugins("string_tag")) { + const out = plugin(tag, text, textStack, meta, options); + if (out) + return void (textStack[0] += out); + } + + for (const plugin of this._getPlugins(`string_${tag}`)) { + const out = plugin(tag, text, textStack, meta, options); + if (out) + return void (textStack[0] += out); + } + + switch (tag) { + case "@b": + case "@bold": + textStack[0] += ``; + this._recursiveRender(text, textStack, meta); + textStack[0] += ``; + break; + case "@i": + case "@italic": + textStack[0] += ``; + this._recursiveRender(text, textStack, meta); + textStack[0] += ``; + break; + case "@s": + case "@strike": + textStack[0] += ``; + this._recursiveRender(text, textStack, meta); + textStack[0] += ``; + break; + case "@u": + case "@underline": + textStack[0] += ``; + this._recursiveRender(text, textStack, meta); + textStack[0] += ``; + break; + case "@sup": + textStack[0] += ``; + this._recursiveRender(text, textStack, meta); + textStack[0] += ``; + break; + case "@sub": + textStack[0] += ``; + this._recursiveRender(text, textStack, meta); + textStack[0] += ``; + break; + case "@kbd": + textStack[0] += ``; + this._recursiveRender(text, textStack, meta); + textStack[0] += ``; + break; + case "@code": + textStack[0] += ``; + this._recursiveRender(text, textStack, meta); + textStack[0] += ``; + break; + case "@style": + { + const [displayText,styles] = Renderer.splitTagByPipe(text); + const classNames = (styles || "").split(";").map(it=>Renderer._STYLE_TAG_ID_TO_STYLE[it.trim()]).filter(Boolean).join(" "); + textStack[0] += ``; + this._recursiveRender(displayText, textStack, meta); + textStack[0] += ``; + break; + } + case "@font": + { + const [displayText,fontFamily] = Renderer.splitTagByPipe(text); + textStack[0] += ``; + this._recursiveRender(displayText, textStack, meta); + textStack[0] += ``; + break; + } + case "@note": + textStack[0] += ``; + this._recursiveRender(text, textStack, meta); + textStack[0] += ``; + break; + case "@tip": + { + const [displayText,titielText] = Renderer.splitTagByPipe(text); + textStack[0] += ``; + this._recursiveRender(displayText, textStack, meta); + textStack[0] += ``; + break; + } + case "@atk": + textStack[0] += `${Renderer.attackTagToFull(text)}`; + break; + case "@h": + textStack[0] += `Hit: `; + break; + case "@m": + textStack[0] += `Miss: `; + break; + case "@color": + { + const [toDisplay,color] = Renderer.splitTagByPipe(text); + const ptColor = this._renderString_renderTag_getBrewColorPart(color); + + textStack[0] += ``; + this._recursiveRender(toDisplay, textStack, meta); + textStack[0] += ``; + break; + } + case "@highlight": + { + const [toDisplay,color] = Renderer.splitTagByPipe(text); + const ptColor = this._renderString_renderTag_getBrewColorPart(color); + + textStack[0] += ptColor ? `` : ``; + textStack[0] += toDisplay; + textStack[0] += ``; + break; + } + case "@help": + { + const [toDisplay,title=""] = Renderer.splitTagByPipe(text); + textStack[0] += ``; + this._recursiveRender(toDisplay, textStack, meta); + textStack[0] += ``; + break; + } + + case "@unit": + { + const [amount,unitSingle,unitPlural] = Renderer.splitTagByPipe(text); + textStack[0] += isNaN(amount) ? unitSingle : Number(amount) > 1 ? (unitPlural || unitSingle.toPlural()) : unitSingle; + break; + } + + case "@comic": + textStack[0] += ``; + this._recursiveRender(text, textStack, meta); + textStack[0] += ``; + break; + case "@comicH1": + textStack[0] += ``; + this._recursiveRender(text, textStack, meta); + textStack[0] += ``; + break; + case "@comicH2": + textStack[0] += ``; + this._recursiveRender(text, textStack, meta); + textStack[0] += ``; + break; + case "@comicH3": + textStack[0] += ``; + this._recursiveRender(text, textStack, meta); + textStack[0] += ``; + break; + case "@comicH4": + textStack[0] += ``; + this._recursiveRender(text, textStack, meta); + textStack[0] += ``; + break; + case "@comicNote": + textStack[0] += ``; + this._recursiveRender(text, textStack, meta); + textStack[0] += ``; + break; + + case "@dc": + { + const [dcText,displayText] = Renderer.splitTagByPipe(text); + textStack[0] += `DC ${displayText || dcText}`; + break; + } + + case "@dcYourSpellSave": + { + const [displayText] = Renderer.splitTagByPipe(text); + textStack[0] += displayText || "your spell save DC"; + break; + } + + case "@dice": + case "@autodice": + case "@damage": + case "@hit": + case "@d20": + case "@chance": + case "@coinflip": + case "@recharge": + case "@ability": + case "@savingThrow": + case "@skillCheck": + { + const fauxEntry = Renderer.utils.getTagEntry(tag, text); + + if (tag === "@recharge") { + const [,flagsRaw] = Renderer.splitTagByPipe(text); + const flags = flagsRaw ? flagsRaw.split("") : null; + textStack[0] += `${flags && flags.includes("m") ? "" : "("}Recharge `; + this._recursiveRender(fauxEntry, textStack, meta); + textStack[0] += `${flags && flags.includes("m") ? "" : ")"}`; + } else { + this._recursiveRender(fauxEntry, textStack, meta); + } + + break; + } + + case "@hitYourSpellAttack": + this._renderString_renderTag_hitYourSpellAttack(textStack, meta, options, tag, text); + break; + + case "@scaledice": + case "@scaledamage": + { + const fauxEntry = Renderer.parseScaleDice(tag, text); + this._recursiveRender(fauxEntry, textStack, meta); + break; + } + + case "@filter": + { + const [displayText,page,...filters] = Renderer.splitTagByPipe(text); + + const filterSubhashMeta = Renderer.getFilterSubhashes(filters); + + const fauxEntry = { + type: "link", + text: displayText, + href: { + type: "internal", + path: `${page}.html`, + hash: HASH_BLANK, + hashPreEncoded: true, + subhashes: filterSubhashMeta.subhashes, + }, + }; + + if (filterSubhashMeta.customHash) + fauxEntry.href.hash = filterSubhashMeta.customHash; + + this._recursiveRender(fauxEntry, textStack, meta); + + break; + } + case "@link": + { + const [displayText,url] = Renderer.splitTagByPipe(text); + let outUrl = url == null ? displayText : url; + if (!outUrl.startsWith("http")) + outUrl = `http://${outUrl}`; + const fauxEntry = { + type: "link", + href: { + type: "external", + url: outUrl, + }, + text: displayText, + }; + this._recursiveRender(fauxEntry, textStack, meta); + + break; + } + case "@5etools": + { + const [displayText,page,hash] = Renderer.splitTagByPipe(text); + const fauxEntry = { + type: "link", + href: { + type: "internal", + path: page, + }, + text: displayText, + }; + if (hash) { + fauxEntry.hash = hash; + fauxEntry.hashPreEncoded = true; + } + this._recursiveRender(fauxEntry, textStack, meta); + + break; + } + + case "@footnote": + { + const [displayText,footnoteText,optTitle] = Renderer.splitTagByPipe(text); + const hoverMeta = Renderer.hover.getInlineHover({ + type: "entries", + name: optTitle ? optTitle.toTitleCase() : "Footnote", + entries: [footnoteText, optTitle ? `{@note ${optTitle}}` : ""].filter(Boolean), + }); + textStack[0] += ``; + this._recursiveRender(displayText, textStack, meta); + textStack[0] += ``; + + break; + } + case "@homebrew": + { + const [newText,oldText] = Renderer.splitTagByPipe(text); + const tooltipEntries = []; + if (newText && oldText) { + tooltipEntries.push("{@b This is a homebrew addition, replacing the following:}"); + } else if (newText) { + tooltipEntries.push("{@b This is a homebrew addition.}"); + } else if (oldText) { + tooltipEntries.push("{@b The following text has been removed with this homebrew:}"); + } + if (oldText) { + tooltipEntries.push(oldText); + } + const hoverMeta = Renderer.hover.getInlineHover({ + type: "entries", + name: "Homebrew Modifications", + entries: tooltipEntries, + }); + textStack[0] += ``; + this._recursiveRender(newText || "[...]", textStack, meta); + textStack[0] += ``; + + break; + } + case "@area": + { + const {areaId, displayText} = Renderer.tag.TAG_LOOKUP.area.getMeta(tag, text); + + if (typeof BookUtil === "undefined") { + textStack[0] += displayText; + } else { + const area = BookUtil.curRender.headerMap[areaId] || { + entry: { + name: "" + } + }; + const hoverMeta = Renderer.hover.getInlineHover(area.entry, { + isLargeBookContent: true, + depth: area.depth + }); + textStack[0] += `${displayText}`; + } + + break; + } + + case "@loader": + { + const {name, path, mode} = this._renderString_getLoaderTagMeta(text); + + const brewUtilName = mode === "homebrew" ? "BrewUtil2" : mode === "prerelease" ? "PrereleaseUtil" : null; + const brewUtil = globalThis[brewUtilName]; + + if (!brewUtil) { + textStack[0] += `${name}`; + + break; + } + + textStack[0] += `${name}`; + break; + } + + case "@book": + case "@adventure": + { + const page = tag === "@book" ? "book.html" : "adventure.html"; + const [displayText,book,chapter,section,rawNumber] = Renderer.splitTagByPipe(text); + const number = rawNumber || 0; + const hash = `${book}${chapter ? `${HASH_PART_SEP}${chapter}${section ? `${HASH_PART_SEP}${UrlUtil.encodeForHash(section)}${number != null ? `${HASH_PART_SEP}${UrlUtil.encodeForHash(number)}` : ""}` : ""}` : ""}`; + const fauxEntry = { + type: "link", + href: { + type: "internal", + path: page, + hash, + hashPreEncoded: true, + }, + text: displayText, + }; + this._recursiveRender(fauxEntry, textStack, meta); + + break; + } + + default: + { + const {name, source, displayText, others, page, hash, hashPreEncoded, pageHover, hashHover, hashPreEncodedHover, preloadId, linkText, subhashes, subhashesHover, isFauxPage} = Renderer.utils.getTagMeta(tag, text); + + const fauxEntry = { + type: "link", + href: { + type: "internal", + path: page, + hash, + hover: { + page, + isFauxPage, + source, + }, + }, + text: (displayText || name), + }; + + if (hashPreEncoded != null) + fauxEntry.href.hashPreEncoded = hashPreEncoded; + if (pageHover != null) + fauxEntry.href.hover.page = pageHover; + if (hashHover != null) + fauxEntry.href.hover.hash = hashHover; + if (hashPreEncodedHover != null) + fauxEntry.href.hover.hashPreEncoded = hashPreEncodedHover; + if (preloadId != null) + fauxEntry.href.hover.preloadId = preloadId; + if (linkText) + fauxEntry.text = linkText; + if (subhashes) + fauxEntry.href.subhashes = subhashes; + if (subhashesHover) + fauxEntry.href.hover.subhashes = subhashesHover; + + this._recursiveRender(fauxEntry, textStack, meta); + + break; + } + } + } + ; + + this._renderString_renderTag_getBrewColorPart = function(color) { + if (!color) + return ""; + const scrubbedColor = BrewUtilShared.getValidColor(color, { + isExtended: true + }); + return scrubbedColor.startsWith("--") ? `var(${scrubbedColor})` : `#${scrubbedColor}`; + } + ; + + this._renderString_renderTag_hitYourSpellAttack = function(textStack, meta, options, tag, text) { + const [displayText] = Renderer.splitTagByPipe(text); + + const fauxEntry = { + type: "dice", + rollable: true, + subType: "d20", + displayText: displayText || "your spell attack modifier", + toRoll: `1d20 + #$prompt_number:title=Enter your Spell Attack Modifier$#`, + }; + return this._recursiveRender(fauxEntry, textStack, meta); + } + ; + + this._renderString_getLoaderTagMeta = function(text, {isDefaultUrl=false}={}) { + const [name,file,mode="homebrew"] = Renderer.splitTagByPipe(text); + + if (!isDefaultUrl) + return { + name, + path: file, + mode + }; + + const path = /^.*?:\/\//.test(file) ? file : `${VeCt.URL_ROOT_BREW}${file}`; + return { + name, + path, + mode + }; + } + ; + + this._renderPrimitive = function(entry, textStack, meta, options) { + textStack[0] += entry; + } + ; + + this._renderLink = function(entry, textStack, meta, options) { + let href = this._renderLink_getHref(entry); + + if (entry.href.hover && this._roll20Ids) { + const procHash = UrlUtil.encodeForHash(entry.href.hash); + const id = this._roll20Ids[procHash]; + if (id) { + href = `http://journal.roll20.net/${id.type}/${id.roll20Id}`; + } + } + + const pluginData = this._getPlugins("link").map(plugin=>plugin(entry, textStack, meta, options)).filter(Boolean); + const isDisableEvents = pluginData.some(it=>it.isDisableEvents); + const additionalAttributes = pluginData.map(it=>it.attributes).filter(Boolean); + + if (this._isInternalLinksDisabled && entry.href.type === "internal") { + textStack[0] += `${this.render(entry.text)}`; + } else if (entry.href.hover?.isFauxPage) { + textStack[0] += `${this.render(entry.text)}`; + } else { + textStack[0] += `${this.render(entry.text)}`; + } + } + ; + + this._renderLink_getHref = function(entry) { + let href; + if (entry.href.type === "internal") { + href = `${this.baseUrl}${entry.href.path}#`; + if (entry.href.hash != null) { + href += entry.href.hashPreEncoded ? entry.href.hash : UrlUtil.encodeForHash(entry.href.hash); + } + if (entry.href.subhashes != null) { + href += Renderer.utils.getLinkSubhashString(entry.href.subhashes); + } + } else if (entry.href.type === "external") { + href = entry.href.url; + } + return href; + } + ; + + this._renderLink_getHoverString = function(entry) { + if (!entry.href.hover || !this._isAddHandlers) + return ""; + + let procHash = entry.href.hover.hash ? entry.href.hover.hashPreEncoded ? entry.href.hover.hash : UrlUtil.encodeForHash(entry.href.hover.hash) : entry.href.hashPreEncoded ? entry.href.hash : UrlUtil.encodeForHash(entry.href.hash); + + if (this._tagExportDict) { + this._tagExportDict[procHash] = { + page: entry.href.hover.page, + source: entry.href.hover.source, + hash: procHash, + }; + } + + if (entry.href.hover.subhashes) { + procHash += Renderer.utils.getLinkSubhashString(entry.href.hover.subhashes); + } + + const pluginData = this._getPlugins("link_attributesHover").map(plugin=>plugin(entry, procHash)).filter(Boolean); + const replacementAttributes = pluginData.map(it=>it.attributesHoverReplace).filter(Boolean); + if (replacementAttributes.length) + return replacementAttributes.join(" "); + + return `onmouseover="Renderer.hover.pHandleLinkMouseOver(event, this)" onmouseleave="Renderer.hover.handleLinkMouseLeave(event, this)" onmousemove="Renderer.hover.handleLinkMouseMove(event, this)" data-vet-page="${entry.href.hover.page.qq()}" data-vet-source="${entry.href.hover.source.qq()}" data-vet-hash="${procHash.qq()}" ${entry.href.hover.preloadId != null ? `data-vet-preload-id="${`${entry.href.hover.preloadId}`.qq()}"` : ""} ${entry.href.hover.isFauxPage ? `data-vet-is-faux-page="true"` : ""} ${Renderer.hover.getPreventTouchString()}`; + } + ; + + this.render = function(entry, depth=0) { + const tempStack = []; + this.recursiveRender(entry, tempStack, { + depth + }); + return tempStack.join(""); + } + ; +}; + +Renderer.ENTRIES_WITH_ENUMERATED_TITLES = [{ + type: "section", + key: "entries", + depth: -1 +}, { + type: "entries", + key: "entries", + depthIncrement: 1 +}, { + type: "options", + key: "entries" +}, { + type: "inset", + key: "entries", + depth: 2 +}, { + type: "insetReadaloud", + key: "entries", + depth: 2 +}, { + type: "variant", + key: "entries", + depth: 2 +}, { + type: "variantInner", + key: "entries", + depth: 2 +}, { + type: "actions", + key: "entries", + depth: 2 +}, { + type: "flowBlock", + key: "entries", + depth: 2 +}, { + type: "optfeature", + key: "entries", + depthIncrement: 1 +}, { + type: "patron", + key: "entries" +}, ]; + +Renderer.ENTRIES_WITH_ENUMERATED_TITLES_LOOKUP = Renderer.ENTRIES_WITH_ENUMERATED_TITLES.mergeMap(it=>({ + [it.type]: it +})); + +Renderer.ENTRIES_WITH_CHILDREN = [...Renderer.ENTRIES_WITH_ENUMERATED_TITLES, { + type: "list", + key: "items" +}, { + type: "table", + key: "rows" +}, ]; + +Renderer._INLINE_HEADER_TERMINATORS = new Set([".", ",", "!", "?", ";", ":", `"`]); + +Renderer._STYLE_TAG_ID_TO_STYLE = { + "small-caps": "small-caps", + "small": "ve-small", + "capitalize": "capitalize", + "dnd-font": "dnd-font", +}; + +Renderer.get = ()=>{ + if (!Renderer.defaultRenderer) + Renderer.defaultRenderer = new Renderer(); + return Renderer.defaultRenderer; +} +; + +Renderer.applyProperties = function(entry, object) { + const propSplit = Renderer.splitByPropertyInjectors(entry); + const len = propSplit.length; + if (len === 1) + return entry; + + let textStack = ""; + + for (let i = 0; i < len; ++i) { + const s = propSplit[i]; + if (!s) + continue; + + if (!s.startsWith("{=")) { + textStack += s; + continue; + } + + if (s.startsWith("{=")) { + const [path,modifiers] = s.slice(2, -1).split("/"); + let fromProp = object[path]; + + if (!modifiers) { + textStack += fromProp; + continue; + } + + if (fromProp == null) + throw new Error(`Could not apply property in "${s}"; "${path}" value was null!`); + + modifiers.split("").sort((a,b)=>Renderer.applyProperties._OP_ORDER.indexOf(a) - Renderer.applyProperties._OP_ORDER.indexOf(b)); + + for (const modifier of modifiers) { + switch (modifier) { + case "a": + fromProp = Renderer.applyProperties._LEADING_AN.has(fromProp[0].toLowerCase()) ? "an" : "a"; + break; + + case "l": + fromProp = fromProp.toLowerCase(); + break; + case "t": + fromProp = fromProp.toTitleCase(); + break; + case "u": + fromProp = fromProp.toUpperCase(); + break; + case "v": + fromProp = Parser.numberToVulgar(fromProp); + break; + case "x": + fromProp = Parser.numberToText(fromProp); + break; + case "r": + fromProp = Math.round(fromProp); + break; + case "f": + fromProp = Math.floor(fromProp); + break; + case "c": + fromProp = Math.ceil(fromProp); + break; + default: + throw new Error(`Unhandled property modifier "${modifier}"`); + } + } + + textStack += fromProp; + } + } + + return textStack; +} +; +Renderer.applyProperties._LEADING_AN = new Set(["a", "e", "i", "o", "u"]); +Renderer.applyProperties._OP_ORDER = ["r", "f", "c", "v", "x", "l", "t", "u", "a", ]; + +Renderer.applyAllProperties = function(entries, object=null) { + let lastObj = null; + const handlers = { + object: (obj)=>{ + lastObj = obj; + return obj; + } + , + string: (str)=>Renderer.applyProperties(str, object || lastObj), + }; + return MiscUtil.getWalker().walk(entries, handlers); +} +; + +Renderer.attackTagToFull = function(tagStr) { + function renderTag(tags) { + return `${tags.includes("m") ? "Melee " : tags.includes("r") ? "Ranged " : tags.includes("g") ? "Magical " : tags.includes("a") ? "Area " : ""}${tags.includes("w") ? "Weapon " : tags.includes("s") ? "Spell " : tags.includes("p") ? "Power " : ""}`; + } + + const tagGroups = tagStr.toLowerCase().split(",").map(it=>it.trim()).filter(it=>it).map(it=>it.split("")); + if (tagGroups.length > 1) { + const seen = new Set(tagGroups.last()); + for (let i = tagGroups.length - 2; i >= 0; --i) { + tagGroups[i] = tagGroups[i].filter(it=>{ + const out = !seen.has(it); + seen.add(it); + return out; + } + ); + } + } + return `${tagGroups.map(it=>renderTag(it)).join(" or ")}Attack:`; +} +; + +Renderer.splitFirstSpace = function(string) { + const firstIndex = string.indexOf(" "); + return firstIndex === -1 ? [string, ""] : [string.substr(0, firstIndex), string.substr(firstIndex + 1)]; +} +; + +Renderer._splitByTagsBase = function(leadingCharacter) { + return function(string) { + let tagDepth = 0; + let char, char2; + const out = []; + let curStr = ""; + let isLastOpen = false; + + const len = string.length; + for (let i = 0; i < len; ++i) { + char = string[i]; + char2 = string[i + 1]; + + switch (char) { + case "{": + isLastOpen = true; + if (char2 === leadingCharacter) { + if (tagDepth++ > 0) { + curStr += "{"; + } else { + out.push(curStr.replace(//g, leadingCharacter)); + curStr = `{${leadingCharacter}`; + ++i; + } + } else + curStr += "{"; + break; + + case "}": + isLastOpen = false; + curStr += "}"; + if (tagDepth !== 0 && --tagDepth === 0) { + out.push(curStr.replace(//g, leadingCharacter)); + curStr = ""; + } + break; + + case leadingCharacter: + { + if (!isLastOpen) + curStr += ""; + else + curStr += leadingCharacter; + break; + } + + default: + isLastOpen = false; + curStr += char; + break; + } + } + + if (curStr) + out.push(curStr.replace(//g, leadingCharacter)); + + return out; + } + ; +} +; + +Renderer.splitByTags = Renderer._splitByTagsBase("@"); +Renderer.splitByPropertyInjectors = Renderer._splitByTagsBase("="); + +Renderer._splitByPipeBase = function(leadingCharacter) { + return function(string) { + let tagDepth = 0; + let char, char2; + const out = []; + let curStr = ""; + + const len = string.length; + for (let i = 0; i < len; ++i) { + char = string[i]; + char2 = string[i + 1]; + + switch (char) { + case "{": + if (char2 === leadingCharacter) + tagDepth++; + curStr += "{"; + + break; + + case "}": + if (tagDepth) + tagDepth--; + curStr += "}"; + + break; + + case "|": + { + if (tagDepth) + curStr += "|"; + else { + out.push(curStr); + curStr = ""; + } + break; + } + + default: + { + curStr += char; + break; + } + } + } + + if (curStr) + out.push(curStr); + return out; + } + ; +} +; + +Renderer.splitTagByPipe = Renderer._splitByPipeBase("@"); + +Renderer.getEntryDice = function(entry, name, opts={}) { + const toDisplay = Renderer.getEntryDiceDisplayText(entry); + + if (entry.rollable === true) + return Renderer.getRollableEntryDice(entry, name, toDisplay, opts); + else + return toDisplay; +} +; + +Renderer.getRollableEntryDice = function(entry, name, toDisplay, {isAddHandlers=true, pluginResults=null, }={}, ) { + const toPack = MiscUtil.copyFast(entry); + if (typeof toPack.toRoll !== "string") { + toPack.toRoll = Renderer.legacyDiceToString(toPack.toRoll); + } + + const handlerPart = isAddHandlers ? `onmousedown="event.preventDefault()" data-packed-dice='${JSON.stringify(toPack).qq()}'` : ""; + + const rollableTitlePart = isAddHandlers ? Renderer.getEntryDiceTitle(toPack.subType) : null; + const titlePart = isAddHandlers ? `title="${[name, rollableTitlePart].filter(Boolean).join(". ").qq()}" ${name ? `data-roll-name="${name}"` : ""}` : name ? `title="${name.qq()}" data-roll-name="${name.qq()}"` : ""; + + const additionalDataPart = (pluginResults || []).filter(it=>it.additionalData).map(it=>{ + return Object.entries(it.additionalData).map(([dataKey,val])=>`${dataKey}='${typeof val === "object" ? JSON.stringify(val).qq() : `${val}`.qq()}'`).join(" "); + } + ).join(" "); + + toDisplay = (pluginResults || []).filter(it=>it.toDisplay)[0]?.toDisplay ?? toDisplay; + + const ptRoll = Renderer.getRollableEntryDice._getPtRoll(toPack); + + return `${toDisplay}${ptRoll}`; +} +; + +Renderer.getRollableEntryDice._getPtRoll = (toPack)=>{ + if (!toPack.autoRoll) + return ""; + + const r = Renderer.dice.parseRandomise2(toPack.toRoll); + return ` (${r})`; +} +; + +Renderer.getEntryDiceTitle = function(subType) { + return `Click to roll. ${subType === "damage" ? "SHIFT to roll a critical hit, CTRL to half damage (rounding down)." : subType === "d20" ? "SHIFT to roll with advantage, CTRL to roll with disadvantage." : "SHIFT/CTRL to roll twice."}`; +} +; + +Renderer.legacyDiceToString = function(array) { + let stack = ""; + array.forEach(r=>{ + stack += `${r.neg ? "-" : stack === "" ? "" : "+"}${r.number || 1}d${r.faces}${r.mod ? r.mod > 0 ? `+${r.mod}` : r.mod : ""}`; + } + ); + return stack; +} +; + +Renderer.getEntryDiceDisplayText = function(entry) { + if (entry.displayText) + return entry.displayText; + return Renderer._getEntryDiceDisplayText_getDiceAsStr(entry); +} +; + +Renderer._getEntryDiceDisplayText_getDiceAsStr = function(entry) { + if (entry.successThresh != null) + return `${entry.successThresh} percent`; + if (typeof entry.toRoll === "string") + return entry.toRoll; + return Renderer.legacyDiceToString(entry.toRoll); +} +; + +Renderer.parseScaleDice = function(tag, text) { + const [baseRoll,progression,addPerProgress,renderMode,displayText] = Renderer.splitTagByPipe(text); + const progressionParse = MiscUtil.parseNumberRange(progression, 1, 9); + const baseLevel = Math.min(...progressionParse); + const options = {}; + const isMultableDice = /^(\d+)d(\d+)$/i.exec(addPerProgress); + + const getSpacing = ()=>{ + let diff = null; + const sorted = [...progressionParse].sort(SortUtil.ascSort); + for (let i = 1; i < sorted.length; ++i) { + const prev = sorted[i - 1]; + const curr = sorted[i]; + if (diff == null) + diff = curr - prev; + else if (curr - prev !== diff) + return null; + } + return diff; + } + ; + + const spacing = getSpacing(); + progressionParse.forEach(k=>{ + const offset = k - baseLevel; + if (isMultableDice && spacing != null) { + options[k] = offset ? `${Number(isMultableDice[1]) * (offset / spacing)}d${isMultableDice[2]}` : ""; + } else { + options[k] = offset ? [...new Array(Math.floor(offset / spacing))].map(_=>addPerProgress).join("+") : ""; + } + } + ); + + const out = { + type: "dice", + rollable: true, + toRoll: baseRoll, + displayText: displayText || addPerProgress, + prompt: { + entry: renderMode === "psi" ? "Spend Psi Points..." : "Cast at...", + mode: renderMode, + options, + }, + }; + if (tag === "@scaledamage") + out.subType = "damage"; + + return out; +} +; + +Renderer.getAbilityData = function(abArr, {isOnlyShort, isCurrentLineage}={}) { + if (isOnlyShort && isCurrentLineage) + return new Renderer._AbilityData({ + asTextShort: "Lineage (choose)" + }); + + const outerStack = (abArr || [null]).map(it=>Renderer.getAbilityData._doRenderOuter(it)); + if (outerStack.length <= 1) + return outerStack[0]; + return new Renderer._AbilityData({ + asText: `Choose one of: ${outerStack.map((it,i)=>`(${Parser.ALPHABET[i].toLowerCase()}) ${it.asText}`).join(" ")}`, + asTextShort: `${outerStack.map((it,i)=>`(${Parser.ALPHABET[i].toLowerCase()}) ${it.asTextShort}`).join(" ")}`, + asCollection: [...new Set(outerStack.map(it=>it.asCollection).flat())], + areNegative: [...new Set(outerStack.map(it=>it.areNegative).flat())], + }); +} +; + +Renderer.getAbilityData._doRenderOuter = function(abObj) { + const mainAbs = []; + const asCollection = []; + const areNegative = []; + const toConvertToText = []; + const toConvertToShortText = []; + + if (abObj != null) { + handleAllAbilities(abObj); + handleAbilitiesChoose(); + return new Renderer._AbilityData({ + asText: toConvertToText.join("; "), + asTextShort: toConvertToShortText.join("; "), + asCollection: asCollection, + areNegative: areNegative, + }); + } + + return new Renderer._AbilityData(); + + function handleAllAbilities(abObj, targetList) { + MiscUtil.copyFast(Parser.ABIL_ABVS).sort((a,b)=>SortUtil.ascSort(abObj[b] || 0, abObj[a] || 0)).forEach(shortLabel=>handleAbility(abObj, shortLabel, targetList)); + } + + function handleAbility(abObj, shortLabel, optToConvertToTextStorage) { + if (abObj[shortLabel] != null) { + const isNegMod = abObj[shortLabel] < 0; + const toAdd = `${shortLabel.uppercaseFirst()} ${(isNegMod ? "" : "+")}${abObj[shortLabel]}`; + + if (optToConvertToTextStorage) { + optToConvertToTextStorage.push(toAdd); + } else { + toConvertToText.push(toAdd); + toConvertToShortText.push(toAdd); + } + + mainAbs.push(shortLabel.uppercaseFirst()); + asCollection.push(shortLabel); + if (isNegMod) + areNegative.push(shortLabel); + } + } + + function handleAbilitiesChoose() { + if (abObj.choose != null) { + const ch = abObj.choose; + let outStack = ""; + if (ch.weighted) { + const w = ch.weighted; + const froms = w.from.map(it=>it.uppercaseFirst()); + const isAny = froms.length === 6; + const isAllEqual = w.weights.unique().length === 1; + let cntProcessed = 0; + + const weightsIncrease = w.weights.filter(it=>it >= 0).sort(SortUtil.ascSort).reverse(); + const weightsReduce = w.weights.filter(it=>it < 0).map(it=>-it).sort(SortUtil.ascSort); + + const areIncreaseShort = []; + const areIncrease = isAny && isAllEqual && w.weights.length > 1 && w.weights[0] >= 0 ? (()=>{ + weightsIncrease.forEach(it=>areIncreaseShort.push(`+${it}`)); + return [`${cntProcessed ? "choose " : ""}${Parser.numberToText(w.weights.length)} different +${weightsIncrease[0]}`]; + } + )() : weightsIncrease.map(it=>{ + areIncreaseShort.push(`+${it}`); + if (isAny) + return `${cntProcessed ? "choose " : ""}any ${cntProcessed++ ? `other ` : ""}+${it}`; + return `one ${cntProcessed++ ? `other ` : ""}ability to increase by ${it}`; + } + ); + + const areReduceShort = []; + const areReduce = isAny && isAllEqual && w.weights.length > 1 && w.weights[0] < 0 ? (()=>{ + weightsReduce.forEach(it=>areReduceShort.push(`-${it}`)); + return [`${cntProcessed ? "choose " : ""}${Parser.numberToText(w.weights.length)} different -${weightsReduce[0]}`]; + } + )() : weightsReduce.map(it=>{ + areReduceShort.push(`-${it}`); + if (isAny) + return `${cntProcessed ? "choose " : ""}any ${cntProcessed++ ? `other ` : ""}-${it}`; + return `one ${cntProcessed++ ? `other ` : ""}ability to decrease by ${it}`; + } + ); + + const startText = isAny ? `Choose ` : `From ${froms.joinConjunct(", ", " and ")} choose `; + + const ptAreaIncrease = isAny ? areIncrease.concat(areReduce).join("; ") : areIncrease.concat(areReduce).joinConjunct(", ", isAny ? "; " : " and "); + toConvertToText.push(`${startText}${ptAreaIncrease}`); + toConvertToShortText.push(`${isAny ? "Any combination " : ""}${areIncreaseShort.concat(areReduceShort).join("/")}${isAny ? "" : ` from ${froms.join("/")}`}`); + } else { + const allAbilities = ch.from.length === 6; + const allAbilitiesWithParent = isAllAbilitiesWithParent(ch); + let amount = ch.amount === undefined ? 1 : ch.amount; + amount = (amount < 0 ? "" : "+") + amount; + if (allAbilities) { + outStack += "any "; + } else if (allAbilitiesWithParent) { + outStack += "any other "; + } + if (ch.count != null && ch.count > 1) { + outStack += `${Parser.numberToText(ch.count)} `; + } + if (allAbilities || allAbilitiesWithParent) { + outStack += `${ch.count > 1 ? "unique " : ""}${amount}`; + } else { + for (let j = 0; j < ch.from.length; ++j) { + let suffix = ""; + if (ch.from.length > 1) { + if (j === ch.from.length - 2) { + suffix = " or "; + } else if (j < ch.from.length - 2) { + suffix = ", "; + } + } + let thsAmount = ` ${amount}`; + if (ch.from.length > 1) { + if (j !== ch.from.length - 1) { + thsAmount = ""; + } + } + outStack += ch.from[j].uppercaseFirst() + thsAmount + suffix; + } + } + } + + if (outStack.trim()) { + toConvertToText.push(`Choose ${outStack}`); + toConvertToShortText.push(outStack.uppercaseFirst()); + } + } + } + + function isAllAbilitiesWithParent(chooseAbs) { + const tempAbilities = []; + for (let i = 0; i < mainAbs.length; ++i) { + tempAbilities.push(mainAbs[i].toLowerCase()); + } + for (let i = 0; i < chooseAbs.from.length; ++i) { + const ab = chooseAbs.from[i].toLowerCase(); + if (!tempAbilities.includes(ab)) + tempAbilities.push(ab); + if (!asCollection.includes(ab.toLowerCase)) + asCollection.push(ab.toLowerCase()); + } + return tempAbilities.length === 6; + } +} +; + +Renderer._AbilityData = function({asText, asTextShort, asCollection, areNegative}={}) { + this.asText = asText || ""; + this.asTextShort = asTextShort || ""; + this.asCollection = asCollection || []; + this.areNegative = areNegative || []; +} +; + +Renderer.getFilterSubhashes = function(filters, namespace=null) { + let customHash = null; + + const subhashes = filters.map(f=>{ + const [fName,fVals,fMeta,fOpts] = f.split("=").map(s=>s.trim()); + const isBoxData = fName.startsWith("fb"); + const key = isBoxData ? `${fName}${namespace ? `.${namespace}` : ""}` : `flst${namespace ? `.${namespace}` : ""}${UrlUtil.encodeForHash(fName)}`; + + let value; + if (isBoxData) { + return { + key, + value: fVals, + preEncoded: true, + }; + } else if (fName === "search") { + return { + key: VeCt.FILTER_BOX_SUB_HASH_SEARCH_PREFIX, + value: UrlUtil.encodeForHash(fVals), + preEncoded: true, + }; + } else if (fName === "hash") { + customHash = fVals; + return null; + } else if (fVals.startsWith("[") && fVals.endsWith("]")) { + const [min,max] = fVals.substring(1, fVals.length - 1).split(";").map(it=>it.trim()); + if (max == null) { + value = [`min=${min}`, `max=${min}`, ].join(HASH_SUB_LIST_SEP); + } else { + value = [min ? `min=${min}` : "", max ? `max=${max}` : "", ].filter(Boolean).join(HASH_SUB_LIST_SEP); + } + } else if (fVals.startsWith("::") && fVals.endsWith("::")) { + value = fVals.substring(2, fVals.length - 2).split(";").map(it=>it.trim()).map(it=>{ + if (it.startsWith("!")) + return `${UrlUtil.encodeForHash(it.slice(1))}=${UrlUtil.mini.compress(false)}`; + return `${UrlUtil.encodeForHash(it)}=${UrlUtil.mini.compress(true)}`; + } + ).join(HASH_SUB_LIST_SEP); + } else { + value = fVals.split(";").map(s=>s.trim()).filter(Boolean).map(s=>{ + if (s.startsWith("!")) + return `${UrlUtil.encodeForHash(s.slice(1))}=2`; + return `${UrlUtil.encodeForHash(s)}=1`; + } + ).join(HASH_SUB_LIST_SEP); + } + + const out = [{ + key, + value, + preEncoded: true, + }]; + + if (fMeta) { + out.push({ + key: `flmt${UrlUtil.encodeForHash(fName)}`, + value: fMeta, + preEncoded: true, + }); + } + + if (fOpts) { + out.push({ + key: `flop${UrlUtil.encodeForHash(fName)}`, + value: fOpts, + preEncoded: true, + }); + } + + return out; + } + ).flat().filter(Boolean); + + return { + customHash, + subhashes, + }; +} +; + +Renderer._cache = { + inlineStatblock: {}, + + async pRunFromEle(ele) { + const cached = Renderer._cache[ele.dataset.rdCache][ele.dataset.rdCacheId]; + await cached.pFn(ele); + }, +}; + +Renderer.utils = { + getBorderTr: (optText=null)=>{ + return `${optText || ""}`; + } + , + + getDividerTr: ()=>{ + return `
            `; + } + , + + getSourceSubText(it) { + return it.sourceSub ? ` \u2014 ${it.sourceSub}` : ""; + }, + + getNameTr: (it,opts)=>{ + opts = opts || {}; + + let dataPart = ""; + let pageLinkPart; + if (opts.page) { + const hash = UrlUtil.URL_TO_HASH_BUILDER[opts.page](it); + dataPart = `data-page="${opts.page}" data-source="${it.source.escapeQuotes()}" data-hash="${hash.escapeQuotes()}" ${opts.extensionData != null ? `data-extension='${JSON.stringify(opts.extensionData).escapeQuotes()}` : ""}'`; + pageLinkPart = SourceUtil.getAdventureBookSourceHref(it.source, it.page); + + if (opts.isEmbeddedEntity) + ExtensionUtil.addEmbeddedToCache(opts.page, it.source, hash, it); + } + + const tagPartSourceStart = `<${pageLinkPart ? `a href="${Renderer.get().baseUrl}${pageLinkPart}"` : "span"}`; + const tagPartSourceEnd = ``; + + const ptBrewSourceLink = Renderer.utils._getNameTr_getPtPrereleaseBrewSourceLink({ + ent: it, + brewUtil: PrereleaseUtil + }) || Renderer.utils._getNameTr_getPtPrereleaseBrewSourceLink({ + ent: it, + brewUtil: BrewUtil2 + }); + + const $ele = $$` + +
            +
            +

            ${opts.prefix || ""}${it._displayName || it.name}${opts.suffix || ""}

            + ${opts.controlRhs || ""} + ${!IS_VTT && ExtensionUtil.ACTIVE && opts.page ? Renderer.utils.getBtnSendToFoundryHtml() : ""} +
            +
            + ${tagPartSourceStart} class="help-subtle stats-source-abbreviation ${it.source ? `${Parser.sourceJsonToColor(it.source)}" title="${Parser.sourceJsonToFull(it.source)}${Renderer.utils.getSourceSubText(it)}` : ""}" ${Parser.sourceJsonToStyle(it.source)}>${it.source ? Parser.sourceJsonToAbv(it.source) : ""}${tagPartSourceEnd} + + ${Renderer.utils.isDisplayPage(it.page) ? ` ${tagPartSourceStart} class="rd__stats-name-page ml-1" title="Page ${it.page}">p${it.page}${tagPartSourceEnd}` : ""} + + ${ptBrewSourceLink} +
            +
            + + `; + + if (opts.asJquery) + return $ele; + else + return $ele[0].outerHTML; + } + , + + _getNameTr_getPtPrereleaseBrewSourceLink({ent, brewUtil}) { + if (!brewUtil.hasSourceJson(ent.source) || !brewUtil.sourceJsonToSource(ent.source)?.url) + return ""; + + return ``; + }, + + getBtnSendToFoundryHtml({isMb=true}={}) { + return ``; + }, + + isDisplayPage(page) { + return page != null && ((!isNaN(page) && page > 0) || isNaN(page)); + }, + + getExcludedTr({entity, dataProp, page, isExcluded}) { + const excludedHtml = Renderer.utils.getExcludedHtml({ + entity, + dataProp, + page, + isExcluded + }); + if (!excludedHtml) + return ""; + return `${excludedHtml}`; + }, + + getExcludedHtml({entity, dataProp, page, isExcluded}) { + if (isExcluded != null && !isExcluded) + return ""; + if (isExcluded == null) { + if (!ExcludeUtil.isInitialised) + return ""; + if (page && !UrlUtil.URL_TO_HASH_BUILDER[page]) + return ""; + const hash = page ? UrlUtil.URL_TO_HASH_BUILDER[page](entity) : UrlUtil.autoEncodeHash(entity); + isExcluded = isExcluded || dataProp === "item" ? Renderer.item.isExcluded(entity, { + hash + }) : ExcludeUtil.isExcluded(hash, dataProp, entity.source); + } + return isExcluded ? `
            Warning: This content has been blocklisted.
            ` : ""; + }, + + getSourceAndPageTrHtml(it, {tag, fnUnpackUid}={}) { + const html = Renderer.utils.getSourceAndPageHtml(it, { + tag, + fnUnpackUid + }); + return html ? `Source: ${html}` : ""; + }, + + _getAltSourceHtmlOrText(it, prop, introText, isText) { + if (!it[prop] || !it[prop].length) + return ""; + + return `${introText} ${it[prop].map(as=>{ + if (as.entry) + return (isText ? Renderer.stripTags : Renderer.get().render)(as.entry); + return `${isText ? "" : ``}${Parser.sourceJsonToAbv(as.source)}${isText ? "" : ``}${Renderer.utils.isDisplayPage(as.page) ? `, page ${as.page}` : ""}`; + } + ).join("; ")}`; + }, + + _getReprintedAsHtmlOrText(ent, {isText, tag, fnUnpackUid}={}) { + if (!ent.reprintedAs) + return ""; + if (!tag || !fnUnpackUid) + return ""; + + const ptReprinted = ent.reprintedAs.map(it=>{ + const uid = it.uid ?? it; + const tag_ = it.tag ?? tag; + + const {name, source, displayText} = fnUnpackUid(uid); + + if (isText) { + return `${Renderer.stripTags(displayText || name)} in ${Parser.sourceJsonToAbv(source)}`; + } + + const asTag = `{@${tag_} ${name}|${source}${displayText ? `|${displayText}` : ""}}`; + + return `${Renderer.get().render(asTag)} in ${Parser.sourceJsonToAbv(source)}`; + } + ).join("; "); + + return `Reprinted as ${ptReprinted}`; + }, + + getSourceAndPageHtml(it, {tag, fnUnpackUid}={}) { + return this._getSourceAndPageHtmlOrText(it, { + tag, + fnUnpackUid + }); + }, + getSourceAndPageText(it, {tag, fnUnpackUid}={}) { + return this._getSourceAndPageHtmlOrText(it, { + isText: true, + tag, + fnUnpackUid + }); + }, + + _getSourceAndPageHtmlOrText(it, {isText, tag, fnUnpackUid}={}) { + const sourceSub = Renderer.utils.getSourceSubText(it); + const baseText = `${isText ? `` : ``}${Parser.sourceJsonToAbv(it.source)}${sourceSub}${isText ? "" : ``}${Renderer.utils.isDisplayPage(it.page) ? `, page ${it.page}` : ""}`; + const reprintedAsText = Renderer.utils._getReprintedAsHtmlOrText(it, { + isText, + tag, + fnUnpackUid + }); + const addSourceText = Renderer.utils._getAltSourceHtmlOrText(it, "additionalSources", "Additional information from", isText); + const otherSourceText = Renderer.utils._getAltSourceHtmlOrText(it, "otherSources", "Also found in", isText); + const externalSourceText = Renderer.utils._getAltSourceHtmlOrText(it, "externalSources", "External sources:", isText); + + const srdText = it.srd ? `${isText ? "" : `the `}SRD${isText ? "" : ``}${typeof it.srd === "string" ? ` (as "${it.srd}")` : ""}` : ""; + const basicRulesText = it.basicRules ? `the Basic Rules${typeof it.basicRules === "string" ? ` (as "${it.basicRules}")` : ""}` : ""; + const srdAndBasicRulesText = (srdText || basicRulesText) ? `Available in ${[srdText, basicRulesText].filter(it=>it).join(" and ")}` : ""; + + return `${[baseText, addSourceText, reprintedAsText, otherSourceText, srdAndBasicRulesText, externalSourceText].filter(it=>it).join(". ")}${baseText && (addSourceText || otherSourceText || srdAndBasicRulesText || externalSourceText) ? "." : ""}`; + }, + + async _pHandleNameClick(ele) { + await MiscUtil.pCopyTextToClipboard($(ele).text()); + JqueryUtil.showCopiedEffect($(ele)); + }, + + getPageTr(it, {tag, fnUnpackUid}={}) { + return `${Renderer.utils.getSourceAndPageTrHtml(it, { + tag, + fnUnpackUid + })}`; + }, + + getAbilityRollerEntry(statblock, ability) { + if (statblock[ability] == null) + return "\u2014"; + return `{@ability ${ability} ${statblock[ability]}}`; + }, + + getAbilityRoller(statblock, ability) { + return Renderer.get().render(Renderer.utils.getAbilityRollerEntry(statblock, ability)); + }, + + getEmbeddedDataHeader(name, style, {isCollapsed=false}={}) { + return ` + `; + }, + + getEmbeddedDataFooter() { + return `
            ${name}[${isCollapsed ? "+" : "\u2013"}]
            `; + }, + + TabButton: function({label, fnChange, fnPopulate, isVisible}) { + this.label = label; + this.fnChange = fnChange; + this.fnPopulate = fnPopulate; + this.isVisible = isVisible; + }, + + _tabs: {}, + _curTab: null, + _tabsPreferredLabel: null, + bindTabButtons({tabButtons, tabLabelReference, $wrpTabs, $pgContent}) { + Renderer.utils._tabs = {}; + Renderer.utils._curTab = null; + + $wrpTabs.find(`.stat-tab-gen`).remove(); + + tabButtons.forEach((tb,i)=>{ + tb.ix = i; + + tb.$t = $(``).click(()=>tb.fnActivateTab({ + isUserInput: true + })); + + tb.fnActivateTab = ({isUserInput=false}={})=>{ + const curTab = Renderer.utils._curTab; + const tabs = Renderer.utils._tabs; + + if (!curTab || curTab.label !== tb.label) { + if (curTab) + curTab.$t.removeClass(`ui-tab__btn-tab-head--active`); + Renderer.utils._curTab = tb; + tb.$t.addClass(`ui-tab__btn-tab-head--active`); + if (curTab) + tabs[curTab.label].$content = $pgContent.children().detach(); + + tabs[tb.label] = tb; + if (!tabs[tb.label].$content && tb.fnPopulate) + tb.fnPopulate(); + else + $pgContent.append(tabs[tb.label].$content); + if (tb.fnChange) + tb.fnChange(); + } + + if (isUserInput) + Renderer.utils._tabsPreferredLabel = tb.label; + } + ; + } + ); + + if (tabButtons.length !== 1) + tabButtons.slice().reverse().forEach(tb=>$wrpTabs.prepend(tb.$t)); + + if (!Renderer.utils._tabsPreferredLabel) + return tabButtons[0].fnActivateTab(); + + const tabButton = tabButtons.find(tb=>tb.label === Renderer.utils._tabsPreferredLabel); + if (tabButton) + return tabButton.fnActivateTab(); + + const ixDesired = tabLabelReference.indexOf(Renderer.utils._tabsPreferredLabel); + if (!~ixDesired) + return tabButtons[0].fnActivateTab(); + const ixsAvailableMetas = tabButtons.map(tb=>{ + const ixMapped = tabLabelReference.indexOf(tb.label); + if (!~ixMapped) + return null; + return { + ixMapped, + label: tb.label, + }; + } + ).filter(Boolean); + if (!ixsAvailableMetas.length) + return tabButtons[0].fnActivateTab(); + const ixMetaHigher = ixsAvailableMetas.find(({ixMapped})=>ixMapped > ixDesired); + if (ixMetaHigher != null) + return (tabButtons.find(it=>it.label === ixMetaHigher.label) || tabButtons[0]).fnActivateTab(); + + const ixMetaMax = ixsAvailableMetas.last(); + (tabButtons.find(it=>it.label === ixMetaMax.label) || tabButtons[0]).fnActivateTab(); + }, + + _pronounceButtonsBound: false, + bindPronounceButtons() { + if (Renderer.utils._pronounceButtonsBound) + return; + Renderer.utils._pronounceButtonsBound = true; + $(`body`).on("click", ".btn-name-pronounce", function() { + const audio = $(this).find(`.name-pronounce`)[0]; + audio.currentTime = 0; + audio.play(); + }); + }, + + async pHasFluffText(entity, prop) { + return entity.hasFluff || ((await Renderer.utils.pGetPredefinedFluff(entity, prop))?.entries?.length || 0) > 0; + }, + + async pHasFluffImages(entity, prop) { + return entity.hasFluffImages || (((await Renderer.utils.pGetPredefinedFluff(entity, prop))?.images?.length || 0) > 0); + }, + + async pGetPredefinedFluff(entry, prop) { + if (!entry.fluff) + return null; + + const mappedProp = `_${prop}`; + const mappedPropAppend = `_append${prop.uppercaseFirst()}`; + const fluff = {}; + + const assignPropsIfExist = (fromObj,...props)=>{ + props.forEach(prop=>{ + if (fromObj[prop]) + fluff[prop] = fromObj[prop]; + } + ); + } + ; + + assignPropsIfExist(entry.fluff, "name", "type", "entries", "images"); + + if (entry.fluff[mappedProp]) { + const fromList = [...((await PrereleaseUtil.pGetBrewProcessed())[prop] || []), ...((await BrewUtil2.pGetBrewProcessed())[prop] || []), ].find(it=>it.name === entry.fluff[mappedProp].name && it.source === entry.fluff[mappedProp].source, ); + if (fromList) { + assignPropsIfExist(fromList, "name", "type", "entries", "images"); + } + } + + if (entry.fluff[mappedPropAppend]) { + const fromList = [...((await PrereleaseUtil.pGetBrewProcessed())[prop] || []), ...((await BrewUtil2.pGetBrewProcessed())[prop] || []), ].find(it=>it.name === entry.fluff[mappedPropAppend].name && it.source === entry.fluff[mappedPropAppend].source, ); + if (fromList) { + if (fromList.entries) { + fluff.entries = MiscUtil.copyFast(fluff.entries || []); + fluff.entries.push(...MiscUtil.copyFast(fromList.entries)); + } + if (fromList.images) { + fluff.images = MiscUtil.copyFast(fluff.images || []); + fluff.images.push(...MiscUtil.copyFast(fromList.images)); + } + } + } + + return fluff; + }, + + async pGetFluff({entity, pFnPostProcess, fnGetFluffData, fluffUrl, fluffBaseUrl, fluffProp}={}) { + let predefinedFluff = await Renderer.utils.pGetPredefinedFluff(entity, fluffProp); + if (predefinedFluff) { + if (pFnPostProcess) + predefinedFluff = await pFnPostProcess(predefinedFluff); + return predefinedFluff; + } + if (!fnGetFluffData && !fluffBaseUrl && !fluffUrl) + return null; + + const fluffIndex = fluffBaseUrl ? await DataUtil.loadJSON(`${Renderer.get().baseUrl}${fluffBaseUrl}fluff-index.json`) : null; + if (fluffIndex && !fluffIndex[entity.source]) + return null; + + const data = fnGetFluffData ? await fnGetFluffData() : fluffIndex && fluffIndex[entity.source] ? await DataUtil.loadJSON(`${Renderer.get().baseUrl}${fluffBaseUrl}${fluffIndex[entity.source]}`) : await DataUtil.loadJSON(`${Renderer.get().baseUrl}${fluffUrl}`); + if (!data) + return null; + + let fluff = (data[fluffProp] || []).find(it=>it.name === entity.name && it.source === entity.source); + if (!fluff && entity._versionBase_name && entity._versionBase_source) + fluff = (data[fluffProp] || []).find(it=>it.name === entity._versionBase_name && it.source === entity._versionBase_source); + if (!fluff) + return null; + + if (pFnPostProcess) + fluff = await pFnPostProcess(fluff); + return fluff; + }, + + _TITLE_SKIP_TYPES: new Set(["entries", "section"]), + async pBuildFluffTab({isImageTab, $content, entity, $headerControls, pFnGetFluff}={}) { + $content.append(Renderer.utils.getBorderTr()); + $content.append(Renderer.utils.getNameTr(entity, { + controlRhs: $headerControls, + asJquery: true + })); + const $td = $(``); + $$`${$td}`.appendTo($content); + $content.append(Renderer.utils.getBorderTr()); + + const fluff = MiscUtil.copyFast((await pFnGetFluff(entity)) || {}); + fluff.entries = fluff.entries || [Renderer.utils.HTML_NO_INFO]; + fluff.images = fluff.images || [Renderer.utils.HTML_NO_IMAGES]; + + $td.fastSetHtml(Renderer.utils.getFluffTabContent({ + entity, + fluff, + isImageTab + })); + }, + + getFluffTabContent({entity, fluff, isImageTab=false}) { + Renderer.get().setFirstSection(true); + return (fluff[isImageTab ? "images" : "entries"] || []).map((ent,i)=>{ + if (isImageTab) + return Renderer.get().render(ent); + + if (i === 0 && ent.name && entity.name && (Renderer.utils._TITLE_SKIP_TYPES).has(ent.type)) { + const entryLowName = ent.name.toLowerCase().trim(); + const entityLowName = entity.name.toLowerCase().trim(); + + if (entryLowName.includes(entityLowName) || entityLowName.includes(entryLowName)) { + const cpy = MiscUtil.copyFast(ent); + delete cpy.name; + return Renderer.get().render(cpy); + } else + return Renderer.get().render(ent); + } else { + if (typeof ent === "string") + return `

            ${Renderer.get().render(ent)}

            `; + else + return Renderer.get().render(ent); + } + } + ).join(""); + }, + + HTML_NO_INFO: "No information available.", + HTML_NO_IMAGES: "No images available.", + + prerequisite: class { + static _WEIGHTS = ["level", "pact", "patron", "spell", "race", "alignment", "ability", "proficiency", "spellcasting", "spellcasting2020", "spellcastingFeature", "spellcastingPrepared", "psionics", "feature", "feat", "background", "item", "itemType", "itemProperty", "campaign", "group", "other", "otherSummary", undefined, ].mergeMap((k,i)=>({ + [k]: i + })); + + static _getShortClassName(className) { + const ixFirstVowel = /[aeiou]/.exec(className).index; + const start = className.slice(0, ixFirstVowel + 1); + let end = className.slice(ixFirstVowel + 1); + end = end.replace(/[aeiou]/g, ""); + return `${start}${end}`.toTitleCase(); + } + + static getHtml(prerequisites, {isListMode=false, blocklistKeys=new Set(), isTextOnly=false, isSkipPrefix=false}={}) { + if (!prerequisites?.length) + return isListMode ? "\u2014" : ""; + + const prereqsShared = prerequisites.length === 1 ? {} : Object.entries(prerequisites.slice(1).reduce((a,b)=>CollectionUtil.objectIntersect(a, b), prerequisites[0]), ).filter(([k,v])=>prerequisites.every(pre=>CollectionUtil.deepEquals(pre[k], v))).mergeMap(([k,v])=>({ + [k]: v + })); + + const shared = Object.keys(prereqsShared).length ? this.getHtml([prereqsShared], { + isListMode, + blocklistKeys, + isTextOnly, + isSkipPrefix: true + }) : null; + + let cntPrerequisites = 0; + let hasNote = false; + const listOfChoices = prerequisites.map(pr=>{ + const ptNote = !isListMode && pr.note ? Renderer.get().render(pr.note) : null; + if (ptNote) { + hasNote = true; + } + + const prereqsToJoin = Object.entries(pr).filter(([k])=>!prereqsShared[k]).sort(([kA],[kB])=>this._WEIGHTS[kA] - this._WEIGHTS[kB]).map(([k,v])=>{ + if (k === "note" || blocklistKeys.has(k)) + return false; + + cntPrerequisites += 1; + + switch (k) { + case "level": + return this._getHtml_level({ + v, + isListMode, + isTextOnly + }); + case "pact": + return this._getHtml_pact({ + v, + isListMode, + isTextOnly + }); + case "patron": + return this._getHtml_patron({ + v, + isListMode, + isTextOnly + }); + case "spell": + return this._getHtml_spell({ + v, + isListMode, + isTextOnly + }); + case "feat": + return this._getHtml_feat({ + v, + isListMode, + isTextOnly + }); + case "feature": + return this._getHtml_feature({ + v, + isListMode, + isTextOnly + }); + case "item": + return this._getHtml_item({ + v, + isListMode, + isTextOnly + }); + case "itemType": + return this._getHtml_itemType({ + v, + isListMode, + isTextOnly + }); + case "itemProperty": + return this._getHtml_itemProperty({ + v, + isListMode, + isTextOnly + }); + case "otherSummary": + return this._getHtml_otherSummary({ + v, + isListMode, + isTextOnly + }); + case "other": + return this._getHtml_other({ + v, + isListMode, + isTextOnly + }); + case "race": + return this._getHtml_race({ + v, + isListMode, + isTextOnly + }); + case "background": + return this._getHtml_background({ + v, + isListMode, + isTextOnly + }); + case "ability": + return this._getHtml_ability({ + v, + isListMode, + isTextOnly + }); + case "proficiency": + return this._getHtml_proficiency({ + v, + isListMode, + isTextOnly + }); + case "spellcasting": + return this._getHtml_spellcasting({ + v, + isListMode, + isTextOnly + }); + case "spellcasting2020": + return this._getHtml_spellcasting2020({ + v, + isListMode, + isTextOnly + }); + case "spellcastingFeature": + return this._getHtml_spellcastingFeature({ + v, + isListMode, + isTextOnly + }); + case "spellcastingPrepared": + return this._getHtml_spellcastingPrepared({ + v, + isListMode, + isTextOnly + }); + case "psionics": + return this._getHtml_psionics({ + v, + isListMode, + isTextOnly + }); + case "alignment": + return this._getHtml_alignment({ + v, + isListMode, + isTextOnly + }); + case "campaign": + return this._getHtml_campaign({ + v, + isListMode, + isTextOnly + }); + case "group": + return this._getHtml_group({ + v, + isListMode, + isTextOnly + }); + default: + throw new Error(`Unhandled key: ${k}`); + } + } + ).filter(Boolean); + + const ptPrereqs = prereqsToJoin.join(prereqsToJoin.some(it=>/ or /.test(it)) ? "; " : ", "); + + return [ptPrereqs, ptNote].filter(Boolean).join(". "); + } + ).filter(Boolean); + + if (!listOfChoices.length && !shared) + return isListMode ? "\u2014" : ""; + if (isListMode) + return [shared, listOfChoices.join("/")].filter(Boolean).join(" + "); + + const sharedSuffix = MiscUtil.findCommonSuffix(listOfChoices, { + isRespectWordBoundaries: true + }); + const listOfChoicesTrimmed = sharedSuffix ? listOfChoices.map(it=>it.slice(0, -sharedSuffix.length)) : listOfChoices; + + const joinedChoices = (hasNote ? listOfChoicesTrimmed.join(" Or, ") : listOfChoicesTrimmed.joinConjunct(listOfChoicesTrimmed.some(it=>/ or /.test(it)) ? "; " : ", ", " or ")) + sharedSuffix; + return `${isSkipPrefix ? "" : `Prerequisite${cntPrerequisites === 1 ? "" : "s"}: `}${[shared, joinedChoices].filter(Boolean).join(", plus ")}`; + } + + static _getHtml_level({v, isListMode}) { + if (typeof v === "number") { + if (isListMode) + return `Lvl ${v}`; + else + return `${Parser.getOrdinalForm(v)} level`; + } else if (!v.class && !v.subclass) { + if (isListMode) + return `Lvl ${v.level}`; + else + return `${Parser.getOrdinalForm(v.level)} level`; + } + + const isLevelVisible = v.level !== 1; + const isSubclassVisible = v.subclass && v.subclass.visible; + const isClassVisible = v.class && (v.class.visible || isSubclassVisible); + if (isListMode) { + const shortNameRaw = isClassVisible ? this._getShortClassName(v.class.name) : null; + return `${isClassVisible ? `${shortNameRaw.slice(0, 4)}${isSubclassVisible ? "*" : "."}` : ""}${isLevelVisible ? ` Lvl ${v.level}` : ""}`; + } else { + let classPart = ""; + if (isClassVisible && isSubclassVisible) + classPart = ` ${v.class.name} (${v.subclass.name})`; + else if (isClassVisible) + classPart = ` ${v.class.name}`; + else if (isSubclassVisible) + classPart = ` <remember to insert class name here> (${v.subclass.name})`; + return `${isLevelVisible ? `${Parser.getOrdinalForm(v.level)} level` : ""}${isClassVisible ? ` ${classPart}` : ""}`; + } + } + + static _getHtml_pact({v, isListMode}) { + return Parser.prereqPactToFull(v); + } + + static _getHtml_patron({v, isListMode}) { + return isListMode ? `${Parser.prereqPatronToShort(v)} patron` : `${v} patron`; + } + + static _getHtml_spell({v, isListMode, isTextOnly}) { + return isListMode ? v.map(sp=>{ + if (typeof sp === "string") + return sp.split("#")[0].split("|")[0].toTitleCase(); + return sp.entrySummary || sp.entry; + } + ).join("/") : v.map(sp=>{ + if (typeof sp === "string") + return Parser.prereqSpellToFull(sp, { + isTextOnly + }); + return isTextOnly ? Renderer.stripTags(sp.entry) : Renderer.get().render(`{@filter ${sp.entry}|spells|${sp.choose}}`); + } + ).joinConjunct(", ", " or "); + } + + static _getHtml_feat({v, isListMode, isTextOnly}) { + return isListMode ? v.map(x=>x.split("|")[0].toTitleCase()).join("/") : v.map(it=>(isTextOnly ? Renderer.stripTags.bind(Renderer) : Renderer.get().render.bind(Renderer.get()))(`{@feat ${it}} feat`)).joinConjunct(", ", " or "); + } + + static _getHtml_feature({v, isListMode, isTextOnly}) { + return isListMode ? v.map(x=>Renderer.stripTags(x).toTitleCase()).join("/") : v.map(it=>isTextOnly ? Renderer.stripTags(it) : Renderer.get().render(it)).joinConjunct(", ", " or "); + } + + static _getHtml_item({v, isListMode}) { + return isListMode ? v.map(x=>x.toTitleCase()).join("/") : v.joinConjunct(", ", " or "); + } + + static _getHtml_itemType({v, isListMode}) { + return isListMode ? v.map(it=>Renderer.item.getType(it)).map(it=>it?.abbreviation).join("+") : v.map(it=>Renderer.item.getType(it)).map(it=>it?.name?.toTitleCase()).joinConjunct(", ", " and "); + } + + static _getHtml_itemProperty({v, isListMode}) { + if (v == null) + return isListMode ? "No Prop." : "No Other Properties"; + + return isListMode ? v.map(it=>Renderer.item.getProperty(it)).map(it=>it?.abbreviation).join("+") : (`${v.map(it=>Renderer.item.getProperty(it)).map(it=>it?.name?.toTitleCase()).joinConjunct(", ", " and ")} Property`); + } + + static _getHtml_otherSummary({v, isListMode, isTextOnly}) { + return isListMode ? (v.entrySummary || Renderer.stripTags(v.entry)) : (isTextOnly ? Renderer.stripTags(v.entry) : Renderer.get().render(v.entry)); + } + + static _getHtml_other({v, isListMode, isTextOnly}) { + return isListMode ? "Special" : (isTextOnly ? Renderer.stripTags(v) : Renderer.get().render(v)); + } + + static _getHtml_race({v, isListMode, isTextOnly}) { + const parts = v.map((it,i)=>{ + if (isListMode) { + return `${it.name.toTitleCase()}${it.subrace != null ? ` (${it.subrace})` : ""}`; + } else { + const raceName = it.displayEntry ? (isTextOnly ? Renderer.stripTags(it.displayEntry) : Renderer.get().render(it.displayEntry)) : i === 0 ? it.name.toTitleCase() : it.name; + return `${raceName}${it.subrace != null ? ` (${it.subrace})` : ""}`; + } + } + ); + return isListMode ? parts.join("/") : parts.joinConjunct(", ", " or "); + } + + static _getHtml_background({v, isListMode, isTextOnly}) { + const parts = v.map((it,i)=>{ + if (isListMode) { + return `${it.name.toTitleCase()}`; + } else { + return it.displayEntry ? (isTextOnly ? Renderer.stripTags(it.displayEntry) : Renderer.get().render(it.displayEntry)) : i === 0 ? it.name.toTitleCase() : it.name; + } + } + ); + return isListMode ? parts.join("/") : parts.joinConjunct(", ", " or "); + } + + static _getHtml_ability({v, isListMode, isTextOnly}) { + + let hadMultipleInner = false; + let hadMultiMultipleInner = false; + let allValuesEqual = null; + + outer: for (const abMeta of v) { + for (const req of Object.values(abMeta)) { + if (allValuesEqual == null) + allValuesEqual = req; + else { + if (req !== allValuesEqual) { + allValuesEqual = null; + break outer; + } + } + } + } + + const abilityOptions = v.map(abMeta=>{ + if (allValuesEqual) { + const abList = Object.keys(abMeta); + hadMultipleInner = hadMultipleInner || abList.length > 1; + return isListMode ? abList.map(ab=>ab.uppercaseFirst()).join(", ") : abList.map(ab=>Parser.attAbvToFull(ab)).joinConjunct(", ", " and "); + } else { + const groups = {}; + + Object.entries(abMeta).forEach(([ab,req])=>{ + (groups[req] = groups[req] || []).push(ab); + } + ); + + let isMulti = false; + const byScore = Object.entries(groups).sort(([reqA],[reqB])=>SortUtil.ascSort(Number(reqB), Number(reqA))).map(([req,abs])=>{ + hadMultipleInner = hadMultipleInner || abs.length > 1; + if (abs.length > 1) + hadMultiMultipleInner = isMulti = true; + + abs = abs.sort(SortUtil.ascSortAtts); + return isListMode ? `${abs.map(ab=>ab.uppercaseFirst()).join(", ")} ${req}+` : `${abs.map(ab=>Parser.attAbvToFull(ab)).joinConjunct(", ", " and ")} ${req} or higher`; + } + ); + + return isListMode ? `${isMulti || byScore.length > 1 ? "(" : ""}${byScore.join(" & ")}${isMulti || byScore.length > 1 ? ")" : ""}` : isMulti ? byScore.joinConjunct("; ", " and ") : byScore.joinConjunct(", ", " and "); + } + } + ); + + if (isListMode) { + return `${abilityOptions.join("/")}${allValuesEqual != null ? ` ${allValuesEqual}+` : ""}`; + } else { + const isComplex = hadMultiMultipleInner || hadMultipleInner || allValuesEqual == null; + const joined = abilityOptions.joinConjunct(hadMultiMultipleInner ? " - " : hadMultipleInner ? "; " : ", ", isComplex ? (isTextOnly ? ` /or/ ` : ` or `) : " or ", ); + return `${joined}${allValuesEqual != null ? ` ${allValuesEqual} or higher` : ""}`; + } + } + + static _getHtml_proficiency({v, isListMode}) { + const parts = v.map(obj=>{ + return Object.entries(obj).map(([profType,prof])=>{ + switch (profType) { + case "armor": + { + return isListMode ? `Prof ${Parser.armorFullToAbv(prof)} armor` : `Proficiency with ${prof} armor`; + } + case "weapon": + { + return isListMode ? `Prof ${Parser.weaponFullToAbv(prof)} weapon` : `Proficiency with a ${prof} weapon`; + } + case "weaponGroup": + { + return isListMode ? `Prof ${Parser.weaponFullToAbv(prof)} weapons` : `${prof.toTitleCase()} Proficiency`; + } + default: + throw new Error(`Unhandled proficiency type: "${profType}"`); + } + } + ); + } + ); + return isListMode ? parts.join("/") : parts.joinConjunct(", ", " or "); + } + + static _getHtml_spellcasting({v, isListMode}) { + return isListMode ? "Spellcasting" : "The ability to cast at least one spell"; + } + + static _getHtml_spellcasting2020({v, isListMode}) { + return isListMode ? "Spellcasting" : "Spellcasting or Pact Magic feature"; + } + + static _getHtml_spellcastingFeature({v, isListMode}) { + return isListMode ? "Spellcasting" : "Spellcasting Feature"; + } + + static _getHtml_spellcastingPrepared({v, isListMode}) { + return isListMode ? "Spellcasting" : "Spellcasting feature from a class that prepares spells"; + } + + static _getHtml_psionics({v, isListMode, isTextOnly}) { + return isListMode ? "Psionics" : (isTextOnly ? Renderer.stripTags : Renderer.get().render.bind(Renderer.get()))("Psionic Talent feature or Wild Talent feat"); + } + + static _getHtml_alignment({v, isListMode}) { + return isListMode ? Parser.alignmentListToFull(v).replace(/\bany\b/gi, "").trim().replace(/\balignment\b/gi, "align").trim().toTitleCase() : Parser.alignmentListToFull(v); + } + + static _getHtml_campaign({v, isListMode}) { + return isListMode ? v.join("/") : `${v.joinConjunct(", ", " or ")} Campaign`; + } + + static _getHtml_group({v, isListMode}) { + return isListMode ? v.map(it=>it.toTitleCase()).join("/") : `${v.map(it=>it.toTitleCase()).joinConjunct(", ", " or ")} Group`; + } + } + , + + getRepeatableEntry(ent) { + if (!ent.repeatable) + return null; + return `{@b Repeatable:} ${ent.repeatableNote || (ent.repeatable ? "Yes" : "No")}`; + }, + + getRepeatableHtml(ent, {isListMode=false}={}) { + const entryRepeatable = Renderer.utils.getRepeatableEntry(ent); + if (entryRepeatable == null) + return isListMode ? "\u2014" : ""; + return Renderer.get().render(entryRepeatable); + }, + + getRenderedSize(size) { + return [...(size ? [size].flat() : [])].sort(SortUtil.ascSortSize).map(sz=>Parser.sizeAbvToFull(sz)).joinConjunct(", ", " or "); + }, + + getMediaUrl(entry, prop, mediaDir) { + if (!entry[prop]) + return ""; + + let href = ""; + if (entry[prop].type === "internal") { + const baseUrl = Renderer.get().baseMediaUrls[mediaDir] || Renderer.get().baseUrl; + const mediaPart = `${mediaDir}/${entry[prop].path}`; + href = baseUrl !== "" ? `${baseUrl}${mediaPart}` : UrlUtil.link(mediaPart); + } else if (entry[prop].type === "external") { + href = entry[prop].url; + } + return href; + }, + + getTagEntry(tag, text) { + switch (tag) { + case "@dice": + case "@autodice": + case "@damage": + case "@hit": + case "@d20": + case "@chance": + case "@recharge": + { + const fauxEntry = { + type: "dice", + rollable: true, + }; + const [rollText,displayText,name,...others] = Renderer.splitTagByPipe(text); + if (displayText) + fauxEntry.displayText = displayText; + + if ((!fauxEntry.displayText && (rollText || "").includes("summonSpellLevel")) || (fauxEntry.displayText && fauxEntry.displayText.includes("summonSpellLevel"))) + fauxEntry.displayText = (fauxEntry.displayText || rollText || "").replace(/summonSpellLevel/g, "the spell's level"); + + if ((!fauxEntry.displayText && (rollText || "").includes("summonClassLevel")) || (fauxEntry.displayText && fauxEntry.displayText.includes("summonClassLevel"))) + fauxEntry.displayText = (fauxEntry.displayText || rollText || "").replace(/summonClassLevel/g, "your class level"); + + if (name) + fauxEntry.name = name; + + switch (tag) { + case "@dice": + case "@autodice": + case "@damage": + { + fauxEntry.toRoll = rollText; + + if (!fauxEntry.displayText && (rollText || "").includes(";")) + fauxEntry.displayText = rollText.replace(/;/g, "/"); + if ((!fauxEntry.displayText && (rollText || "").includes("#$")) || (fauxEntry.displayText && fauxEntry.displayText.includes("#$"))) + fauxEntry.displayText = (fauxEntry.displayText || rollText).replace(/#\$prompt_number[^$]*\$#/g, "(n)"); + fauxEntry.displayText = fauxEntry.displayText || fauxEntry.toRoll; + + if (tag === "@damage") + fauxEntry.subType = "damage"; + if (tag === "@autodice") + fauxEntry.autoRoll = true; + + return fauxEntry; + } + case "@d20": + case "@hit": + { + let mod; + if (!isNaN(rollText)) { + const n = Number(rollText); + mod = `${n >= 0 ? "+" : ""}${n}`; + } else + mod = /^\s+[-+]/.test(rollText) ? rollText : `+${rollText}`; + fauxEntry.displayText = fauxEntry.displayText || mod; + fauxEntry.toRoll = `1d20${mod}`; + fauxEntry.subType = "d20"; + fauxEntry.d20mod = mod; + if (tag === "@hit") + fauxEntry.context = { + type: "hit" + }; + return fauxEntry; + } + case "@chance": + { + const [textSuccess,textFailure] = others; + fauxEntry.toRoll = `1d100`; + fauxEntry.successThresh = Number(rollText); + fauxEntry.chanceSuccessText = textSuccess; + fauxEntry.chanceFailureText = textFailure; + return fauxEntry; + } + case "@recharge": + { + const flags = displayText ? displayText.split("") : null; + fauxEntry.toRoll = "1d6"; + const asNum = Number(rollText || 6); + fauxEntry.successThresh = 7 - asNum; + fauxEntry.successMax = 6; + fauxEntry.displayText = `${asNum}${asNum < 6 ? `\u20136` : ""}`; + fauxEntry.chanceSuccessText = "Recharged!"; + fauxEntry.chanceFailureText = "Did not recharge"; + fauxEntry.isColorSuccessFail = true; + return fauxEntry; + } + } + + return fauxEntry; + } + + case "@ability": + case "@savingThrow": + { + const fauxEntry = { + type: "dice", + rollable: true, + subType: "d20", + context: { + type: tag === "@ability" ? "abilityCheck" : "savingThrow" + }, + }; + + const [abilAndScoreOrScore,displayText,name,...others] = Renderer.splitTagByPipe(text); + + let[abil,...rawScoreOrModParts] = abilAndScoreOrScore.split(" ").map(it=>it.trim()).filter(Boolean); + abil = abil.toLowerCase(); + + fauxEntry.context.ability = abil; + + if (name) + fauxEntry.name = name; + else { + if (tag === "@ability") + fauxEntry.name = Parser.attAbvToFull(abil); + else if (tag === "@savingThrow") + fauxEntry.name = `${Parser.attAbvToFull(abil)} save`; + } + + const rawScoreOrMod = rawScoreOrModParts.join(" "); + if (isNaN(rawScoreOrMod) && tag === "@savingThrow") { + if (displayText) + fauxEntry.displayText = displayText; + else + fauxEntry.displayText = rawScoreOrMod; + + fauxEntry.toRoll = `1d20${rawScoreOrMod}`; + fauxEntry.d20mod = rawScoreOrMod; + } else { + const scoreOrMod = Number(rawScoreOrMod) || 0; + const mod = (tag === "@ability" ? Parser.getAbilityModifier : UiUtil.intToBonus)(scoreOrMod); + + if (displayText) + fauxEntry.displayText = displayText; + else { + if (tag === "@ability") + fauxEntry.displayText = `${scoreOrMod} (${mod})`; + else + fauxEntry.displayText = mod; + } + + fauxEntry.toRoll = `1d20${mod}`; + fauxEntry.d20mod = mod; + } + + return fauxEntry; + } + + case "@skillCheck": + { + const fauxEntry = { + type: "dice", + rollable: true, + subType: "d20", + context: { + type: "skillCheck" + }, + }; + + const [skillAndMod,displayText,name,...others] = Renderer.splitTagByPipe(text); + + const parts = skillAndMod.split(" ").map(it=>it.trim()).filter(Boolean); + const namePart = parts.shift(); + const bonusPart = parts.join(" "); + const skill = namePart.replace(/_/g, " "); + + let mod = bonusPart; + if (!isNaN(bonusPart)) + mod = UiUtil.intToBonus(Number(bonusPart) || 0); + else if (bonusPart.startsWith("#$")) + mod = `+${bonusPart}`; + + fauxEntry.context.skill = skill; + fauxEntry.displayText = displayText || mod; + + if (name) + fauxEntry.name = name; + else + fauxEntry.name = skill.toTitleCase(); + + fauxEntry.toRoll = `1d20${mod}`; + fauxEntry.d20mod = mod; + + return fauxEntry; + } + + case "@coinflip": + { + const [displayText,name,textSuccess,textFailure] = Renderer.splitTagByPipe(text); + + const fauxEntry = { + type: "dice", + toRoll: "1d2", + successThresh: 1, + successMax: 2, + displayText: displayText || "flip a coin", + chanceSuccessText: textSuccess || `Heads`, + chanceFailureText: textFailure || `Tails`, + isColorSuccessFail: !textSuccess && !textFailure, + rollable: true, + }; + + return fauxEntry; + } + + default: + throw new Error(`Unhandled tag "${tag}"`); + } + }, + + getTagMeta(tag, text) { + switch (tag) { + case "@deity": + { + let[name,pantheon,source,displayText,...others] = Renderer.splitTagByPipe(text); + pantheon = pantheon || "forgotten realms"; + source = source || Parser.getTagSource(tag, source); + const hash = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_DEITIES]({ + name, + pantheon, + source + }); + + return { + name, + displayText, + others, + + page: UrlUtil.PG_DEITIES, + source, + hash, + + hashPreEncoded: true, + }; + } + + case "@card": + { + const unpacked = DataUtil.deck.unpackUidCard(text); + const {name, set, source, displayText} = unpacked; + const hash = UrlUtil.URL_TO_HASH_BUILDER["card"]({ + name, + set, + source + }); + + return { + name, + displayText, + + isFauxPage: true, + page: "card", + source, + hash, + hashPreEncoded: true, + }; + } + + case "@classFeature": + { + const unpacked = DataUtil.class.unpackUidClassFeature(text); + + const classPageHash = `${UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_CLASSES]({ + name: unpacked.className, + source: unpacked.classSource + })}${HASH_PART_SEP}${UrlUtil.getClassesPageStatePart({ + feature: { + ixLevel: unpacked.level - 1, + ixFeature: 0 + } + })}`; + + return { + name: unpacked.name, + displayText: unpacked.displayText, + + page: UrlUtil.PG_CLASSES, + source: unpacked.source, + hash: classPageHash, + hashPreEncoded: true, + + pageHover: "classfeature", + hashHover: UrlUtil.URL_TO_HASH_BUILDER["classFeature"](unpacked), + hashPreEncodedHover: true, + }; + } + + case "@subclassFeature": + { + const unpacked = DataUtil.class.unpackUidSubclassFeature(text); + + const classPageHash = `${UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_CLASSES]({ + name: unpacked.className, + source: unpacked.classSource + })}${HASH_PART_SEP}${UrlUtil.getClassesPageStatePart({ + feature: { + ixLevel: unpacked.level - 1, + ixFeature: 0 + } + })}`; + + return { + name: unpacked.name, + displayText: unpacked.displayText, + + page: UrlUtil.PG_CLASSES, + source: unpacked.source, + hash: classPageHash, + hashPreEncoded: true, + + pageHover: "subclassfeature", + hashHover: UrlUtil.URL_TO_HASH_BUILDER["subclassFeature"](unpacked), + hashPreEncodedHover: true, + }; + } + + case "@quickref": + { + const unpacked = DataUtil.quickreference.unpackUid(text); + + const hash = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_QUICKREF](unpacked); + + return { + name: unpacked.name, + displayText: unpacked.displayText, + + page: UrlUtil.PG_QUICKREF, + source: unpacked.source, + hash, + hashPreEncoded: true, + }; + } + + default: + return Renderer.utils._getTagMeta_generic(tag, text); + } + }, + + _getTagMeta_generic(tag, text) { + const {name, source, displayText, others} = DataUtil.generic.unpackUid(text, tag); + const hash = UrlUtil.encodeForHash([name, source]); + + const out = { + name, + displayText, + others, + + page: null, + source, + hash, + + preloadId: null, + subhashes: null, + linkText: null, + + hashPreEncoded: true, + }; + + switch (tag) { + case "@spell": + out.page = UrlUtil.PG_SPELLS; + break; + case "@item": + out.page = UrlUtil.PG_ITEMS; + break; + case "@condition": + case "@disease": + case "@status": + out.page = UrlUtil.PG_CONDITIONS_DISEASES; + break; + case "@background": + out.page = UrlUtil.PG_BACKGROUNDS; + break; + case "@race": + out.page = UrlUtil.PG_RACES; + break; + case "@optfeature": + out.page = UrlUtil.PG_OPT_FEATURES; + break; + case "@reward": + out.page = UrlUtil.PG_REWARDS; + break; + case "@feat": + out.page = UrlUtil.PG_FEATS; + break; + case "@psionic": + out.page = UrlUtil.PG_PSIONICS; + break; + case "@object": + out.page = UrlUtil.PG_OBJECTS; + break; + case "@boon": + case "@cult": + out.page = UrlUtil.PG_CULTS_BOONS; + break; + case "@trap": + case "@hazard": + out.page = UrlUtil.PG_TRAPS_HAZARDS; + break; + case "@variantrule": + out.page = UrlUtil.PG_VARIANTRULES; + break; + case "@table": + out.page = UrlUtil.PG_TABLES; + break; + case "@vehicle": + case "@vehupgrade": + out.page = UrlUtil.PG_VEHICLES; + break; + case "@action": + out.page = UrlUtil.PG_ACTIONS; + break; + case "@language": + out.page = UrlUtil.PG_LANGUAGES; + break; + case "@charoption": + out.page = UrlUtil.PG_CHAR_CREATION_OPTIONS; + break; + case "@recipe": + out.page = UrlUtil.PG_RECIPES; + break; + case "@deck": + out.page = UrlUtil.PG_DECKS; + break; + + case "@legroup": + { + out.page = "legendaryGroup"; + out.isFauxPage = true; + break; + } + + case "@creature": + { + out.page = UrlUtil.PG_BESTIARY; + + if (others.length) { + const [type,value] = others[0].split("=").map(it=>it.trim().toLowerCase()).filter(Boolean); + if (type && value) { + switch (type) { + case VeCt.HASH_SCALED: + { + const targetCrNum = Parser.crToNumber(value); + out.preloadId = Renderer.monster.getCustomHashId({ + name, + source, + _isScaledCr: true, + _scaledCr: targetCrNum + }); + out.subhashes = [{ + key: VeCt.HASH_SCALED, + value: targetCrNum + }, ]; + out.linkText = displayText || `${name} (CR ${value})`; + break; + } + + case VeCt.HASH_SCALED_SPELL_SUMMON: + { + const scaledSpellNum = Number(value); + out.preloadId = Renderer.monster.getCustomHashId({ + name, + source, + _isScaledSpellSummon: true, + _scaledSpellSummonLevel: scaledSpellNum + }); + out.subhashes = [{ + key: VeCt.HASH_SCALED_SPELL_SUMMON, + value: scaledSpellNum + }, ]; + out.linkText = displayText || `${name} (Spell Level ${value})`; + break; + } + + case VeCt.HASH_SCALED_CLASS_SUMMON: + { + const scaledClassNum = Number(value); + out.preloadId = Renderer.monster.getCustomHashId({ + name, + source, + _isScaledClassSummon: true, + _scaledClassSummonLevel: scaledClassNum + }); + out.subhashes = [{ + key: VeCt.HASH_SCALED_CLASS_SUMMON, + value: scaledClassNum + }, ]; + out.linkText = displayText || `${name} (Class Level ${value})`; + break; + } + } + } + } + + break; + } + + case "@class": + { + out.page = UrlUtil.PG_CLASSES; + + if (others.length) { + const [subclassShortName,subclassSource,featurePart] = others; + + if (subclassSource) + out.source = subclassSource; + + const classStateOpts = { + subclass: { + shortName: subclassShortName.trim(), + source: subclassSource ? subclassSource.trim() : Parser.SRC_PHB, + }, + }; + + const hoverSubhashObj = UrlUtil.unpackSubHash(UrlUtil.getClassesPageStatePart(classStateOpts)); + out.subhashesHover = [{ + key: "state", + value: hoverSubhashObj.state, + preEncoded: true + }]; + + if (featurePart) { + const featureParts = featurePart.trim().split("-"); + classStateOpts.feature = { + ixLevel: featureParts[0] || "0", + ixFeature: featureParts[1] || "0", + }; + } + + const subhashObj = UrlUtil.unpackSubHash(UrlUtil.getClassesPageStatePart(classStateOpts)); + + out.subhashes = [{ + key: "state", + value: subhashObj.state.join(HASH_SUB_LIST_SEP), + preEncoded: true + }, { + key: "fltsource", + value: "clear" + }, { + key: "flstmiscellaneous", + value: "clear" + }, ]; + } + + break; + } + + case "@skill": + { + out.isFauxPage = true; + out.page = "skill"; + break; + } + case "@sense": + { + out.isFauxPage = true; + out.page = "sense"; + break; + } + case "@itemMastery": + { + out.isFauxPage = true; + out.page = "itemMastery"; + break; + } + case "@cite": + { + out.isFauxPage = true; + out.page = "citation"; + break; + } + + default: + throw new Error(`Unhandled tag "${tag}"`); + } + + return out; + }, + + applyTemplate(ent, templateString, {fnPreApply, mapCustom}={}) { + return templateString.replace(/{{([^}]+)}}/g, (fullMatch,strArgs)=>{ + if (fnPreApply) + fnPreApply(fullMatch, strArgs); + + if (strArgs === "item.dmg1") { + return Renderer.item._getTaggedDamage(ent.dmg1); + } else if (strArgs === "item.dmg2") { + return Renderer.item._getTaggedDamage(ent.dmg2); + } + + if (mapCustom && mapCustom[strArgs]) + return mapCustom[strArgs]; + + const args = strArgs.split(" ").map(arg=>arg.trim()).filter(Boolean); + + if (args.length === 1) { + return Renderer.utils._applyTemplate_getValue(ent, args[0]); + } else if (args.length === 2) { + const val = Renderer.utils._applyTemplate_getValue(ent, args[1]); + switch (args[0]) { + case "getFullImmRes": + return Parser.getFullImmRes(val); + default: + throw new Error(`Unknown template function "${args[0]}"`); + } + } else + throw new Error(`Unhandled number of arguments ${args.length}`); + } + ); + }, + + _applyTemplate_getValue(ent, prop) { + const spl = prop.split("."); + switch (spl[0]) { + case "item": + { + const path = spl.slice(1); + if (!path.length) + return `{@i missing key path}`; + return MiscUtil.get(ent, ...path); + } + default: + return `{@i unknown template root: "${spl[0]}"}`; + } + }, + + getFlatEntries(entry) { + const out = []; + const depthStack = []; + + const recurse = ({obj})=>{ + let isPopDepth = false; + + Renderer.ENTRIES_WITH_ENUMERATED_TITLES.forEach(meta=>{ + if (obj.type !== meta.type) + return; + + const kName = "name"; + if (obj[kName] == null) + return; + + isPopDepth = true; + + const curDepth = depthStack.length ? depthStack.last() : 0; + const nxtDepth = meta.depth ? meta.depth : meta.depthIncrement ? curDepth + meta.depthIncrement : curDepth; + + depthStack.push(Math.min(nxtDepth, 2, ), ); + + const cpyObj = MiscUtil.copyFast(obj); + + out.push({ + depth: curDepth, + entry: cpyObj, + key: meta.key, + ix: out.length, + name: cpyObj.name, + }); + + cpyObj[meta.key] = cpyObj[meta.key].map(child=>{ + if (!child.type) + return child; + const childMeta = Renderer.ENTRIES_WITH_ENUMERATED_TITLES_LOOKUP[child.type]; + if (!childMeta) + return child; + + const kNameChild = "name"; + if (child[kName] == null) + return child; + + const ixNextRef = out.length; + + recurse({ + obj: child + }); + + return { + IX_FLAT_REF: ixNextRef + }; + } + ); + } + ); + + if (isPopDepth) + depthStack.pop(); + } + ; + + recurse({ + obj: entry + }); + + return out; + }, + + getLinkSubhashString(subhashes) { + let out = ""; + const len = subhashes.length; + for (let i = 0; i < len; ++i) { + const subHash = subhashes[i]; + if (subHash.preEncoded) + out += `${HASH_PART_SEP}${subHash.key}${HASH_SUB_KV_SEP}`; + else + out += `${HASH_PART_SEP}${UrlUtil.encodeForHash(subHash.key)}${HASH_SUB_KV_SEP}`; + if (subHash.value != null) { + if (subHash.preEncoded) + out += subHash.value; + else + out += UrlUtil.encodeForHash(subHash.value); + } else { + out += subHash.values.map(v=>UrlUtil.encodeForHash(v)).join(HASH_SUB_LIST_SEP); + } + } + return out; + }, + + initFullEntries_(ent, {propEntries="entries", propFullEntries="_fullEntries"}={}) { + ent[propFullEntries] = ent[propFullEntries] || (ent[propEntries] ? MiscUtil.copyFast(ent[propEntries]) : []); + }, + + lazy: { + _getIntersectionConfig() { + return { + rootMargin: "150px 0px", + threshold: 0.01, + }; + }, + + _OBSERVERS: {}, + getCreateObserver({observerId, fnOnObserve}) { + if (!Renderer.utils.lazy._OBSERVERS[observerId]) { + const observer = Renderer.utils.lazy._OBSERVERS[observerId] = new IntersectionObserver(Renderer.utils.lazy.getFnOnIntersect({ + observerId, + fnOnObserve, + }),Renderer.utils.lazy._getIntersectionConfig(),); + + observer._TRACKED = new Set(); + + observer.track = it=>{ + observer._TRACKED.add(it); + return observer.observe(it); + } + ; + + observer.untrack = it=>{ + observer._TRACKED.delete(it); + return observer.unobserve(it); + } + ; + + observer._printListener = evt=>{ + if (!observer._TRACKED.size) + return; + + [...observer._TRACKED].forEach(it=>{ + observer.untrack(it); + fnOnObserve({ + observer, + entry: { + target: it, + }, + }); + } + ); + + alert(`All content must be loaded prior to printing. Please cancel the print and wait a few moments for loading to complete!`); + } + ; + window.addEventListener("beforeprint", observer._printListener); + } + return Renderer.utils.lazy._OBSERVERS[observerId]; + }, + + destroyObserver({observerId}) { + const observer = Renderer.utils.lazy._OBSERVERS[observerId]; + if (!observer) + return; + + observer.disconnect(); + window.removeEventListener("beforeprint", observer._printListener); + }, + + getFnOnIntersect({observerId, fnOnObserve}) { + return obsEntries=>{ + const observer = Renderer.utils.lazy._OBSERVERS[observerId]; + + obsEntries.forEach(entry=>{ + if (entry.intersectionRatio <= 0) + return; + + observer.untrack(entry.target); + fnOnObserve({ + observer, + entry, + }); + } + ); + } + ; + }, + }, +}; + +Renderer.tag = class { + static _TagBase = class { + tagName; + defaultSource = null; + page = null; + + get tag() { + return `@${this.tagName}`; + } + + getStripped(tag, text) { + text = text.replace(/<\$([^$]+)\$>/gi, ""); + return this._getStripped(tag, text); + } + + _getStripped(tag, text) { + throw new Error("Unimplemented!"); + } + + getMeta(tag, text) { + return this._getMeta(tag, text); + } + _getMeta(tag, text) { + throw new Error("Unimplemented!"); + } + } + ; + + static _TagBaseAt = class extends this._TagBase { + get tag() { + return `@${this.tagName}`; + } + } + ; + + static _TagBaseHash = class extends this._TagBase { + get tag() { + return `#${this.tagName}`; + } + } + ; + + static _TagTextStyle = class extends this._TagBaseAt { + _getStripped(tag, text) { + return text; + } + } + ; + + static TagBoldShort = class extends this._TagTextStyle { + tagName = "b"; + } + ; + + static TagBoldLong = class extends this._TagTextStyle { + tagName = "bold"; + } + ; + + static TagItalicShort = class extends this._TagTextStyle { + tagName = "i"; + } + ; + + static TagItalicLong = class extends this._TagTextStyle { + tagName = "italic"; + } + ; + + static TagStrikethroughShort = class extends this._TagTextStyle { + tagName = "s"; + } + ; + + static TagStrikethroughLong = class extends this._TagTextStyle { + tagName = "strike"; + } + ; + + static TagUnderlineShort = class extends this._TagTextStyle { + tagName = "u"; + } + ; + + static TagUnderlineLong = class extends this._TagTextStyle { + tagName = "underline"; + } + ; + + static TagSup = class extends this._TagTextStyle { + tagName = "sup"; + } + ; + + static TagSub = class extends this._TagTextStyle { + tagName = "sub"; + } + ; + + static TagKbd = class extends this._TagTextStyle { + tagName = "kbd"; + } + ; + + static TagCode = class extends this._TagTextStyle { + tagName = "code"; + } + ; + + static TagStyle = class extends this._TagTextStyle { + tagName = "style"; + } + ; + + static TagFont = class extends this._TagTextStyle { + tagName = "font"; + } + ; + + static TagComic = class extends this._TagTextStyle { + tagName = "comic"; + } + ; + + static TagComicH1 = class extends this._TagTextStyle { + tagName = "comicH1"; + } + ; + + static TagComicH2 = class extends this._TagTextStyle { + tagName = "comicH2"; + } + ; + + static TagComicH3 = class extends this._TagTextStyle { + tagName = "comicH3"; + } + ; + + static TagComicH4 = class extends this._TagTextStyle { + tagName = "comicH4"; + } + ; + + static TagComicNote = class extends this._TagTextStyle { + tagName = "comicNote"; + } + ; + + static TagNote = class extends this._TagTextStyle { + tagName = "note"; + } + ; + + static TagTip = class extends this._TagTextStyle { + tagName = "tip"; + } + ; + + static TagUnit = class extends this._TagBaseAt { + tagName = "unit"; + + _getStripped(tag, text) { + const [amount,unitSingle,unitPlural] = Renderer.splitTagByPipe(text); + return isNaN(amount) ? unitSingle : Number(amount) > 1 ? (unitPlural || unitSingle.toPlural()) : unitSingle; + } + } + ; + + static TagHit = class extends this._TagBaseAt { + tagName = "h"; + + _getStripped(tag, text) { + return "Hit: "; + } + } + ; + + static TagMiss = class extends this._TagBaseAt { + tagName = "m"; + + _getStripped(tag, text) { + return "Miss: "; + } + } + ; + + static TagAtk = class extends this._TagBaseAt { + tagName = "atk"; + + _getStripped(tag, text) { + return Renderer.attackTagToFull(text); + } + } + ; + + static TagHitYourSpellAttack = class extends this._TagBaseAt { + tagName = "hitYourSpellAttack"; + + _getStripped(tag, text) { + const [displayText] = Renderer.splitTagByPipe(text); + return displayText || "your spell attack modifier"; + } + } + ; + + static TagDc = class extends this._TagBaseAt { + tagName = "dc"; + + _getStripped(tag, text) { + const [dcText,displayText] = Renderer.splitTagByPipe(text); + return `DC ${displayText || dcText}`; + } + } + ; + + static TagDcYourSpellSave = class extends this._TagBaseAt { + tagName = "dcYourSpellSave"; + + _getStripped(tag, text) { + const [displayText] = Renderer.splitTagByPipe(text); + return displayText || "your spell save DC"; + } + } + ; + + static _TagDiceFlavor = class extends this._TagBaseAt { + _getStripped(tag, text) { + const [rollText,displayText] = Renderer.splitTagByPipe(text); + switch (tag) { + case "@damage": + case "@dice": + case "@autodice": + { + return displayText || rollText.replace(/;/g, "/"); + } + case "@d20": + case "@hit": + { + return displayText || (()=>{ + const n = Number(rollText); + if (!isNaN(n)) + return `${n >= 0 ? "+" : ""}${n}`; + return rollText; + } + )(); + } + case "@recharge": + { + const asNum = Number(rollText || 6); + if (isNaN(asNum)) { + throw new Error(`Could not parse "${rollText}" as a number!`); + } + return `(Recharge ${asNum}${asNum < 6 ? `\u20136` : ""})`; + } + case "@chance": + { + return displayText || `${rollText} percent`; + } + case "@ability": + { + const [,rawScore] = rollText.split(" ").map(it=>it.trim().toLowerCase()).filter(Boolean); + const score = Number(rawScore) || 0; + return displayText || `${score} (${Parser.getAbilityModifier(score)})`; + } + case "@savingThrow": + case "@skillCheck": + { + return displayText || rollText; + } + } + throw new Error(`Unhandled tag: ${tag}`); + } + } + ; + + static TaChance = class extends this._TagDiceFlavor { + tagName = "chance"; + } + ; + + static TaD20 = class extends this._TagDiceFlavor { + tagName = "d20"; + } + ; + + static TaDamage = class extends this._TagDiceFlavor { + tagName = "damage"; + } + ; + + static TaDice = class extends this._TagDiceFlavor { + tagName = "dice"; + } + ; + + static TaAutodice = class extends this._TagDiceFlavor { + tagName = "autodice"; + } + ; + + static TaHit = class extends this._TagDiceFlavor { + tagName = "hit"; + } + ; + + static TaRecharge = class extends this._TagDiceFlavor { + tagName = "recharge"; + } + ; + + static TaAbility = class extends this._TagDiceFlavor { + tagName = "ability"; + } + ; + + static TaSavingThrow = class extends this._TagDiceFlavor { + tagName = "savingThrow"; + } + ; + + static TaSkillCheck = class extends this._TagDiceFlavor { + tagName = "skillCheck"; + } + ; + + static _TagDiceFlavorScaling = class extends this._TagBaseAt { + _getStripped(tag, text) { + const [,,addPerProgress,,displayText] = Renderer.splitTagByPipe(text); + return displayText || addPerProgress; + } + } + ; + + static TagScaledice = class extends this._TagDiceFlavorScaling { + tagName = "scaledice"; + } + ; + + static TagScaledamage = class extends this._TagDiceFlavorScaling { + tagName = "scaledamage"; + } + ; + + static TagCoinflip = class extends this._TagBaseAt { + tagName = "coinflip"; + + _getStripped(tag, text) { + const [displayText] = Renderer.splitTagByPipe(text); + return displayText || "flip a coin"; + } + } + ; + + static _TagPipedNoDisplayText = class extends this._TagBaseAt { + _getStripped(tag, text) { + const parts = Renderer.splitTagByPipe(text); + return parts[0]; + } + } + ; + + static Tag5etools = class extends this._TagPipedNoDisplayText { + tagName = "5etools"; + } + ; + + static TagAdventure = class extends this._TagPipedNoDisplayText { + tagName = "adventure"; + } + ; + + static TagBook = class extends this._TagPipedNoDisplayText { + tagName = "book"; + } + ; + + static TagFilter = class extends this._TagPipedNoDisplayText { + tagName = "filter"; + } + ; + + static TagFootnote = class extends this._TagPipedNoDisplayText { + tagName = "footnote"; + } + ; + + static TagLink = class extends this._TagPipedNoDisplayText { + tagName = "link"; + } + ; + + static TagLoader = class extends this._TagPipedNoDisplayText { + tagName = "loader"; + } + ; + + static TagColor = class extends this._TagPipedNoDisplayText { + tagName = "color"; + } + ; + + static TagHighlight = class extends this._TagPipedNoDisplayText { + tagName = "highlight"; + } + ; + + static TagHelp = class extends this._TagPipedNoDisplayText { + tagName = "help"; + } + ; + + static _TagPipedDisplayTextThird = class extends this._TagBaseAt { + _getStripped(tag, text) { + const parts = Renderer.splitTagByPipe(text); + return parts.length >= 3 ? parts[2] : parts[0]; + } + } + ; + + static TagAction = class extends this._TagPipedDisplayTextThird { + tagName = "action"; + defaultSource = Parser.SRC_PHB; + page = UrlUtil.PG_ACTIONS; + } + ; + + static TagBackground = class extends this._TagPipedDisplayTextThird { + tagName = "background"; + defaultSource = Parser.SRC_PHB; + page = UrlUtil.PG_BACKGROUNDS; + } + ; + + static TagBoon = class extends this._TagPipedDisplayTextThird { + tagName = "boon"; + defaultSource = Parser.SRC_MTF; + page = UrlUtil.PG_CULTS_BOONS; + } + ; + + static TagCharoption = class extends this._TagPipedDisplayTextThird { + tagName = "charoption"; + defaultSource = Parser.SRC_MOT; + page = UrlUtil.PG_CHAR_CREATION_OPTIONS; + } + ; + + static TagClass = class extends this._TagPipedDisplayTextThird { + tagName = "class"; + defaultSource = Parser.SRC_PHB; + page = UrlUtil.PG_CLASSES; + } + ; + + static TagCondition = class extends this._TagPipedDisplayTextThird { + tagName = "condition"; + defaultSource = Parser.SRC_PHB; + page = UrlUtil.PG_CONDITIONS_DISEASES; + } + ; + + static TagCreature = class extends this._TagPipedDisplayTextThird { + tagName = "creature"; + defaultSource = Parser.SRC_MM; + page = UrlUtil.PG_BESTIARY; + } + ; + + static TagCult = class extends this._TagPipedDisplayTextThird { + tagName = "cult"; + defaultSource = Parser.SRC_MTF; + page = UrlUtil.PG_CULTS_BOONS; + } + ; + + static TagDeck = class extends this._TagPipedDisplayTextThird { + tagName = "deck"; + defaultSource = Parser.SRC_DMG; + page = UrlUtil.PG_DECKS; + } + ; + + static TagDisease = class extends this._TagPipedDisplayTextThird { + tagName = "disease"; + defaultSource = Parser.SRC_DMG; + page = UrlUtil.PG_CONDITIONS_DISEASES; + } + ; + + static TagFeat = class extends this._TagPipedDisplayTextThird { + tagName = "feat"; + defaultSource = Parser.SRC_PHB; + page = UrlUtil.PG_FEATS; + } + ; + + static TagHazard = class extends this._TagPipedDisplayTextThird { + tagName = "hazard"; + defaultSource = Parser.SRC_DMG; + page = UrlUtil.PG_TRAPS_HAZARDS; + } + ; + + static TagItem = class extends this._TagPipedDisplayTextThird { + tagName = "item"; + defaultSource = Parser.SRC_DMG; + page = UrlUtil.PG_ITEMS; + } + ; + + static TagItemMastery = class extends this._TagPipedDisplayTextThird { + tagName = "itemMastery"; + defaultSource = VeCt.STR_GENERIC; + page = "itemMastery"; + } + ; + + static TagLanguage = class extends this._TagPipedDisplayTextThird { + tagName = "language"; + defaultSource = Parser.SRC_PHB; + page = UrlUtil.PG_LANGUAGES; + } + ; + + static TagLegroup = class extends this._TagPipedDisplayTextThird { + tagName = "legroup"; + defaultSource = Parser.SRC_MM; + page = "legendaryGroup"; + } + ; + + static TagObject = class extends this._TagPipedDisplayTextThird { + tagName = "object"; + defaultSource = Parser.SRC_DMG; + page = UrlUtil.PG_OBJECTS; + } + ; + + static TagOptfeature = class extends this._TagPipedDisplayTextThird { + tagName = "optfeature"; + defaultSource = Parser.SRC_PHB; + page = UrlUtil.PG_OPT_FEATURES; + } + ; + + static TagPsionic = class extends this._TagPipedDisplayTextThird { + tagName = "psionic"; + defaultSource = Parser.SRC_UATMC; + page = UrlUtil.PG_PSIONICS; + } + ; + + static TagRace = class extends this._TagPipedDisplayTextThird { + tagName = "race"; + defaultSource = Parser.SRC_PHB; + page = UrlUtil.PG_RACES; + } + ; + + static TagRecipe = class extends this._TagPipedDisplayTextThird { + tagName = "recipe"; + defaultSource = Parser.SRC_HF; + page = UrlUtil.PG_RECIPES; + } + ; + + static TagReward = class extends this._TagPipedDisplayTextThird { + tagName = "reward"; + defaultSource = Parser.SRC_DMG; + page = UrlUtil.PG_REWARDS; + } + ; + + static TagVehicle = class extends this._TagPipedDisplayTextThird { + tagName = "vehicle"; + defaultSource = Parser.SRC_GoS; + page = UrlUtil.PG_VEHICLES; + } + ; + + static TagVehupgrade = class extends this._TagPipedDisplayTextThird { + tagName = "vehupgrade"; + defaultSource = Parser.SRC_GoS; + page = UrlUtil.PG_VEHICLES; + } + ; + + static TagSense = class extends this._TagPipedDisplayTextThird { + tagName = "sense"; + defaultSource = Parser.SRC_PHB; + page = "sense"; + } + ; + + static TagSkill = class extends this._TagPipedDisplayTextThird { + tagName = "skill"; + defaultSource = Parser.SRC_PHB; + page = "skill"; + } + ; + + static TagSpell = class extends this._TagPipedDisplayTextThird { + tagName = "spell"; + defaultSource = Parser.SRC_PHB; + page = UrlUtil.PG_SPELLS; + } + ; + + static TagStatus = class extends this._TagPipedDisplayTextThird { + tagName = "status"; + defaultSource = Parser.SRC_PHB; + page = UrlUtil.PG_CONDITIONS_DISEASES; + } + ; + + static TagTable = class extends this._TagPipedDisplayTextThird { + tagName = "table"; + defaultSource = Parser.SRC_DMG; + page = UrlUtil.PG_TABLES; + } + ; + + static TagTrap = class extends this._TagPipedDisplayTextThird { + tagName = "trap"; + defaultSource = Parser.SRC_DMG; + page = UrlUtil.PG_TRAPS_HAZARDS; + } + ; + + static TagVariantrule = class extends this._TagPipedDisplayTextThird { + tagName = "variantrule"; + defaultSource = Parser.SRC_DMG; + page = UrlUtil.PG_VARIANTRULES; + } + ; + + static TagCite = class extends this._TagPipedDisplayTextThird { + tagName = "cite"; + defaultSource = Parser.SRC_PHB; + page = "citation"; + } + ; + + static _TagPipedDisplayTextFourth = class extends this._TagBaseAt { + _getStripped(tag, text) { + const parts = Renderer.splitTagByPipe(text); + return parts.length >= 4 ? parts[3] : parts[0]; + } + } + ; + + static TagCard = class extends this._TagPipedDisplayTextFourth { + tagName = "card"; + defaultSource = Parser.SRC_DMG; + page = "card"; + } + ; + + static TagDeity = class extends this._TagPipedDisplayTextFourth { + tagName = "deity"; + defaultSource = Parser.SRC_PHB; + page = UrlUtil.PG_DEITIES; + } + ; + + static _TagPipedDisplayTextSixth = class extends this._TagBaseAt { + _getStripped(tag, text) { + const parts = Renderer.splitTagByPipe(text); + return parts.length >= 6 ? parts[5] : parts[0]; + } + } + ; + + static TagClassFeature = class extends this._TagPipedDisplayTextSixth { + tagName = "classFeature"; + defaultSource = Parser.SRC_PHB; + page = UrlUtil.PG_CLASSES; + } + ; + + static _TagPipedDisplayTextEight = class extends this._TagBaseAt { + _getStripped(tag, text) { + const parts = Renderer.splitTagByPipe(text); + return parts.length >= 8 ? parts[7] : parts[0]; + } + } + ; + + static TagSubclassFeature = class extends this._TagPipedDisplayTextEight { + tagName = "subclassFeature"; + defaultSource = Parser.SRC_PHB; + page = UrlUtil.PG_CLASSES; + } + ; + + static TagQuickref = class extends this._TagBaseAt { + tagName = "quickref"; + defaultSource = Parser.SRC_PHB; + page = UrlUtil.PG_QUICKREF; + + _getStripped(tag, text) { + const {name, displayText} = DataUtil.quickreference.unpackUid(text); + return displayText || name; + } + } + ; + + static TagArea = class extends this._TagBaseAt { + tagName = "area"; + + _getStripped(tag, text) { + const [compactText,,flags] = Renderer.splitTagByPipe(text); + + return flags && flags.includes("x") ? compactText : `${flags && flags.includes("u") ? "A" : "a"}rea ${compactText}`; + } + + _getMeta(tag, text) { + const [compactText,areaId,flags] = Renderer.splitTagByPipe(text); + + const displayText = flags && flags.includes("x") ? compactText : `${flags && flags.includes("u") ? "A" : "a"}rea ${compactText}`; + + return { + areaId, + displayText, + }; + } + } + ; + + static TagHomebrew = class extends this._TagBaseAt { + tagName = "homebrew"; + + _getStripped(tag, text) { + const [newText,oldText] = Renderer.splitTagByPipe(text); + if (newText && oldText) { + return `${newText} [this is a homebrew addition, replacing the following: "${oldText}"]`; + } else if (newText) { + return `${newText} [this is a homebrew addition]`; + } else if (oldText) { + return `[the following text has been removed due to homebrew: ${oldText}]`; + } else + throw new Error(`Homebrew tag had neither old nor new text!`); + } + } + ; + + static TagItemEntry = class extends this._TagBaseHash { + tagName = "itemEntry"; + defaultSource = Parser.SRC_DMG; + } + ; + + static TAGS = [new this.TagBoldShort(), new this.TagBoldLong(), new this.TagItalicShort(), new this.TagItalicLong(), new this.TagStrikethroughShort(), new this.TagStrikethroughLong(), new this.TagUnderlineShort(), new this.TagUnderlineLong(), new this.TagSup(), new this.TagSub(), new this.TagKbd(), new this.TagCode(), new this.TagStyle(), new this.TagFont(), + new this.TagComic(), new this.TagComicH1(), new this.TagComicH2(), new this.TagComicH3(), new this.TagComicH4(), new this.TagComicNote(), + new this.TagNote(), new this.TagTip(), + new this.TagUnit(), + new this.TagHit(), new this.TagMiss(), + new this.TagAtk(), + new this.TagHitYourSpellAttack(), + new this.TagDc(), + new this.TagDcYourSpellSave(), + new this.TaChance(), new this.TaD20(), new this.TaDamage(), new this.TaDice(), new this.TaAutodice(), new this.TaHit(), new this.TaRecharge(), new this.TaAbility(), new this.TaSavingThrow(), new this.TaSkillCheck(), + new this.TagScaledice(), new this.TagScaledamage(), + new this.TagCoinflip(), + new this.Tag5etools(), new this.TagAdventure(), new this.TagBook(), new this.TagFilter(), new this.TagFootnote(), new this.TagLink(), new this.TagLoader(), new this.TagColor(), new this.TagHighlight(), new this.TagHelp(), + new this.TagQuickref(), + new this.TagArea(), + new this.TagAction(), new this.TagBackground(), new this.TagBoon(), new this.TagCharoption(), new this.TagClass(), new this.TagCondition(), new this.TagCreature(), new this.TagCult(), new this.TagDeck(), new this.TagDisease(), new this.TagFeat(), new this.TagHazard(), new this.TagItem(), new this.TagItemMastery(), new this.TagLanguage(), new this.TagLegroup(), new this.TagObject(), new this.TagOptfeature(), new this.TagPsionic(), new this.TagRace(), new this.TagRecipe(), new this.TagReward(), new this.TagVehicle(), new this.TagVehupgrade(), new this.TagSense(), new this.TagSkill(), new this.TagSpell(), new this.TagStatus(), new this.TagTable(), new this.TagTrap(), new this.TagVariantrule(), new this.TagCite(), + new this.TagCard(), new this.TagDeity(), + new this.TagClassFeature({ + tagName: "classFeature" + }), + new this.TagSubclassFeature({ + tagName: "subclassFeature" + }), + new this.TagHomebrew(), + new this.TagItemEntry(), ]; + + static TAG_LOOKUP = {}; + + static _init() { + this.TAGS.forEach(tag=>{ + this.TAG_LOOKUP[tag.tag] = tag; + this.TAG_LOOKUP[tag.tagName] = tag; + } + ); + + return null; + } + + static _ = this._init(); + + static getPage(tag) { + const tagInfo = this.TAG_LOOKUP[tag]; + return tagInfo?.page; + } +} +; + +Renderer.events = class { + static handleClick_copyCode(evt, ele) { + const $e = $(ele).parent().next("pre"); + MiscUtil.pCopyTextToClipboard($e.text()); + JqueryUtil.showCopiedEffect($e); + } + + static handleClick_toggleCodeWrap(evt, ele) { + const nxt = !StorageUtil.syncGet("rendererCodeWrap"); + StorageUtil.syncSet("rendererCodeWrap", nxt); + const $btn = $(ele).toggleClass("active", nxt); + const $e = $btn.parent().next("pre"); + $e.toggleClass("rd__pre-wrap", nxt); + } + + static bindGeneric({element=document.body}={}) { + const $ele = $(element).on("click", `[data-rd-data-embed-header]`, evt=>{ + Renderer.events.handleClick_dataEmbedHeader(evt, evt.currentTarget); + } + ); + + Renderer.events._HEADER_TOGGLE_CLICK_SELECTORS.forEach(selector=>{ + $ele.on("click", selector, evt=>{ + Renderer.events.handleClick_headerToggleButton(evt, evt.currentTarget, { + selector + }); + } + ); + } + ); + } + + static handleClick_dataEmbedHeader(evt, ele) { + evt.stopPropagation(); + evt.preventDefault(); + + const $ele = $(ele); + $ele.find(".rd__data-embed-name").toggleVe(); + $ele.find(".rd__data-embed-toggle").text($ele.text().includes("+") ? "[\u2013]" : "[+]"); + $ele.closest("table").find("tbody").toggleVe(); + } + + static _HEADER_TOGGLE_CLICK_SELECTORS = [`[data-rd-h-toggle-button]`, `[data-rd-h-special-toggle-button]`, ]; + + static handleClick_headerToggleButton(evt, ele, {selector=false}={}) { + evt.stopPropagation(); + evt.preventDefault(); + + const isShow = this._handleClick_headerToggleButton_doToggleEle(ele, { + selector + }); + + if (!EventUtil.isCtrlMetaKey(evt)) + return; + + Renderer.events._HEADER_TOGGLE_CLICK_SELECTORS.forEach(selector=>{ + [...document.querySelectorAll(selector)].filter(eleOther=>eleOther !== ele).forEach(eleOther=>{ + Renderer.events._handleClick_headerToggleButton_doToggleEle(eleOther, { + selector, + force: isShow + }); + } + ); + } + ); + } + + static _handleClick_headerToggleButton_doToggleEle(ele, {selector=false, force=null}={}) { + const isShow = force != null ? force : ele.innerHTML.includes("+"); + + let eleNxt = ele.closest(".rd__h").nextElementSibling; + + while (eleNxt) { + if (eleNxt.classList.contains("float-clear")) { + eleNxt = eleNxt.nextElementSibling; + continue; + } + + if (selector !== `[data-rd-h-special-toggle-button]`) { + const eleToCheck = Renderer.events._handleClick_headerToggleButton_getEleToCheck(eleNxt); + if (eleToCheck.classList.contains("rd__b-special") || (eleToCheck.classList.contains("rd__h") && !eleToCheck.classList.contains("rd__h--3")) || (eleToCheck.classList.contains("rd__b") && !eleToCheck.classList.contains("rd__b--3"))) + break; + } + + eleNxt.classList.toggle("rd__ele-toggled-hidden", !isShow); + eleNxt = eleNxt.nextElementSibling; + } + + ele.innerHTML = isShow ? "[\u2013]" : "[+]"; + + return isShow; + } + + static _handleClick_headerToggleButton_getEleToCheck(eleNxt) { + if (eleNxt.type === 3) + return eleNxt; + if (!eleNxt.classList.contains("rd__b") || eleNxt.classList.contains("rd__b--3")) + return eleNxt; + const childNodes = [...eleNxt.childNodes].filter(it=>(it.type === 3 && (it.textContent || "").trim()) || it.type !== 3); + if (childNodes.length !== 1) + return eleNxt; + if (childNodes[0].classList.contains("rd__b")) + return Renderer.events._handleClick_headerToggleButton_getEleToCheck(childNodes[0]); + return eleNxt; + } + + static handleLoad_inlineStatblock(ele) { + const observer = Renderer.utils.lazy.getCreateObserver({ + observerId: "inlineStatblock", + fnOnObserve: Renderer.events._handleLoad_inlineStatblock_fnOnObserve.bind(Renderer.events), + }); + + observer.track(ele.parentNode); + } + + static _handleLoad_inlineStatblock_fnOnObserve({entry}) { + const ele = entry.target; + + const tag = ele.dataset.rdTag.uq(); + const page = ele.dataset.rdPage.uq(); + const source = ele.dataset.rdSource.uq(); + const name = ele.dataset.rdName.uq(); + const displayName = ele.dataset.rdDisplayName.uq(); + const hash = ele.dataset.rdHash.uq(); + const style = ele.dataset.rdStyle.uq(); + + DataLoader.pCacheAndGet(page, Parser.getTagSource(tag, source), hash).then(toRender=>{ + const tr = ele.closest("tr"); + + if (!toRender) { + tr.innerHTML = `Failed to load ${tag ? Renderer.get().render(`{@${tag} ${name}|${source}${displayName ? `|${displayName}` : ""}}`) : displayName || name}!`; + throw new Error(`Could not find tag: "${tag}" (page/prop: "${page}") hash: "${hash}"`); + } + + const headerName = displayName || (name ?? toRender.name ?? (toRender.entries?.length ? toRender.entries?.[0]?.name : "(Unknown)")); + + const fnRender = Renderer.hover.getFnRenderCompact(page); + const tbl = tr.closest("table"); + const nxt = e_({ + outer: Renderer.utils.getEmbeddedDataHeader(headerName, style) + fnRender(toRender, { + isEmbeddedEntity: true + }) + Renderer.utils.getEmbeddedDataFooter(), + }); + tbl.parentNode.replaceChild(nxt, tbl, ); + + const nxtTgt = nxt.querySelector(`[data-rd-embedded-data-render-target="true"]`); + + const fnBind = Renderer.hover.getFnBindListenersCompact(page); + if (fnBind) + fnBind(toRender, nxtTgt); + } + ); + } +} +; + +Renderer.feat = class { + static _mergeAbilityIncrease_getListItemText(abilityObj) { + return Renderer.feat._mergeAbilityIncrease_getText(abilityObj); + } + + static _mergeAbilityIncrease_getListItemItem(abilityObj) { + return { + type: "item", + name: "Ability Score Increase.", + entry: Renderer.feat._mergeAbilityIncrease_getText(abilityObj), + }; + } + + static _mergeAbilityIncrease_getText(abilityObj) { + const maxScore = abilityObj.max ?? 20; + + if (!abilityObj.choose) { + return Object.keys(abilityObj).filter(k=>k !== "max").map(ab=>`Increase your ${Parser.attAbvToFull(ab)} score by ${abilityObj[ab]}, to a maximum of ${maxScore}.`).join(" "); + } + + if (abilityObj.choose.from.length === 6) { + return abilityObj.choose.entry ? Renderer.get().render(abilityObj.choose.entry) : `Increase one ability score of your choice by ${abilityObj.choose.amount ?? 1}, to a maximum of ${maxScore}.`; + } + + const abbChoicesText = abilityObj.choose.from.map(it=>Parser.attAbvToFull(it)).joinConjunct(", ", " or "); + return `Increase your ${abbChoicesText} by ${abilityObj.choose.amount ?? 1}, to a maximum of ${maxScore}.`; + } + + static initFullEntries(feat) { + if (!feat.ability || feat._fullEntries || !feat.ability.length) + return; + + const abilsToDisplay = feat.ability.filter(it=>!it.hidden); + if (!abilsToDisplay.length) + return; + + Renderer.utils.initFullEntries_(feat); + + const targetList = feat._fullEntries.find(e=>e.type === "list"); + + if (targetList && targetList.items.every(it=>it.type === "item")) { + abilsToDisplay.forEach(abilObj=>targetList.items.unshift(Renderer.feat._mergeAbilityIncrease_getListItemItem(abilObj))); + return; + } + + if (targetList) { + abilsToDisplay.forEach(abilObj=>targetList.items.unshift(Renderer.feat._mergeAbilityIncrease_getListItemText(abilObj))); + return; + } + + abilsToDisplay.forEach(abilObj=>feat._fullEntries.unshift(Renderer.feat._mergeAbilityIncrease_getListItemText(abilObj))); + + setTimeout(()=>{ + throw new Error(`Could not find object of type "list" in "entries" for feat "${feat.name}" from source "${feat.source}" when merging ability scores! Reformat the feat to include a "list"-type entry.`); + } + , 1); + } + + static getFeatRendereableEntriesMeta(ent) { + Renderer.feat.initFullEntries(ent); + return { + entryMain: { + entries: ent._fullEntries || ent.entries + }, + }; + } + + static getJoinedCategoryPrerequisites(category, rdPrereqs) { + const ptCategory = category ? `${category.toTitleCase()} Feat` : ""; + + return ptCategory && rdPrereqs ? `${ptCategory} (${rdPrereqs})` : (ptCategory || rdPrereqs); + } + + static getCompactRenderedString(feat, opts) { + opts = opts || {}; + + const renderer = Renderer.get().setFirstSection(true); + const renderStack = []; + + const ptCategoryPrerequisite = Renderer.feat.getJoinedCategoryPrerequisites(feat.category, Renderer.utils.prerequisite.getHtml(feat.prerequisite), ); + const ptRepeatable = Renderer.utils.getRepeatableHtml(feat); + + renderStack.push(` + ${Renderer.utils.getExcludedTr({ + entity: feat, + dataProp: "feat", + page: UrlUtil.PG_FEATS + })} + ${opts.isSkipNameRow ? "" : Renderer.utils.getNameTr(feat, { + page: UrlUtil.PG_FEATS + })} + + ${ptCategoryPrerequisite ? `

            ${ptCategoryPrerequisite}

            ` : ""} + ${ptRepeatable ? `

            ${ptRepeatable}

            ` : ""} + `); + renderer.recursiveRender(Renderer.feat.getFeatRendereableEntriesMeta(feat)?.entryMain, renderStack, { + depth: 2 + }); + renderStack.push(``); + + return renderStack.join(""); + } + + static pGetFluff(feat) { + return Renderer.utils.pGetFluff({ + entity: feat, + fnGetFluffData: DataUtil.featFluff.loadJSON.bind(DataUtil.featFluff), + fluffProp: "featFluff", + }); + } +} +; + +Renderer.class = class { + static getCompactRenderedString(cls) { + if (cls.__prop === "subclass") + return Renderer.subclass.getCompactRenderedString(cls); + + const clsEntry = { + type: "section", + name: cls.name, + source: cls.source, + page: cls.page, + entries: MiscUtil.copyFast((cls.classFeatures || []).flat()), + }; + + return Renderer.hover.getGenericCompactRenderedString(clsEntry); + } + + static getHitDiceEntry(clsHd) { + return clsHd ? { + toRoll: `${clsHd.number}d${clsHd.faces}`, + rollable: true + } : null; + } + static getHitPointsAtFirstLevel(clsHd) { + return clsHd ? `${clsHd.number * clsHd.faces} + your Constitution modifier` : null; + } + static getHitPointsAtHigherLevels(className, clsHd, hdEntry) { + return className && clsHd && hdEntry ? `${Renderer.getEntryDice(hdEntry, "Hit die")} (or + ${((clsHd.number * clsHd.faces) / 2 + 1)}) + your Constitution modifier per ${className} level after 1st` : null; + } + + static getRenderedArmorProfs(armorProfs) { + return armorProfs.map(a=>Renderer.get().render(a.full ? a.full : a === "light" || a === "medium" || a === "heavy" ? `{@filter ${a} armor|items|type=${a} armor}` : a)).join(", "); + } + static getRenderedWeaponProfs(weaponProfs) { + return weaponProfs.map(w=>Renderer.get().render(w === "simple" || w === "martial" ? `{@filter ${w} weapons|items|type=${w} weapon}` : w.optional ? `${w.proficiency}` : w)).join(", "); + } + static getRenderedToolProfs(toolProfs) { + return toolProfs.map(it=>Renderer.get().render(it)).join(", "); + } + static getRenderedSkillProfs(skills) { + return `${Parser.skillProficienciesToFull(skills).uppercaseFirst()}.`; + } + + static getWalkerFilterDereferencedFeatures() { + return MiscUtil.getWalker({ + keyBlocklist: MiscUtil.GENERIC_WALKER_ENTRIES_KEY_BLOCKLIST, + isAllowDeleteObjects: true, + isDepthFirst: true, + }); + } + + static mutFilterDereferencedClassFeatures({walker, cpyCls, pageFilter, filterValues, isUseSubclassSources=false, }, ) { + walker = walker || Renderer.class.getWalkerFilterDereferencedFeatures(); + + cpyCls.classFeatures = cpyCls.classFeatures.map((lvlFeatures,ixLvl)=>{ + return walker.walk(lvlFeatures, { + object: (obj)=>{ + if (!obj.source) + return obj; + const fText = obj.isClassFeatureVariant ? { + isClassFeatureVariant: true + } : null; + + const isDisplay = [obj.source, ...(obj.otherSources || []).map(it=>it.source)].some(src=>pageFilter.filterBox.toDisplayByFilters(filterValues, ...[{ + filter: pageFilter.sourceFilter, + value: isUseSubclassSources && src === cpyCls.source ? pageFilter.getActiveSource(filterValues) : src, + }, pageFilter.levelFilter ? { + filter: pageFilter.levelFilter, + value: ixLvl + 1, + } : null, { + filter: pageFilter.optionsFilter, + value: fText, + }, ].filter(Boolean), )); + + return isDisplay ? obj : null; + } + , + array: (arr)=>{ + return arr.filter(it=>it != null); + } + , + }, ); + } + ); + } + + static mutFilterDereferencedSubclassFeatures({walker, cpySc, pageFilter, filterValues, }, ) { + walker = walker || Renderer.class.getWalkerFilterDereferencedFeatures(); + + cpySc.subclassFeatures = cpySc.subclassFeatures.map(lvlFeatures=>{ + const level = CollectionUtil.bfs(lvlFeatures, { + prop: "level" + }); + + return walker.walk(lvlFeatures, { + object: (obj)=>{ + if (obj.entries && !obj.entries.length) + return null; + if (!obj.source) + return obj; + const fText = obj.isClassFeatureVariant ? { + isClassFeatureVariant: true + } : null; + + const isDisplay = [obj.source, ...(obj.otherSources || []).map(it=>it.source)].some(src=>pageFilter.filterBox.toDisplayByFilters(filterValues, ...[{ + filter: pageFilter.sourceFilter, + value: src, + }, pageFilter.levelFilter ? { + filter: pageFilter.levelFilter, + value: level, + } : null, { + filter: pageFilter.optionsFilter, + value: fText, + }, ].filter(Boolean), )); + + return isDisplay ? obj : null; + } + , + array: (arr)=>{ + return arr.filter(it=>it != null); + } + , + }, ); + } + ); + } +} +; + +Renderer.subclass = class { + static getCompactRenderedString(sc) { + const scEntry = { + type: "section", + name: sc.name, + source: sc.source, + page: sc.page, + entries: MiscUtil.copyFast((sc.subclassFeatures || []).flat()), + }; + + return Renderer.hover.getGenericCompactRenderedString(scEntry); + } +} +; + +Renderer.spell = class { + static getCompactRenderedString(spell, opts) { + opts = opts || {}; + + const renderer = Renderer.get(); + const renderStack = []; + + renderStack.push(` + ${Renderer.utils.getExcludedTr({ + entity: spell, + dataProp: "spell", + page: UrlUtil.PG_SPELLS + })} + ${Renderer.utils.getNameTr(spell, { + page: UrlUtil.PG_SPELLS, + isEmbeddedEntity: opts.isEmbeddedEntity + })} + + + + + + + + + + + + + + + + + + + + + + +
            LevelSchoolCasting TimeRange
            ${Parser.spLevelToFull(spell.level)}${Parser.spMetaToFull(spell.meta)}${Parser.spSchoolAndSubschoolsAbvsToFull(spell.school, spell.subschools)}${Parser.spTimeListToFull(spell.time)}${Parser.spRangeToFull(spell.range)}
            ComponentsDuration
            ${Parser.spComponentsToFull(spell.components, spell.level)}${Parser.spDurationToFull(spell.duration)}
            + + `); + + renderStack.push(``); + const entryList = { + type: "entries", + entries: spell.entries + }; + renderer.recursiveRender(entryList, renderStack, { + depth: 1 + }); + if (spell.entriesHigherLevel) { + const higherLevelsEntryList = { + type: "entries", + entries: spell.entriesHigherLevel + }; + renderer.recursiveRender(higherLevelsEntryList, renderStack, { + depth: 2 + }); + } + const fromClassList = Renderer.spell.getCombinedClasses(spell, "fromClassList"); + if (fromClassList.length) { + const [current] = Parser.spClassesToCurrentAndLegacy(fromClassList); + renderStack.push(`
            Classes: ${Parser.spMainClassesToFull(current)}
            `); + } + renderStack.push(``); + + return renderStack.join(""); + } + + static _SpellSourceManager = class { + _cache = null; + + populate({brew, isForce=false}) { + if (this._cache && !isForce) + return; + + this._cache = { + classes: {}, + + groups: {}, + + races: {}, + backgrounds: {}, + feats: {}, + optionalfeatures: {}, + }; + + (brew.class || []).forEach(c=>{ + c.source = c.source || Parser.SRC_PHB; + + (c.classSpells || []).forEach(itm=>{ + this._populate_fromClass_classSubclass({ + itm, + className: c.name, + classSource: c.source, + }); + + this._populate_fromClass_group({ + itm, + className: c.name, + classSource: c.source, + }); + } + ); + } + ); + + (brew.subclass || []).forEach(sc=>{ + sc.classSource = sc.classSource || Parser.SRC_PHB; + sc.shortName = sc.shortName || sc.name; + sc.source = sc.source || sc.classSource; + + (sc.subclassSpells || []).forEach(itm=>{ + this._populate_fromClass_classSubclass({ + itm, + className: sc.className, + classSource: sc.classSource, + subclassShortName: sc.shortName, + subclassName: sc.name, + subclassSource: sc.source, + }); + + this._populate_fromClass_group({ + itm, + className: sc.className, + classSource: sc.classSource, + subclassShortName: sc.shortName, + subclassName: sc.name, + subclassSource: sc.source, + }); + } + ); + + Object.entries(sc.subSubclassSpells || {}).forEach(([subSubclassName,arr])=>{ + arr.forEach(itm=>{ + this._populate_fromClass_classSubclass({ + itm, + className: sc.className, + classSource: sc.classSource, + subclassShortName: sc.shortName, + subclassName: sc.name, + subclassSource: sc.source, + subSubclassName, + }); + + this._populate_fromClass_group({ + itm, + className: sc.className, + classSource: sc.classSource, + subclassShortName: sc.shortName, + subclassName: sc.name, + subclassSource: sc.source, + subSubclassName, + }); + } + ); + } + ); + } + ); + + (brew.spellList || []).forEach(spellList=>this._populate_fromGroup_group({ + spellList + })); + } + + _populate_fromClass_classSubclass({itm, className, classSource, subclassShortName, subclassName, subclassSource, subSubclassName, }, ) { + if (itm.groupName) + return; + + if (itm.className) { + return this._populate_fromClass_doAdd({ + tgt: MiscUtil.getOrSet(this._cache.classes, "class", (itm.classSource || Parser.SRC_PHB).toLowerCase(), itm.className.toLowerCase(), {}, ), + className, + classSource, + subclassShortName, + subclassName, + subclassSource, + subSubclassName, + }); + } + + let[name,source] = `${itm}`.toLowerCase().split("|"); + source = source || Parser.SRC_PHB.toLowerCase(); + + this._populate_fromClass_doAdd({ + tgt: MiscUtil.getOrSet(this._cache.classes, "spell", source, name, { + fromClassList: [], + fromSubclass: [] + }, ), + className, + classSource, + subclassShortName, + subclassName, + subclassSource, + subSubclassName, + }); + } + + _populate_fromClass_doAdd({tgt, className, classSource, subclassShortName, subclassName, subclassSource, subSubclassName, schools, }, ) { + if (subclassShortName) { + const toAdd = { + class: { + name: className, + source: classSource + }, + subclass: { + name: subclassName || subclassShortName, + shortName: subclassShortName, + source: subclassSource + }, + }; + if (subSubclassName) + toAdd.subclass.subSubclass = subSubclassName; + if (schools) + toAdd.schools = schools; + + tgt.fromSubclass = tgt.fromSubclass || []; + tgt.fromSubclass.push(toAdd); + return; + } + + const toAdd = { + name: className, + source: classSource + }; + if (schools) + toAdd.schools = schools; + + tgt.fromClassList = tgt.fromClassList || []; + tgt.fromClassList.push(toAdd); + } + + _populate_fromClass_group({itm, className, classSource, subclassShortName, subclassName, subclassSource, subSubclassName, }, ) { + if (!itm.groupName) + return; + + return this._populate_fromClass_doAdd({ + tgt: MiscUtil.getOrSet(this._cache.classes, "group", (itm.groupSource || Parser.SRC_PHB).toLowerCase(), itm.groupName.toLowerCase(), {}, ), + className, + classSource, + subclassShortName, + subclassName, + subclassSource, + subSubclassName, + schools: itm.spellSchools, + }); + } + + _populate_fromGroup_group({spellList, }, ) { + const spellListSourceLower = (spellList.source || "").toLowerCase(); + const spellListNameLower = (spellList.name || "").toLowerCase(); + + spellList.spells.forEach(spell=>{ + if (typeof spell === "string") { + const {name, source} = DataUtil.proxy.unpackUid("spell", spell, "spell", { + isLower: true + }); + return MiscUtil.set(this._cache.groups, "spell", source, name, spellListSourceLower, spellListNameLower, { + name: spellList.name, + source: spellList.source + }); + } + + throw new Error(`Grouping spells based on other spell lists is not yet supported!`); + } + ); + } + + mutateSpell({spell: sp, lowName, lowSource}) { + lowName = lowName || sp.name.toLowerCase(); + lowSource = lowSource || sp.source.toLowerCase(); + + this._mutateSpell_brewGeneric({ + sp, + lowName, + lowSource, + propSpell: "races", + prop: "race" + }); + this._mutateSpell_brewGeneric({ + sp, + lowName, + lowSource, + propSpell: "backgrounds", + prop: "background" + }); + this._mutateSpell_brewGeneric({ + sp, + lowName, + lowSource, + propSpell: "feats", + prop: "feat" + }); + this._mutateSpell_brewGeneric({ + sp, + lowName, + lowSource, + propSpell: "optionalfeatures", + prop: "optionalfeature" + }); + this._mutateSpell_brewGroup({ + sp, + lowName, + lowSource + }); + this._mutateSpell_brewClassesSubclasses({ + sp, + lowName, + lowSource + }); + } + + _mutateSpell_brewClassesSubclasses({sp, lowName, lowSource}) { + if (!this._cache?.classes) + return; + + if (this._cache.classes.spell?.[lowSource]?.[lowName]?.fromClassList?.length) { + sp._tmpClasses.fromClassList = sp._tmpClasses.fromClassList || []; + sp._tmpClasses.fromClassList.push(...this._cache.classes.spell[lowSource][lowName].fromClassList); + } + + if (this._cache.classes.spell?.[lowSource]?.[lowName]?.fromSubclass?.length) { + sp._tmpClasses.fromSubclass = sp._tmpClasses.fromSubclass || []; + sp._tmpClasses.fromSubclass.push(...this._cache.classes.spell[lowSource][lowName].fromSubclass); + } + + if (this._cache.classes.class && sp.classes?.fromClassList) { + (sp._tmpClasses = sp._tmpClasses || {}).fromClassList = sp._tmpClasses.fromClassList || []; + + outer: for (const srcLower in this._cache.classes.class) { + const searchForClasses = this._cache.classes.class[srcLower]; + + for (const clsLowName in searchForClasses) { + const spellHasClass = sp.classes?.fromClassList?.some(cls=>(cls.source || "").toLowerCase() === srcLower && cls.name.toLowerCase() === clsLowName); + if (!spellHasClass) + continue; + + const fromDetails = searchForClasses[clsLowName]; + + if (fromDetails.fromClassList) { + sp._tmpClasses.fromClassList.push(...this._mutateSpell_getListFilteredBySchool({ + sp, + arr: fromDetails.fromClassList + })); + } + + if (fromDetails.fromSubclass) { + sp._tmpClasses.fromSubclass = sp._tmpClasses.fromSubclass || []; + sp._tmpClasses.fromSubclass.push(...this._mutateSpell_getListFilteredBySchool({ + sp, + arr: fromDetails.fromSubclass + })); + } + + break outer; + } + } + } + + if (this._cache.classes.group && (sp.groups?.length || sp._tmpGroups?.length)) { + const groups = Renderer.spell.getCombinedGeneric(sp, { + propSpell: "groups" + }); + + (sp._tmpClasses = sp._tmpClasses || {}).fromClassList = sp._tmpClasses.fromClassList || []; + + outer: for (const srcLower in this._cache.classes.group) { + const searchForGroups = this._cache.classes.group[srcLower]; + + for (const groupLowName in searchForGroups) { + const spellHasGroup = groups?.some(grp=>(grp.source || "").toLowerCase() === srcLower && grp.name.toLowerCase() === groupLowName); + if (!spellHasGroup) + continue; + + const fromDetails = searchForGroups[groupLowName]; + + if (fromDetails.fromClassList) { + sp._tmpClasses.fromClassList.push(...this._mutateSpell_getListFilteredBySchool({ + sp, + arr: fromDetails.fromClassList + })); + } + + if (fromDetails.fromSubclass) { + sp._tmpClasses.fromSubclass = sp._tmpClasses.fromSubclass || []; + sp._tmpClasses.fromSubclass.push(...this._mutateSpell_getListFilteredBySchool({ + sp, + arr: fromDetails.fromSubclass + })); + } + + break outer; + } + } + } + } + + _mutateSpell_getListFilteredBySchool({arr, sp}) { + return arr.filter(it=>{ + if (!it.schools) + return true; + return it.schools.includes(sp.school); + } + ).map(it=>{ + if (!it.schools) + return it; + const out = MiscUtil.copyFast(it); + delete it.schools; + return it; + } + ); + } + + _mutateSpell_brewGeneric({sp, lowName, lowSource, propSpell, prop}) { + if (!this._cache?.[propSpell]) + return; + + const propTmp = `_tmp${propSpell.uppercaseFirst()}`; + + if (this._cache[propSpell]?.spell?.[lowSource]?.[lowName]?.length) { + (sp[propTmp] = sp[propTmp] || []).push(...this._cache[propSpell].spell[lowSource][lowName]); + } + + if (this._cache?.[propSpell]?.[prop] && sp[propSpell]) { + sp[propTmp] = sp[propTmp] || []; + + outer: for (const srcLower in this._cache[propSpell][prop]) { + const searchForExisting = this._cache[propSpell][prop][srcLower]; + + for (const lowName in searchForExisting) { + const spellHasEnt = sp[propSpell].some(it=>(it.source || "").toLowerCase() === srcLower && it.name.toLowerCase() === lowName); + if (!spellHasEnt) + continue; + + const fromDetails = searchForExisting[lowName]; + + sp[propTmp].push(...fromDetails); + + break outer; + } + } + } + } + + _mutateSpell_brewGroup({sp, lowName, lowSource}) { + if (!this._cache?.groups) + return; + + if (this._cache.groups.spell?.[lowSource]?.[lowName]) { + Object.values(this._cache.groups.spell[lowSource][lowName]).forEach(bySource=>{ + Object.values(bySource).forEach(byName=>{ + sp._tmpGroups.push(byName); + } + ); + } + ); + } + + } + } + ; + + static populatePrereleaseLookup(brew, {isForce=false}={}) { + Renderer.spell._spellSourceManagerPrerelease.populate({ + brew, + isForce + }); + } + + static populateBrewLookup(brew, {isForce=false}={}) { + Renderer.spell._spellSourceManagerBrew.populate({ + brew, + isForce + }); + } + + static prePopulateHover(data) { + (data.spell || []).forEach(sp=>Renderer.spell.initBrewSources(sp)); + } + + static prePopulateHoverPrerelease(data) { + Renderer.spell.populatePrereleaseLookup(data); + } + + static prePopulateHoverBrew(data) { + Renderer.spell.populateBrewLookup(data); + } + + static _BREW_SOURCES_TMP_PROPS = ["_tmpSourcesInit", "_tmpClasses", "_tmpRaces", "_tmpBackgrounds", "_tmpFeats", "_tmpOptionalfeatures", "_tmpGroups", ]; + static uninitBrewSources(sp) { + Renderer.spell._BREW_SOURCES_TMP_PROPS.forEach(prop=>delete sp[prop]); + } + + static initBrewSources(sp) { + if (sp._tmpSourcesInit) + return; + sp._tmpSourcesInit = true; + + sp._tmpClasses = {}; + sp._tmpRaces = []; + sp._tmpBackgrounds = []; + sp._tmpFeats = []; + sp._tmpOptionalfeatures = []; + sp._tmpGroups = []; + + const lowName = sp.name.toLowerCase(); + const lowSource = sp.source.toLowerCase(); + + for (const manager of [Renderer.spell._spellSourceManagerPrerelease, Renderer.spell._spellSourceManagerBrew]) { + manager.mutateSpell({ + spell: sp, + lowName, + lowSource + }); + } + } + + static getCombinedClasses(sp, prop) { + return [...((sp.classes || {})[prop] || []), ...((sp._tmpClasses || {})[prop] || []), ].filter(it=>{ + if (!ExcludeUtil.isInitialised) + return true; + + switch (prop) { + case "fromClassList": + case "fromClassListVariant": + { + const hash = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_CLASSES](it); + if (ExcludeUtil.isExcluded(hash, "class", it.source, { + isNoCount: true + })) + return false; + + if (prop !== "fromClassListVariant") + return true; + if (it.definedInSource) + return !ExcludeUtil.isExcluded("*", "classFeature", it.definedInSource, { + isNoCount: true + }); + + return true; + } + case "fromSubclass": + case "fromSubclassVariant": + { + const hash = UrlUtil.URL_TO_HASH_BUILDER["subclass"]({ + name: it.subclass.name, + shortName: it.subclass.shortName, + source: it.subclass.source, + className: it.class.name, + classSource: it.class.source, + }); + + if (prop !== "fromSubclassVariant") + return !ExcludeUtil.isExcluded(hash, "subclass", it.subclass.source, { + isNoCount: true + }); + if (it.class.definedInSource) + return !Renderer.spell.isExcludedSubclassVariantSource({ + classDefinedInSource: it.class.definedInSource + }); + + return true; + } + default: + throw new Error(`Unhandled prop "${prop}"`); + } + } + ); + } + + static isExcludedSubclassVariantSource({classDefinedInSource, subclassDefinedInSource}) { + return (classDefinedInSource != null && ExcludeUtil.isExcluded("*", "classFeature", classDefinedInSource, { + isNoCount: true + })) || (subclassDefinedInSource != null && ExcludeUtil.isExcluded("*", "subclassFeature", subclassDefinedInSource, { + isNoCount: true + })); + } + + static getCombinedGeneric(sp, {propSpell, prop}) { + const propSpellTmp = `_tmp${propSpell.uppercaseFirst()}`; + return [...(sp[propSpell] || []), ...(sp[propSpellTmp] || []), ].filter(it=>{ + if (!ExcludeUtil.isInitialised || !prop) + return true; + const hash = UrlUtil.URL_TO_HASH_BUILDER[prop](it); + return !ExcludeUtil.isExcluded(hash, prop, it.source, { + isNoCount: true + }); + } + ).sort(SortUtil.ascSortGenericEntity.bind(SortUtil)); + } + + static pGetFluff(sp) { + return Renderer.utils.pGetFluff({ + entity: sp, + fluffBaseUrl: `data/spells/`, + fluffProp: "spellFluff", + }); + } +} +; + +Renderer.spell._spellSourceManagerPrerelease = new Renderer.spell._SpellSourceManager(); +Renderer.spell._spellSourceManagerBrew = new Renderer.spell._SpellSourceManager(); + +Renderer.condition = class { + static getCompactRenderedString(cond) { + const renderer = Renderer.get(); + const renderStack = []; + + renderStack.push(` + ${Renderer.utils.getExcludedTr({ + entity: cond, + dataProp: cond.__prop || cond._type, + page: UrlUtil.PG_CONDITIONS_DISEASES + })} + ${Renderer.utils.getNameTr(cond, { + page: UrlUtil.PG_CONDITIONS_DISEASES + })} + + `); + renderer.recursiveRender({ + entries: cond.entries + }, renderStack); + renderStack.push(``); + + return renderStack.join(""); + } + + static pGetFluff(it) { + return Renderer.utils.pGetFluff({ + entity: it, + fnGetFluffData: it.__prop === "condition" ? DataUtil.conditionFluff.loadJSON.bind(DataUtil.conditionFluff) : null, + fluffProp: it.__prop === "condition" ? "conditionFluff" : "diseaseFluff", + }); + } +} +; + +Renderer.background = class { + static getCompactRenderedString(bg) { + return Renderer.generic.getCompactRenderedString(bg, { + dataProp: "background", + page: UrlUtil.PG_BACKGROUNDS, + }, ); + } + + static pGetFluff(bg) { + return Renderer.utils.pGetFluff({ + entity: bg, + fnGetFluffData: DataUtil.backgroundFluff.loadJSON.bind(DataUtil.backgroundFluff), + fluffProp: "backgroundFluff", + }); + } +} +; + +Renderer.backgroundFeature = class { + static getCompactRenderedString(ent) { + return Renderer.generic.getCompactRenderedString(ent); + } +} +; + +Renderer.optionalfeature = class { + static getListPrerequisiteLevelText(prerequisites) { + if (!prerequisites || !prerequisites.some(it=>it.level)) + return "\u2014"; + const levelPart = prerequisites.find(it=>it.level).level; + return levelPart.level || levelPart; + } + + static getPreviouslyPrintedEntry(ent) { + if (!ent.previousVersion) + return null; + return `{@i An earlier version of this ${ent.featureType.map(t=>Parser.optFeatureTypeToFull(t)).join("/")} is available in }${Parser.sourceJsonToFull(ent.previousVersion.source)} {@i as {@optfeature ${ent.previousVersion.name}|${ent.previousVersion.source}}.}`; + } + + static getTypeEntry(ent) { + return `{@note Type: ${Renderer.optionalfeature.getTypeText(ent)}}`; + } + + static getCostEntry(ent) { + if (!ent.consumes?.name) + return null; + + const ptPrefix = "Cost: "; + const tksUnit = ent.consumes.name.split(" ").map(it=>it.trim()).filter(Boolean); + tksUnit.last(tksUnit.last()[ent.consumes.amount != null && ent.consumes.amount !== 1 ? "toPlural" : "toString"]()); + const ptUnit = ` ${tksUnit.join(" ")}`; + + if (ent.consumes?.amountMin != null && ent.consumes?.amountMax != null) + return `{@i ${ptPrefix}${ent.consumes.amountMin}\u2013${ent.consumes.amountMax}${ptUnit}}`; + return `{@i ${ptPrefix}${ent.consumes.amount ?? 1}${ptUnit}}`; + } + + static getPreviouslyPrintedText(ent) { + const entry = Renderer.optionalfeature.getPreviouslyPrintedEntry(ent); + if (!entry) + return ""; + return `

            ${Renderer.get().render(entry)}

            `; + } + + static getTypeText(ent) { + const commonPrefix = ent.featureType.length > 1 ? MiscUtil.findCommonPrefix(ent.featureType.map(fs=>Parser.optFeatureTypeToFull(fs)), { + isRespectWordBoundaries: true + }) : ""; + + return [commonPrefix.trim() || null, ent.featureType.map(ft=>Parser.optFeatureTypeToFull(ft).substring(commonPrefix.length)).join("/"), ].filter(Boolean).join(" "); + } + + static getCostHtml(ent) { + const entry = Renderer.optionalfeature.getCostEntry(ent); + if (!entry) + return ""; + + return Renderer.get().render(entry); + } + + static getCompactRenderedString(ent) { + const ptCost = Renderer.optionalfeature.getCostHtml(ent); + return ` + ${Renderer.utils.getExcludedTr({ + entity: ent, + dataProp: "optionalfeature", + page: UrlUtil.PG_OPT_FEATURES + })} + ${Renderer.utils.getNameTr(ent, { + page: UrlUtil.PG_OPT_FEATURES + })} + + ${ent.prerequisite ? `

            ${Renderer.utils.prerequisite.getHtml(ent.prerequisite)}

            ` : ""} + ${ptCost ? `

            ${ptCost}

            ` : ""} + ${Renderer.get().render({ + entries: ent.entries + }, 1)} + + ${Renderer.optionalfeature.getPreviouslyPrintedText(ent)} +

            ${Renderer.get().render(Renderer.optionalfeature.getTypeEntry(ent))}

            + `; + } +} +; + +Renderer.reward = class { + static getRewardRenderableEntriesMeta(ent) { + const ptSubtitle = [(ent.type || "").toTitleCase(), ent.rarity ? ent.rarity.toTitleCase() : "", ].filter(Boolean).join(", "); + + return { + entriesContent: [ptSubtitle ? `{@i ${ptSubtitle}}` : "", ...ent.entries, ].filter(Boolean), + }; + } + + static getRenderedString(ent) { + const entriesMeta = Renderer.reward.getRewardRenderableEntriesMeta(ent); + return `${Renderer.get().setFirstSection(true).render({ + entries: entriesMeta.entriesContent + }, 1)}`; + } + + static getCompactRenderedString(ent) { + return ` + ${Renderer.utils.getExcludedTr({ + entity: ent, + dataProp: "reward", + page: UrlUtil.PG_REWARDS + })} + ${Renderer.utils.getNameTr(ent, { + page: UrlUtil.PG_REWARDS + })} + ${Renderer.reward.getRenderedString(ent)} + `; + } + + static pGetFluff(ent) { + return Renderer.utils.pGetFluff({ + entity: ent, + fnGetFluffData: DataUtil.rewardFluff.loadJSON.bind(DataUtil.rewardFluff), + fluffProp: "rewardFluff", + }); + } +} +; + +Renderer.race = class { + static getRaceRenderableEntriesMeta(race) { + return { + entryMain: race._isBaseRace ? { + type: "entries", + entries: race._baseRaceEntries + } : { + type: "entries", + entries: race.entries + }, + }; + } + + static getCompactRenderedString(race, {isStatic=false}={}) { + const renderer = Renderer.get(); + const renderStack = []; + + renderStack.push(` + ${Renderer.utils.getExcludedTr({ + entity: race, + dataProp: "race", + page: UrlUtil.PG_RACES + })} + ${Renderer.utils.getNameTr(race, { + page: UrlUtil.PG_RACES + })} + + + + + + + + + + + + +
            Ability ScoresSizeSpeed
            ${Renderer.getAbilityData(race.ability).asText}${(race.size || [Parser.SZ_VARIES]).map(sz=>Parser.sizeAbvToFull(sz)).join("/")}${Parser.getSpeedString(race)}
            + + + `); + renderer.recursiveRender(Renderer.race.getRaceRenderableEntriesMeta(race).entryMain, renderStack, { + depth: 1 + }); + renderStack.push(""); + + const ptHeightWeight = Renderer.race.getHeightAndWeightPart(race, { + isStatic + }); + if (ptHeightWeight) + renderStack.push(`
            ${ptHeightWeight}`); + + return renderStack.join(""); + } + + static getRenderedSize(race) { + return (race.size || [Parser.SZ_VARIES]).map(sz=>Parser.sizeAbvToFull(sz)).join("/"); + } + + static getHeightAndWeightPart(race, {isStatic=false}={}) { + if (!race.heightAndWeight) + return null; + if (race._isBaseRace) + return null; + return Renderer.get().render({ + entries: Renderer.race.getHeightAndWeightEntries(race, { + isStatic + }) + }); + } + + static getHeightAndWeightEntries(race, {isStatic=false}={}) { + const colLabels = ["Base Height", "Base Weight", "Height Modifier", "Weight Modifier"]; + const colStyles = ["col-2-3 ve-text-center", "col-2-3 ve-text-center", "col-2-3 ve-text-center", "col-2 ve-text-center"]; + + const cellHeightMod = !isStatic ? `+${race.heightAndWeight.heightMod}` : `+${race.heightAndWeight.heightMod}`; + const cellWeightMod = !isStatic ? `ร— ${race.heightAndWeight.weightMod || "1"} lb.` : `ร— ${race.heightAndWeight.weightMod || "1"} lb.`; + + const row = [Renderer.race.getRenderedHeight(race.heightAndWeight.baseHeight), `${race.heightAndWeight.baseWeight} lb.`, cellHeightMod, cellWeightMod, ]; + + if (!isStatic) { + colLabels.push(""); + colStyles.push("col-3-1 ve-text-center"); + row.push(`
            +
            +
            =
            +
            +
            ;
            +
            +
            lb.
            +
            + +
            `); + } + + return ["You may roll for your character's height and weight on the Random Height and Weight table. The roll in the Height Modifier column adds a number (in inches) to the character's base height. To get a weight, multiply the number you rolled for height by the roll in the Weight Modifier column and add the result (in pounds) to the base weight.", { + type: "table", + caption: "Random Height and Weight", + colLabels, + colStyles, + rows: [row], + }, ]; + } + + static getRenderedHeight(height) { + const heightFeet = Number(Math.floor(height / 12).toFixed(3)); + const heightInches = Number((height % 12).toFixed(3)); + return `${heightFeet ? `${heightFeet}'` : ""}${heightInches ? `${heightInches}"` : ""}`; + } + + static mergeSubraces(races, opts) { + opts = opts || {}; + + const out = []; + races.forEach(r=>{ + if (r.size && typeof r.size === "string") + r.size = [r.size]; + + if (r.lineage && r.lineage !== true) { + r = MiscUtil.copyFast(r); + + if (r.lineage === "VRGR") { + r.ability = r.ability || [{ + choose: { + weighted: { + from: [...Parser.ABIL_ABVS], + weights: [2, 1], + }, + }, + }, { + choose: { + weighted: { + from: [...Parser.ABIL_ABVS], + weights: [1, 1, 1], + }, + }, + }, ]; + } else if (r.lineage === "UA1") { + r.ability = r.ability || [{ + choose: { + weighted: { + from: [...Parser.ABIL_ABVS], + weights: [2, 1], + }, + }, + }, ]; + } + + r.entries = r.entries || []; + r.entries.push({ + type: "entries", + name: "Languages", + entries: ["You can speak, read, and write Common and one other language that you and your DM agree is appropriate for your character."], + }); + + r.languageProficiencies = r.languageProficiencies || [{ + "common": true, + "anyStandard": 1 + }]; + } + + if (r.subraces && !r.subraces.length) + delete r.subraces; + + if (r.subraces) { + r.subraces.forEach(sr=>{ + sr.source = sr.source || r.source; + sr._isSubRace = true; + } + ); + + r.subraces.sort((a,b)=>SortUtil.ascSortLower(a.name || "_", b.name || "_") || SortUtil.ascSortLower(Parser.sourceJsonToAbv(a.source), Parser.sourceJsonToAbv(b.source))); + } + + if (opts.isAddBaseRaces && r.subraces) { + const baseRace = MiscUtil.copyFast(r); + + baseRace._isBaseRace = true; + + const isAnyNoName = r.subraces.some(it=>!it.name); + if (isAnyNoName) { + baseRace._rawName = baseRace.name; + baseRace.name = `${baseRace.name} (Base)`; + } + + const nameCounts = {}; + r.subraces.filter(sr=>sr.name).forEach(sr=>nameCounts[sr.name.toLowerCase()] = (nameCounts[sr.name.toLowerCase()] || 0) + 1); + nameCounts._ = r.subraces.filter(sr=>!sr.name).length; + + const lst = { + type: "list", + items: r.subraces.map(sr=>{ + const count = nameCounts[(sr.name || "_").toLowerCase()]; + const idName = Renderer.race.getSubraceName(r.name, sr.name); + return `{@race ${idName}|${sr.source}${count > 1 ? `|${idName} (${Parser.sourceJsonToAbv(sr.source)})` : ""}}`; + } + ), + }; + + Renderer.race._mutBaseRaceEntries(baseRace, lst); + baseRace._subraces = r.subraces.map(sr=>({ + name: Renderer.race.getSubraceName(r.name, sr.name), + source: sr.source + })); + + delete baseRace.subraces; + + out.push(baseRace); + } + + out.push(...Renderer.race._mergeSubraces(r)); + } + ); + + return out; + } + + static _mutMakeBaseRace(baseRace) { + if (baseRace._isBaseRace) + return; + + baseRace._isBaseRace = true; + + Renderer.race._mutBaseRaceEntries(baseRace, { + type: "list", + items: [] + }); + } + + static _mutBaseRaceEntries(baseRace, lst) { + baseRace._baseRaceEntries = [{ + type: "section", + entries: ["This race has multiple subraces, as listed below:", lst, ], + }, { + type: "section", + entries: [{ + type: "entries", + entries: [{ + type: "entries", + name: "Traits", + entries: [...MiscUtil.copyFast(baseRace.entries), ], + }, ], + }, ], + }, ]; + } + + static getSubraceName(raceName, subraceName) { + if (!subraceName) + return raceName; + + const mBrackets = /^(.*?)(\(.*?\))$/i.exec(raceName || ""); + if (!mBrackets) + return `${raceName} (${subraceName})`; + + const bracketPart = mBrackets[2].substring(1, mBrackets[2].length - 1); + return `${mBrackets[1]}(${[bracketPart, subraceName].join("; ")})`; + } + + static _mergeSubraces(race) { + if (!race.subraces) + return [race]; + return MiscUtil.copyFast(race.subraces).map(s=>Renderer.race._getMergedSubrace(race, s)); + } + + static _getMergedSubrace(race, cpySr) { + const cpy = MiscUtil.copyFast(race); + cpy._baseName = cpy.name; + cpy._baseSource = cpy.source; + cpy._baseSrd = cpy.srd; + cpy._baseBasicRules = cpy.basicRules; + delete cpy.subraces; + delete cpy.srd; + delete cpy.basicRules; + delete cpy._versions; + delete cpy.hasFluff; + delete cpy.hasFluffImages; + delete cpySr.__prop; + + if (cpySr.name) { + cpy._subraceName = cpySr.name; + + if (cpySr.alias) { + cpy.alias = cpySr.alias.map(it=>Renderer.race.getSubraceName(cpy.name, it)); + delete cpySr.alias; + } + + cpy.name = Renderer.race.getSubraceName(cpy.name, cpySr.name); + delete cpySr.name; + } + if (cpySr.ability) { + if ((cpySr.overwrite && cpySr.overwrite.ability) || !cpy.ability) + cpy.ability = cpySr.ability.map(()=>({})); + + if (cpy.ability.length !== cpySr.ability.length) + throw new Error(`Race and subrace ability array lengths did not match!`); + cpySr.ability.forEach((obj,i)=>Object.assign(cpy.ability[i], obj)); + delete cpySr.ability; + } + if (cpySr.entries) { + cpySr.entries.forEach(ent=>{ + if (!ent.data?.overwrite) + return cpy.entries.push(ent); + + const toOverwrite = cpy.entries.findIndex(it=>it.name?.toLowerCase()?.trim() === ent.data.overwrite.toLowerCase().trim()); + if (~toOverwrite) + cpy.entries[toOverwrite] = ent; + else + cpy.entries.push(ent); + } + ); + delete cpySr.entries; + } + + if (cpySr.traitTags) { + if (cpySr.overwrite && cpySr.overwrite.traitTags) + cpy.traitTags = cpySr.traitTags; + else + cpy.traitTags = (cpy.traitTags || []).concat(cpySr.traitTags); + delete cpySr.traitTags; + } + + if (cpySr.languageProficiencies) { + if (cpySr.overwrite && cpySr.overwrite.languageProficiencies) + cpy.languageProficiencies = cpySr.languageProficiencies; + else + cpy.languageProficiencies = cpy.languageProficiencies = (cpy.languageProficiencies || []).concat(cpySr.languageProficiencies); + delete cpySr.languageProficiencies; + } + + if (cpySr.skillProficiencies) { + if (!cpy.skillProficiencies || (cpySr.overwrite && cpySr.overwrite["skillProficiencies"])) + cpy.skillProficiencies = cpySr.skillProficiencies; + else { + if (!cpySr.skillProficiencies.length || !cpy.skillProficiencies.length) + throw new Error(`No items!`); + if (cpySr.skillProficiencies.length > 1 || cpy.skillProficiencies.length > 1) + throw new Error(`Subrace merging does not handle choices!`); + if (cpySr.skillProficiencies.choose) { + if (cpy.skillProficiencies.choose) + throw new Error(`Subrace choose merging is not supported!!`); + cpy.skillProficiencies.choose = cpySr.skillProficiencies.choose; + delete cpySr.skillProficiencies.choose; + } + Object.assign(cpy.skillProficiencies[0], cpySr.skillProficiencies[0]); + } + + delete cpySr.skillProficiencies; + } + + Object.assign(cpy, cpySr); + + Object.entries(cpy).forEach(([k,v])=>{ + if (v != null) + return; + delete cpy[k]; + } + ); + + return cpy; + } + + static adoptSubraces(allRaces, subraces) { + const nxtData = []; + + subraces.forEach(sr=>{ + if (!sr.raceName || !sr.raceSource) + throw new Error(`Subrace was missing parent "raceName" and/or "raceSource"!`); + + const _baseRace = allRaces.find(r=>r.name === sr.raceName && r.source === sr.raceSource); + if (!_baseRace) + throw new Error(`Could not find parent race for subrace "${sr.name}" (${sr.source})!`); + + if ((_baseRace._seenSubraces || []).some(it=>it.name === sr.name && it.source === sr.source)) + return; + (_baseRace._seenSubraces = _baseRace._seenSubraces || []).push({ + name: sr.name, + source: sr.source + }); + + if (!_baseRace._isBaseRace && (PrereleaseUtil.hasSourceJson(_baseRace.source) || BrewUtil2.hasSourceJson(_baseRace.source))) { + Renderer.race._mutMakeBaseRace(_baseRace); + } + + if (_baseRace._isBaseRace) { + const subraceListEntry = ((_baseRace._baseRaceEntries[0] || {}).entries || []).find(it=>it.type === "list"); + subraceListEntry.items.push(`{@race ${_baseRace._rawName || _baseRace.name} (${sr.name})|${sr.source || _baseRace.source}}`); + } + + let baseRace = nxtData.find(r=>r.name === sr.raceName && r.source === sr.raceSource); + if (!baseRace) { + baseRace = MiscUtil.copyFast(_baseRace); + if (baseRace._rawName) { + baseRace.name = baseRace._rawName; + delete baseRace._rawName; + } + delete baseRace._isBaseRace; + delete baseRace._baseRaceEntries; + + nxtData.push(baseRace); + } + + baseRace.subraces = baseRace.subraces || []; + baseRace.subraces.push(sr); + } + ); + + return nxtData; + } + + static bindListenersHeightAndWeight(race, ele) { + if (!race.heightAndWeight) + return; + if (race._isBaseRace) + return; + + const $render = $(ele); + + const $dispResult = $render.find(`.race__disp-result-height-weight`); + const $dispHeight = $render.find(`.race__disp-result-height`); + const $dispWeight = $render.find(`.race__disp-result-weight`); + + const lock = new VeLock(); + let hasRolled = false; + let resultHeight; + let resultWeightMod; + + const $btnRollHeight = $render.find(`[data-race-heightmod="true"]`).html(race.heightAndWeight.heightMod).addClass("roller").mousedown(evt=>evt.preventDefault()).click(async()=>{ + try { + await lock.pLock(); + + if (!hasRolled) + return pDoFullRoll(true); + await pRollHeight(); + updateDisplay(); + } finally { + lock.unlock(); + } + } + ); + + const isWeightRoller = race.heightAndWeight.weightMod && isNaN(race.heightAndWeight.weightMod); + const $btnRollWeight = $render.find(`[data-race-weightmod="true"]`).html(isWeightRoller ? `(${race.heightAndWeight.weightMod})` : race.heightAndWeight.weightMod || "1").click(async()=>{ + try { + await lock.pLock(); + + if (!hasRolled) + return pDoFullRoll(true); + await pRollWeight(); + updateDisplay(); + } finally { + lock.unlock(); + } + } + ); + if (isWeightRoller) + $btnRollWeight.mousedown(evt=>evt.preventDefault()); + + const $btnRoll = $render.find(`button.race__btn-roll-height-weight`).click(async()=>pDoFullRoll()); + + const pRollHeight = async()=>{ + const mResultHeight = await Renderer.dice.pRoll2(race.heightAndWeight.heightMod, { + isUser: false, + label: "Height Modifier", + name: race.name, + }); + if (mResultHeight == null) + return; + resultHeight = mResultHeight; + } + ; + + const pRollWeight = async()=>{ + const weightModRaw = race.heightAndWeight.weightMod || "1"; + const mResultWeightMod = isNaN(weightModRaw) ? await Renderer.dice.pRoll2(weightModRaw, { + isUser: false, + label: "Weight Modifier", + name: race.name, + }) : Number(weightModRaw); + if (mResultWeightMod == null) + return; + resultWeightMod = mResultWeightMod; + } + ; + + const updateDisplay = ()=>{ + const renderedHeight = Renderer.race.getRenderedHeight(race.heightAndWeight.baseHeight + resultHeight); + const totalWeight = race.heightAndWeight.baseWeight + (resultWeightMod * resultHeight); + $dispHeight.text(renderedHeight); + $dispWeight.text(Number(totalWeight.toFixed(3))); + } + ; + + const pDoFullRoll = async isPreLocked=>{ + try { + if (!isPreLocked) + await lock.pLock(); + + $btnRoll.parent().removeClass(`ve-flex-vh-center`).addClass(`split-v-center`); + await pRollHeight(); + await pRollWeight(); + $dispResult.removeClass(`ve-hidden`); + updateDisplay(); + + hasRolled = true; + } finally { + if (!isPreLocked) + lock.unlock(); + } + } + ; + } + + static bindListenersCompact(race, ele) { + Renderer.race.bindListenersHeightAndWeight(race, ele); + } + + static pGetFluff(race) { + return Renderer.utils.pGetFluff({ + entity: race, + fnGetFluffData: DataUtil.raceFluff.loadJSON.bind(DataUtil.raceFluff), + fluffProp: "raceFluff", + }); + } +} +; + +Renderer.raceFeature = class { + static getCompactRenderedString(ent) { + return Renderer.generic.getCompactRenderedString(ent); + } +} +; + +Renderer.deity = class { + static _BASE_PART_TRANSLATORS = { + "alignment": { + name: "Alignment", + displayFn: (it)=>it.map(a=>Parser.alignmentAbvToFull(a)).join(" ").toTitleCase(), + }, + "pantheon": { + name: "Pantheon", + }, + "category": { + name: "Category", + displayFn: it=>typeof it === "string" ? it : it.join(", "), + }, + "domains": { + name: "Domains", + displayFn: (it)=>it.join(", "), + }, + "province": { + name: "Province", + }, + "altNames": { + name: "Alternate Names", + displayFn: (it)=>it.join(", "), + }, + "symbol": { + name: "Symbol", + }, + }; + + static getDeityRenderableEntriesMeta(ent) { + return { + entriesAttributes: [...Object.entries(Renderer.deity._BASE_PART_TRANSLATORS).map(([prop,{name, displayFn}])=>{ + if (ent[prop] == null) + return null; + + const displayVal = displayFn ? displayFn(ent[prop]) : ent[prop]; + return { + name, + entry: `{@b ${name}:} ${displayVal}`, + }; + } + ).filter(Boolean), ...Object.entries(ent.customProperties || {}).map(([name,val])=>({ + name, + entry: `{@b ${name}:} ${val}`, + })), ].sort(({name: nameA},{name: nameB})=>SortUtil.ascSortLower(nameA, nameB)).map(({entry})=>entry), + }; + } + + static getCompactRenderedString(ent) { + const renderer = Renderer.get(); + const entriesMeta = Renderer.deity.getDeityRenderableEntriesMeta(ent); + return ` + ${Renderer.utils.getExcludedTr({ + entity: ent, + dataProp: "deity", + page: UrlUtil.PG_DEITIES + })} + ${Renderer.utils.getNameTr(ent, { + suffix: ent.title ? `, ${ent.title.toTitleCase()}` : "", + page: UrlUtil.PG_DEITIES + })} + + ${entriesMeta.entriesAttributes.map(entry=>`
            ${Renderer.get().render(entry)}
            `).join("")} + + ${ent.entries ? `
            ${renderer.render({ + entries: ent.entries + }, 1)}` : ""} + `; + } +} +; + +Renderer.object = class { + static CHILD_PROPS = ["actionEntries"]; + + static RENDERABLE_ENTRIES_PROP_ORDER__ATTRIBUTES = ["entryCreatureCapacity", "entryCargoCapacity", "entryArmorClass", "entryHitPoints", "entrySpeed", "entryAbilityScores", "entryDamageImmunities", "entryDamageResistances", "entryDamageVulnerabilities", "entryConditionImmunities", ]; + + static getObjectRenderableEntriesMeta(ent) { + return { + entrySize: `{@i ${ent.objectType !== "GEN" ? `${Renderer.utils.getRenderedSize(ent.size)} ${ent.creatureType ? Parser.monTypeToFullObj(ent.creatureType).asText : "object"}` : `Variable size object`}}`, + + entryCreatureCapacity: ent.capCrew != null || ent.capPassenger != null ? `{@b Creature Capacity:} ${Renderer.vehicle.getShipCreatureCapacity(ent)}` : null, + entryCargoCapacity: ent.capCargo != null ? `{@b Cargo Capacity:} ${Renderer.vehicle.getShipCargoCapacity(ent)}` : null, + entryArmorClass: ent.ac != null ? `{@b Armor Class:} ${ent.ac.special ?? ent.ac}` : null, + entryHitPoints: ent.hp != null ? `{@b Hit Points:} ${ent.hp.special ?? ent.hp}` : null, + entrySpeed: ent.speed != null ? `{@b Speed:} ${Parser.getSpeedString(ent)}` : null, + entryAbilityScores: Parser.ABIL_ABVS.some(ab=>ent[ab] != null) ? `{@b Ability Scores:} ${Parser.ABIL_ABVS.filter(ab=>ent[ab] != null).map(ab=>`${ab.toUpperCase()} ${Renderer.utils.getAbilityRollerEntry(ent, ab)}`).join(", ")}` : null, + entryDamageImmunities: ent.immune != null ? `{@b Damage Immunities:} ${Parser.getFullImmRes(ent.immune)}` : null, + entryDamageResistances: ent.resist ? `{@b Damage Resistances:} ${Parser.getFullImmRes(ent.resist)}` : null, + entryDamageVulnerabilities: ent.vulnerable ? `{@b Damage Vulnerabilities:} ${Parser.getFullImmRes(ent.vulnerable)}` : null, + entryConditionImmunities: ent.conditionImmune ? `{@b Condition Immunities:} ${Parser.getFullCondImm(ent.conditionImmune, { + isEntry: true + })}` : null, + }; + } + + static getCompactRenderedString(obj, opts) { + return Renderer.object.getRenderedString(obj, { + ...opts, + isCompact: true + }); + } + + static getRenderedString(ent, opts) { + opts = opts || {}; + + const renderer = Renderer.get().setFirstSection(true); + + const hasToken = ent.tokenUrl || ent.hasToken; + const extraThClasses = !opts.isCompact && hasToken ? ["objs__name--token"] : null; + + const entriesMeta = Renderer.object.getObjectRenderableEntriesMeta(ent); + + const ptAttribs = Renderer.object.RENDERABLE_ENTRIES_PROP_ORDER__ATTRIBUTES.filter(prop=>entriesMeta[prop]).map(prop=>`${Renderer.get().render(entriesMeta[prop])}
            `).join(""); + + return ` + ${Renderer.utils.getExcludedTr({ + entity: ent, + dataProp: "object", + page: opts.page || UrlUtil.PG_OBJECTS + })} + ${Renderer.utils.getNameTr(ent, { + page: opts.page || UrlUtil.PG_OBJECTS, + extraThClasses, + isEmbeddedEntity: opts.isEmbeddedEntity + })} + ${Renderer.get().render(entriesMeta.entrySize)} + ${ptAttribs} + + ${ent.entries ? renderer.render({ + entries: ent.entries + }, 2) : ""} + ${ent.actionEntries ? renderer.render({ + entries: ent.actionEntries + }, 2) : ""} + + `; + } + + static getTokenUrl(obj) { + return obj.tokenUrl || UrlUtil.link(`${Renderer.get().baseMediaUrls["img"] || Renderer.get().baseUrl}img/objects/tokens/${Parser.sourceJsonToAbv(obj.source)}/${Parser.nameToTokenName(obj.name)}.png`); + } + + static pGetFluff(obj) { + return Renderer.utils.pGetFluff({ + entity: obj, + fnGetFluffData: DataUtil.objectFluff.loadJSON.bind(DataUtil.objectFluff), + fluffProp: "objectFluff", + }); + } +} +; + +Renderer.trap = class { + static CHILD_PROPS = ["trigger", "effect", "eActive", "eDynamic", "eConstant", "countermeasures"]; + + static getTrapRenderableEntriesMeta(ent) { + return { + entriesAttributes: [ent.trigger ? { + type: "entries", + name: "Trigger", + entries: ent.trigger, + } : null, + ent.effect ? { + type: "entries", + name: "Effect", + entries: ent.effect, + } : null, + ent.initiative ? { + type: "entries", + name: "Initiative", + entries: Renderer.trap.getTrapInitiativeEntries(ent), + } : null, ent.eActive ? { + type: "entries", + name: "Active Elements", + entries: ent.eActive, + } : null, ent.eDynamic ? { + type: "entries", + name: "Dynamic Elements", + entries: ent.eDynamic, + } : null, ent.eConstant ? { + type: "entries", + name: "Constant Elements", + entries: ent.eConstant, + } : null, + ent.countermeasures ? { + type: "entries", + name: "Countermeasures", + entries: ent.countermeasures, + } : null, ].filter(Boolean), + }; + } + + static getTrapInitiativeEntries(ent) { + return [`The trap acts on ${Parser.trapInitToFull(ent.initiative)}${ent.initiativeNote ? ` (${ent.initiativeNote})` : ""}.`]; + } + + static getRenderedTrapPart(renderer, ent) { + const entriesMeta = Renderer.trap.getTrapRenderableEntriesMeta(ent); + if (!entriesMeta.entriesAttributes.length) + return ""; + return renderer.render({ + entries: entriesMeta.entriesAttributes + }, 1); + } + + static getCompactRenderedString(ent, opts) { + return Renderer.traphazard.getCompactRenderedString(ent, opts); + } + + static pGetFluff(ent) { + return Renderer.traphazard.pGetFluff(ent); + } +} +; + +Renderer.hazard = class { + static getCompactRenderedString(ent, opts) { + return Renderer.traphazard.getCompactRenderedString(ent, opts); + } + + static pGetFluff(ent) { + return Renderer.traphazard.pGetFluff(ent); + } +} +; + +Renderer.traphazard = class { + static getSubtitle(ent) { + const type = ent.trapHazType || "HAZ"; + if (type === "GEN") + return null; + + const ptThreat = ent.threat ? ent.threat.toTitleCase() : null; + + const ptTypeThreat = [Parser.trapHazTypeToFull(type), ent.threat ? ent.threat.toTitleCase() : null, ].filter(Boolean).join(", "); + + const parenPart = [ent.tier ? Parser.tierToFullLevel(ent.tier) : null, Renderer.traphazard.getTrapLevelPart(ent), ].filter(Boolean).join(", "); + + return parenPart ? `${ptTypeThreat} (${parenPart})` : ptTypeThreat; + } + + static getTrapLevelPart(ent) { + return ent.level?.min != null && ent.level?.max != null ? `level ${ent.level.min}${ent.level.min !== ent.level.max ? `\u2013${ent.level.max}` : ""}` : null; + } + + static getCompactRenderedString(ent, opts) { + opts = opts || {}; + + const renderer = Renderer.get(); + const subtitle = Renderer.traphazard.getSubtitle(ent); + return ` + ${Renderer.utils.getExcludedTr({ + entity: ent, + dataProp: ent.__prop, + page: UrlUtil.PG_TRAPS_HAZARDS + })} + ${Renderer.utils.getNameTr(ent, { + page: UrlUtil.PG_TRAPS_HAZARDS, + isEmbeddedEntity: opts.isEmbeddedEntity + })} + ${subtitle ? `${subtitle}` : ""} + + ${renderer.render({ + entries: ent.entries + }, 2)} + ${Renderer.trap.getRenderedTrapPart(renderer, ent)} + + `; + } + + static pGetFluff(ent) { + return Renderer.utils.pGetFluff({ + entity: ent, + fnGetFluffData: ent.__prop === "trap" ? DataUtil.trapFluff.loadJSON.bind(DataUtil.trapFluff) : DataUtil.hazardFluff.loadJSON.bind(DataUtil.hazardFluff), + fluffProp: ent.__prop === "trap" ? "trapFluff" : "hazardFluff", + }); + } +} +; + +Renderer.cultboon = class { + static getCultRenderableEntriesMeta(ent) { + if (!ent.goal && !ent.cultists && !ent.signaturespells) + return null; + + const fauxList = { + type: "list", + style: "list-hang-notitle", + items: [], + }; + + if (ent.goal) { + fauxList.items.push({ + type: "item", + name: "Goals:", + entry: ent.goal.entry, + }); + } + + if (ent.cultists) { + fauxList.items.push({ + type: "item", + name: "Typical Cultists:", + entry: ent.cultists.entry, + }); + } + if (ent.signaturespells) { + fauxList.items.push({ + type: "item", + name: "Signature Spells:", + entry: ent.signaturespells.entry, + }); + } + + return { + listGoalsCultistsSpells: fauxList + }; + } + + static doRenderCultParts(ent, renderer, renderStack) { + const cultEntriesMeta = Renderer.cultboon.getCultRenderableEntriesMeta(ent); + if (!cultEntriesMeta) + return; + renderer.recursiveRender(cultEntriesMeta.listGoalsCultistsSpells, renderStack, { + depth: 2 + }); + } + + static getBoonRenderableEntriesMeta(ent) { + if (!ent.ability && !ent.signaturespells) + return null; + + const benefits = { + type: "list", + style: "list-hang-notitle", + items: [] + }; + + if (ent.ability) { + benefits.items.push({ + type: "item", + name: "Ability Score Adjustment:", + entry: ent.ability ? ent.ability.entry : "None", + }); + } + + if (ent.signaturespells) { + benefits.items.push({ + type: "item", + name: "Signature Spells:", + entry: ent.signaturespells ? ent.signaturespells.entry : "None", + }); + } + + return { + listBenefits: benefits + }; + } + + static doRenderBoonParts(ent, renderer, renderStack) { + const boonEntriesMeta = Renderer.cultboon.getBoonRenderableEntriesMeta(ent); + if (!boonEntriesMeta) + return; + renderer.recursiveRender(boonEntriesMeta.listBenefits, renderStack, { + depth: 1 + }); + } + + static _getCompactRenderedString_cult({ent, renderer}) { + const renderStack = []; + + Renderer.cultboon.doRenderCultParts(ent, renderer, renderStack); + renderer.recursiveRender({ + entries: ent.entries + }, renderStack, { + depth: 2 + }); + + return `${Renderer.utils.getExcludedTr({ + entity: ent, + dataProp: "cult", + page: UrlUtil.PG_CULTS_BOONS + })} + ${Renderer.utils.getNameTr(ent, { + page: UrlUtil.PG_CULTS_BOONS + })} +
            + ${renderStack.join("")}`; + } + + static _getCompactRenderedString_boon({ent, renderer}) { + const renderStack = []; + + Renderer.cultboon.doRenderBoonParts(ent, renderer, renderStack); + renderer.recursiveRender({ + entries: ent.entries + }, renderStack, { + depth: 1 + }); + ent._displayName = ent._displayName || ent.name; + + return `${Renderer.utils.getExcludedTr({ + entity: ent, + dataProp: "boon", + page: UrlUtil.PG_CULTS_BOONS + })} + ${Renderer.utils.getNameTr(ent, { + page: UrlUtil.PG_CULTS_BOONS + })} + ${renderStack.join("")}`; + } + + static getCompactRenderedString(ent) { + const renderer = Renderer.get(); + switch (ent.__prop) { + case "cult": + return Renderer.cultboon._getCompactRenderedString_cult({ + ent, + renderer + }); + case "boon": + return Renderer.cultboon._getCompactRenderedString_boon({ + ent, + renderer + }); + default: + throw new Error(`Unhandled prop "${ent.__prop}"`); + } + } +} +; + +Renderer.monster = class { + static CHILD_PROPS = ["action", "bonus", "reaction", "trait", "legendary", "mythic", "variant", "spellcasting"]; + + static getShortName(mon, {isTitleCase=false, isSentenceCase=false, isUseDisplayName=false}={}) { + const name = isUseDisplayName ? (mon._displayName ?? mon.name) : mon.name; + const shortName = isUseDisplayName ? (mon._displayShortName ?? mon.shortName) : mon.shortName; + + const prefix = mon.isNamedCreature ? "" : isTitleCase || isSentenceCase ? "The " : "the "; + if (shortName === true) + return `${prefix}${name}`; + else if (shortName) + return `${prefix}${!prefix && isTitleCase ? shortName.toTitleCase() : shortName.toLowerCase()}`; + + const out = Renderer.monster.getShortNameFromName(name, { + isNamedCreature: mon.isNamedCreature + }); + return `${prefix}${out}`; + } + + static getShortNameFromName(name, {isNamedCreature=false}={}) { + const base = name.split(",")[0]; + let out = base.replace(/(?:adult|ancient|young) \w+ (dragon|dracolich)/gi, "$1"); + out = isNamedCreature ? out.split(" ")[0] : out.toLowerCase(); + return out; + } + + static getLegendaryActionIntro(mon, {renderer=Renderer.get(), isUseDisplayName=false}={}) { + return renderer.render(Renderer.monster.getLegendaryActionIntroEntry(mon, { + isUseDisplayName + })); + } + + static getLegendaryActionIntroEntry(mon, {isUseDisplayName=false}={}) { + if (mon.legendaryHeader) { + return { + entries: mon.legendaryHeader + }; + } + + const legendaryActions = mon.legendaryActions || 3; + const legendaryNameTitle = Renderer.monster.getShortName(mon, { + isTitleCase: true, + isUseDisplayName + }); + return { + entries: [`${legendaryNameTitle} can take ${legendaryActions} legendary action${legendaryActions > 1 ? "s" : ""}, choosing from the options below. Only one legendary action can be used at a time and only at the end of another creature's turn. ${legendaryNameTitle} regains spent legendary actions at the start of its turn.`, ], + }; + } + + static getSectionIntro(mon, {renderer=Renderer.get(), prop}) { + const headerProp = `${prop}Header`; + if (mon[headerProp]) + return renderer.render({ + entries: mon[headerProp] + }); + return ""; + } + + static getSave(renderer, attr, mod) { + if (attr === "special") + return renderer.render(mod); + return renderer.render(`${attr.uppercaseFirst()} {@savingThrow ${attr} ${mod}}`); + } + + static dragonCasterVariant = class { + static _LVL_TO_COLOR_TO_SPELLS__UNOFFICIAL = { + 2: { + black: ["darkness", "Melf's acid arrow", "fog cloud", "scorching ray"], + green: ["ray of sickness", "charm person", "detect thoughts", "invisibility", "suggestion"], + white: ["ice knife|XGE", "Snilloc's snowball swarm|XGE"], + brass: ["see invisibility", "magic mouth", "blindness/deafness", "sleep", "detect thoughts"], + bronze: ["gust of wind", "misty step", "locate object", "blur", "witch bolt", "thunderwave", "shield"], + copper: ["knock", "sleep", "detect thoughts", "blindness/deafness", "tasha's hideous laughter"], + }, + 3: { + blue: ["wall of sand|XGE", "thunder step|XGE", "lightning bolt", "blink", "magic missile", "slow"], + red: ["fireball", "scorching ray", "haste", "erupting earth|XGE", "Aganazzar's scorcher|XGE"], + gold: ["slow", "fireball", "dispel magic", "counterspell", "Aganazzar's scorcher|XGE", "shield"], + silver: ["sleet storm", "protection from energy", "catnap|XGE", "locate object", "identify", "Leomund's tiny hut"], + }, + 4: { + black: ["vitriolic sphere|XGE", "sickening radiance|XGE", "Evard's black tentacles", "blight", "hunger of Hadar"], + white: ["fire shield", "ice storm", "sleet storm"], + brass: ["charm monster|XGE", "sending", "wall of sand|XGE", "hypnotic pattern", "tongues"], + copper: ["polymorph", "greater invisibility", "confusion", "stinking cloud", "major image", "charm monster|XGE"], + }, + 5: { + blue: ["telekinesis", "hold monster", "dimension door", "wall of stone", "wall of force"], + green: ["cloudkill", "charm monster|XGE", "modify memory", "mislead", "hallucinatory terrain", "dimension door"], + bronze: ["steel wind strike|XGE", "control winds|XGE", "watery sphere|XGE", "storm sphere|XGE", "tidal wave|XGE"], + gold: ["hold monster", "immolation|XGE", "wall of fire", "greater invisibility", "dimension door"], + silver: ["cone of cold", "ice storm", "teleportation circle", "skill empowerment|XGE", "creation", "Mordenkainen's private sanctum"], + }, + 6: { + white: ["cone of cold", "wall of ice"], + brass: ["scrying", "Rary's telepathic bond", "Otto's irresistible dance", "legend lore", "hold monster", "dream"], + }, + 7: { + black: ["power word pain|XGE", "finger of death", "disintegrate", "hold monster"], + blue: ["chain lightning", "forcecage", "teleport", "etherealness"], + green: ["project image", "mirage arcane", "prismatic spray", "teleport"], + bronze: ["whirlwind|XGE", "chain lightning", "scatter|XGE", "teleport", "disintegrate", "lightning bolt"], + copper: ["symbol", "simulacrum", "reverse gravity", "project image", "Bigby's hand", "mental prison|XGE", "seeming"], + silver: ["Otiluke's freezing sphere", "prismatic spray", "wall of ice", "contingency", "arcane gate"], + }, + 8: { + gold: ["sunburst", "delayed blast fireball", "antimagic field", "teleport", "globe of invulnerability", "maze"], + }, + }; + static _LVL_TO_COLOR_TO_SPELLS__FTD = { + 1: { + deep: ["command", "dissonant whispers", "faerie fire"], + }, + 2: { + black: ["blindness/deafness", "create or destroy water"], + green: ["invisibility", "speak with animals"], + white: ["gust of wind"], + brass: ["create or destroy water", "speak with animals"], + bronze: ["beast sense", "detect thoughts", "speak with animals"], + copper: ["lesser restoration", "phantasmal force"], + }, + 3: { + blue: ["create or destroy water", "major image"], + red: ["bane", "heat metal", "hypnotic pattern", "suggestion"], + gold: ["bless", "cure wounds", "slow", "suggestion", "zone of truth"], + silver: ["beacon of hope", "calm emotions", "hold person", "zone of truth"], + deep: ["command", "dissonant whispers", "faerie fire", "water breathing"], + }, + 4: { + black: ["blindness/deafness", "create or destroy water", "plant growth"], + white: ["gust of wind"], + brass: ["create or destroy water", "speak with animals", "suggestion"], + copper: ["lesser restoration", "phantasmal force", "stone shape"], + }, + 5: { + blue: ["arcane eye", "create or destroy water", "major image"], + red: ["bane", "dominate person", "heat metal", "hypnotic pattern", "suggestion"], + green: ["invisibility", "plant growth", "speak with animals"], + bronze: ["beast sense", "control water", "detect thoughts", "speak with animals"], + gold: ["bless", "commune", "cure wounds", "geas", "slow", "suggestion", "zone of truth"], + silver: ["beacon of hope", "calm emotions", "hold person", "polymorph", "zone of truth"], + }, + 6: { + white: ["gust of wind", "ice storm"], + brass: ["create or destroy water", "locate creature", "speak with animals", "suggestion"], + deep: ["command", "dissonant whispers", "faerie fire", "passwall", "water breathing"], + }, + 7: { + black: ["blindness/deafness", "create or destroy water", "insect plague", "plant growth"], + blue: ["arcane eye", "create or destroy water", "major image", "project image"], + red: ["bane", "dominate person", "heat metal", "hypnotic pattern", "power word stun", "suggestion"], + green: ["invisibility", "mass suggestion", "plant growth", "speak with animals"], + bronze: ["beast sense", "control water", "detect thoughts", "heroes' feast", "speak with animals"], + copper: ["lesser restoration", "move earth", "phantasmal force", "stone shape"], + silver: ["beacon of hope", "calm emotions", "hold person", "polymorph", "teleport", "zone of truth"], + }, + 8: { + gold: ["bless", "commune", "cure wounds", "geas", "plane shift", "slow", "suggestion", "word of recall", "zone of truth"], + }, + }; + + static getAvailableColors() { + const out = new Set(); + + const add = (lookup)=>Object.values(lookup).forEach(obj=>Object.keys(obj).forEach(k=>out.add(k))); + add(Renderer.monster.dragonCasterVariant._LVL_TO_COLOR_TO_SPELLS__UNOFFICIAL); + add(Renderer.monster.dragonCasterVariant._LVL_TO_COLOR_TO_SPELLS__FTD); + + return [...out].sort(SortUtil.ascSortLower); + } + + static hasCastingColorVariant(dragon) { + return dragon.dragonCastingColor && !dragon.spellcasting; + } + + static getMeta(dragon) { + const chaMod = Parser.getAbilityModNumber(dragon.cha); + const pb = Parser.crToPb(dragon.cr); + const maxSpellLevel = Math.floor(Parser.crToNumber(dragon.cr) / 3); + + return { + chaMod, + pb, + maxSpellLevel, + spellSaveDc: pb + chaMod + 8, + spellToHit: pb + chaMod, + exampleSpellsUnofficial: Renderer.monster.dragonCasterVariant._getMeta_getExampleSpells({ + dragon, + maxSpellLevel, + spellLookup: Renderer.monster.dragonCasterVariant._LVL_TO_COLOR_TO_SPELLS__UNOFFICIAL, + }), + exampleSpellsFtd: Renderer.monster.dragonCasterVariant._getMeta_getExampleSpells({ + dragon, + maxSpellLevel, + spellLookup: Renderer.monster.dragonCasterVariant._LVL_TO_COLOR_TO_SPELLS__FTD, + }), + }; + } + + static _getMeta_getExampleSpells({dragon, maxSpellLevel, spellLookup}) { + if (spellLookup[maxSpellLevel]?.[dragon.dragonCastingColor]) + return spellLookup[maxSpellLevel][dragon.dragonCastingColor]; + + const flatKeys = Object.entries(spellLookup).map(([lvl,group])=>{ + return Object.keys(group).map(color=>`${lvl}${color}`); + } + ).flat().mergeMap(it=>({ + [it]: true + })); + + while (--maxSpellLevel > -1) { + const lookupKey = `${maxSpellLevel}${dragon.dragonCastingColor}`; + if (flatKeys[lookupKey]) + return spellLookup[maxSpellLevel][dragon.dragonCastingColor]; + } + return []; + } + + static getSpellcasterDetailsPart({chaMod, maxSpellLevel, spellSaveDc, spellToHit, isSeeSpellsPageNote=false}) { + const levelString = maxSpellLevel === 0 ? `${chaMod === 1 ? "This" : "These"} spells are Cantrips.` : `${chaMod === 1 ? "The" : "Each"} spell's level can be no higher than ${Parser.spLevelToFull(maxSpellLevel)}.`; + + return `This dragon can innately cast ${Parser.numberToText(chaMod)} spell${chaMod === 1 ? "" : "s"}, once per day${chaMod === 1 ? "" : " each"}, requiring no material components. ${levelString} The dragon's spell save DC is {@dc ${spellSaveDc}}, and it has {@hit ${spellToHit}} to hit with spell attacks.${isSeeSpellsPageNote ? ` See the {@filter spell page|spells|level=${[...new Array(maxSpellLevel + 1)].map((it,i)=>i).join(";")}} for a list of spells the dragon is capable of casting.` : ""}`; + } + + static getVariantEntries(dragon) { + if (!Renderer.monster.dragonCasterVariant.hasCastingColorVariant(dragon)) + return []; + + const meta = Renderer.monster.dragonCasterVariant.getMeta(dragon); + const {exampleSpellsUnofficial, exampleSpellsFtd} = meta; + + const vFtd = exampleSpellsFtd?.length ? { + type: "variant", + name: "Dragons as Innate Spellcasters", + source: Parser.SRC_FTD, + entries: [`${Renderer.monster.dragonCasterVariant.getSpellcasterDetailsPart(meta)}`, `A suggested spell list is shown below, but you can also choose spells to reflect the dragon's character. A dragon who innately casts {@filter druid|spells|class=druid} spells feels different from one who casts {@filter warlock|spells|class=warlock} spells. You can also give a dragon spells of a higher level than this rule allows, but such a tweak might increase the dragon's challenge rating\u2014especially if those spells deal damage or impose conditions on targets.`, { + type: "list", + items: exampleSpellsFtd.map(it=>`{@spell ${it}}`), + }, ], + } : null; + + const vBasic = { + type: "variant", + name: "Dragons as Innate Spellcasters", + entries: ["Dragons are innately magical creatures that can master a few spells as they age, using this variant.", `A young or older dragon can innately cast a number of spells equal to its Charisma modifier. Each spell can be cast once per day, requiring no material components, and the spell's level can be no higher than one-third the dragon's challenge rating (rounded down). The dragon's bonus to hit with spell attacks is equal to its proficiency bonus + its Charisma bonus. The dragon's spell save DC equals 8 + its proficiency bonus + its Charisma modifier.`, `{@note ${Renderer.monster.dragonCasterVariant.getSpellcasterDetailsPart({ + ...meta, + isSeeSpellsPageNote: true + })}${exampleSpellsUnofficial?.length ? ` A selection of examples are shown below:` : ""}}`, ], + }; + if (dragon.source !== Parser.SRC_MM) { + vBasic.source = Parser.SRC_MM; + vBasic.page = 86; + } + if (exampleSpellsUnofficial) { + const ls = { + type: "list", + style: "list-italic", + items: exampleSpellsUnofficial.map(it=>`{@spell ${it}}`), + }; + vBasic.entries.push(ls); + } + + return [vFtd, vBasic].filter(Boolean); + } + + static getHtml(dragon, {renderer=null}={}) { + const variantEntrues = Renderer.monster.dragonCasterVariant.getVariantEntries(dragon); + if (!variantEntrues.length) + return null; + return variantEntrues.map(it=>renderer.render(it)).join(""); + } + } + ; + + static getCrScaleTarget({win, $btnScale, initialCr, cbRender, isCompact, }, ) { + const evtName = "click.cr-scaler"; + + let slider; + + const $body = $(win.document.body); + function cleanSliders() { + $body.find(`.mon__cr_slider_wrp`).remove(); + $btnScale.off(evtName); + if (slider) + slider.destroy(); + } + + cleanSliders(); + + const $wrp = $(`
            `); + + const cur = Parser.CRS.indexOf(initialCr); + if (cur === -1) + throw new Error(`Initial CR ${initialCr} was not valid!`); + + const comp = BaseComponent.fromObject({ + min: 0, + max: Parser.CRS.length - 1, + cur, + }); + slider = new ComponentUiUtil.RangeSlider({ + comp, + propMin: "min", + propMax: "max", + propCurMin: "cur", + fnDisplay: ix=>Parser.CRS[ix], + }); + slider.$get().appendTo($wrp); + + $btnScale.off(evtName).on(evtName, (evt)=>evt.stopPropagation()); + $wrp.on(evtName, (evt)=>evt.stopPropagation()); + $body.off(evtName).on(evtName, cleanSliders); + + comp._addHookBase("cur", ()=>{ + cbRender(Parser.crToNumber(Parser.CRS[comp._state.cur])); + $body.off(evtName); + cleanSliders(); + } + ); + + $btnScale.after($wrp); + } + + static getSelSummonSpellLevel(mon) { + if (mon.summonedBySpellLevel == null) + return; + + return e_({ + tag: "select", + clazz: "input-xs form-control form-control--minimal w-initial inline-block ve-popwindow__hidden", + name: "mon__sel-summon-spell-level", + children: [e_({ + tag: "option", + val: "-1", + text: "\u2014" + }), ...[...new Array(VeCt.SPELL_LEVEL_MAX + 1 - mon.summonedBySpellLevel)].map((_,i)=>e_({ + tag: "option", + val: i + mon.summonedBySpellLevel, + text: i + mon.summonedBySpellLevel, + })), ], + }); + } + + static getSelSummonClassLevel(mon) { + if (mon.summonedByClass == null) + return; + + return e_({ + tag: "select", + clazz: "input-xs form-control form-control--minimal w-initial inline-block ve-popwindow__hidden", + name: "mon__sel-summon-class-level", + children: [e_({ + tag: "option", + val: "-1", + text: "\u2014" + }), ...[...new Array(VeCt.LEVEL_MAX)].map((_,i)=>e_({ + tag: "option", + val: i + 1, + text: i + 1, + })), ], + }); + } + + static getCompactRenderedStringSection(mon, renderer, title, key, depth) { + if (!mon[key]) + return ""; + + const noteKey = `${key}Note`; + + const toRender = key === "lairActions" || key === "regionalEffects" ? [{ + type: "entries", + entries: mon[key] + }] : mon[key]; + + const ptHeader = mon[key] ? Renderer.monster.getSectionIntro(mon, { + prop: key + }) : ""; + + return `

            ${title}${mon[noteKey] ? ` (${mon[noteKey]})` : ""}

            + + ${key === "legendary" && mon.legendary ? `

            ${Renderer.monster.getLegendaryActionIntro(mon)}

            ` : ""} + ${ptHeader ? `

            ${ptHeader}

            ` : ""} + ${toRender.map(it=>it.rendered || renderer.render(it, depth)).join("")} + `; + } + + static getTypeAlignmentPart(mon) { + const typeObj = Parser.monTypeToFullObj(mon.type); + + return `${mon.level ? `${Parser.getOrdinalForm(mon.level)}-level ` : ""}${typeObj.asTextSidekick ? `${typeObj.asTextSidekick}; ` : ""}${Renderer.utils.getRenderedSize(mon.size)}${mon.sizeNote ? ` ${mon.sizeNote}` : ""} ${typeObj.asText}${mon.alignment ? `, ${mon.alignmentPrefix ? Renderer.get().render(mon.alignmentPrefix) : ""}${Parser.alignmentListToFull(mon.alignment).toTitleCase()}` : ""}`; + } + static getSavesPart(mon) { + return `${Object.keys(mon.save || {}).sort(SortUtil.ascSortAtts).map(s=>Renderer.monster.getSave(Renderer.get(), s, mon.save[s])).join(", ")}`; + } + static getSensesPart(mon) { + return `${mon.senses ? `${Renderer.monster.getRenderedSenses(mon.senses)}, ` : ""}passive Perception ${mon.passive || "\u2014"}`; + } + + static getRenderWithPlugins({renderer, mon, fn}) { + return renderer.withPlugin({ + pluginTypes: ["dice", ], + fnPlugin: ()=>{ + if (mon.summonedBySpellLevel == null && mon._summonedByClass_level == null) + return null; + if (mon._summonedByClass_level) { + return { + additionalData: { + "data-summoned-by-class-level": mon._summonedByClass_level, + }, + }; + } + return { + additionalData: { + "data-summoned-by-spell-level": mon._summonedBySpell_level ?? mon.summonedBySpellLevel, + }, + }; + } + , + fn, + }); + } + + static getCompactRenderedString(mon, opts) { + const renderer = Renderer.get(); + return Renderer.monster.getRenderWithPlugins({ + renderer, + mon, + fn: ()=>Renderer.monster._getCompactRenderedString(mon, renderer, opts), + }); + } + + static _getCompactRenderedString(mon, renderer, opts) { + opts = opts || {}; + if (opts.isCompact === undefined) + opts.isCompact = true; + + const renderStack = []; + const legGroup = DataUtil.monster.getMetaGroup(mon); + const hasToken = mon.tokenUrl || mon.hasToken; + const extraThClasses = !opts.isCompact && hasToken ? ["mon__name--token"] : null; + + const isShowCrScaler = ScaleCreature.isCrInScaleRange(mon); + const isShowSpellLevelScaler = opts.isShowScalers && !isShowCrScaler && mon.summonedBySpellLevel != null; + const isShowClassLevelScaler = opts.isShowScalers && !isShowSpellLevelScaler && mon.summonedByClass != null; + + const fnGetSpellTraits = Renderer.monster.getSpellcastingRenderedTraits.bind(Renderer.monster, renderer); + const allTraits = Renderer.monster.getOrderedTraits(mon, { + fnGetSpellTraits + }); + const allActions = Renderer.monster.getOrderedActions(mon, { + fnGetSpellTraits + }); + const allBonusActions = Renderer.monster.getOrderedBonusActions(mon, { + fnGetSpellTraits + }); + const allReactions = Renderer.monster.getOrderedReactions(mon, { + fnGetSpellTraits + }); + + let ptCrSpellLevel = `\u2014`; + if (isShowSpellLevelScaler || isShowClassLevelScaler) { + const selHtml = isShowSpellLevelScaler ? Renderer.monster.getSelSummonSpellLevel(mon)?.outerHTML : Renderer.monster.getSelSummonClassLevel(mon)?.outerHTML; + ptCrSpellLevel = `${selHtml || ""}`; + } else if (isShowCrScaler) { + ptCrSpellLevel = ` + ${Parser.monCrToFull(mon.cr, { + isMythic: !!mon.mythic + })} + ${opts.isShowScalers && !opts.isScaledCr && Parser.isValidCr(mon.cr ? (mon.cr.cr || mon.cr) : null) ? ` + + ` : ""} + ${opts.isScaledCr ? ` + + ` : ""} + `; + } + + renderStack.push(` + ${Renderer.utils.getExcludedTr({ + entity: mon, + dataProp: "monster", + page: opts.page || UrlUtil.PG_BESTIARY + })} + ${Renderer.utils.getNameTr(mon, { + page: opts.page || UrlUtil.PG_BESTIARY, + extensionData: { + _scaledCr: mon._scaledCr, + _scaledSpellSummonLevel: mon._scaledSpellSummonLevel, + _scaledClassSummonLevel: mon._scaledClassSummonLevel + }, + extraThClasses, + isEmbeddedEntity: opts.isEmbeddedEntity + })} + ${Renderer.monster.getTypeAlignmentPart(mon)} +
            + + + + + + + + ${mon.pbNote || Parser.crToNumber(mon.cr) < VeCt.CR_CUSTOM ? `` : ""} + ${hasToken && !opts.isCompact ? `` : ""} + + + + + + ${ptCrSpellLevel} + ${mon.pbNote || Parser.crToNumber(mon.cr) < VeCt.CR_CUSTOM ? `` : ""} + ${hasToken && !opts.isCompact ? `` : ""} + +
            Armor ClassHit PointsSpeed${isShowSpellLevelScaler ? "Spell Level" : isShowClassLevelScaler ? "Class Level" : "Challenge"}PB
            ${Parser.acToFull(mon.ac)}${Renderer.monster.getRenderedHp(mon.hp)}${Parser.getSpeedString(mon)}${mon.pbNote ?? UiUtil.intToBonus(Parser.crToPb(mon.cr), { + isPretty: true + })}
            + +
            + ${Renderer.monster.getRenderedAbilityScores(mon)} +
            + +
            + ${mon.resource ? mon.resource.map(res=>`

            ${res.name} ${Renderer.monster.getRenderedResource(res)}

            `).join("") : ""} + ${mon.save ? `

            Saving Throws ${Renderer.monster.getSavesPart(mon)}

            ` : ""} + ${mon.skill ? `

            Skills ${Renderer.monster.getSkillsString(renderer, mon)}

            ` : ""} + ${mon.vulnerable ? `

            Damage Vuln. ${Parser.getFullImmRes(mon.vulnerable)}

            ` : ""} + ${mon.resist ? `

            Damage Res. ${Parser.getFullImmRes(mon.resist)}

            ` : ""} + ${mon.immune ? `

            Damage Imm. ${Parser.getFullImmRes(mon.immune)}

            ` : ""} + ${mon.conditionImmune ? `

            Condition Imm. ${Parser.getFullCondImm(mon.conditionImmune)}

            ` : ""} + ${opts.isHideSenses ? "" : `

            Senses ${Renderer.monster.getSensesPart(mon)}

            `} + ${opts.isHideLanguages ? "" : `

            Languages ${Renderer.monster.getRenderedLanguages(mon.languages)}

            `} +
            + + ${allTraits ? `
            + + ${allTraits.map(it=>it.rendered || renderer.render(it, 2)).join("")} + ` : ""} + ${Renderer.monster.getCompactRenderedStringSection({ + ...mon, + action: allActions + }, renderer, "Actions", "action", 2)} + ${Renderer.monster.getCompactRenderedStringSection({ + ...mon, + bonus: allBonusActions + }, renderer, "Bonus Actions", "bonus", 2)} + ${Renderer.monster.getCompactRenderedStringSection({ + ...mon, + reaction: allReactions + }, renderer, "Reactions", "reaction", 2)} + ${Renderer.monster.getCompactRenderedStringSection(mon, renderer, "Legendary Actions", "legendary", 2)} + ${Renderer.monster.getCompactRenderedStringSection(mon, renderer, "Mythic Actions", "mythic", 2)} + ${legGroup && legGroup.lairActions ? Renderer.monster.getCompactRenderedStringSection(legGroup, renderer, "Lair Actions", "lairActions", 1) : ""} + ${legGroup && legGroup.regionalEffects ? Renderer.monster.getCompactRenderedStringSection(legGroup, renderer, "Regional Effects", "regionalEffects", 1) : ""} + ${mon.variant || (mon.dragonCastingColor && !mon.spellcasting) || mon.summonedBySpell ? ` + + ${mon.variant ? mon.variant.map(it=>it.rendered || renderer.render(it)).join("") : ""} + ${mon.dragonCastingColor ? Renderer.monster.dragonCasterVariant.getHtml(mon, { + renderer + }) : ""} + ${mon.footer ? renderer.render({ + entries: mon.footer + }) : ""} + ${mon.summonedBySpell ? `
            Summoned By: ${renderer.render(`{@spell ${mon.summonedBySpell}}`)}
            ` : ""} + + ` : ""} + `); + + return renderStack.join(""); + } + + static _getFormulaMax(formula) { + return Renderer.dice.parseRandomise2(`dmax(${formula})`); + } + + static getRenderedHp(hp, isPlainText) { + if (hp.special != null) + return isPlainText ? Renderer.stripTags(hp.special) : Renderer.get().render(hp.special); + + if (/^\d+d1$/.exec(hp.formula)) { + return hp.average; + } + + if (isPlainText) + return `${hp.average} (${hp.formula})`; + + const maxVal = Renderer.monster._getFormulaMax(hp.formula); + const maxStr = maxVal ? `Maximum: ${maxVal}` : ""; + return `${maxStr ? `` : ""}${hp.average}${maxStr ? "" : ""} ${Renderer.get().render(`({@dice ${hp.formula}|${hp.formula}|Hit Points})`)}`; + } + + static getRenderedResource(res, isPlainText) { + if (!res.formula) + return `${res.value}`; + + if (isPlainText) + return `${res.value} (${res.formula})`; + + const maxVal = Renderer.monster._getFormulaMax(res.formula); + const maxStr = maxVal ? `Maximum: ${maxVal}` : ""; + return `${maxStr ? `` : ""}${res.value}${maxStr ? "" : ""} ${Renderer.get().render(`({@dice ${res.formula}|${res.formula}|${res.name}})`)}`; + } + + static getSafeAbilityScore(mon, abil, {isDefaultTen=false}={}) { + if (!mon || abil == null) + return isDefaultTen ? 10 : 0; + if (mon[abil] == null) + return isDefaultTen ? 10 : 0; + return typeof mon[abil] === "number" ? mon[abil] : (isDefaultTen ? 10 : 0); + } + + static getRenderedAbilityScores(mon) { + const byAbil = {}; + const byValue = {}; + + Parser.ABIL_ABVS.forEach(ab=>{ + if (mon[ab] == null || typeof mon[ab] === "number") + return; + + const meta = { + abil: ab, + value: mon[ab].special + }; + byAbil[meta.abil] = meta; + meta.family = (byValue[meta.value] = byValue[meta.value] || []); + meta.family.push(meta); + } + ); + + const seenAbs = new Set(); + const ptSpecial = Parser.ABIL_ABVS.map(ab=>{ + const meta = byAbil[ab]; + if (!meta) + return null; + if (seenAbs.has(meta.abil)) + return null; + meta.family.forEach(meta=>seenAbs.add(meta.abil)); + return `${meta.family.map(meta=>meta.abil.toUpperCase()).join(", ")} ${meta.value}`; + } + ).filter(Boolean).map(r=>`${r}`).join(""); + + if (Parser.ABIL_ABVS.every(ab=>mon[ab] != null && typeof mon[ab] !== "number")) + return ptSpecial; + + const absRemaining = Parser.ABIL_ABVS.filter(ab=>!seenAbs.has(ab)); + + return ` + ${absRemaining.map(ab=>`${ab.toUpperCase()}`).join("")} + + + ${absRemaining.map(ab=>`${Renderer.utils.getAbilityRoller(mon, ab)}`).join("")} + `; + } + + static getSpellcastingRenderedTraits(renderer, mon, displayAsProp="trait") { + const out = []; + (mon.spellcasting || []).filter(it=>(it.displayAs || "trait") === displayAsProp).forEach(entry=>{ + entry.type = entry.type || "spellcasting"; + const renderStack = []; + renderer.recursiveRender(entry, renderStack, { + depth: 2 + }); + const rendered = renderStack.join(""); + if (!rendered.length) + return; + out.push({ + name: entry.name, + rendered + }); + } + ); + return out; + } + + static getOrderedTraits(mon, {fnGetSpellTraits}={}) { + let traits = mon.trait ? MiscUtil.copyFast(mon.trait) : null; + + if (fnGetSpellTraits) { + const spellTraits = fnGetSpellTraits(mon, "trait"); + if (spellTraits.length) + traits = traits ? traits.concat(spellTraits) : spellTraits; + } + + if (traits?.length) + return traits.sort((a,b)=>SortUtil.monTraitSort(a, b)); + return null; + } + + static getOrderedActions(mon, {fnGetSpellTraits}={}) { + return Renderer.monster._getOrderedActionsBonusActions({ + mon, + fnGetSpellTraits, + prop: "action" + }); + } + static getOrderedBonusActions(mon, {fnGetSpellTraits}={}) { + return Renderer.monster._getOrderedActionsBonusActions({ + mon, + fnGetSpellTraits, + prop: "bonus" + }); + } + static getOrderedReactions(mon, {fnGetSpellTraits}={}) { + return Renderer.monster._getOrderedActionsBonusActions({ + mon, + fnGetSpellTraits, + prop: "reaction" + }); + } + + static _getOrderedActionsBonusActions({mon, fnGetSpellTraits, prop}={}) { + let actions = mon[prop] ? MiscUtil.copyFast(mon[prop]) : null; + + let spellActions; + if (fnGetSpellTraits) { + spellActions = fnGetSpellTraits(mon, prop); + } + + if (!spellActions?.length && !actions?.length) + return null; + if (!actions?.length) + return spellActions; + if (!spellActions?.length) + return actions; + + const ixLastAttack = actions.findLastIndex(it=>it.entries && it.entries.length && typeof it.entries[0] === "string" && it.entries[0].includes(`{@atk `)); + const ixNext = actions.findIndex((act,ix)=>ix > ixLastAttack && act.name && SortUtil.ascSortLower(act.name, "Spellcasting") >= 0); + if (~ixNext) + actions.splice(ixNext, 0, ...spellActions); + else + actions.push(...spellActions); + return actions; + } + + static getSkillsString(renderer, mon) { + if (!mon.skill) + return ""; + + function doSortMapJoinSkillKeys(obj, keys, joinWithOr) { + const toJoin = keys.sort(SortUtil.ascSort).map(s=>`${renderer.render(`{@skill ${s.toTitleCase()}}`)} ${Renderer.get().render(`{@skillCheck ${s.replace(/ /g, "_")} ${obj[s]}}`)}`); + return joinWithOr ? toJoin.joinConjunct(", ", " or ") : toJoin.join(", "); + } + + const skills = doSortMapJoinSkillKeys(mon.skill, Object.keys(mon.skill).filter(k=>k !== "other" && k !== "special")); + if (mon.skill.other || mon.skill.special) { + const others = mon.skill.other && mon.skill.other.map(it=>{ + if (it.oneOf) { + return `plus one of the following: ${doSortMapJoinSkillKeys(it.oneOf, Object.keys(it.oneOf), true)}`; + } + throw new Error(`Unhandled monster "other" skill properties!`); + } + ); + const special = mon.skill.special && Renderer.get().render(mon.skill.special); + return [skills, others, special].filter(Boolean).join(", "); + } + return skills; + } + + static getTokenUrl(mon) { + return mon.tokenUrl || UrlUtil.link(`${Renderer.get().baseMediaUrls["img"] || Renderer.get().baseUrl}img/${Parser.sourceJsonToAbv(mon.source)}/${Parser.nameToTokenName(mon.name)}.png`); + } + + static postProcessFluff(mon, fluff) { + const cpy = MiscUtil.copyFast(fluff); + + const thisGroup = DataUtil.monster.getMetaGroup(mon); + const handleGroupProp = (prop,name)=>{ + if (thisGroup && thisGroup[prop]) { + cpy.entries = cpy.entries || []; + cpy.entries.push({ + type: "entries", + entries: [{ + type: "entries", + name, + entries: MiscUtil.copyFast(thisGroup[prop]), + }, ], + }); + } + } + ; + + handleGroupProp("lairActions", "Lair Actions"); + handleGroupProp("regionalEffects", "Regional Effects"); + handleGroupProp("mythicEncounter", `${mon.name} as a Mythic Encounter`); + + return cpy; + } + + static _FN_TAG_SENSES = null; + static _SENSE_TAG_METAS = null; + static getRenderedSenses(senses, isPlainText) { + if (typeof senses === "string") + senses = [senses]; + if (isPlainText) + return senses.join(", "); + + if (!Renderer.monster._FN_TAG_SENSES) { + Renderer.monster._SENSE_TAG_METAS = [...MiscUtil.copyFast(Parser.SENSES), ...(PrereleaseUtil.getBrewProcessedFromCache("sense") || []), ...(BrewUtil2.getBrewProcessedFromCache("sense") || []), ]; + const seenNames = new Set(); + Renderer.monster._SENSE_TAG_METAS.filter(it=>{ + if (seenNames.has(it.name.toLowerCase())) + return false; + seenNames.add(it.name.toLowerCase()); + return true; + } + ).forEach(it=>it._re = new RegExp(`\\b(?${it.name.escapeRegexp()})\\b`,"gi")); + Renderer.monster._FN_TAG_SENSES = str=>{ + Renderer.monster._SENSE_TAG_METAS.forEach(({name, source, _re})=>str = str.replace(_re, (...m)=>`{@sense ${m.last().sense}|${source}}`)); + return str; + } + ; + } + + const senseStr = senses.map(str=>{ + const tagSplit = Renderer.splitByTags(str); + str = ""; + const len = tagSplit.length; + for (let i = 0; i < len; ++i) { + const s = tagSplit[i]; + + if (!s) + continue; + + if (s.startsWith("{@")) { + str += s; + continue; + } + + str += Renderer.monster._FN_TAG_SENSES(s); + } + return str; + } + ).join(", ").replace(/(^| |\()(blind|blinded)(\)| |$)/gi, (...m)=>`${m[1]}{@condition blinded||${m[2]}}${m[3]}`); + + return Renderer.get().render(senseStr); + } + + static getRenderedLanguages(languages) { + if (typeof languages === "string") + languages = [languages]; + return languages ? languages.map(it=>Renderer.get().render(it)).join(", ") : "\u2014"; + } + + static initParsed(mon) { + mon._pTypes = mon._pTypes || Parser.monTypeToFullObj(mon.type); + if (!mon._pCr) { + if (Parser.crToNumber(mon.cr) === VeCt.CR_CUSTOM) + mon._pCr = "Special"; + else if (Parser.crToNumber(mon.cr) === VeCt.CR_UNKNOWN) + mon._pCr = "Unknown"; + else + mon._pCr = mon.cr == null ? "\u2014" : (mon.cr.cr || mon.cr); + } + if (!mon._fCr) { + mon._fCr = [mon._pCr]; + if (mon.cr) { + if (mon.cr.lair) + mon._fCr.push(mon.cr.lair); + if (mon.cr.coven) + mon._fCr.push(mon.cr.coven); + } + } + } + + static updateParsed(mon) { + delete mon._pTypes; + delete mon._pCr; + delete mon._fCr; + Renderer.monster.initParsed(mon); + } + + static getRenderedVariants(mon, {renderer=null}={}) { + renderer = renderer || Renderer.get(); + const dragonVariant = Renderer.monster.dragonCasterVariant.getHtml(mon, { + renderer + }); + const variants = mon.variant; + if (!variants && !dragonVariant) + return null; + + const rStack = []; + (variants || []).forEach(v=>renderer.recursiveRender(v, rStack)); + if (dragonVariant) + rStack.push(dragonVariant); + return rStack.join(""); + } + + static getRenderedEnvironment(envs) { + return (envs || []).sort(SortUtil.ascSortLower).map(it=>it.toTitleCase()).join(", "); + } + + static getRenderedAltArtEntry(meta, {isPlainText=false}={}) { + return `${isPlainText ? "" : `
            `}${meta.displayName || meta.name}; ${isPlainText ? "" : ``}${Parser.sourceJsonToAbv(meta.source)}${Renderer.utils.isDisplayPage(meta.page) ? ` p${meta.page}` : ""}${isPlainText ? "" : `
            `}`; + } + + static pGetFluff(mon) { + return Renderer.utils.pGetFluff({ + entity: mon, + pFnPostProcess: Renderer.monster.postProcessFluff.bind(null, mon), + fluffBaseUrl: `data/bestiary/`, + fluffProp: "monsterFluff", + }); + } + + static getCustomHashId(mon) { + if (!mon._isScaledCr && !mon._isScaledSpellSummon && !mon._scaledClassSummonLevel) + return null; + + const {name, source, _scaledCr: scaledCr, _scaledSpellSummonLevel: scaledSpellSummonLevel, _scaledClassSummonLevel: scaledClassSummonLevel, } = mon; + + return [name, source, scaledCr ?? "", scaledSpellSummonLevel ?? "", scaledClassSummonLevel ?? "", ].join("__").toLowerCase(); + } + + static getUnpackedCustomHashId(customHashId) { + if (!customHashId) + return null; + + const [,,scaledCr,scaledSpellSummonLevel,scaledClassSummonLevel] = customHashId.split("__").map(it=>it.trim()); + + if (!scaledCr && !scaledSpellSummonLevel && !scaledClassSummonLevel) + return null; + + return { + _scaledCr: scaledCr ? Number(scaledCr) : null, + _scaledSpellSummonLevel: scaledSpellSummonLevel ? Number(scaledSpellSummonLevel) : null, + _scaledClassSummonLevel: scaledClassSummonLevel ? Number(scaledClassSummonLevel) : null, + customHashId, + }; + } + + static async pGetModifiedCreature(monRaw, customHashId) { + if (!customHashId) + return monRaw; + const {_scaledCr, _scaledSpellSummonLevel, _scaledClassSummonLevel} = Renderer.monster.getUnpackedCustomHashId(customHashId); + if (_scaledCr) + return ScaleCreature.scale(monRaw, _scaledCr); + if (_scaledSpellSummonLevel) + return ScaleSpellSummonedCreature.scale(monRaw, _scaledSpellSummonLevel); + if (_scaledClassSummonLevel) + return ScaleClassSummonedCreature.scale(monRaw, _scaledClassSummonLevel); + throw new Error(`Unhandled custom hash ID "${customHashId}"`); + } + + static _bindListenersScale(mon, ele) { + const page = UrlUtil.PG_BESTIARY; + const source = mon.source; + const hash = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_BESTIARY](mon); + + const fnRender = Renderer.hover.getFnRenderCompact(page); + + const $content = $(ele); + + $content.find(".mon__btn-scale-cr").click(evt=>{ + evt.stopPropagation(); + const win = (evt.view || {}).window; + + const $btn = $(evt.target).closest("button"); + const initialCr = mon._originalCr != null ? mon._originalCr : mon.cr.cr || mon.cr; + const lastCr = mon.cr.cr || mon.cr; + + Renderer.monster.getCrScaleTarget({ + win, + $btnScale: $btn, + initialCr: lastCr, + isCompact: true, + cbRender: async(targetCr)=>{ + const original = await DataLoader.pCacheAndGet(page, source, hash); + const toRender = Parser.numberToCr(targetCr) === initialCr ? original : await ScaleCreature.scale(original, targetCr); + + $content.empty().append(fnRender(toRender)); + + Renderer.monster._bindListenersScale(toRender, ele); + } + , + }); + } + ); + + $content.find(".mon__btn-reset-cr").click(async()=>{ + const toRender = await DataLoader.pCacheAndGet(page, source, hash); + $content.empty().append(fnRender(toRender)); + + Renderer.monster._bindListenersScale(toRender, ele); + } + ); + + const $selSummonSpellLevel = $content.find(`[name="mon__sel-summon-spell-level"]`).change(async()=>{ + const original = await DataLoader.pCacheAndGet(page, source, hash); + const spellLevel = Number($selSummonSpellLevel.val()); + + const toRender = ~spellLevel ? await ScaleSpellSummonedCreature.scale(original, spellLevel) : original; + + $content.empty().append(fnRender(toRender)); + + Renderer.monster._bindListenersScale(toRender, ele); + } + ).val(mon._summonedBySpell_level != null ? `${mon._summonedBySpell_level}` : "-1"); + + const $selSummonClassLevel = $content.find(`[name="mon__sel-summon-class-level"]`).change(async()=>{ + const original = await DataLoader.pCacheAndGet(page, source, hash); + const classLevel = Number($selSummonClassLevel.val()); + + const toRender = ~classLevel ? await ScaleClassSummonedCreature.scale(original, classLevel) : original; + + $content.empty().append(fnRender(toRender)); + + Renderer.monster._bindListenersScale(toRender, ele); + } + ).val(mon._summonedByClass_level != null ? `${mon._summonedByClass_level}` : "-1"); + } + + static bindListenersCompact(mon, ele) { + Renderer.monster._bindListenersScale(mon, ele); + } + + static hover = class { + static bindFluffImageMouseover({mon, $ele}) { + $ele.on("mouseover", evt=>this._pOnFluffImageMouseover({ + evt, + mon, + $ele + })); + } + + static async _pOnFluffImageMouseover({evt, mon, $ele}) { + $ele.off("mouseover"); + + const fluff = mon ? await Renderer.monster.pGetFluff(mon) : null; + + if (fluff?.images?.length) + return this._pOnFluffImageMouseover_hasImage({ + mon, + $ele, + fluff + }); + return this._pOnFluffImageMouseover_noImage({ + mon, + $ele + }); + } + + static _pOnFluffImageMouseover_noImage({mon, $ele}) { + const hoverMeta = this.getMakePredefinedFluffImageHoverNoImage({ + name: mon?.name + }); + $ele.on("mouseover", evt=>hoverMeta.mouseOver(evt, $ele[0])).on("mousemove", evt=>hoverMeta.mouseMove(evt, $ele[0])).on("mouseleave", evt=>hoverMeta.mouseLeave(evt, $ele[0])).trigger("mouseover"); + } + + static _pOnFluffImageMouseover_hasImage({mon, $ele, fluff}) { + const hoverMeta = this.getMakePredefinedFluffImageHoverHasImage({ + imageHref: fluff.images[0].href, + name: mon.name + }); + $ele.on("mouseover", evt=>hoverMeta.mouseOver(evt, $ele[0])).on("mousemove", evt=>hoverMeta.mouseMove(evt, $ele[0])).on("mouseleave", evt=>hoverMeta.mouseLeave(evt, $ele[0])).trigger("mouseover"); + } + + static getMakePredefinedFluffImageHoverNoImage({name}) { + return Renderer.hover.getMakePredefinedHover({ + type: "entries", + entries: [Renderer.utils.HTML_NO_IMAGES, ], + data: { + hoverTitle: name ? `Image \u2014 ${name}` : "Image", + }, + }, { + isBookContent: true + }, ); + } + + static getMakePredefinedFluffImageHoverHasImage({imageHref, name}) { + return Renderer.hover.getMakePredefinedHover({ + type: "image", + href: imageHref, + data: { + hoverTitle: name ? `Image \u2014 ${name}` : "Image", + }, + }, { + isBookContent: true + }, ); + } + } + ; +} +; +Renderer.monster.CHILD_PROPS_EXTENDED = [...Renderer.monster.CHILD_PROPS, "lairActions", "regionalEffects"]; + +Renderer.monster.CHILD_PROPS_EXTENDED.forEach(prop=>{ + const propFull = `monster${prop.uppercaseFirst()}`; + Renderer[propFull] = { + getCompactRenderedString(ent) { + return Renderer.generic.getCompactRenderedString(ent); + }, + }; +} +); + +Renderer.monsterAction.getWeaponLookupName = act=>{ + return (act.name || "").replace(/\(.*\)$/, "").trim().toLowerCase(); +} +; + +Renderer.legendaryGroup = class { + static getCompactRenderedString(legGroup, opts) { + opts = opts || {}; + + const ent = Renderer.legendaryGroup.getSummaryEntry(legGroup); + if (!ent) + return ""; + + return ` + ${Renderer.utils.getNameTr(legGroup, { + isEmbeddedEntity: opts.isEmbeddedEntity + })} + + ${Renderer.get().setFirstSection(true).render(ent)} + + ${Renderer.utils.getPageTr(legGroup)}`; + } + + static getSummaryEntry(legGroup) { + if (!legGroup || (!legGroup.lairActions && !legGroup.regionalEffects && !legGroup.mythicEncounter)) + return null; + + return { + type: "section", + entries: [legGroup.lairActions ? { + name: "Lair Actions", + type: "entries", + entries: legGroup.lairActions + } : null, legGroup.regionalEffects ? { + name: "Regional Effects", + type: "entries", + entries: legGroup.regionalEffects + } : null, legGroup.mythicEncounter ? { + name: "As a Mythic Encounter", + type: "entries", + entries: legGroup.mythicEncounter + } : null, ].filter(Boolean), + }; + } +} +; + +Renderer.item = class { + static _sortProperties(a, b) { + return SortUtil.ascSort(Renderer.item.getProperty(a, { + isIgnoreMissing: true + })?.name || "", Renderer.item.getProperty(b, { + isIgnoreMissing: true + })?.name || ""); + } + + static _getPropertiesText(item, {renderer=null}={}) { + renderer = renderer || Renderer.get(); + + if (!item.property) { + const parts = []; + if (item.dmg2) + parts.push(`alt. ${Renderer.item._renderDamage(item.dmg2, { + renderer + })}`); + if (item.range) + parts.push(`range ${item.range} ft.`); + return `${item.dmg1 && parts.length ? " - " : ""}${parts.join(", ")}`; + } + + let renderedDmg2 = false; + + const renderedProperties = item.property.sort(Renderer.item._sortProperties).map(p=>{ + const pFull = Renderer.item.getProperty(p); + + if (pFull.template) { + const toRender = Renderer.utils.applyTemplate(item, pFull.template, { + fnPreApply: (fullMatch,variablePath)=>{ + if (variablePath === "item.dmg2") + renderedDmg2 = true; + } + , + mapCustom: { + "prop_name": pFull.name + }, + }, ); + + return renderer.render(toRender); + } else + return pFull.name; + } + ); + + if (!renderedDmg2 && item.dmg2) + renderedProperties.unshift(`alt. ${Renderer.item._renderDamage(item.dmg2, { + renderer + })}`); + + return `${item.dmg1 && renderedProperties.length ? " - " : ""}${renderedProperties.join(", ")}`; + } + + static _getTaggedDamage(dmg, {renderer=null}={}) { + if (!dmg) + return ""; + + renderer = renderer || Renderer.get(); + + Renderer.stripTags(dmg.trim()); + + return renderer.render(`{@damage ${dmg}}`); + } + + static _renderDamage(dmg, {renderer=null}={}) { + renderer = renderer || Renderer.get(); + return renderer.render(Renderer.item._getTaggedDamage(dmg, { + renderer + })); + } + + static getDamageAndPropertiesText(item, {renderer=null}={}) { + renderer = renderer || Renderer.get(); + + const damagePartsPre = []; + const damageParts = []; + + if (item.mastery) + damagePartsPre.push(`Mastery: ${item.mastery.map(it=>renderer.render(`{@itemMastery ${it}}`)).join(", ")}`); + + if (item.ac != null) { + const prefix = item.type === "S" ? "+" : ""; + const suffix = (item.type === "LA" || item.bardingType === "LA") || ((item.type === "MA" || item.bardingType === "MA") && item.dexterityMax === null) ? " + Dex" : (item.type === "MA" || item.bardingType === "MA") ? ` + Dex (max ${item.dexterityMax ?? 2})` : ""; + damageParts.push(`AC ${prefix}${item.ac}${suffix}`); + } + if (item.acSpecial != null) + damageParts.push(item.ac != null ? item.acSpecial : `AC ${item.acSpecial}`); + + if (item.dmg1) + damageParts.push(Renderer.item._renderDamage(item.dmg1, { + renderer + })); + + if (item.speed != null) + damageParts.push(`Speed: ${item.speed}`); + if (item.carryingCapacity) + damageParts.push(`Carrying Capacity: ${item.carryingCapacity} lb.`); + + if (item.vehSpeed || item.capCargo || item.capPassenger || item.crew || item.crewMin || item.crewMax || item.vehAc || item.vehHp || item.vehDmgThresh || item.travelCost || item.shippingCost) { + const vehPartUpper = item.vehSpeed ? `Speed: ${Parser.numberToVulgar(item.vehSpeed)} mph` : null; + + const vehPartMiddle = item.capCargo || item.capPassenger ? `Carrying Capacity: ${[item.capCargo ? `${Parser.numberToFractional(item.capCargo)} ton${item.capCargo === 0 || item.capCargo > 1 ? "s" : ""} cargo` : null, item.capPassenger ? `${item.capPassenger} passenger${item.capPassenger === 1 ? "" : "s"}` : null].filter(Boolean).join(", ")}` : null; + + const {travelCostFull, shippingCostFull} = Parser.itemVehicleCostsToFull(item); + + const vehPartLower = [item.crew ? `Crew ${item.crew}` : null, item.crewMin && item.crewMax ? `Crew ${item.crewMin}-${item.crewMax}` : null, item.vehAc ? `AC ${item.vehAc}` : null, item.vehHp ? `HP ${item.vehHp}${item.vehDmgThresh ? `, Damage Threshold ${item.vehDmgThresh}` : ""}` : null, ].filter(Boolean).join(", "); + + damageParts.push([vehPartUpper, vehPartMiddle, + travelCostFull ? `Personal Travel Cost: ${travelCostFull} per mile per passenger` : null, shippingCostFull ? `Shipping Cost: ${shippingCostFull} per 100 pounds per mile` : null, + vehPartLower, ].filter(Boolean).join(renderer.getLineBreak())); + } + + const damage = [damagePartsPre.join(", "), damageParts.join(", "), ].filter(Boolean).join(renderer.getLineBreak()); + const damageType = item.dmgType ? Parser.dmgTypeToFull(item.dmgType) : ""; + const propertiesTxt = Renderer.item._getPropertiesText(item, { + renderer + }); + + return [damage, damageType, propertiesTxt]; + } + + static getTypeRarityAndAttunementText(item) { + const typeRarity = [item._typeHtml === "other" ? "" : item._typeHtml, (item.rarity && Renderer.item.doRenderRarity(item.rarity) ? item.rarity : ""), ].filter(Boolean).join(", "); + + return [item.reqAttune ? `${typeRarity} ${item._attunement}` : typeRarity, item._subTypeHtml || "", item.tier ? `${item.tier} tier` : "", ]; + } + + static getAttunementAndAttunementCatText(item, prop="reqAttune") { + let attunement = null; + let attunementCat = VeCt.STR_NO_ATTUNEMENT; + if (item[prop] != null && item[prop] !== false) { + if (item[prop] === true) { + attunementCat = "Requires Attunement"; + attunement = "(requires attunement)"; + } else if (item[prop] === "optional") { + attunementCat = "Attunement Optional"; + attunement = "(attunement optional)"; + } else if (item[prop].toLowerCase().startsWith("by")) { + attunementCat = "Requires Attunement By..."; + attunement = `(requires attunement ${Renderer.get().render(item[prop])})`; + } else { + attunementCat = "Requires Attunement"; + attunement = `(requires attunement ${Renderer.get().render(item[prop])})`; + } + } + return [attunement, attunementCat]; + } + + static getHtmlAndTextTypes(item) { + const typeHtml = []; + const typeListText = []; + const subTypeHtml = []; + + let showingBase = false; + if (item.wondrous) { + typeHtml.push(`wondrous item${item.tattoo ? ` (tattoo)` : ""}`); + typeListText.push("wondrous item"); + } + if (item.tattoo) { + typeListText.push("tattoo"); + } + if (item.staff) { + typeHtml.push("staff"); + typeListText.push("staff"); + } + if (item.ammo) { + typeHtml.push(`ammunition`); + typeListText.push("ammunition"); + } + if (item.firearm) { + subTypeHtml.push("firearm"); + typeListText.push("firearm"); + } + if (item.age) { + subTypeHtml.push(item.age); + typeListText.push(item.age); + } + if (item.weaponCategory) { + typeHtml.push(`weapon${item.baseItem ? ` (${Renderer.get().render(`{@item ${item.baseItem}}`)})` : ""}`); + subTypeHtml.push(`${item.weaponCategory} weapon`); + typeListText.push(`${item.weaponCategory} weapon`); + showingBase = true; + } + if (item.staff && (item.type !== "M" && item.typeAlt !== "M")) { + subTypeHtml.push("melee weapon"); + typeListText.push("melee weapon"); + } + if (item.type) + Renderer.item._getHtmlAndTextTypes_type({ + type: item.type, + typeHtml, + typeListText, + subTypeHtml, + showingBase, + item + }); + if (item.typeAlt) + Renderer.item._getHtmlAndTextTypes_type({ + type: item.typeAlt, + typeHtml, + typeListText, + subTypeHtml, + showingBase, + item + }); + if (item.poison) { + typeHtml.push(`poison${item.poisonTypes ? ` (${item.poisonTypes.joinConjunct(", ", " or ")})` : ""}`); + typeListText.push("poison"); + } + return [typeListText, typeHtml.join(", "), subTypeHtml.join(", ")]; + } + + static _getHtmlAndTextTypes_type({type, typeHtml, typeListText, subTypeHtml, showingBase, item}) { + const fullType = Renderer.item.getItemTypeName(type); + + const isSub = (typeListText.some(it=>it.includes("weapon")) && fullType.includes("weapon")) || (typeListText.some(it=>it.includes("armor")) && fullType.includes("armor")); + + if (!showingBase && !!item.baseItem) + (isSub ? subTypeHtml : typeHtml).push(`${fullType} (${Renderer.get().render(`{@item ${item.baseItem}}`)})`); + else if (type === "S") + (isSub ? subTypeHtml : typeHtml).push(Renderer.get().render(`armor ({@item shield|phb})`)); + else + (isSub ? subTypeHtml : typeHtml).push(fullType); + + typeListText.push(fullType); + } + + static _GET_RENDERED_ENTRIES_WALKER = null; + + static getRenderedEntries(item, {isCompact=false, wrappedTypeAllowlist=null}={}) { + const renderer = Renderer.get(); + + Renderer.item._GET_RENDERED_ENTRIES_WALKER = Renderer.item._GET_RENDERED_ENTRIES_WALKER || MiscUtil.getWalker({ + keyBlocklist: new Set([...MiscUtil.GENERIC_WALKER_ENTRIES_KEY_BLOCKLIST, "data", ]), + }); + + const handlersName = { + string: (str)=>Renderer.item._getRenderedEntries_handlerConvertNamesToItalics.bind(Renderer.item, item, item.name)(str), + }; + + const handlersVariantName = item._variantName == null ? null : { + string: (str)=>Renderer.item._getRenderedEntries_handlerConvertNamesToItalics.bind(Renderer.item, item, item._variantName)(str), + }; + + const renderStack = []; + if (item._fullEntries || item.entries?.length) { + const entry = MiscUtil.copyFast({ + type: "entries", + entries: item._fullEntries || item.entries + }); + let procEntry = Renderer.item._GET_RENDERED_ENTRIES_WALKER.walk(entry, handlersName); + if (handlersVariantName) + procEntry = Renderer.item._GET_RENDERED_ENTRIES_WALKER.walk(entry, handlersVariantName); + if (wrappedTypeAllowlist) + procEntry.entries = procEntry.entries.filter(it=>!it?.data?.[VeCt.ENTDATA_ITEM_MERGED_ENTRY_TAG] || wrappedTypeAllowlist.has(it?.data?.[VeCt.ENTDATA_ITEM_MERGED_ENTRY_TAG])); + renderer.recursiveRender(procEntry, renderStack, { + depth: 1 + }); + } + + if (item._fullAdditionalEntries || item.additionalEntries) { + const additionEntries = MiscUtil.copyFast({ + type: "entries", + entries: item._fullAdditionalEntries || item.additionalEntries + }); + let procAdditionEntries = Renderer.item._GET_RENDERED_ENTRIES_WALKER.walk(additionEntries, handlersName); + if (handlersVariantName) + procAdditionEntries = Renderer.item._GET_RENDERED_ENTRIES_WALKER.walk(additionEntries, handlersVariantName); + if (wrappedTypeAllowlist) + procAdditionEntries.entries = procAdditionEntries.entries.filter(it=>!it?.data?.[VeCt.ENTDATA_ITEM_MERGED_ENTRY_TAG] || wrappedTypeAllowlist.has(it?.data?.[VeCt.ENTDATA_ITEM_MERGED_ENTRY_TAG])); + renderer.recursiveRender(procAdditionEntries, renderStack, { + depth: 1 + }); + } + + if (!isCompact && item.lootTables) { + renderStack.push(`
            Found On: ${item.lootTables.sort(SortUtil.ascSortLower).map(tbl=>renderer.render(`{@table ${tbl}}`)).join(", ")}
            `); + } + + return renderStack.join("").trim(); + } + + static _getRenderedEntries_handlerConvertNamesToItalics(item, baseName, str) { + if (item._fIsMundane) + return str; + + const stack = []; + let depth = 0; + + const tgtLen = baseName.length; + const tgtName = item.sentient ? baseName : baseName.toLowerCase(); + + const tgtNamePlural = tgtName.toPlural(); + const tgtLenPlural = tgtNamePlural.length; + + const tgtNameNoBraces = tgtName.replace(/ \(.*$/, ""); + const tgtLenNoBraces = tgtNameNoBraces.length; + + const len = str.length; + for (let i = 0; i < len; ++i) { + const c = str[i]; + + switch (c) { + case "{": + { + if (str[i + 1] === "@") + depth++; + stack.push(c); + break; + } + case "}": + { + if (depth) + depth--; + stack.push(c); + break; + } + default: + stack.push(c); + break; + } + + if (depth) + continue; + + if (stack.slice(-tgtLen).join("")[item.sentient ? "toString" : "toLowerCase"]() === tgtName) { + stack.splice(stack.length - tgtLen, tgtLen, `{@i ${stack.slice(-tgtLen).join("")}}`); + } else if (stack.slice(-tgtLenPlural).join("")[item.sentient ? "toString" : "toLowerCase"]() === tgtNamePlural) { + stack.splice(stack.length - tgtLenPlural, tgtLenPlural, `{@i ${stack.slice(-tgtLenPlural).join("")}}`); + } else if (stack.slice(-tgtLenNoBraces).join("")[item.sentient ? "toString" : "toLowerCase"]() === tgtNameNoBraces) { + stack.splice(stack.length - tgtLenNoBraces, tgtLenNoBraces, `{@i ${stack.slice(-tgtLenNoBraces).join("")}}`); + } + } + + return stack.join(""); + } + + static getCompactRenderedString(item, opts) { + opts = opts || {}; + + const [damage,damageType,propertiesTxt] = Renderer.item.getDamageAndPropertiesText(item); + const [typeRarityText,subTypeText,tierText] = Renderer.item.getTypeRarityAndAttunementText(item); + + return ` + ${Renderer.utils.getExcludedTr({ + entity: item, + dataProp: "item", + page: UrlUtil.PG_ITEMS + })} + ${Renderer.utils.getNameTr(item, { + page: UrlUtil.PG_ITEMS, + isEmbeddedEntity: opts.isEmbeddedEntity + })} + ${Renderer.item.getTypeRarityAndAttunementHtml(typeRarityText, subTypeText, tierText)} + + ${[Parser.itemValueToFullMultiCurrency(item), Parser.itemWeightToFull(item)].filter(Boolean).join(", ").uppercaseFirst()} + ${damage} ${damageType} ${propertiesTxt} + + ${Renderer.item.hasEntries(item) ? `${Renderer.utils.getDividerTr()}${Renderer.item.getRenderedEntries(item, { + isCompact: true + })}` : ""}`; + } + + static hasEntries(item) { + return item._fullAdditionalEntries?.length || item._fullEntries?.length || item.entries?.length; + } + + static getTypeRarityAndAttunementHtml(typeRarityText, subTypeText, tierText) { + return `
            + ${typeRarityText || tierText ? `
            +
            ${(typeRarityText || "").uppercaseFirst()}
            +
            ${(tierText || "").uppercaseFirst()}
            +
            ` : ""} + ${subTypeText ? `
            ${subTypeText.uppercaseFirst()}
            ` : ""} +
            `; + } + + static _hiddenRarity = new Set(["none", "unknown", "unknown (magic)", "varies"]); + static doRenderRarity(rarity) { + return !Renderer.item._hiddenRarity.has(rarity); + } + + static _propertyMap = {}; + static _addProperty(prt) { + if (Renderer.item._propertyMap[prt.abbreviation]) + return; + const cpy = MiscUtil.copyFast(prt); + Renderer.item._propertyMap[prt.abbreviation] = prt.name ? cpy : { + ...cpy, + name: (prt.entries || prt.entriesTemplate)[0].name.toLowerCase(), + }; + } + + static getProperty(abbv, {isIgnoreMissing=false}={}) { + if (!isIgnoreMissing && !Renderer.item._propertyMap[abbv]) + throw new Error(`Item property ${abbv} not found. You probably meant to load the property reference first.`); + return Renderer.item._propertyMap[abbv]; + } + + static _typeMap = {}; + static _addType(typ) { + if (Renderer.item._typeMap[typ.abbreviation]?.entries || Renderer.item._typeMap[typ.abbreviation]?.entriesTemplate) + return; + const cpy = MiscUtil.copyFast(typ); + + Object.entries(Renderer.item._typeMap[typ.abbreviation] || {}).forEach(([k,v])=>{ + if (cpy[k]) + return; + cpy[k] = v; + } + ); + + cpy.name = cpy.name || (cpy.entries || cpy.entriesTemplate)[0].name.toLowerCase(); + + Renderer.item._typeMap[typ.abbreviation] = cpy; + } + + static getType(abbv) { + if (!Renderer.item._typeMap[abbv]) + throw new Error(`Item type ${abbv} not found. You probably meant to load the type reference first.`); + return Renderer.item._typeMap[abbv]; + } + + static entryMap = {}; + static _addEntry(ent) { + if (Renderer.item.entryMap[ent.source]?.[ent.name]) + return; + MiscUtil.set(Renderer.item.entryMap, ent.source, ent.name, ent); + } + + static _additionalEntriesMap = {}; + static _addAdditionalEntries(ent) { + if (Renderer.item._additionalEntriesMap[ent.appliesTo]) + return; + Renderer.item._additionalEntriesMap[ent.appliesTo] = MiscUtil.copyFast(ent.entries); + } + + static _masteryMap = {}; + static _addMastery(ent) { + const lookupSource = ent.source.toLowerCase(); + const lookupName = ent.name.toLowerCase(); + if (Renderer.item._masteryMap[lookupSource]?.[lookupName]) + return; + MiscUtil.set(Renderer.item._masteryMap, lookupSource, lookupName, ent); + } + + static _getMastery(uid) { + const {name, source} = DataUtil.proxy.unpackUid("itemMastery", uid, "itemMastery", { + isLower: true + }); + const out = MiscUtil.get(Renderer.item._masteryMap, source, name); + if (!out) + throw new Error(`Item mastry ${uid} not found. You probably meant to load the mastery reference first.`); + return out; + } + + static async _pAddPrereleaseBrewPropertiesAndTypes() { + if (typeof PrereleaseUtil !== "undefined") + Renderer.item.addPrereleaseBrewPropertiesAndTypesFrom({ + data: await PrereleaseUtil.pGetBrewProcessed() + }); + if (typeof BrewUtil2 !== "undefined") + Renderer.item.addPrereleaseBrewPropertiesAndTypesFrom({ + data: await BrewUtil2.pGetBrewProcessed() + }); + } + + static addPrereleaseBrewPropertiesAndTypesFrom({data}) { + (data.itemProperty || []).forEach(it=>Renderer.item._addProperty(it)); + (data.itemType || []).forEach(it=>Renderer.item._addType(it)); + (data.itemEntry || []).forEach(it=>Renderer.item._addEntry(it)); + (data.itemTypeAdditionalEntries || []).forEach(it=>Renderer.item._addAdditionalEntries(it)); + (data.itemMastery || []).forEach(it=>Renderer.item._addMastery(it)); + } + + static _addBasePropertiesAndTypes(baseItemData) { + Object.entries(Parser.ITEM_TYPE_JSON_TO_ABV).forEach(([abv,name])=>Renderer.item._addType({ + abbreviation: abv, + name + })); + + (baseItemData.itemProperty || []).forEach(it=>Renderer.item._addProperty(it)); + (baseItemData.itemType || []).forEach(it=>Renderer.item._addType(it)); + (baseItemData.itemEntry || []).forEach(it=>Renderer.item._addEntry(it)); + (baseItemData.itemTypeAdditionalEntries || []).forEach(it=>Renderer.item._addAdditionalEntries(it)); + (baseItemData.itemMastery || []).forEach(it=>Renderer.item._addMastery(it)); + + baseItemData.baseitem.forEach(it=>it._isBaseItem = true); + } + + static async _pGetSiteUnresolvedRefItems_pLoadItems() { + const itemData = await DataUtil.loadJSON(`${Renderer.get().baseUrl}data/items.json`); + const items = itemData.item; + itemData.itemGroup.forEach(it=>it._isItemGroup = true); + return [...items, ...itemData.itemGroup]; + } + + static async pGetSiteUnresolvedRefItems() { + const itemList = await Renderer.item._pGetSiteUnresolvedRefItems_pLoadItems(); + const baseItemsJson = await DataUtil.loadJSON(`${Renderer.get().baseUrl}data/items-base.json`); + const baseItems = await Renderer.item._pGetAndProcBaseItems(baseItemsJson); + const {genericVariants, linkedLootTables} = await Renderer.item._pGetCacheSiteGenericVariants(); + const specificVariants = Renderer.item._createSpecificVariants(baseItems, genericVariants, { + linkedLootTables + }); + const allItems = [...itemList, ...baseItems, ...genericVariants, ...specificVariants]; + Renderer.item._enhanceItems(allItems); + + return { + item: allItems, + itemEntry: baseItemsJson.itemEntry, + }; + } + + static _pGettingSiteGenericVariants = null; + static async _pGetCacheSiteGenericVariants() { + Renderer.item._pGettingSiteGenericVariants = Renderer.item._pGettingSiteGenericVariants || (async()=>{ + const [genericVariants,linkedLootTables] = Renderer.item._getAndProcGenericVariants(await DataUtil.loadJSON(`${Renderer.get().baseUrl}data/magicvariants.json`)); + return { + genericVariants, + linkedLootTables + }; + } + )(); + return Renderer.item._pGettingSiteGenericVariants; + } + + static async pBuildList() { + return DataLoader.pCacheAndGetAllSite(UrlUtil.PG_ITEMS); + } + + static async _pGetAndProcBaseItems(baseItemData) { + Renderer.item._addBasePropertiesAndTypes(baseItemData); + await Renderer.item._pAddPrereleaseBrewPropertiesAndTypes(); + return baseItemData.baseitem; + } + + static _getAndProcGenericVariants(variantData) { + variantData.magicvariant.forEach(Renderer.item._genericVariants_addInheritedPropertiesToSelf); + return [variantData.magicvariant, variantData.linkedLootTables]; + } + + static _initFullEntries(item) { + Renderer.utils.initFullEntries_(item); + } + + static _initFullAdditionalEntries(item) { + Renderer.utils.initFullEntries_(item, { + propEntries: "additionalEntries", + propFullEntries: "_fullAdditionalEntries" + }); + } + + static _createSpecificVariants(baseItems, genericVariants, opts) { + opts = opts || {}; + + const genericAndSpecificVariants = []; + baseItems.forEach((curBaseItem)=>{ + curBaseItem._category = "Basic"; + if (curBaseItem.entries == null) + curBaseItem.entries = []; + + if (curBaseItem.packContents) + return; + genericVariants.forEach((curGenericVariant)=>{ + if (!Renderer.item._createSpecificVariants_hasRequiredProperty(curBaseItem, curGenericVariant)) + return; + if (Renderer.item._createSpecificVariants_hasExcludedProperty(curBaseItem, curGenericVariant)) + return; + + genericAndSpecificVariants.push(Renderer.item._createSpecificVariants_createSpecificVariant(curBaseItem, curGenericVariant, opts)); + } + ); + } + ); + return genericAndSpecificVariants; + } + + static _createSpecificVariants_hasRequiredProperty(baseItem, genericVariant) { + return genericVariant.requires.some(req=>Renderer.item._createSpecificVariants_isRequiresExcludesMatch(baseItem, req, "every")); + } + + static _createSpecificVariants_hasExcludedProperty(baseItem, genericVariant) { + const curExcludes = genericVariant.excludes || {}; + return Renderer.item._createSpecificVariants_isRequiresExcludesMatch(baseItem, genericVariant.excludes, "some"); + } + + static _createSpecificVariants_isRequiresExcludesMatch(candidate, requirements, method) { + if (candidate == null || requirements == null) + return false; + + return Object.entries(requirements)[method](([reqKey,reqVal])=>{ + if (reqVal instanceof Array) { + return candidate[reqKey]instanceof Array ? candidate[reqKey].some(it=>reqVal.includes(it)) : reqVal.includes(candidate[reqKey]); + } + + if (reqVal != null && typeof reqVal === "object") { + return Renderer.item._createSpecificVariants_isRequiresExcludesMatch(candidate[reqKey], reqVal, method); + } + + return candidate[reqKey]instanceof Array ? candidate[reqKey].some(it=>reqVal === it) : reqVal === candidate[reqKey]; + } + ); + } + + static _createSpecificVariants_createSpecificVariant(baseItem, genericVariant, opts) { + const inherits = genericVariant.inherits; + const specificVariant = MiscUtil.copyFast(baseItem); + + specificVariant.__prop = "item"; + + delete specificVariant._isBaseItem; + + specificVariant._isEnhanced = false; + delete specificVariant._fullEntries; + + specificVariant._baseName = baseItem.name; + specificVariant._baseSrd = baseItem.srd; + specificVariant._baseBasicRules = baseItem.basicRules; + if (baseItem.source !== inherits.source) + specificVariant._baseSource = baseItem.source; + + specificVariant._variantName = genericVariant.name; + + delete specificVariant.value; + + delete specificVariant.srd; + delete specificVariant.basicRules; + delete specificVariant.page; + + delete specificVariant.hasFluff; + delete specificVariant.hasFluffImages; + + specificVariant._category = "Specific Variant"; + Object.entries(inherits).forEach(([inheritedProperty,val])=>{ + switch (inheritedProperty) { + case "namePrefix": + specificVariant.name = `${val}${specificVariant.name}`; + break; + case "nameSuffix": + specificVariant.name = `${specificVariant.name}${val}`; + break; + case "entries": + { + Renderer.item._initFullEntries(specificVariant); + + const appliedPropertyEntries = Renderer.applyAllProperties(val, Renderer.item._getInjectableProps(baseItem, inherits)); + appliedPropertyEntries.forEach((ent,i)=>specificVariant._fullEntries.splice(i, 0, ent)); + break; + } + case "vulnerable": + case "resist": + case "immune": + { + break; + } + case "conditionImmune": + { + specificVariant[inheritedProperty] = [...specificVariant[inheritedProperty] || [], ...val].unique(); + break; + } + case "nameRemove": + { + specificVariant.name = specificVariant.name.replace(new RegExp(val.escapeRegexp(),"g"), ""); + + break; + } + case "weightExpression": + case "valueExpression": + { + const exp = Renderer.item._createSpecificVariants_evaluateExpression(baseItem, specificVariant, inherits, inheritedProperty); + + const result = Renderer.dice.parseRandomise2(exp); + if (result != null) { + switch (inheritedProperty) { + case "weightExpression": + specificVariant.weight = result; + break; + case "valueExpression": + specificVariant.value = result; + break; + } + } + + break; + } + case "barding": + { + specificVariant.bardingType = baseItem.type; + break; + } + case "propertyAdd": + { + specificVariant.property = [...(specificVariant.property || []), ...val.filter(it=>!specificVariant.property || !specificVariant.property.includes(it)), ]; + break; + } + case "propertyRemove": + { + if (specificVariant.property) { + specificVariant.property = specificVariant.property.filter(it=>!val.includes(it)); + if (!specificVariant.property.length) + delete specificVariant.property; + } + break; + } + default: + specificVariant[inheritedProperty] = val; + } + } + ); + + Renderer.item._createSpecificVariants_mergeVulnerableResistImmune({ + specificVariant, + inherits + }); + + genericVariant.variants = genericVariant.variants || []; + if (!genericVariant.variants.some(it=>it.base?.name === baseItem.name && it.base?.source === baseItem.source)) + genericVariant.variants.push({ + base: baseItem, + specificVariant + }); + + specificVariant.genericVariant = { + name: genericVariant.name, + source: genericVariant.source, + }; + + if (opts.linkedLootTables && opts.linkedLootTables[specificVariant.source] && opts.linkedLootTables[specificVariant.source][specificVariant.name]) { + (specificVariant.lootTables = specificVariant.lootTables || []).push(...opts.linkedLootTables[specificVariant.source][specificVariant.name]); + } + + if (baseItem.source !== Parser.SRC_PHB && baseItem.source !== Parser.SRC_DMG) { + Renderer.item._initFullEntries(specificVariant); + specificVariant._fullEntries.unshift({ + type: "wrapper", + wrapped: `{@note The {@item ${baseItem.name}|${baseItem.source}|base item} can be found in ${Parser.sourceJsonToFull(baseItem.source)}${baseItem.page ? `, page ${baseItem.page}` : ""}.}`, + data: { + [VeCt.ENTDATA_ITEM_MERGED_ENTRY_TAG]: "note", + }, + }); + } + + return specificVariant; + } + + static _createSpecificVariants_evaluateExpression(baseItem, specificVariant, inherits, inheritedProperty) { + return inherits[inheritedProperty].replace(/\[\[([^\]]+)]]/g, (...m)=>{ + const propPath = m[1].split("."); + return propPath[0] === "item" ? MiscUtil.get(specificVariant, ...propPath.slice(1)) : propPath[0] === "baseItem" ? MiscUtil.get(baseItem, ...propPath.slice(1)) : MiscUtil.get(specificVariant, ...propPath); + } + ); + } + + static _PROPS_VULN_RES_IMMUNE = ["vulnerable", "resist", "immune", ]; + static _createSpecificVariants_mergeVulnerableResistImmune({specificVariant, inherits}) { + const fromBase = {}; + Renderer.item._PROPS_VULN_RES_IMMUNE.filter(prop=>specificVariant[prop]).forEach(prop=>fromBase[prop] = [...specificVariant[prop]]); + + Renderer.item._PROPS_VULN_RES_IMMUNE.forEach(prop=>{ + const val = inherits[prop]; + + if (val === undefined) + return; + + if (val == null) + return delete fromBase[prop]; + + const valSet = new Set(); + val.forEach(it=>{ + if (typeof it === "string") + valSet.add(it); + if (!it?.[prop]?.length) + return; + it?.[prop].forEach(itSub=>{ + if (typeof itSub === "string") + valSet.add(itSub); + } + ); + } + ); + + Renderer.item._PROPS_VULN_RES_IMMUNE.filter(it=>it !== prop).forEach(propOther=>{ + if (!fromBase[propOther]) + return; + + fromBase[propOther] = fromBase[propOther].filter(it=>{ + if (typeof it === "string") + return !valSet.has(it); + + if (it?.[propOther]?.length) { + it[propOther] = it[propOther].filter(itSub=>{ + if (typeof itSub === "string") + return !valSet.has(itSub); + return true; + } + ); + } + + return true; + } + ); + + if (!fromBase[propOther].length) + delete fromBase[propOther]; + } + ); + } + ); + + Renderer.item._PROPS_VULN_RES_IMMUNE.forEach(prop=>{ + if (fromBase[prop] || inherits[prop]) + specificVariant[prop] = [...(fromBase[prop] || []), ...(inherits[prop] || [])].unique(); + else + delete specificVariant[prop]; + } + ); + } + + static _enhanceItems(allItems) { + allItems.forEach((item)=>Renderer.item.enhanceItem(item)); + return allItems; + } + + static async pGetGenericAndSpecificVariants(genericVariants, opts) { + opts = opts || {}; + + let baseItems; + if (opts.baseItems) { + baseItems = opts.baseItems; + } else { + const baseItemData = await DataUtil.loadJSON(`${Renderer.get().baseUrl}data/items-base.json`); + Renderer.item._addBasePropertiesAndTypes(baseItemData); + baseItems = [...baseItemData.baseitem, ...(opts.additionalBaseItems || [])]; + } + + await Renderer.item._pAddPrereleaseBrewPropertiesAndTypes(); + genericVariants.forEach(Renderer.item._genericVariants_addInheritedPropertiesToSelf); + const specificVariants = Renderer.item._createSpecificVariants(baseItems, genericVariants); + const outSpecificVariants = Renderer.item._enhanceItems(specificVariants); + + if (opts.isSpecificVariantsOnly) + return outSpecificVariants; + + const outGenericVariants = Renderer.item._enhanceItems(genericVariants); + return [...outGenericVariants, ...outSpecificVariants]; + } + + static _getInjectableProps(baseItem, inherits) { + return { + baseName: baseItem.name, + dmgType: baseItem.dmgType ? Parser.dmgTypeToFull(baseItem.dmgType) : null, + bonusAc: inherits.bonusAc, + bonusWeapon: inherits.bonusWeapon, + bonusWeaponAttack: inherits.bonusWeaponAttack, + bonusWeaponDamage: inherits.bonusWeaponDamage, + bonusWeaponCritDamage: inherits.bonusWeaponCritDamage, + bonusSpellAttack: inherits.bonusSpellAttack, + bonusSpellSaveDc: inherits.bonusSpellSaveDc, + bonusSavingThrow: inherits.bonusSavingThrow, + }; + } + + static _INHERITED_PROPS_BLOCKLIST = new Set(["entries", "rarity", + "namePrefix", "nameSuffix", ]); + static _genericVariants_addInheritedPropertiesToSelf(genericVariant) { + if (genericVariant._isInherited) + return; + genericVariant._isInherited = true; + + for (const prop in genericVariant.inherits) { + if (Renderer.item._INHERITED_PROPS_BLOCKLIST.has(prop)) + continue; + + const val = genericVariant.inherits[prop]; + + if (val == null) + delete genericVariant[prop]; + else if (genericVariant[prop]) { + if (genericVariant[prop]instanceof Array && val instanceof Array) + genericVariant[prop] = MiscUtil.copyFast(genericVariant[prop]).concat(val); + else + genericVariant[prop] = val; + } else + genericVariant[prop] = genericVariant.inherits[prop]; + } + + if (!genericVariant.entries && genericVariant.inherits.entries) { + genericVariant.entries = MiscUtil.copyFast(Renderer.applyAllProperties(genericVariant.inherits.entries, genericVariant.inherits)); + } + + if (genericVariant.inherits.rarity == null) + delete genericVariant.rarity; + else if (genericVariant.inherits.rarity === "varies") {} else + genericVariant.rarity = genericVariant.inherits.rarity; + + if (genericVariant.requires.armor) + genericVariant.armor = genericVariant.requires.armor; + } + + static getItemTypeName(t) { + return Renderer.item.getType(t).name?.toLowerCase() || t; + } + + static enhanceItem(item) { + if (item._isEnhanced) + return; + item._isEnhanced = true; + if (item.noDisplay) + return; + if (item.type === "GV") + item._category = "Generic Variant"; + if (item._category == null) + item._category = "Other"; + if (item.entries == null) + item.entries = []; + if (item.type && (Renderer.item.getType(item.type)?.entries || Renderer.item.getType(item.type)?.entriesTemplate)) { + Renderer.item._initFullEntries(item); + + const propetyEntries = Renderer.item._enhanceItem_getItemPropertyTypeEntries({ + item, + ent: Renderer.item.getType(item.type) + }); + propetyEntries.forEach(e=>item._fullEntries.push({ + type: "wrapper", + wrapped: e, + data: { + [VeCt.ENTDATA_ITEM_MERGED_ENTRY_TAG]: "type" + } + })); + } + if (item.property) { + item.property.forEach(p=>{ + const entProperty = Renderer.item.getProperty(p); + if (!entProperty.entries && !entProperty.entriesTemplate) + return; + + Renderer.item._initFullEntries(item); + + const propetyEntries = Renderer.item._enhanceItem_getItemPropertyTypeEntries({ + item, + ent: entProperty + }); + propetyEntries.forEach(e=>item._fullEntries.push({ + type: "wrapper", + wrapped: e, + data: { + [VeCt.ENTDATA_ITEM_MERGED_ENTRY_TAG]: "property" + } + })); + } + ); + } + if (item.type === "LA" || item.type === "MA" || item.type === "HA") { + if (item.stealth) { + Renderer.item._initFullEntries(item); + item._fullEntries.push({ + type: "wrapper", + wrapped: "The wearer has disadvantage on Dexterity ({@skill Stealth}) checks.", + data: { + [VeCt.ENTDATA_ITEM_MERGED_ENTRY_TAG]: "type" + } + }); + } + if (item.type === "HA" && item.strength) { + Renderer.item._initFullEntries(item); + item._fullEntries.push({ + type: "wrapper", + wrapped: `If the wearer has a Strength score lower than ${item.strength}, their speed is reduced by 10 feet.`, + data: { + [VeCt.ENTDATA_ITEM_MERGED_ENTRY_TAG]: "type" + } + }); + } + } + if (item.type === "SCF") { + if (item._isItemGroup) { + if (item.scfType === "arcane" && item.source !== Parser.SRC_ERLW) { + Renderer.item._initFullEntries(item); + item._fullEntries.push({ + type: "wrapper", + wrapped: "An arcane focus is a special item\u2014an orb, a crystal, a rod, a specially constructed staff, a wand-like length of wood, or some similar item\u2014designed to channel the power of arcane spells. A sorcerer, warlock, or wizard can use such an item as a spellcasting focus.", + data: { + [VeCt.ENTDATA_ITEM_MERGED_ENTRY_TAG]: "type.SCF" + } + }); + } + if (item.scfType === "druid") { + Renderer.item._initFullEntries(item); + item._fullEntries.push({ + type: "wrapper", + wrapped: "A druidic focus might be a sprig of mistletoe or holly, a wand or scepter made of yew or another special wood, a staff drawn whole out of a living tree, or a totem object incorporating feathers, fur, bones, and teeth from sacred animals. A druid can use such an object as a spellcasting focus.", + data: { + [VeCt.ENTDATA_ITEM_MERGED_ENTRY_TAG]: "type.SCF" + } + }); + } + if (item.scfType === "holy") { + Renderer.item._initFullEntries(item); + item._fullEntries.push({ + type: "wrapper", + wrapped: "A holy symbol is a representation of a god or pantheon. It might be an amulet depicting a symbol representing a deity, the same symbol carefully engraved or inlaid as an emblem on a shield, or a tiny box holding a fragment of a sacred relic. A cleric or paladin can use a holy symbol as a spellcasting focus. To use the symbol in this way, the caster must hold it in hand, wear it visibly, or bear it on a shield.", + data: { + [VeCt.ENTDATA_ITEM_MERGED_ENTRY_TAG]: "type.SCF" + } + }); + } + } else { + if (item.scfType === "arcane") { + Renderer.item._initFullEntries(item); + item._fullEntries.push({ + type: "wrapper", + wrapped: "An arcane focus is a special item designed to channel the power of arcane spells. A sorcerer, warlock, or wizard can use such an item as a spellcasting focus.", + data: { + [VeCt.ENTDATA_ITEM_MERGED_ENTRY_TAG]: "type.SCF" + } + }); + } + if (item.scfType === "druid") { + Renderer.item._initFullEntries(item); + item._fullEntries.push({ + type: "wrapper", + wrapped: "A druid can use this object as a spellcasting focus.", + data: { + [VeCt.ENTDATA_ITEM_MERGED_ENTRY_TAG]: "type.SCF" + } + }); + } + if (item.scfType === "holy") { + Renderer.item._initFullEntries(item); + + item._fullEntries.push({ + type: "wrapper", + wrapped: "A holy symbol is a representation of a god or pantheon.", + data: { + [VeCt.ENTDATA_ITEM_MERGED_ENTRY_TAG]: "type.SCF" + } + }); + item._fullEntries.push({ + type: "wrapper", + wrapped: "A cleric or paladin can use a holy symbol as a spellcasting focus. To use the symbol in this way, the caster must hold it in hand, wear it visibly, or bear it on a shield.", + data: { + [VeCt.ENTDATA_ITEM_MERGED_ENTRY_TAG]: "type.SCF" + } + }); + } + } + } + + (item.mastery || []).forEach(uid=>{ + const mastery = Renderer.item._getMastery(uid); + + if (!mastery) + throw new Error(`Item mastery ${uid} not found. You probably meant to load the property/type reference first; see \`Renderer.item.pPopulatePropertyAndTypeReference()\`.`); + if (!mastery.entries && !mastery.entriesTemplate) + return; + + Renderer.item._initFullEntries(item); + + item._fullEntries.push({ + type: "wrapper", + wrapped: { + type: "entries", + name: `Mastery: ${mastery.name}`, + source: mastery.source, + page: mastery.page, + entries: Renderer.item._enhanceItem_getItemPropertyTypeEntries({ + item, + ent: mastery + }), + }, + data: { + [VeCt.ENTDATA_ITEM_MERGED_ENTRY_TAG]: "mastery", + }, + }); + } + ); + + if (item.type === "T" || item.type === "AT" || item.type === "INS" || item.type === "GS") { + Renderer.item._initFullAdditionalEntries(item); + item._fullAdditionalEntries.push({ + type: "wrapper", + wrapped: { + type: "hr" + }, + data: { + [VeCt.ENTDATA_ITEM_MERGED_ENTRY_TAG]: "type" + } + }); + item._fullAdditionalEntries.push({ + type: "wrapper", + wrapped: `{@note See the {@variantrule Tool Proficiencies|XGE} entry for more information.}`, + data: { + [VeCt.ENTDATA_ITEM_MERGED_ENTRY_TAG]: "type" + } + }); + } + + if (item.type === "INS" || item.type === "GS") + item.additionalSources = item.additionalSources || []; + if (item.type === "INS") { + if (!item.additionalSources.find(it=>it.source === "XGE" && it.page === 83)) + item.additionalSources.push({ + "source": "XGE", + "page": 83 + }); + } else if (item.type === "GS") { + if (!item.additionalSources.find(it=>it.source === "XGE" && it.page === 81)) + item.additionalSources.push({ + "source": "XGE", + "page": 81 + }); + } + + if (item.type && Renderer.item._additionalEntriesMap[item.type]) { + Renderer.item._initFullAdditionalEntries(item); + const additional = Renderer.item._additionalEntriesMap[item.type]; + item._fullAdditionalEntries.push({ + type: "wrapper", + wrapped: { + type: "entries", + entries: additional + }, + data: { + [VeCt.ENTDATA_ITEM_MERGED_ENTRY_TAG]: "type" + } + }); + } + + const [typeListText,typeHtml,subTypeHtml] = Renderer.item.getHtmlAndTextTypes(item); + item._typeListText = typeListText; + item._typeHtml = typeHtml; + item._subTypeHtml = subTypeHtml; + + const [attune,attuneCat] = Renderer.item.getAttunementAndAttunementCatText(item); + item._attunement = attune; + item._attunementCategory = attuneCat; + + if (item.reqAttuneAlt) { + const [attuneAlt,attuneCatAlt] = Renderer.item.getAttunementAndAttunementCatText(item, "reqAttuneAlt"); + item._attunementCategory = [attuneCat, attuneCatAlt]; + } + + if (item._isItemGroup) { + Renderer.item._initFullEntries(item); + item._fullEntries.push({ + type: "wrapper", + wrapped: "Multiple variations of this item exist, as listed below:", + data: { + [VeCt.ENTDATA_ITEM_MERGED_ENTRY_TAG]: "magicvariant" + } + }); + item._fullEntries.push({ + type: "wrapper", + wrapped: { + type: "list", + items: item.items.map(it=>typeof it === "string" ? `{@item ${it}}` : `{@item ${it.name}|${it.source}}`), + }, + data: { + [VeCt.ENTDATA_ITEM_MERGED_ENTRY_TAG]: "magicvariant" + }, + }); + } + + if (item.variants && item.variants.length) { + item.variants.sort((a,b)=>SortUtil.ascSortLower(a.base.name, b.base.name) || SortUtil.ascSortLower(a.base.source, b.base.source)); + + Renderer.item._initFullEntries(item); + item._fullEntries.push({ + type: "wrapper", + wrapped: { + type: "entries", + name: "Base items", + entries: ["This item variant can be applied to the following base items:", { + type: "list", + items: item.variants.map(({base, specificVariant})=>{ + return `{@item ${base.name}|${base.source}} ({@item ${specificVariant.name}|${specificVariant.source}})`; + } + ), + }, ], + }, + data: { + [VeCt.ENTDATA_ITEM_MERGED_ENTRY_TAG]: "magicvariant" + }, + }); + } + } + + static _enhanceItem_getItemPropertyTypeEntries({item, ent}) { + if (!ent.entriesTemplate) + return MiscUtil.copyFast(ent.entries); + return MiscUtil.getWalker({ + keyBlocklist: MiscUtil.GENERIC_WALKER_ENTRIES_KEY_BLOCKLIST, + }).walk(MiscUtil.copyFast(ent.entriesTemplate), { + string: (str)=>{ + return Renderer.utils.applyTemplate(item, str, ); + } + , + }, ); + } + + static unenhanceItem(item) { + if (!item._isEnhanced) + return; + delete item._isEnhanced; + delete item._fullEntries; + } + + static async pGetSiteUnresolvedRefItemsFromPrereleaseBrew({brewUtil, brew=null}) { + if (brewUtil == null && brew == null) + return []; + + brew = brew || await brewUtil.pGetBrewProcessed(); + + (brew.itemProperty || []).forEach(p=>Renderer.item._addProperty(p)); + (brew.itemType || []).forEach(t=>Renderer.item._addType(t)); + (brew.itemEntry || []).forEach(it=>Renderer.item._addEntry(it)); + (brew.itemTypeAdditionalEntries || []).forEach(it=>Renderer.item._addAdditionalEntries(it)); + + let items = [...(brew.baseitem || []), ...(brew.item || [])]; + + if (brew.itemGroup) { + const itemGroups = MiscUtil.copyFast(brew.itemGroup); + itemGroups.forEach(it=>it._isItemGroup = true); + items = [...items, ...itemGroups]; + } + + Renderer.item._enhanceItems(items); + + let isReEnhanceVariants = false; + + if (brew.baseitem && brew.baseitem.length) { + isReEnhanceVariants = true; + + const {genericVariants} = await Renderer.item._pGetCacheSiteGenericVariants(); + + const variants = await Renderer.item.pGetGenericAndSpecificVariants(genericVariants, { + baseItems: brew.baseitem || [], + isSpecificVariantsOnly: true + }, ); + items = [...items, ...variants]; + } + + if (brew.magicvariant && brew.magicvariant.length) { + isReEnhanceVariants = true; + + const variants = await Renderer.item.pGetGenericAndSpecificVariants(brew.magicvariant, { + additionalBaseItems: brew.baseitem || [] + }, ); + items = [...items, ...variants]; + } + + if (isReEnhanceVariants) { + const {genericVariants} = await Renderer.item._pGetCacheSiteGenericVariants(); + genericVariants.forEach(item=>{ + Renderer.item.unenhanceItem(item); + Renderer.item.enhanceItem(item); + } + ); + } + + return items; + } + + static async pGetItemsFromPrerelease() { + return DataLoader.pCacheAndGetAllPrerelease(UrlUtil.PG_ITEMS); + } + + static async pGetItemsFromBrew() { + return DataLoader.pCacheAndGetAllBrew(UrlUtil.PG_ITEMS); + } + + static _pPopulatePropertyAndTypeReference = null; + static pPopulatePropertyAndTypeReference() { + return Renderer.item._pPopulatePropertyAndTypeReference || (async()=>{ + const data = await DataUtil.loadJSON(`${Renderer.get().baseUrl}data/items-base.json`); + + Object.entries(Parser.ITEM_TYPE_JSON_TO_ABV).forEach(([abv,name])=>Renderer.item._addType({ + abbreviation: abv, + name + })); + data.itemProperty.forEach(p=>Renderer.item._addProperty(p)); + data.itemType.forEach(t=>Renderer.item._addType(t)); + data.itemEntry.forEach(it=>Renderer.item._addEntry(it)); + data.itemTypeAdditionalEntries.forEach(e=>Renderer.item._addAdditionalEntries(e)); + + await Renderer.item._pAddPrereleaseBrewPropertiesAndTypes(); + } + )(); + } + + static async getAllIndexableItems(rawVariants, rawBaseItems) { + const basicItems = await Renderer.item._pGetAndProcBaseItems(rawBaseItems); + const [genericVariants,linkedLootTables] = await Renderer.item._getAndProcGenericVariants(rawVariants); + const specificVariants = Renderer.item._createSpecificVariants(basicItems, genericVariants, { + linkedLootTables + }); + + [...genericVariants, ...specificVariants].forEach(item=>{ + if (item.variants) + delete item.variants; + } + ); + + return specificVariants; + } + + static isMundane(item) { + return item.rarity === "none" || item.rarity === "unknown" || item._category === "Basic"; + } + + static isExcluded(item, {hash=null}={}) { + const name = item.name; + const source = item.source || item.inherits?.source; + + hash = hash || UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_ITEMS]({ + name, + source + }); + + if (ExcludeUtil.isExcluded(hash, "item", source)) + return true; + + if (item._isBaseItem) + return ExcludeUtil.isExcluded(hash, "baseitem", source); + if (item._isItemGroup) + return ExcludeUtil.isExcluded(hash, "itemGroup", source); + if (item._variantName) { + if (ExcludeUtil.isExcluded(hash, "_specificVariant", source)) + return true; + + const baseHash = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_ITEMS]({ + name: item._baseName, + source: item._baseSource || source + }); + if (ExcludeUtil.isExcluded(baseHash, "baseitem", item._baseSource || source)) + return true; + + const variantHash = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_ITEMS]({ + name: item._variantName, + source: source + }); + return ExcludeUtil.isExcluded(variantHash, "magicvariant", source); + } + if (item.type === "GV") + return ExcludeUtil.isExcluded(hash, "magicvariant", source); + + return false; + } + + static pGetFluff(item) { + return Renderer.utils.pGetFluff({ + entity: item, + fnGetFluffData: DataUtil.itemFluff.loadJSON.bind(DataUtil.itemFluff), + fluffProp: "itemFluff", + }); + } +} +; + +Renderer.psionic = class { + static enhanceMode(mode) { + if (mode._isEnhanced) + return; + + mode.name = [mode.name, Renderer.psionic._enhanceMode_getModeTitleBracketPart({ + mode: mode + })].filter(Boolean).join(" "); + + if (mode.submodes) { + mode.submodes.forEach(sm=>{ + sm.name = [sm.name, Renderer.psionic._enhanceMode_getModeTitleBracketPart({ + mode: sm + })].filter(Boolean).join(" "); + } + ); + } + + mode._isEnhanced = true; + } + + static _enhanceMode_getModeTitleBracketPart({mode}) { + const modeTitleBracketArray = []; + + if (mode.cost) + modeTitleBracketArray.push(Renderer.psionic._enhanceMode_getModeTitleCost({ + mode + })); + if (mode.concentration) + modeTitleBracketArray.push(Renderer.psionic._enhanceMode_getModeTitleConcentration({ + mode + })); + + if (modeTitleBracketArray.length === 0) + return null; + return `(${modeTitleBracketArray.join("; ")})`; + } + + static _enhanceMode_getModeTitleCost({mode}) { + const costMin = mode.cost.min; + const costMax = mode.cost.max; + const costString = costMin === costMax ? costMin : `${costMin}-${costMax}`; + return `${costString} psi`; + } + + static _enhanceMode_getModeTitleConcentration({mode}) { + return `conc., ${mode.concentration.duration} ${mode.concentration.unit}.`; + } + + static getPsionicRenderableEntriesMeta(ent) { + const entriesContent = []; + + return { + entryTypeOrder: `{@i ${Renderer.psionic.getTypeOrderString(ent)}}`, + entryContent: ent.entries ? { + entries: ent.entries, + type: "entries" + } : null, + entryFocus: ent.focus ? `{@b {@i Psychic Focus.}} ${ent.focus}` : null, + entriesModes: ent.modes ? ent.modes.flatMap(mode=>Renderer.psionic._getModeEntries(mode)) : null, + }; + } + + static _getModeEntries(mode, renderer) { + Renderer.psionic.enhanceMode(mode); + + return [{ + type: mode.type || "entries", + name: mode.name, + entries: mode.entries, + }, mode.submodes ? Renderer.psionic._getSubModesEntry(mode.submodes) : null, ].filter(Boolean); + } + + static _getSubModesEntry(subModes) { + return { + type: "list", + style: "list-hang-notitle", + items: subModes.map(sm=>({ + type: "item", + name: sm.name, + entries: sm.entries, + })), + }; + } + + static getTypeOrderString(psi) { + const typeMeta = Parser.psiTypeToMeta(psi.type); + return typeMeta.hasOrder ? typeMeta.isAltDisplay ? `${typeMeta.full} (${psi.order})` : `${psi.order} ${typeMeta.full}` : typeMeta.full; + } + + static getBodyHtml(ent, {renderer=null, entriesMeta=null}={}) { + renderer ||= Renderer.get().setFirstSection(true); + entriesMeta ||= Renderer.psionic.getPsionicRenderableEntriesMeta(ent); + + return `${entriesMeta.entryContent ? renderer.render(entriesMeta.entryContent) : ""} + ${entriesMeta.entryFocus ? `

            ${renderer.render(entriesMeta.entryFocus)}

            ` : ""} + ${entriesMeta.entriesModes ? entriesMeta.entriesModes.map(entry=>renderer.render(entry, 2)).join("") : ""}`; + } + + static getCompactRenderedString(ent) { + const renderer = Renderer.get().setFirstSection(true); + const entriesMeta = Renderer.psionic.getPsionicRenderableEntriesMeta(ent); + + return ` + ${Renderer.utils.getExcludedTr({ + entity: ent, + dataProp: "psionic", + page: UrlUtil.PG_PSIONICS + })} + ${Renderer.utils.getNameTr(ent, { + page: UrlUtil.PG_PSIONICS + })} + +

            ${renderer.render(entriesMeta.entryTypeOrder)}

            + ${Renderer.psionic.getBodyHtml(ent, { + renderer, + entriesMeta + })} + + `; + } +} +; + +Renderer.rule = class { + static getCompactRenderedString(rule) { + return ` + + ${Renderer.get().setFirstSection(true).render(rule)} + + `; + } +} +; + +Renderer.variantrule = class { + static getCompactRenderedString(rule) { + const cpy = MiscUtil.copyFast(rule); + delete cpy.name; + return ` + ${Renderer.utils.getExcludedTr({ + entity: rule, + dataProp: "variantrule", + page: UrlUtil.PG_VARIANTRULES + })} + ${Renderer.utils.getNameTr(rule, { + page: UrlUtil.PG_VARIANTRULES + })} + + ${Renderer.get().setFirstSection(true).render(cpy)} + + `; + } +} +; + +Renderer.table = class { + static getCompactRenderedString(it) { + it.type = it.type || "table"; + const cpy = MiscUtil.copyFast(it); + delete cpy.name; + return ` + ${Renderer.utils.getExcludedTr({ + entity: it, + dataProp: "table", + page: UrlUtil.PG_TABLES + })} + ${Renderer.utils.getNameTr(it, { + page: UrlUtil.PG_TABLES + })} + + ${Renderer.get().setFirstSection(true).render(it)} + + `; + } + + static getConvertedEncounterOrNamesTable({group, tableRaw, fnGetNameCaption, colLabel1}) { + const getPadded = (number)=>{ + if (tableRaw.diceExpression === "d100") + return String(number).padStart(2, "0"); + return String(number); + } + ; + + const nameCaption = fnGetNameCaption(group, tableRaw); + return { + name: nameCaption, + type: "table", + source: group?.source, + page: group?.page, + caption: nameCaption, + colLabels: [`{@dice ${tableRaw.diceExpression}}`, colLabel1, tableRaw.rollAttitude ? `Attitude` : null, ].filter(Boolean), + colStyles: ["col-2 text-center", tableRaw.rollAttitude ? "col-8" : "col-10", tableRaw.rollAttitude ? `col-2 text-center` : null, ].filter(Boolean), + rows: tableRaw.table.map(it=>[`${getPadded(it.min)}${it.max != null && it.max !== it.min ? `-${getPadded(it.max)}` : ""}`, it.result, tableRaw.rollAttitude ? it.resultAttitude || "\u2014" : null, ].filter(Boolean)), + footnotes: tableRaw.footnotes, + }; + } + + static getConvertedEncounterTableName(group, tableRaw) { + return `${group.name}${tableRaw.caption ? ` ${tableRaw.caption}` : ""}${/\bencounters?\b/i.test(group.name) ? "" : " Encounters"}${tableRaw.minlvl && tableRaw.maxlvl ? ` (Levels ${tableRaw.minlvl}\u2014${tableRaw.maxlvl})` : ""}`; + } + + static getConvertedNameTableName(group, tableRaw) { + return `${group.name} Names \u2013 ${tableRaw.option}`; + } + + static getHeaderRowMetas(ent) { + if (!ent.colLabels?.length && !ent.colLabelGroups?.length) + return null; + + if (ent.colLabels?.length) + return [ent.colLabels]; + + const maxHeight = Math.max(...ent.colLabelGroups.map(clg=>clg.colLabels?.length || 0)); + + const padded = ent.colLabelGroups.map(clg=>{ + const out = [...(clg.colLabels || [])]; + while (out.length < maxHeight) + out.unshift(""); + return out; + } + ); + + return [...new Array(maxHeight)].map((_,i)=>padded.map(lbls=>lbls[i])); + } + + static _RE_TABLE_ROW_DASHED_NUMBERS = /^\d+([-\u2012\u2013]\d+)?/; + static getAutoConvertedRollMode(table, {headerRowMetas}={}) { + if (headerRowMetas === undefined) + headerRowMetas = Renderer.table.getHeaderRowMetas(table); + + if (!headerRowMetas || headerRowMetas.last().length < 2) + return RollerUtil.ROLL_COL_NONE; + + const rollColMode = RollerUtil.getColRollType(headerRowMetas.last()[0]); + if (!rollColMode) + return RollerUtil.ROLL_COL_NONE; + + if (!Renderer.table.isEveryRowRollable(table.rows)) + return RollerUtil.ROLL_COL_NONE; + + return rollColMode; + } + + static isEveryRowRollable(rows) { + return rows.every(row=>{ + if (!row) + return false; + const [cell] = row; + return Renderer.table.isRollableCell(cell); + } + ); + } + + static isRollableCell(cell) { + if (cell == null) + return false; + if (cell?.roll) + return true; + + if (typeof cell === "number") + return Number.isInteger(cell); + + return typeof cell === "string" && Renderer.table._RE_TABLE_ROW_DASHED_NUMBERS.test(cell); + } +} +; + +Renderer.vehicle = class { + static CHILD_PROPS = ["movement", "weapon", "other", "action", "trait", "reaction", "control", "actionStation"]; + + static getVehicleRenderableEntriesMeta(ent) { + return { + entryDamageImmunities: ent.immune ? `{@b Damage Immunities} ${Parser.getFullImmRes(ent.immune)}` : null, + entryConditionImmunities: ent.conditionImmune ? `{@b Condition Immunities} ${Parser.getFullCondImm(ent.conditionImmune, { + isEntry: true + })}` : null, + }; + } + + static getCompactRenderedString(veh, opts) { + return Renderer.vehicle.getRenderedString(veh, { + ...opts, + isCompact: true + }); + } + + static getRenderedString(ent, opts) { + opts = opts || {}; + + if (ent.upgradeType) + return Renderer.vehicleUpgrade.getCompactRenderedString(ent, opts); + + ent.vehicleType ||= "SHIP"; + switch (ent.vehicleType) { + case "SHIP": + return Renderer.vehicle._getRenderedString_ship(ent, opts); + case "SPELLJAMMER": + return Renderer.vehicle._getRenderedString_spelljammer(ent, opts); + case "INFWAR": + return Renderer.vehicle._getRenderedString_infwar(ent, opts); + case "CREATURE": + return Renderer.monster.getCompactRenderedString(ent, { + ...opts, + isHideLanguages: true, + isHideSenses: true, + isCompact: opts.isCompact ?? false, + page: UrlUtil.PG_VEHICLES + }); + case "OBJECT": + return Renderer.object.getCompactRenderedString(ent, { + ...opts, + isCompact: opts.isCompact ?? false, + page: UrlUtil.PG_VEHICLES + }); + default: + throw new Error(`Unhandled vehicle type "${ent.vehicleType}"`); + } + } + + static ship = class { + static PROPS_RENDERABLE_ENTRIES_ATTRIBUTES = ["entryCreatureCapacity", "entryCargoCapacity", "entryTravelPace", "entryTravelPaceNote", ]; + + static getVehicleShipRenderableEntriesMeta(ent) { + const entriesOtherActions = (ent.other || []).filter(it=>it.name === "Actions"); + const entriesOtherOthers = (ent.other || []).filter(it=>it.name !== "Actions"); + + return { + entrySizeDimensions: `{@i ${Parser.sizeAbvToFull(ent.size)} vehicle${ent.dimensions ? ` (${ent.dimensions.join(" by ")})` : ""}}`, + entryCreatureCapacity: ent.capCrew != null || ent.capPassenger != null ? `{@b Creature Capacity} ${Renderer.vehicle.getShipCreatureCapacity(ent)}` : null, + entryCargoCapacity: ent.capCargo != null ? `{@b Cargo Capacity} ${Renderer.vehicle.getShipCargoCapacity(ent)}` : null, + entryTravelPace: ent.pace != null ? `{@b Travel Pace} ${ent.pace} miles per hour (${ent.pace * 24} miles per day)` : null, + entryTravelPaceNote: ent.pace != null ? `[{@b Speed} ${ent.pace * 10} ft.]` : null, + entryTravelPaceNoteTitle: ent.pace != null ? `Based on "Special Travel Pace," DMG p242` : null, + + entriesOtherActions: entriesOtherActions.length ? entriesOtherActions : null, + entriesOtherOthers: entriesOtherOthers.length ? entriesOtherOthers : null, + }; + } + + static getLocomotionEntries(loc) { + return { + type: "list", + style: "list-hang-notitle", + items: [{ + type: "item", + name: `Locomotion (${loc.mode})`, + entries: loc.entries, + }, ], + }; + } + + static getSpeedEntries(spd) { + return { + type: "list", + style: "list-hang-notitle", + items: [{ + type: "item", + name: `Speed (${spd.mode})`, + entries: spd.entries, + }, ], + }; + } + + static getActionPart_(renderer, veh) { + return renderer.render({ + entries: veh.action + }); + } + + static getSectionTitle_(title) { + return `

            ${title}

            `; + } + + static getSectionHpEntriesMeta_({entry, isEach=false}) { + return { + entryArmorClass: entry.ac ? `{@b Armor Class} ${entry.ac}` : null, + entryHitPoints: entry.hp ? `{@b Hit Points} ${entry.hp}${isEach ? ` each` : ""}${entry.dt ? ` (damage threshold ${entry.dt})` : ""}${entry.hpNote ? `; ${entry.hpNote}` : ""}` : null, + }; + } + + static getSectionHpPart_(renderer, entry, isEach) { + const entriesMetaSection = Renderer.vehicle.ship.getSectionHpEntriesMeta_({ + entry, + isEach + }); + + const props = ["entryArmorClass", "entryHitPoints", ]; + + if (!props.some(prop=>entriesMetaSection[prop])) + return ""; + + return props.map(prop=>`
            ${renderer.render(entriesMetaSection[prop])}
            `).join(""); + } + + static getControlSection_(renderer, control) { + if (!control) + return ""; + return ` +

            Control: ${control.name}

            + + ${Renderer.vehicle.ship.getSectionHpPart_(renderer, control)} +
            ${renderer.render({ + entries: control.entries + })}
            + + `; + } + + static _getMovementSection_getLocomotionSection({renderer, entry}) { + const asList = Renderer.vehicle.ship.getLocomotionEntries(entry); + return `
            ${renderer.render(asList)}
            `; + } + + static _getMovementSection_getSpeedSection({renderer, entry}) { + const asList = Renderer.vehicle.ship.getSpeedEntries(entry); + return `
            ${renderer.render(asList)}
            `; + } + + static getMovementSection_(renderer, move) { + if (!move) + return ""; + + return ` +

            ${move.isControl ? `Control and ` : ""}Movement: ${move.name}

            + + ${Renderer.vehicle.ship.getSectionHpPart_(renderer, move)} + ${(move.locomotion || []).map(entry=>Renderer.vehicle.ship._getMovementSection_getLocomotionSection({ + renderer, + entry + })).join("")} + ${(move.speed || []).map(entry=>Renderer.vehicle.ship._getMovementSection_getSpeedSection({ + renderer, + entry + })).join("")} + + `; + } + + static getWeaponSection_(renderer, weap) { + return ` +

            Weapons: ${weap.name}${weap.count ? ` (${weap.count})` : ""}

            + + ${Renderer.vehicle.ship.getSectionHpPart_(renderer, weap, !!weap.count)} + ${renderer.render({ + entries: weap.entries + })} + + `; + } + + static getOtherSection_(renderer, oth) { + return ` +

            ${oth.name}

            + + ${Renderer.vehicle.ship.getSectionHpPart_(renderer, oth)} + ${renderer.render({ + entries: oth.entries + })} + + `; + } + + static getCrewCargoPaceSection_(ent, {entriesMetaShip=null}={}) { + entriesMetaShip ||= Renderer.vehicle.ship.getVehicleShipRenderableEntriesMeta(ent); + if (!Renderer.vehicle.ship.PROPS_RENDERABLE_ENTRIES_ATTRIBUTES.some(prop=>entriesMetaShip[prop])) + return ""; + + return ` + ${entriesMetaShip.entryCreatureCapacity ? `
            ${Renderer.get().render(entriesMetaShip.entryCreatureCapacity)}
            ` : ""} + ${entriesMetaShip.entryCargoCapacity ? `
            ${Renderer.get().render(entriesMetaShip.entryCargoCapacity)}
            ` : ""} + ${entriesMetaShip.entryTravelPace ? `
            ${Renderer.get().render(entriesMetaShip.entryTravelPace)}
            ` : ""} + ${entriesMetaShip.entryTravelPaceNote ? `
            ${Renderer.get().render(entriesMetaShip.entryTravelPaceNote)}
            ` : ""} + `; + } + } + ; + + static spelljammer = class { + static getVehicleSpelljammerRenderableEntriesMeta(ent) { + const ptAc = ent.hull?.ac ? `${ent.hull.ac}${ent.hull.acFrom ? ` (${ent.hull.acFrom.join(", ")})` : ""}` : "\u2014"; + + const ptSpeed = ent.speed != null ? Parser.getSpeedString(ent, { + isSkipZeroWalk: true + }) : ""; + const ptPace = Renderer.vehicle.spelljammer._getVehicleSpelljammerRenderableEntriesMeta_getPtPace({ + ent + }); + + const ptSpeedPace = [ptSpeed, ptPace].filter(Boolean).join(" "); + + return { + entryTableSummary: { + type: "table", + style: "summary", + colStyles: ["col-6", "col-6"], + rows: [[`{@b Armor Class:} ${ptAc}`, `{@b Cargo:} ${ent.capCargo ? `${ent.capCargo} ton${ent.capCargo === 1 ? "" : "s"}` : "\u2014"}`, ], [`{@b Hit Points:} ${ent.hull?.hp ?? "\u2014"}`, `{@b Crew:} ${ent.capCrew ?? "\u2014"}${ent.capCrewNote ? ` ${ent.capCrewNote}` : ""}`, ], [`{@b Damage Threshold:} ${ent.hull?.dt ?? "\u2014"}`, `{@b Keel/Beam:} ${(ent.dimensions || ["\u2014"]).join("/")}`, ], [`{@b Speed:} ${ptSpeedPace}`, `{@b Cost:} ${ent.cost != null ? Parser.vehicleCostToFull(ent) : "\u2014"}`, ], ], + }, + }; + } + + static _getVehicleSpelljammerRenderableEntriesMeta_getPtPace(ent) { + if (!ent.pace) + return ""; + + const isMulti = Object.keys(ent.pace).length > 1; + + const out = Parser.SPEED_MODES.map(mode=>{ + const pace = ent.pace[mode]; + if (!pace) + return null; + + const asNum = Parser.vulgarToNumber(pace); + return `{@tip ${isMulti && mode !== "walk" ? `${mode} ` : ""}${pace} mph|${asNum * 24} miles per day}`; + } + ).filter(Boolean).join(", "); + + return `(${out})`; + } + + static getSummarySection_(renderer, ent) { + const entriesMetaSpelljammer = Renderer.vehicle.spelljammer.getVehicleSpelljammerRenderableEntriesMeta(ent); + + return `${renderer.render(entriesMetaSpelljammer.entryTableSummary)}`; + } + + static getSectionWeaponEntriesMeta(entry) { + const isMultiple = entry.count != null && entry.count > 1; + + return { + entryName: `${isMultiple ? `${entry.count} ` : ""}${entry.name}${entry.crew ? ` (Crew: ${entry.crew}${isMultiple ? " each" : ""})` : ""}`, + }; + } + + static getWeaponSection_(renderer, entry) { + const entriesMetaSectionWeapon = Renderer.vehicle.spelljammer.getSectionWeaponEntriesMeta(entry); + + const ptAction = entry.action?.length ? entry.action.map(act=>`
            ${renderer.render(act, 2)}
            `).join("") : ""; + return ` +

            ${entriesMetaSectionWeapon.entryName}

            + + ${Renderer.vehicle.spelljammer.getSectionHpCostPart_(renderer, entry)} + ${entry.entries?.length ? `
            ${renderer.render({ + entries: entry.entries + })}
            ` : ""} + ${ptAction} + + `; + } + + static getSectionHpCostEntriesMeta(entry) { + const ptCosts = entry.costs?.length ? entry.costs.map(cost=>{ + return `${Parser.vehicleCostToFull(cost) || "\u2014"}${cost.note ? ` (${cost.note})` : ""}`; + } + ).join(", ") : "\u2014"; + + return { + entryArmorClass: `{@b Armor Class:} ${entry.ac == null ? "\u2014" : entry.ac}`, + entryHitPoints: `{@b Hit Points:} ${entry.hp == null ? "\u2014" : entry.hp}`, + entryCost: `{@b Cost:} ${ptCosts}`, + }; + } + + static getSectionHpCostPart_(renderer, entry) { + const entriesMetaSectionHpCost = Renderer.vehicle.spelljammer.getSectionHpCostEntriesMeta(entry); + + return ` +
            ${renderer.render(entriesMetaSectionHpCost.entryArmorClass)}
            +
            ${renderer.render(entriesMetaSectionHpCost.entryHitPoints)}
            +
            ${renderer.render(entriesMetaSectionHpCost.entryCost)}
            + `; + } + } + ; + + static _getAbilitySection(veh) { + return Parser.ABIL_ABVS.some(it=>veh[it] != null) ? ` + + + + + + + + + + + + + + + + + +
            STRDEXCONINTWISCHA
            ${Renderer.utils.getAbilityRoller(veh, "str")}${Renderer.utils.getAbilityRoller(veh, "dex")}${Renderer.utils.getAbilityRoller(veh, "con")}${Renderer.utils.getAbilityRoller(veh, "int")}${Renderer.utils.getAbilityRoller(veh, "wis")}${Renderer.utils.getAbilityRoller(veh, "cha")}
            + ` : ""; + } + + static _getResImmVulnSection(ent, {entriesMeta=null}={}) { + entriesMeta ||= Renderer.vehicle.getVehicleRenderableEntriesMeta(ent); + + const props = ["entryDamageImmunities", "entryConditionImmunities", ]; + + if (!props.some(prop=>entriesMeta[prop])) + return ""; + + return ` + ${props.filter(prop=>entriesMeta[prop]).map(prop=>`
            ${Renderer.get().render(entriesMeta[prop])}
            `).join("")} + `; + } + + static _getTraitSection(renderer, veh) { + return veh.trait ? `

            Traits

            +
            + + ${Renderer.monster.getOrderedTraits(veh, renderer).map(it=>it.rendered || renderer.render(it, 2)).join("")} + ` : ""; + } + + static _getRenderedString_ship(ent, opts) { + const renderer = Renderer.get(); + const entriesMeta = Renderer.vehicle.getVehicleRenderableEntriesMeta(ent); + const entriesMetaShip = Renderer.vehicle.ship.getVehicleShipRenderableEntriesMeta(ent); + + const hasToken = ent.tokenUrl || ent.hasToken; + const extraThClasses = !opts.isCompact && hasToken ? ["veh__name--token"] : null; + + return ` + ${Renderer.utils.getExcludedTr({ + entity: ent, + dataProp: "vehicle", + page: UrlUtil.PG_VEHICLES + })} + ${Renderer.utils.getNameTr(ent, { + extraThClasses, + page: UrlUtil.PG_VEHICLES + })} + ${Renderer.get().render(entriesMetaShip.entrySizeDimensions)} + ${Renderer.vehicle.ship.getCrewCargoPaceSection_(ent, { + entriesMetaShip + })} + ${Renderer.vehicle._getAbilitySection(ent)} + ${Renderer.vehicle._getResImmVulnSection(ent, { + entriesMeta + })} + ${ent.action ? Renderer.vehicle.ship.getSectionTitle_("Actions") : ""} + ${ent.action ? `${Renderer.vehicle.ship.getActionPart_(renderer, ent)}` : ""} + ${(entriesMetaShip.entriesOtherActions || []).map(Renderer.vehicle.ship.getOtherSection_.bind(this, renderer)).join("")} + ${ent.hull ? `${Renderer.vehicle.ship.getSectionTitle_("Hull")} + + ${Renderer.vehicle.ship.getSectionHpPart_(renderer, ent.hull)} + ` : ""} + ${Renderer.vehicle._getTraitSection(renderer, ent)} + ${(ent.control || []).map(Renderer.vehicle.ship.getControlSection_.bind(this, renderer)).join("")} + ${(ent.movement || []).map(Renderer.vehicle.ship.getMovementSection_.bind(this, renderer)).join("")} + ${(ent.weapon || []).map(Renderer.vehicle.ship.getWeaponSection_.bind(this, renderer)).join("")} + ${(entriesMetaShip.entriesOtherOthers || []).map(Renderer.vehicle.ship.getOtherSection_.bind(this, renderer)).join("")} + `; + } + + static getShipCreatureCapacity(veh) { + return [veh.capCrew ? `${veh.capCrew} crew` : null, veh.capPassenger ? `${veh.capPassenger} passenger${veh.capPassenger === 1 ? "" : "s"}` : null, ].filter(Boolean).join(", "); + } + + static getShipCargoCapacity(veh) { + return typeof veh.capCargo === "string" ? veh.capCargo : `${veh.capCargo} ton${veh.capCargo === 1 ? "" : "s"}`; + } + + static _getRenderedString_spelljammer(veh, opts) { + const renderer = Renderer.get(); + + const hasToken = veh.tokenUrl || veh.hasToken; + const extraThClasses = !opts.isCompact && hasToken ? ["veh__name--token"] : null; + + return ` + ${Renderer.utils.getExcludedTr({ + entity: veh, + dataProp: "vehicle", + page: UrlUtil.PG_VEHICLES + })} + ${Renderer.utils.getNameTr(veh, { + extraThClasses, + page: UrlUtil.PG_VEHICLES + })} + ${Renderer.vehicle.spelljammer.getSummarySection_(renderer, veh)} + ${(veh.weapon || []).map(Renderer.vehicle.spelljammer.getWeaponSection_.bind(this, renderer)).join("")} + `; + } + + static infwar = class { + static PROPS_RENDERABLE_ENTRIES_ATTRIBUTES = ["entryCreatureCapacity", "entryCargoCapacity", "entryArmorClass", "entryHitPoints", "entrySpeed", ]; + + static getVehicleInfwarRenderableEntriesMeta(ent) { + const dexMod = Parser.getAbilityModNumber(ent.dex); + + return { + entrySizeWeight: `{@i ${Parser.sizeAbvToFull(ent.size)} vehicle (${ent.weight.toLocaleString()} lb.)}`, + entryCreatureCapacity: `{@b Creature Capacity} ${Renderer.vehicle.getInfwarCreatureCapacity(ent)}`, + entryCargoCapacity: `{@b Cargo Capacity} ${Parser.weightToFull(ent.capCargo)}`, + entryArmorClass: `{@b Armor Class} ${dexMod === 0 ? `19` : `${19 + dexMod} (19 while motionless)`}`, + entryHitPoints: `{@b Hit Points} ${ent.hp.hp} (damage threshold ${ent.hp.dt}, mishap threshold ${ent.hp.mt})`, + entrySpeed: `{@b Speed} ${ent.speed} ft.`, + entrySpeedNote: `[{@b Travel Pace} ${Math.floor(ent.speed / 10)} miles per hour (${Math.floor(ent.speed * 24 / 10)} miles per day)]`, + entrySpeedNoteTitle: `Based on "Special Travel Pace," DMG p242`, + }; + } + } + ; + + static _getRenderedString_infwar(ent, opts) { + const renderer = Renderer.get(); + const entriesMeta = Renderer.vehicle.getVehicleRenderableEntriesMeta(ent); + const entriesMetaInfwar = Renderer.vehicle.infwar.getVehicleInfwarRenderableEntriesMeta(ent); + + const hasToken = ent.tokenUrl || ent.hasToken; + const extraThClasses = !opts.isCompact && hasToken ? ["veh__name--token"] : null; + + return ` + ${Renderer.utils.getExcludedTr({ + entity: ent, + datProp: "vehicle", + page: UrlUtil.PG_VEHICLES + })} + ${Renderer.utils.getNameTr(ent, { + extraThClasses, + page: UrlUtil.PG_VEHICLES + })} + ${renderer.render(entriesMetaInfwar.entrySizeWeight)} + + ${Renderer.vehicle.infwar.PROPS_RENDERABLE_ENTRIES_ATTRIBUTES.map(prop=>`
            ${renderer.render(entriesMetaInfwar[prop])}
            `).join("")} +
            ${renderer.render(entriesMetaInfwar.entrySpeedNote)}
            + + ${Renderer.vehicle._getAbilitySection(ent)} + ${Renderer.vehicle._getResImmVulnSection(ent, { + entriesMeta + })} + ${Renderer.vehicle._getTraitSection(renderer, ent)} + ${Renderer.monster.getCompactRenderedStringSection(ent, renderer, "Action Stations", "actionStation", 2)} + ${Renderer.monster.getCompactRenderedStringSection(ent, renderer, "Reactions", "reaction", 2)} + `; + } + + static getInfwarCreatureCapacity(veh) { + return `${veh.capCreature} Medium creatures`; + } + + static pGetFluff(veh) { + return Renderer.utils.pGetFluff({ + entity: veh, + fnGetFluffData: DataUtil.vehicleFluff.loadJSON.bind(DataUtil.vehicleFluff), + fluffProp: "vehicleFluff", + }); + } + + static getTokenUrl(veh) { + return veh.tokenUrl || UrlUtil.link(`${Renderer.get().baseMediaUrls["img"] || Renderer.get().baseUrl}img/vehicles/tokens/${Parser.sourceJsonToAbv(veh.source)}/${Parser.nameToTokenName(veh.name)}.png`); + } +} +; + +Renderer.vehicleUpgrade = class { + static getUpgradeSummary(ent) { + return [ent.upgradeType ? ent.upgradeType.map(t=>Parser.vehicleTypeToFull(t)) : null, ent.prerequisite ? Renderer.utils.prerequisite.getHtml(ent.prerequisite) : null, ].filter(Boolean).join(", "); + } + + static getCompactRenderedString(ent, opts) { + return `${Renderer.utils.getExcludedTr({ + entity: ent, + dataProp: "vehicleUpgrade", + page: UrlUtil.PG_VEHICLES + })} + ${Renderer.utils.getNameTr(ent, { + page: UrlUtil.PG_VEHICLES + })} + ${Renderer.vehicleUpgrade.getUpgradeSummary(ent)} +
            + ${Renderer.get().render({ + entries: ent.entries + }, 1)}`; + } +} +; + +Renderer.action = class { + static getCompactRenderedString(it) { + const cpy = MiscUtil.copyFast(it); + delete cpy.name; + return `${Renderer.utils.getExcludedTr({ + entity: it, + dataProp: "action", + page: UrlUtil.PG_ACTIONS + })} + ${Renderer.utils.getNameTr(it, { + page: UrlUtil.PG_ACTIONS + })} + ${Renderer.get().setFirstSection(true).render(cpy)}`; + } +} +; + +Renderer.language = class { + static getLanguageRenderableEntriesMeta(ent) { + const hasMeta = ent.typicalSpeakers || ent.script; + + const entriesContent = []; + + if (ent.entries) + entriesContent.push(...ent.entries); + if (ent.dialects) { + entriesContent.push(`This language is a family which includes the following dialects: ${ent.dialects.sort(SortUtil.ascSortLower).join(", ")}. Creatures that speak different dialects of the same language can communicate with one another.`); + } + + if (!entriesContent.length && !hasMeta) + entriesContent.push("{@i No information available.}"); + + return { + entryType: ent.type ? `{@i ${ent.type.toTitleCase()} language}` : null, + entryTypicalSpeakers: ent.typicalSpeakers ? `{@b Typical Speakers:} ${ent.typicalSpeakers.join(", ")}` : null, + entryScript: ent.script ? `{@b Script:} ${ent.script}` : null, + entriesContent: entriesContent.length ? entriesContent : null, + }; + } + + static getCompactRenderedString(ent) { + return Renderer.language.getRenderedString(ent); + } + + static getRenderedString(ent, {isSkipNameRow=false}={}) { + const entriesMeta = Renderer.language.getLanguageRenderableEntriesMeta(ent); + + return ` + ${Renderer.utils.getExcludedTr({ + entity: ent, + dataProp: "language", + page: UrlUtil.PG_LANGUAGES + })} + ${isSkipNameRow ? "" : Renderer.utils.getNameTr(ent, { + page: UrlUtil.PG_LANGUAGES + })} + ${entriesMeta.entryType ? `${Renderer.get().render(entriesMeta.entryType)}` : ""} + ${entriesMeta.entryTypicalSpeakers || entriesMeta.entryScript ? ` + ${[entriesMeta.entryTypicalSpeakers, entriesMeta.entryScript].filter(Boolean).map(entry=>`
            ${Renderer.get().render(entry)}
            `).join("")} + ` : ""} + ${entriesMeta.entriesContent ? ` + ${Renderer.get().setFirstSection(true).render({ + entries: entriesMeta.entriesContent + })} + ` : ""}`; + } + + static pGetFluff(it) { + return Renderer.utils.pGetFluff({ + entity: it, + fnGetFluffData: DataUtil.languageFluff.loadJSON.bind(DataUtil.languageFluff), + fluffProp: "languageFluff", + }); + } +} +; + +Renderer.adventureBook = class { + static getEntryIdLookup(bookData, doThrowError=true) { + const out = {}; + const titlesRel = {}; + const titlesRelChapter = {}; + + let chapIx; + const depthStack = []; + const handlers = { + object: (obj)=>{ + Renderer.ENTRIES_WITH_ENUMERATED_TITLES.forEach(meta=>{ + if (obj.type !== meta.type) + return; + + const curDepth = depthStack.length ? depthStack.last() : 0; + const nxtDepth = meta.depth ? meta.depth : meta.depthIncrement ? curDepth + meta.depthIncrement : curDepth; + + depthStack.push(Math.min(nxtDepth, 2, ), ); + + if (obj.id) { + if (out[obj.id]) { + (out.__BAD = out.__BAD || []).push(obj.id); + } else { + out[obj.id] = { + chapter: chapIx, + entry: obj, + depth: depthStack.last(), + }; + + if (obj.name) { + out[obj.id].name = obj.name; + + const cleanName = obj.name.toLowerCase(); + out[obj.id].nameClean = cleanName; + + titlesRel[cleanName] = titlesRel[cleanName] || 0; + out[obj.id].ixTitleRel = titlesRel[cleanName]++; + + MiscUtil.getOrSet(titlesRelChapter, chapIx, cleanName, -1); + out[obj.id].ixTitleRelChapter = ++titlesRelChapter[chapIx][cleanName]; + } + } + } + } + ); + + return obj; + } + , + postObject: (obj)=>{ + Renderer.ENTRIES_WITH_ENUMERATED_TITLES.forEach(meta=>{ + if (obj.type !== meta.type) + return; + + depthStack.pop(); + } + ); + } + , + }; + + bookData.forEach((chap,_chapIx)=>{ + chapIx = _chapIx; + MiscUtil.getWalker({ + isNoModification: true + }).walk(chap, handlers); + } + ); + + if (doThrowError) + if (out.__BAD) + throw new Error(`IDs were already in storage: ${out.__BAD.map(it=>`"${it}"`).join(", ")}`); + + return out; + } + + static _isAltMissingCoverUsed = false; + static getCoverUrl(contents) { + return contents.coverUrl || `${Renderer.get().baseMediaUrls["img"] || Renderer.get().baseUrl}img/covers/blank${Math.random() <= 0.05 && !Renderer.adventureBook._isAltMissingCoverUsed && (Renderer.adventureBook._isAltMissingCoverUsed = true) ? "-alt" : ""}.webp`; + } +} +; + +Renderer.charoption = class { + static getCompactRenderedString(ent) { + const prerequisite = Renderer.utils.prerequisite.getHtml(ent.prerequisite); + const preText = Renderer.charoption.getOptionTypePreText(ent); + return ` + ${Renderer.utils.getExcludedTr({ + entity: ent, + dataProp: "charoption", + page: UrlUtil.PG_CHAR_CREATION_OPTIONS + })} + ${Renderer.utils.getNameTr(ent, { + page: UrlUtil.PG_CHAR_CREATION_OPTIONS + })} + + ${prerequisite ? `

            ${prerequisite}

            ` : ""} + ${preText || ""}${Renderer.get().setFirstSection(true).render({ + type: "entries", + entries: ent.entries + })} + + `; + } + + static getCharoptionRenderableEntriesMeta(ent) { + const optsMapped = ent.optionType.map(it=>Renderer.charoption._OPTION_TYPE_ENTRIES[it]).filter(Boolean); + if (!optsMapped.length) + return null; + + return { + entryOptionType: { + type: "entries", + entries: optsMapped + }, + }; + } + + static _OPTION_TYPE_ENTRIES = { + "RF:B": `{@note You may replace the standard feature of your background with this feature.}`, + "CS": `{@note See the {@adventure Character Secrets|IDRotF|0|character secrets} section for more information.}`, + }; + + static getOptionTypePreText(ent) { + const meta = Renderer.charoption.getCharoptionRenderableEntriesMeta(ent); + if (!meta) + return ""; + return Renderer.get().render(meta.entryOptionType); + } + + static pGetFluff(it) { + return Renderer.utils.pGetFluff({ + entity: it, + fnGetFluffData: DataUtil.charoptionFluff.loadJSON.bind(DataUtil.charoptionFluff), + fluffProp: "charoptionFluff", + }); + } +} +; + +Renderer.recipe = class { + static _getEntryMetasTime(ent) { + if (!Object.keys(ent.time || {}).length) + return null; + + return ["total", "preparation", "cooking", ...Object.keys(ent.time), ].unique().filter(prop=>ent.time[prop]).map((prop,i,arr)=>{ + const val = ent.time[prop]; + + const ptsTime = (val.min != null && val.max != null ? [Parser.getMinutesToFull(val.min), Parser.getMinutesToFull(val.max), ] : [Parser.getMinutesToFull(val)]); + + const suffix = MiscUtil.findCommonSuffix(ptsTime, { + isRespectWordBoundaries: true + }); + const ptTime = ptsTime.map(it=>!suffix.length ? it : it.slice(0, -suffix.length)).join(" to "); + + return { + entryName: `{@b {@style ${prop.toTitleCase()} Time:|small-caps}}`, + entryContent: `${ptTime}${suffix}`, + }; + } + ); + } + + static getRecipeRenderableEntriesMeta(ent) { + return { + entryMakes: ent.makes ? `{@b {@style Makes|small-caps}} ${ent._scaleFactor ? `${ent._scaleFactor}ร— ` : ""}${ent.makes}` : null, + entryServes: ent.serves ? `{@b {@style Serves|small-caps}} ${ent.serves.min ?? ent.serves.exact}${ent.serves.min != null ? " to " : ""}${ent.serves.max ?? ""}` : null, + entryMetasTime: Renderer.recipe._getEntryMetasTime(ent), + entryIngredients: { + entries: ent._fullIngredients + }, + entryEquipment: ent._fullEquipment?.length ? { + entries: ent._fullEquipment + } : null, + entryCooksNotes: ent.noteCook ? { + entries: ent.noteCook + } : null, + entryInstructions: { + entries: ent.instructions + }, + }; + } + + static getCompactRenderedString(ent) { + return `${Renderer.utils.getExcludedTr({ + entity: ent, + dataProp: "recipe", + page: UrlUtil.PG_RECIPES + })} + ${Renderer.utils.getNameTr(ent, { + page: UrlUtil.PG_RECIPES + })} + + ${Renderer.recipe.getBodyHtml(ent)} + `; + } + + static getBodyHtml(ent) { + const entriesMeta = Renderer.recipe.getRecipeRenderableEntriesMeta(ent); + + const ptTime = Renderer.recipe.getTimeHtml(ent, { + entriesMeta + }); + const {ptMakes, ptServes} = Renderer.recipe.getMakesServesHtml(ent, { + entriesMeta + }); + + return `
            +
            + ${ptTime || ""} + + ${ptMakes || ""} + ${ptServes || ""} + +
            ${Renderer.get().render(entriesMeta.entryIngredients, 0)}
            + + ${entriesMeta.entryEquipment ? `
            Equipment
            ${Renderer.get().render(entriesMeta.entryEquipment)}
            ` : ""} + + ${entriesMeta.entryCooksNotes ? `
            Cook's Notes
            ${Renderer.get().render(entriesMeta.entryCooksNotes)}
            ` : ""} +
            + +
            + ${Renderer.get().setFirstSection(true).render(entriesMeta.entryInstructions, 2)} +
            +
            `; + } + + static getMakesServesHtml(ent, {entriesMeta=null}={}) { + entriesMeta ||= Renderer.recipe.getRecipeRenderableEntriesMeta(ent); + const ptMakes = entriesMeta.entryMakes ? `
            ${Renderer.get().render(entriesMeta.entryMakes)}
            ` : null; + const ptServes = entriesMeta.entryServes ? `
            ${Renderer.get().render(entriesMeta.entryServes)}
            ` : null; + return { + ptMakes, + ptServes + }; + } + + static getTimeHtml(ent, {entriesMeta=null}={}) { + entriesMeta ||= Renderer.recipe.getRecipeRenderableEntriesMeta(ent); + if (!entriesMeta.entryMetasTime) + return ""; + + return entriesMeta.entryMetasTime.map(({entryName, entryContent},i,arr)=>{ + return `
            + ${Renderer.get().render(entryName)} + ${Renderer.get().render(entryContent)} +
            `; + } + ).join(""); + } + + static pGetFluff(it) { + return Renderer.utils.pGetFluff({ + entity: it, + fnGetFluffData: DataUtil.recipeFluff.loadJSON.bind(DataUtil.recipeFluff), + fluffProp: "recipeFluff", + }); + } + + static populateFullIngredients(r) { + r._fullIngredients = Renderer.applyAllProperties(MiscUtil.copyFast(r.ingredients)); + if (r.equipment) + r._fullEquipment = Renderer.applyAllProperties(MiscUtil.copyFast(r.equipment)); + } + + static _RE_AMOUNT = /(?{=amount\d+(?:\/[^}]+)?})/g; + static _SCALED_PRECISION_LIMIT = 10 ** 6; + static getScaledRecipe(r, scaleFactor) { + const cpyR = MiscUtil.copyFast(r); + + ["ingredients", "equipment"].forEach(prop=>{ + if (!cpyR[prop]) + return; + + MiscUtil.getWalker({ + keyBlocklist: MiscUtil.GENERIC_WALKER_ENTRIES_KEY_BLOCKLIST + }).walk(cpyR[prop], { + object: (obj)=>{ + if (obj.type !== "ingredient") + return obj; + + const objOriginal = MiscUtil.copyFast(obj); + + Object.keys(obj).filter(k=>/^amount\d+/.test(k)).forEach(k=>{ + let base = obj[k]; + + if (Math.round(base) !== base && base < 20) { + const divOneSixth = obj[k] / 0.166; + if (Math.abs(divOneSixth - Math.round(divOneSixth)) < 0.05) + base = (1 / 6) * Math.round(divOneSixth); + } + + let scaled = base * scaleFactor; + obj[k] = Math.round(base * scaleFactor * Renderer.recipe._SCALED_PRECISION_LIMIT) / Renderer.recipe._SCALED_PRECISION_LIMIT; + } + ); + + const amountsOriginal = Object.keys(objOriginal).filter(k=>/^amount\d+$/.test(k)).map(k=>objOriginal[k]); + const amountsScaled = Object.keys(obj).filter(k=>/^amount\d+$/.test(k)).map(k=>obj[k]); + + const entryParts = obj.entry.split(Renderer.recipe._RE_AMOUNT).filter(Boolean); + const entryPartsOut = entryParts.slice(0, entryParts.findIndex(it=>Renderer.recipe._RE_AMOUNT.test(it)) + 1); + let ixAmount = 0; + for (let i = entryPartsOut.length; i < entryParts.length; ++i) { + let pt = entryParts[i]; + + if (Renderer.recipe._RE_AMOUNT.test(pt)) { + ixAmount++; + entryPartsOut.push(pt); + continue; + } + + if (amountsOriginal[ixAmount] == null || amountsScaled[ixAmount] == null) { + entryPartsOut.push(pt); + continue; + } + + const isSingleToPlural = amountsOriginal[ixAmount] <= 1 && amountsScaled[ixAmount] > 1; + const isPluralToSingle = amountsOriginal[ixAmount] > 1 && amountsScaled[ixAmount] <= 1; + + if (!isSingleToPlural && !isPluralToSingle) { + entryPartsOut.push(pt); + continue; + } + + if (isSingleToPlural) + pt = Renderer.recipe._getPluralizedUnits(pt); + else if (isPluralToSingle) + pt = Renderer.recipe._getSingleizedUnits(pt); + entryPartsOut.push(pt); + } + + obj.entry = entryPartsOut.join(""); + + Renderer.recipe._mutWrapOriginalAmounts({ + obj, + objOriginal + }); + + return obj; + } + , + }, ); + } + ); + + Renderer.recipe.populateFullIngredients(cpyR); + + if (cpyR.serves) { + if (cpyR.serves.min) + cpyR.serves.min *= scaleFactor; + if (cpyR.serves.max) + cpyR.serves.max *= scaleFactor; + if (cpyR.serves.exact) + cpyR.serves.exact *= scaleFactor; + } + + cpyR._displayName = `${cpyR.name} (ร—${scaleFactor})`; + cpyR._scaleFactor = scaleFactor; + + return cpyR; + } + + static _UNITS_SINGLE_TO_PLURAL_S = ["bundle", "cup", "handful", "ounce", "packet", "piece", "pound", "slice", "sprig", "square", "strip", "tablespoon", "teaspoon", "wedge", ]; + static _UNITS_SINGLE_TO_PLURAL_ES = ["dash", "inch", ]; + static _FNS_SINGLE_TO_PLURAL = []; + static _FNS_PLURAL_TO_SINGLE = []; + + static _getSingleizedUnits(str) { + if (!Renderer.recipe._FNS_PLURAL_TO_SINGLE.length) { + Renderer.recipe._FNS_PLURAL_TO_SINGLE = [...Renderer.recipe._UNITS_SINGLE_TO_PLURAL_S.map(word=>str=>str.replace(new RegExp(`\\b${word.escapeRegexp()}s\\b`,"gi"), (...m)=>m[0].slice(0, -1))), ...Renderer.recipe._UNITS_SINGLE_TO_PLURAL_ES.map(word=>str=>str.replace(new RegExp(`\\b${word.escapeRegexp()}es\\b`,"gi"), (...m)=>m[0].slice(0, -2))), ]; + } + + Renderer.recipe._FNS_PLURAL_TO_SINGLE.forEach(fn=>str = fn(str)); + + return str; + } + + static _getPluralizedUnits(str) { + if (!Renderer.recipe._FNS_SINGLE_TO_PLURAL.length) { + Renderer.recipe._FNS_SINGLE_TO_PLURAL = [...Renderer.recipe._UNITS_SINGLE_TO_PLURAL_S.map(word=>str=>str.replace(new RegExp(`\\b${word.escapeRegexp()}\\b`,"gi"), (...m)=>`${m[0]}s`)), ...Renderer.recipe._UNITS_SINGLE_TO_PLURAL_ES.map(word=>str=>str.replace(new RegExp(`\\b${word.escapeRegexp()}\\b`,"gi"), (...m)=>`${m[0]}es`)), ]; + } + + Renderer.recipe._FNS_SINGLE_TO_PLURAL.forEach(fn=>str = fn(str)); + + return str; + } + + static _mutWrapOriginalAmounts({obj, objOriginal}) { + const parts = []; + let stack = ""; + let depth = 0; + for (let i = 0; i < obj.entry.length; ++i) { + const c = obj.entry[i]; + switch (c) { + case "{": + { + if (!depth && stack) { + parts.push(stack); + stack = ""; + } + depth++; + stack += c; + break; + } + case "}": + { + depth--; + stack += c; + if (!depth && stack) { + parts.push(stack); + stack = ""; + } + break; + } + default: + stack += c; + } + } + if (stack) + parts.push(stack); + obj.entry = parts.map(pt=>pt.replace(Renderer.recipe._RE_AMOUNT, (...m)=>{ + const ixStart = m.slice(-3, -2)[0]; + if (ixStart !== 0 || m[0].length !== pt.length) + return m[0]; + + const originalValue = Renderer.applyProperties(m.last().tagAmount, objOriginal); + return `{@help ${m.last().tagAmount}|In the original recipe: ${originalValue}}`; + } + )).join(""); + } + + static getCustomHashId(it) { + if (!it._scaleFactor) + return null; + + const {name, source, _scaleFactor: scaleFactor, } = it; + + return [name, source, scaleFactor ?? "", ].join("__").toLowerCase(); + } + + static getUnpackedCustomHashId(customHashId) { + if (!customHashId) + return null; + + const [,,scaleFactor] = customHashId.split("__").map(it=>it.trim()); + + if (!scaleFactor) + return null; + + return { + _scaleFactor: scaleFactor ? Number(scaleFactor) : null, + customHashId, + }; + } + + static async pGetModifiedRecipe(ent, customHashId) { + if (!customHashId) + return ent; + const {_scaleFactor} = Renderer.recipe.getUnpackedCustomHashId(customHashId); + if (_scaleFactor == null) + return ent; + return Renderer.recipe.getScaledRecipe(ent, _scaleFactor); + } +} +; + +Renderer.card = class { + static getFullEntries(ent) { + const entries = [...ent.entries || []]; + if (ent.suit && (ent.valueName || ent.value)) { + const suitAndValue = `${((ent.valueName || "") || Parser.numberToText(ent.value)).toTitleCase()} of ${ent.suit.toTitleCase()}`; + if (suitAndValue.toLowerCase() !== ent.name.toLowerCase()) + entries.unshift(`{@i ${suitAndValue}}`); + } + return entries; + } + + static getCompactRenderedString(ent) { + const fullEntries = Renderer.card.getFullEntries(ent); + return ` + ${Renderer.utils.getNameTr(ent)} + + ${Renderer.get().setFirstSection(true).render({ + ...ent.face, + maxHeight: 40, + maxHeightUnits: "vh" + })} + ${fullEntries?.length ? `
            + ${Renderer.get().setFirstSection(true).render({ + type: "entries", + entries: fullEntries + }, 1)}` : ""} + + `; + } +} +; + +Renderer.deck = class { + static getCompactRenderedString(ent) { + const lstCards = { + name: "Cards", + entries: [{ + type: "list", + columns: 3, + items: ent.cards.map(card=>`{@card ${card.name}|${card.set}|${card.source}}`), + }, ], + }; + + return ` + ${Renderer.utils.getNameTr(ent)} + + ${Renderer.get().setFirstSection(true).render({ + type: "entries", + entries: ent.entries + }, 1)} +
            + ${Renderer.get().setFirstSection(true).render(lstCards, 1)} + + `; + } +} +; + +Renderer.skill = class { + static getCompactRenderedString(ent) { + return Renderer.generic.getCompactRenderedString(ent); + } +} +; + +Renderer.sense = class { + static getCompactRenderedString(ent) { + return Renderer.generic.getCompactRenderedString(ent); + } +} +; + +Renderer.itemMastery = class { + static getCompactRenderedString(ent) { + return Renderer.generic.getCompactRenderedString(ent); + } +} +; + +Renderer.generic = class { + static getCompactRenderedString(ent, opts) { + opts = opts || {}; + const prerequisite = Renderer.utils.prerequisite.getHtml(ent.prerequisite); + + return ` + ${opts.dataProp && opts.page ? Renderer.utils.getExcludedTr({ + entity: ent, + dataProp: opts.dataProp, + page: opts.page + }) : ""} + ${opts.isSkipNameRow ? "" : Renderer.utils.getNameTr(ent, { + page: opts.page + })} + + ${prerequisite ? `

            ${prerequisite}

            ` : ""} + ${Renderer.get().setFirstSection(true).render({ + entries: ent.entries + })} + + ${opts.isSkipPageRow ? "" : Renderer.utils.getPageTr(ent)}`; + } + + static FEATURE__SKILLS_ALL = Object.keys(Parser.SKILL_TO_ATB_ABV).sort(SortUtil.ascSortLower); + + static FEATURE__TOOLS_ARTISANS = ["alchemist's supplies", "brewer's supplies", "calligrapher's supplies", "carpenter's tools", "cartographer's tools", "cobbler's tools", "cook's utensils", "glassblower's tools", "jeweler's tools", "leatherworker's tools", "mason's tools", "painter's supplies", "potter's tools", "smith's tools", "tinker's tools", "weaver's tools", "woodcarver's tools", ]; + static FEATURE__TOOLS_MUSICAL_INSTRUMENTS = ["bagpipes", "drum", "dulcimer", "flute", "horn", "lute", "lyre", "pan flute", "shawm", "viol", ]; + static FEATURE__TOOLS_ALL = ["artisan's tools", + ...this.FEATURE__TOOLS_ARTISANS, ...this.FEATURE__TOOLS_MUSICAL_INSTRUMENTS, + "disguise kit", "forgery kit", "gaming set", "herbalism kit", "musical instrument", "navigator's tools", "thieves' tools", "poisoner's kit", "vehicles (land)", "vehicles (water)", "vehicles (air)", "vehicles (space)", ]; + + static FEATURE__LANGUAGES_ALL = Parser.LANGUAGES_ALL.map(it=>it.toLowerCase()); + static FEATURE__LANGUAGES_STANDARD__CHOICE_OBJ = { + from: [...Parser.LANGUAGES_STANDARD.map(it=>({ + name: it.toLowerCase(), + prop: "languageProficiencies", + group: "languagesStandard", + })), ...Parser.LANGUAGES_EXOTIC.map(it=>({ + name: it.toLowerCase(), + prop: "languageProficiencies", + group: "languagesExotic", + })), ...Parser.LANGUAGES_SECRET.map(it=>({ + name: it.toLowerCase(), + prop: "languageProficiencies", + group: "languagesSecret", + })), ], + groups: { + languagesStandard: { + name: "Standard Languages", + }, + languagesExotic: { + name: "Exotic Languages", + hint: "With your DM's permission, you can choose an exotic language.", + }, + languagesSecret: { + name: "Secret Languages", + hint: "With your DM's permission, you can choose a secret language.", + }, + }, + }; + + static FEATURE__SAVING_THROWS_ALL = [...Parser.ABIL_ABVS]; + + static _SKILL_TOOL_LANGUAGE_KEYS__SKILL_ANY = new Set(["anySkill"]); + static _SKILL_TOOL_LANGUAGE_KEYS__TOOL_ANY = new Set(["anyTool", "anyArtisansTool"]); + static _SKILL_TOOL_LANGUAGE_KEYS__LANGAUGE_ANY = new Set(["anyLanguage", "anyStandardLanguage", "anyExoticLanguage"]); + + static getSkillSummary({skillProfs, skillToolLanguageProfs, isShort=false}) { + return this._summariseProfs({ + profGroupArr: skillProfs, + skillToolLanguageProfs, + setValid: new Set(this.FEATURE__SKILLS_ALL), + setValidAny: this._SKILL_TOOL_LANGUAGE_KEYS__SKILL_ANY, + isShort, + hoverTag: "skill", + }); + } + + static getToolSummary({toolProfs, skillToolLanguageProfs, isShort=false}) { + return this._summariseProfs({ + profGroupArr: toolProfs, + skillToolLanguageProfs, + setValid: new Set(this.FEATURE__TOOLS_ALL), + setValidAny: this._SKILL_TOOL_LANGUAGE_KEYS__TOOL_ANY, + isShort, + }); + } + + static getLanguageSummary({languageProfs, skillToolLanguageProfs, isShort=false}) { + return this._summariseProfs({ + profGroupArr: languageProfs, + skillToolLanguageProfs, + setValid: new Set(this.FEATURE__LANGUAGES_ALL), + setValidAny: this._SKILL_TOOL_LANGUAGE_KEYS__LANGAUGE_ANY, + isShort, + }); + } + + static _summariseProfs({profGroupArr, skillToolLanguageProfs, setValid, setValidAny, isShort, hoverTag}) { + if (!profGroupArr?.length && !skillToolLanguageProfs?.length) + return { + summary: "", + collection: [] + }; + + const collectionSet = new Set(); + + const handleProfGroup = (profGroup,{isValidate=true}={})=>{ + let sep = ", "; + + const toJoin = Object.entries(profGroup).sort(([kA],[kB])=>this._summariseProfs_sortKeys(kA, kB)).filter(([k,v])=>v && (!isValidate || setValid.has(k) || setValidAny.has(k))).map(([k,v],i)=>{ + const vMapped = this.getMappedAnyProficiency({ + keyAny: k, + countRaw: v + }) ?? v; + + if (k === "choose") { + sep = "; "; + + const chooseProfs = vMapped.from.filter(s=>!isValidate || setValid.has(s)).map(s=>{ + collectionSet.add(s); + return this._summariseProfs_getEntry({ + str: s, + isShort, + hoverTag + }); + } + ); + return `${isShort ? `${i === 0 ? "C" : "c"}hoose ` : ""}${v.count || 1} ${isShort ? `of` : `from`} ${chooseProfs.joinConjunct(", ", " or ")}`; + } + + collectionSet.add(k); + return this._summariseProfs_getEntry({ + str: k, + isShort, + hoverTag + }); + } + ); + + return toJoin.join(sep); + } + ; + + const summary = [...(profGroupArr || []).map(profGroup=>handleProfGroup(profGroup, { + isValidate: false + })), ...(skillToolLanguageProfs || []).map(profGroup=>handleProfGroup(profGroup)), ].filter(Boolean).join(` or `); + + return { + summary, + collection: [...collectionSet].sort(SortUtil.ascSortLower) + }; + } + + static _summariseProfs_sortKeys(a, b, {setValidAny=null}={}) { + if (a === b) + return 0; + if (a === "choose") + return 2; + if (b === "choose") + return -2; + if (setValidAny) { + if (setValidAny.has(a)) + return 1; + if (setValidAny.has(b)) + return -1; + } + return SortUtil.ascSort(a, b); + } + + static _summariseProfs_getEntry({str, isShort, hoverTag}) { + return isShort ? str.toTitleCase() : hoverTag ? `{@${hoverTag} ${str.toTitleCase()}}` : str.toTitleCase(); + } + + static getMappedAnyProficiency({keyAny, countRaw}) { + const mappedCount = !isNaN(countRaw) ? Number(countRaw) : 1; + if (mappedCount <= 0) + return null; + + switch (keyAny) { + case "anySkill": + return { + name: mappedCount === 1 ? `Any Skill` : `Any ${mappedCount} Skills`, + from: this.FEATURE__SKILLS_ALL.map(it=>({ + name: it, + prop: "skillProficiencies" + })), + count: mappedCount, + }; + case "anyTool": + return { + name: mappedCount === 1 ? `Any Tool` : `Any ${mappedCount} Tools`, + from: this.FEATURE__TOOLS_ALL.map(it=>({ + name: it, + prop: "toolProficiencies" + })), + count: mappedCount, + }; + case "anyArtisansTool": + return { + name: mappedCount === 1 ? `Any Artisan's Tool` : `Any ${mappedCount} Artisan's Tools`, + from: this.FEATURE__TOOLS_ARTISANS.map(it=>({ + name: it, + prop: "toolProficiencies" + })), + count: mappedCount, + }; + case "anyMusicalInstrument": + return { + name: mappedCount === 1 ? `Any Musical Instrument` : `Any ${mappedCount} Musical Instruments`, + from: this.FEATURE__TOOLS_MUSICAL_INSTRUMENTS.map(it=>({ + name: it, + prop: "toolProficiencies" + })), + count: mappedCount, + }; + case "anyLanguage": + return { + name: mappedCount === 1 ? `Any Language` : `Any ${mappedCount} Languages`, + from: this.FEATURE__LANGUAGES_ALL.map(it=>({ + name: it, + prop: "languageProficiencies" + })), + count: mappedCount, + }; + case "anyStandardLanguage": + return { + name: mappedCount === 1 ? `Any Standard Language` : `Any ${mappedCount} Standard Languages`, + ...MiscUtil.copyFast(this.FEATURE__LANGUAGES_STANDARD__CHOICE_OBJ), + count: mappedCount, + }; + case "anyExoticLanguage": + return { + name: mappedCount === 1 ? `Any Exotic Language` : `Any ${mappedCount} Exotic Languages`, + ...MiscUtil.copyFast(this.FEATURE__LANGUAGES_STANDARD__CHOICE_OBJ), + count: mappedCount, + }; + case "anySavingThrow": + return { + name: mappedCount === 1 ? `Any Saving Throw` : `Any ${mappedCount} Saving Throws`, + from: this.FEATURE__SAVING_THROWS_ALL.map(it=>({ + name: it, + prop: "savingThrowProficiencies" + })), + count: mappedCount, + }; + + case "anyWeapon": + throw new Error(`Property handling for "anyWeapon" is unimplemented!`); + case "anyArmor": + throw new Error(`Property handling for "anyArmor" is unimplemented!`); + + default: + return null; + } + } +} +; + +Renderer.hover = { + LinkMeta: function() { + this.isHovered = false; + this.isLoading = false; + this.isPermanent = false; + this.windowMeta = null; + }, + + _BAR_HEIGHT: 16, + + _linkCache: {}, + _eleCache: new Map(), + _entryCache: {}, + _isInit: false, + _dmScreen: null, + _lastId: 0, + _contextMenu: null, + _contextMenuLastClicked: null, + + bindDmScreen(screen) { + this._dmScreen = screen; + }, + + _getNextId() { + return ++Renderer.hover._lastId; + }, + + _doInit() { + if (!Renderer.hover._isInit) { + Renderer.hover._isInit = true; + + $(document.body).on("click", ()=>Renderer.hover.cleanTempWindows()); + + Renderer.hover._contextMenu = ContextUtil.getMenu([new ContextUtil.Action("Maximize All",()=>{ + const $permWindows = $(`.hoverborder[data-perm="true"]`); + $permWindows.attr("data-display-title", "false"); + } + ,), new ContextUtil.Action("Minimize All",()=>{ + const $permWindows = $(`.hoverborder[data-perm="true"]`); + $permWindows.attr("data-display-title", "true"); + } + ,), null, new ContextUtil.Action("Close Others",()=>{ + const hoverId = Renderer.hover._contextMenuLastClicked?.hoverId; + Renderer.hover._doCloseAllWindows({ + hoverIdBlocklist: new Set([hoverId]) + }); + } + ,), new ContextUtil.Action("Close All",()=>Renderer.hover._doCloseAllWindows(),), ]); + } + }, + + cleanTempWindows() { + for (const [key,meta] of Renderer.hover._eleCache.entries()) { + if (!meta.isPermanent && meta.windowMeta && typeof key === "number") { + meta.windowMeta.doClose(); + Renderer.hover._eleCache.delete(key); + return; + } + + if (!meta.isPermanent && meta.windowMeta && !document.body.contains(key)) { + meta.windowMeta.doClose(); + return; + } + + if (!meta.isPermanent && meta.isHovered && meta.windowMeta) { + const bounds = key.getBoundingClientRect(); + if (EventUtil._mouseX < bounds.x || EventUtil._mouseY < bounds.y || EventUtil._mouseX > bounds.x + bounds.width || EventUtil._mouseY > bounds.y + bounds.height) { + meta.windowMeta.doClose(); + } + } + } + }, + + _doCloseAllWindows({hoverIdBlocklist=null}={}) { + Object.entries(Renderer.hover._WINDOW_METAS).filter(([hoverId,meta])=>hoverIdBlocklist == null || !hoverIdBlocklist.has(Number(hoverId))).forEach(([,meta])=>meta.doClose()); + }, + + _getSetMeta(ele) { + if (!Renderer.hover._eleCache.has(ele)) + Renderer.hover._eleCache.set(ele, new Renderer.hover.LinkMeta()); + return Renderer.hover._eleCache.get(ele); + }, + + _handleGenericMouseOverStart({evt, ele}) { + if (Renderer.hover.isSmallScreen(evt) && !evt.shiftKey) + return; + + Renderer.hover.cleanTempWindows(); + + const meta = Renderer.hover._getSetMeta(ele); + if (meta.isHovered || meta.isLoading) + return; + ele.style.cursor = "progress"; + + meta.isHovered = true; + meta.isLoading = true; + meta.isPermanent = evt.shiftKey; + + return meta; + }, + + _doPredefinedShowStart({entryId}) { + Renderer.hover.cleanTempWindows(); + + const meta = Renderer.hover._getSetMeta(entryId); + + meta.isPermanent = true; + + return meta; + }, + + async pHandleLinkMouseOver(evt, ele, opts) { + Renderer.hover._doInit(); + + let page, source, hash, preloadId, customHashId, isFauxPage; + if (opts) { + page = opts.page; + source = opts.source; + hash = opts.hash; + preloadId = opts.preloadId; + customHashId = opts.customHashId; + isFauxPage = !!opts.isFauxPage; + } else { + page = ele.dataset.vetPage; + source = ele.dataset.vetSource; + hash = ele.dataset.vetHash; + preloadId = ele.dataset.vetPreloadId; + isFauxPage = ele.dataset.vetIsFauxPage; + } + + let meta = Renderer.hover._handleGenericMouseOverStart({ + evt, + ele + }); + if (meta == null) + return; + + if ((EventUtil.isCtrlMetaKey(evt)) && Renderer.hover._pageToFluffFn(page)) + meta.isFluff = true; + + let toRender; + if (preloadId != null) { + switch (page) { + case UrlUtil.PG_BESTIARY: + { + const {_scaledCr: scaledCr, _scaledSpellSummonLevel: scaledSpellSummonLevel, _scaledClassSummonLevel: scaledClassSummonLevel} = Renderer.monster.getUnpackedCustomHashId(preloadId); + + const baseMon = await DataLoader.pCacheAndGet(page, source, hash); + if (scaledCr != null) { + toRender = await ScaleCreature.scale(baseMon, scaledCr); + } else if (scaledSpellSummonLevel != null) { + toRender = await ScaleSpellSummonedCreature.scale(baseMon, scaledSpellSummonLevel); + } else if (scaledClassSummonLevel != null) { + toRender = await ScaleClassSummonedCreature.scale(baseMon, scaledClassSummonLevel); + } + break; + } + } + } else if (customHashId) { + toRender = await DataLoader.pCacheAndGet(page, source, hash); + toRender = await Renderer.hover.pApplyCustomHashId(page, toRender, customHashId); + } else { + if (meta.isFluff) + toRender = await Renderer.hover.pGetHoverableFluff(page, source, hash); + else + toRender = await DataLoader.pCacheAndGet(page, source, hash); + } + + meta.isLoading = false; + + if (opts?.isDelay) { + meta.isDelayed = true; + ele.style.cursor = "help"; + await MiscUtil.pDelay(1100); + meta.isDelayed = false; + } + + ele.style.cursor = ""; + + if (!meta || (!meta.isHovered && !meta.isPermanent)) + return; + + const tmpEvt = meta._tmpEvt; + delete meta._tmpEvt; + + const win = (evt.view || {}).window; + + const $content = meta.isFluff ? Renderer.hover.$getHoverContent_fluff(page, toRender) : Renderer.hover.$getHoverContent_stats(page, toRender); + + const compactReferenceData = { + page, + source, + hash, + }; + + if (meta.windowMeta && !meta.isPermanent) { + meta.windowMeta.doClose(); + meta.windowMeta = null; + } + + meta.windowMeta = Renderer.hover.getShowWindow($content, Renderer.hover.getWindowPositionFromEvent(tmpEvt || evt, { + isPreventFlicker: !meta.isPermanent + }), { + title: toRender ? toRender.name : "", + isPermanent: meta.isPermanent, + pageUrl: isFauxPage ? null : `${Renderer.get().baseUrl}${page}#${hash}`, + cbClose: ()=>meta.isHovered = meta.isPermanent = meta.isLoading = meta.isFluff = false, + isBookContent: page === UrlUtil.PG_RECIPES, + compactReferenceData, + sourceData: toRender, + }, ); + + if (!meta.isFluff && !win?._IS_POPOUT) { + const fnBind = Renderer.hover.getFnBindListenersCompact(page); + if (fnBind) + fnBind(toRender, $content); + } + }, + + handleInlineMouseOver(evt, ele, entry, opts) { + Renderer.hover._doInit(); + + entry = entry || JSON.parse(ele.dataset.vetEntry); + + let meta = Renderer.hover._handleGenericMouseOverStart({ + evt, + ele + }); + if (meta == null) + return; + + meta.isLoading = false; + + ele.style.cursor = ""; + + if (!meta || (!meta.isHovered && !meta.isPermanent)) + return; + + const tmpEvt = meta._tmpEvt; + delete meta._tmpEvt; + + const win = (evt.view || {}).window; + + const $content = Renderer.hover.$getHoverContent_generic(entry, opts); + + if (meta.windowMeta && !meta.isPermanent) { + meta.windowMeta.doClose(); + meta.windowMeta = null; + } + + meta.windowMeta = Renderer.hover.getShowWindow($content, Renderer.hover.getWindowPositionFromEvent(tmpEvt || evt, { + isPreventFlicker: !meta.isPermanent + }), { + title: entry?.name || "", + isPermanent: meta.isPermanent, + pageUrl: null, + cbClose: ()=>meta.isHovered = meta.isPermanent = meta.isLoading = false, + isBookContent: true, + sourceData: entry, + }, ); + }, + + async pGetHoverableFluff(page, source, hash, opts) { + let toRender = await DataLoader.pCacheAndGet(`${page}Fluff`, source, hash, opts); + + if (!toRender) { + const entity = await DataLoader.pCacheAndGet(page, source, hash, opts); + + const pFnGetFluff = Renderer.hover._pageToFluffFn(page); + if (!pFnGetFluff && opts?.isSilent) + return null; + + toRender = await pFnGetFluff(entity); + } + + if (!toRender) + return toRender; + + if (toRender && (!toRender.name || !toRender.source)) { + const toRenderParent = await DataLoader.pCacheAndGet(page, source, hash, opts); + toRender = MiscUtil.copyFast(toRender); + toRender.name = toRenderParent.name; + toRender.source = toRenderParent.source; + } + + return toRender; + }, + + handleLinkMouseLeave(evt, ele) { + const meta = Renderer.hover._eleCache.get(ele); + ele.style.cursor = ""; + + if (!meta || meta.isPermanent) + return; + + if (evt.shiftKey) { + meta.isPermanent = true; + meta.windowMeta.setIsPermanent(true); + return; + } + + meta.isHovered = false; + if (meta.windowMeta) { + meta.windowMeta.doClose(); + meta.windowMeta = null; + } + }, + + handleLinkMouseMove(evt, ele) { + const meta = Renderer.hover._eleCache.get(ele); + if (!meta || meta.isPermanent) + return; + + if (meta.isDelayed) { + meta._tmpEvt = evt; + return; + } + + if (!meta.windowMeta) + return; + + meta.windowMeta.setPosition(Renderer.hover.getWindowPositionFromEvent(evt, { + isPreventFlicker: !evt.shiftKey && !meta.isPermanent + })); + + if (evt.shiftKey && !meta.isPermanent) { + meta.isPermanent = true; + meta.windowMeta.setIsPermanent(true); + } + }, + + handlePredefinedMouseOver(evt, ele, entryId, opts) { + opts = opts || {}; + + const meta = Renderer.hover._handleGenericMouseOverStart({ + evt, + ele + }); + if (meta == null) + return; + + Renderer.hover.cleanTempWindows(); + + const toRender = Renderer.hover._entryCache[entryId]; + + meta.isLoading = false; + if (!meta.isHovered && !meta.isPermanent) + return; + + const $content = Renderer.hover.$getHoverContent_generic(toRender, opts); + meta.windowMeta = Renderer.hover.getShowWindow($content, Renderer.hover.getWindowPositionFromEvent(evt, { + isPreventFlicker: !meta.isPermanent + }), { + title: toRender.data && toRender.data.hoverTitle != null ? toRender.data.hoverTitle : toRender.name, + isPermanent: meta.isPermanent, + cbClose: ()=>meta.isHovered = meta.isPermanent = meta.isLoading = false, + sourceData: toRender, + }, ); + + ele.style.cursor = ""; + }, + + doPredefinedShow(entryId, opts) { + opts = opts || {}; + + const meta = Renderer.hover._doPredefinedShowStart({ + entryId + }); + if (meta == null) + return; + + Renderer.hover.cleanTempWindows(); + + const toRender = Renderer.hover._entryCache[entryId]; + + const $content = Renderer.hover.$getHoverContent_generic(toRender, opts); + meta.windowMeta = Renderer.hover.getShowWindow($content, Renderer.hover.getWindowPositionExact((window.innerWidth / 2) - (Renderer.hover._DEFAULT_WIDTH_PX / 2), 100), { + title: toRender.data && toRender.data.hoverTitle != null ? toRender.data.hoverTitle : toRender.name, + isPermanent: meta.isPermanent, + cbClose: ()=>meta.isHovered = meta.isPermanent = meta.isLoading = false, + sourceData: toRender, + }, ); + }, + + handlePredefinedMouseLeave(evt, ele) { + return Renderer.hover.handleLinkMouseLeave(evt, ele); + }, + + handlePredefinedMouseMove(evt, ele) { + return Renderer.hover.handleLinkMouseMove(evt, ele); + }, + + _WINDOW_POSITION_PROPS_FROM_EVENT: ["isFromBottom", "isFromRight", "clientX", "window", "isPreventFlicker", "bcr", ], + + getWindowPositionFromEvent(evt, {isPreventFlicker=false}={}) { + const ele = evt.target; + const win = evt?.view?.window || window; + + const bcr = ele.getBoundingClientRect().toJSON(); + + const isFromBottom = bcr.top > win.innerHeight / 2; + const isFromRight = bcr.left > win.innerWidth / 2; + + return { + mode: "autoFromElement", + isFromBottom, + isFromRight, + clientX: EventUtil.getClientX(evt), + window: win, + isPreventFlicker, + bcr, + }; + }, + + getWindowPositionExact(x, y, evt=null) { + return { + window: evt?.view?.window || window, + mode: "exact", + x, + y, + }; + }, + + getWindowPositionExactVisibleBottom(x, y, evt=null) { + return { + ...Renderer.hover.getWindowPositionExact(x, y, evt), + mode: "exactVisibleBottom", + }; + }, + + _WINDOW_METAS: {}, + MIN_Z_INDEX: 200, + _MAX_Z_INDEX: 300, + _DEFAULT_WIDTH_PX: 600, + _BODY_SCROLLER_WIDTH_PX: 15, + + _getZIndex() { + const zIndices = Object.values(Renderer.hover._WINDOW_METAS).map(it=>it.zIndex); + if (!zIndices.length) + return Renderer.hover.MIN_Z_INDEX; + return Math.max(...zIndices); + }, + + _getNextZIndex(hoverId) { + const cur = Renderer.hover._getZIndex(); + if (hoverId != null && Renderer.hover._WINDOW_METAS[hoverId].zIndex === cur) + return cur; + const out = cur + 1; + + if (out > Renderer.hover._MAX_Z_INDEX) { + const sortedWindowMetas = Object.entries(Renderer.hover._WINDOW_METAS).sort(([kA,vA],[kB,vB])=>SortUtil.ascSort(vA.zIndex, vB.zIndex)); + + if (sortedWindowMetas.length >= (Renderer.hover._MAX_Z_INDEX - Renderer.hover.MIN_Z_INDEX)) { + sortedWindowMetas.forEach(([k,v])=>{ + v.setZIndex(Renderer.hover.MIN_Z_INDEX); + } + ); + } else { + sortedWindowMetas.forEach(([k,v],i)=>{ + v.setZIndex(Renderer.hover.MIN_Z_INDEX + i); + } + ); + } + + return Renderer.hover._getNextZIndex(hoverId); + } else + return out; + }, + + _isIntersectRect(r1, r2) { + return r1.left <= r2.right && r2.left <= r1.right && r1.top <= r2.bottom && r2.top <= r1.bottom; + }, + + getShowWindow($content, position, opts) { + opts = opts || {}; + + Renderer.hover._doInit(); + + const initialWidth = opts.width == null ? Renderer.hover._DEFAULT_WIDTH_PX : opts.width; + const initialZIndex = Renderer.hover._getNextZIndex(); + + const $body = $(position.window.document.body); + const $hov = $(`
            `).css({ + "right": -initialWidth, + "width": initialWidth, + "zIndex": initialZIndex, + }); + const $wrpContent = $(`
            `); + if (opts.height != null) + $wrpContent.css("height", opts.height); + const $hovTitle = $(`${opts.title || ""}`); + + const hoverWindow = {}; + const hoverId = Renderer.hover._getNextId(); + Renderer.hover._WINDOW_METAS[hoverId] = hoverWindow; + const mouseUpId = `mouseup.${hoverId} touchend.${hoverId}`; + const mouseMoveId = `mousemove.${hoverId} touchmove.${hoverId}`; + const resizeId = `resize.${hoverId}`; + const drag = {}; + + const $brdrTopRightResize = $(`
            `).on("mousedown touchstart", (evt)=>Renderer.hover._getShowWindow_handleDragMousedown({ + hoverWindow, + hoverId, + $hov, + drag, + $wrpContent + }, { + evt, + type: 1 + })); + + const $brdrRightResize = $(`
            `).on("mousedown touchstart", (evt)=>Renderer.hover._getShowWindow_handleDragMousedown({ + hoverWindow, + hoverId, + $hov, + drag, + $wrpContent + }, { + evt, + type: 2 + })); + + const $brdrBottomRightResize = $(`
            `).on("mousedown touchstart", (evt)=>Renderer.hover._getShowWindow_handleDragMousedown({ + hoverWindow, + hoverId, + $hov, + drag, + $wrpContent + }, { + evt, + type: 3 + })); + + const $brdrBtm = $(`
            `).on("mousedown touchstart", (evt)=>Renderer.hover._getShowWindow_handleDragMousedown({ + hoverWindow, + hoverId, + $hov, + drag, + $wrpContent + }, { + evt, + type: 4 + })); + + const $brdrBtmLeftResize = $(`
            `).on("mousedown touchstart", (evt)=>Renderer.hover._getShowWindow_handleDragMousedown({ + hoverWindow, + hoverId, + $hov, + drag, + $wrpContent + }, { + evt, + type: 5 + })); + + const $brdrLeftResize = $(`
            `).on("mousedown touchstart", (evt)=>Renderer.hover._getShowWindow_handleDragMousedown({ + hoverWindow, + hoverId, + $hov, + drag, + $wrpContent + }, { + evt, + type: 6 + })); + + const $brdrTopLeftResize = $(`
            `).on("mousedown touchstart", (evt)=>Renderer.hover._getShowWindow_handleDragMousedown({ + hoverWindow, + hoverId, + $hov, + drag, + $wrpContent + }, { + evt, + type: 7 + })); + + const $brdrTopResize = $(`
            `).on("mousedown touchstart", (evt)=>Renderer.hover._getShowWindow_handleDragMousedown({ + hoverWindow, + hoverId, + $hov, + drag, + $wrpContent + }, { + evt, + type: 8 + })); + + const $brdrTop = $(`
            `).on("mousedown touchstart", (evt)=>Renderer.hover._getShowWindow_handleDragMousedown({ + hoverWindow, + hoverId, + $hov, + drag, + $wrpContent + }, { + evt, + type: 9 + })).on("contextmenu", (evt)=>{ + Renderer.hover._contextMenuLastClicked = { + hoverId, + }; + ContextUtil.pOpenMenu(evt, Renderer.hover._contextMenu); + } + ); + + $(position.window.document).on(mouseUpId, (evt)=>{ + if (drag.type) { + if (drag.type < 9) { + $wrpContent.css("max-height", ""); + $hov.css("max-width", ""); + } + Renderer.hover._getShowWindow_adjustPosition({ + $hov, + $wrpContent, + position + }); + + if (drag.type === 9) { + if (EventUtil.isUsingTouch() && evt.target.classList.contains("hwin__top-border-icon")) { + evt.preventDefault(); + drag.type = 0; + $(evt.target).click(); + return; + } + + if (this._dmScreen && opts.compactReferenceData) { + const panel = this._dmScreen.getPanelPx(EventUtil.getClientX(evt), EventUtil.getClientY(evt)); + if (!panel) + return; + this._dmScreen.setHoveringPanel(panel); + const target = panel.getAddButtonPos(); + + if (Renderer.hover._getShowWindow_isOverHoverTarget({ + evt, + target + })) { + panel.doPopulate_Stats(opts.compactReferenceData.page, opts.compactReferenceData.source, opts.compactReferenceData.hash); + Renderer.hover._getShowWindow_doClose({ + $hov, + position, + mouseUpId, + mouseMoveId, + resizeId, + hoverId, + opts, + hoverWindow + }); + } + this._dmScreen.resetHoveringButton(); + } + } + drag.type = 0; + } + } + ).on(mouseMoveId, (evt)=>{ + const args = { + $wrpContent, + $hov, + drag, + evt + }; + switch (drag.type) { + case 1: + Renderer.hover._getShowWindow_handleNorthDrag(args); + Renderer.hover._getShowWindow_handleEastDrag(args); + break; + case 2: + Renderer.hover._getShowWindow_handleEastDrag(args); + break; + case 3: + Renderer.hover._getShowWindow_handleSouthDrag(args); + Renderer.hover._getShowWindow_handleEastDrag(args); + break; + case 4: + Renderer.hover._getShowWindow_handleSouthDrag(args); + break; + case 5: + Renderer.hover._getShowWindow_handleSouthDrag(args); + Renderer.hover._getShowWindow_handleWestDrag(args); + break; + case 6: + Renderer.hover._getShowWindow_handleWestDrag(args); + break; + case 7: + Renderer.hover._getShowWindow_handleNorthDrag(args); + Renderer.hover._getShowWindow_handleWestDrag(args); + break; + case 8: + Renderer.hover._getShowWindow_handleNorthDrag(args); + break; + case 9: + { + const diffX = drag.startX - EventUtil.getClientX(evt); + const diffY = drag.startY - EventUtil.getClientY(evt); + $hov.css("left", drag.baseLeft - diffX).css("top", drag.baseTop - diffY); + drag.startX = EventUtil.getClientX(evt); + drag.startY = EventUtil.getClientY(evt); + drag.baseTop = parseFloat($hov.css("top")); + drag.baseLeft = parseFloat($hov.css("left")); + + if (this._dmScreen) { + const panel = this._dmScreen.getPanelPx(EventUtil.getClientX(evt), EventUtil.getClientY(evt)); + if (!panel) + return; + this._dmScreen.setHoveringPanel(panel); + const target = panel.getAddButtonPos(); + + if (Renderer.hover._getShowWindow_isOverHoverTarget({ + evt, + target + })) + this._dmScreen.setHoveringButton(panel); + else + this._dmScreen.resetHoveringButton(); + } + break; + } + } + } + ); + $(position.window).on(resizeId, ()=>Renderer.hover._getShowWindow_adjustPosition({ + $hov, + $wrpContent, + position + })); + + $brdrTop.attr("data-display-title", false); + $brdrTop.on("dblclick", ()=>Renderer.hover._getShowWindow_doToggleMinimizedMaximized({ + $brdrTop, + $hov + })); + $brdrTop.append($hovTitle); + const $brdTopRhs = $(`
            `).appendTo($brdrTop); + + if (opts.pageUrl && !position.window._IS_POPOUT && !Renderer.get().isInternalLinksDisabled()) { + const $btnGotoPage = $(``).appendTo($brdTopRhs); + } + + if (!position.window._IS_POPOUT && !opts.isPopout) { + const $btnPopout = $(``).on("click", evt=>{ + evt.stopPropagation(); + return Renderer.hover._getShowWindow_pDoPopout({ + $hov, + position, + mouseUpId, + mouseMoveId, + resizeId, + hoverId, + opts, + hoverWindow, + $content + }, { + evt + }); + } + ).appendTo($brdTopRhs); + } + + if (opts.sourceData) { + const btnPopout = e_({ + tag: "span", + clazz: `hwin__top-border-icon hwin__top-border-icon--text`, + title: "Show Source Data", + text: "{}", + click: evt=>{ + evt.stopPropagation(); + evt.preventDefault(); + + const $content = Renderer.hover.$getHoverContent_statsCode(opts.sourceData); + Renderer.hover.getShowWindow($content, Renderer.hover.getWindowPositionFromEvent(evt), { + title: [opts.sourceData._displayName || opts.sourceData.name, "Source Data"].filter(Boolean).join(" \u2014 "), + isPermanent: true, + isBookContent: true, + }, ); + } + , + }); + $brdTopRhs.append(btnPopout); + } + + const $btnClose = $(``).on("click", (evt)=>{ + evt.stopPropagation(); + + if (EventUtil.isCtrlMetaKey(evt)) { + Renderer.hover._doCloseAllWindows(); + return; + } + + Renderer.hover._getShowWindow_doClose({ + $hov, + position, + mouseUpId, + mouseMoveId, + resizeId, + hoverId, + opts, + hoverWindow + }); + } + ).appendTo($brdTopRhs); + + $wrpContent.append($content); + + $hov.append($brdrTopResize).append($brdrTopRightResize).append($brdrRightResize).append($brdrBottomRightResize).append($brdrBtmLeftResize).append($brdrLeftResize).append($brdrTopLeftResize) + .append($brdrTop).append($wrpContent).append($brdrBtm); + + $body.append($hov); + + Renderer.hover._getShowWindow_setPosition({ + $hov, + $wrpContent, + position + }, position); + + hoverWindow.$windowTitle = $hovTitle; + hoverWindow.zIndex = initialZIndex; + hoverWindow.setZIndex = Renderer.hover._getNextZIndex.bind(this, { + $hov, + hoverWindow + }); + + hoverWindow.setPosition = Renderer.hover._getShowWindow_setPosition.bind(this, { + $hov, + $wrpContent, + position + }); + hoverWindow.setIsPermanent = Renderer.hover._getShowWindow_setIsPermanent.bind(this, { + opts, + $brdrTop + }); + hoverWindow.doClose = Renderer.hover._getShowWindow_doClose.bind(this, { + $hov, + position, + mouseUpId, + mouseMoveId, + resizeId, + hoverId, + opts, + hoverWindow + }); + hoverWindow.doMaximize = Renderer.hover._getShowWindow_doMaximize.bind(this, { + $brdrTop, + $hov + }); + hoverWindow.doZIndexToFront = Renderer.hover._getShowWindow_doZIndexToFront.bind(this, { + $hov, + hoverWindow, + hoverId + }); + + if (opts.isPopout) + Renderer.hover._getShowWindow_pDoPopout({ + $hov, + position, + mouseUpId, + mouseMoveId, + resizeId, + hoverId, + opts, + hoverWindow, + $content + }); + + return hoverWindow; + }, + + _getShowWindow_doClose({$hov, position, mouseUpId, mouseMoveId, resizeId, hoverId, opts, hoverWindow}) { + $hov.remove(); + $(position.window.document).off(mouseUpId); + $(position.window.document).off(mouseMoveId); + $(position.window).off(resizeId); + + delete Renderer.hover._WINDOW_METAS[hoverId]; + + if (opts.cbClose) + opts.cbClose(hoverWindow); + }, + + _getShowWindow_handleDragMousedown({hoverWindow, hoverId, $hov, drag, $wrpContent}, {evt, type}) { + if (evt.which === 0 || evt.which === 1) + evt.preventDefault(); + hoverWindow.zIndex = Renderer.hover._getNextZIndex(hoverId); + $hov.css({ + "z-index": hoverWindow.zIndex, + "animation": "initial", + }); + drag.type = type; + drag.startX = EventUtil.getClientX(evt); + drag.startY = EventUtil.getClientY(evt); + drag.baseTop = parseFloat($hov.css("top")); + drag.baseLeft = parseFloat($hov.css("left")); + drag.baseHeight = $wrpContent.height(); + drag.baseWidth = parseFloat($hov.css("width")); + if (type < 9) { + $wrpContent.css({ + "height": drag.baseHeight, + "max-height": "initial", + }); + $hov.css("max-width", "initial"); + } + }, + + _getShowWindow_isOverHoverTarget({evt, target}) { + return EventUtil.getClientX(evt) >= target.left && EventUtil.getClientX(evt) <= target.left + target.width && EventUtil.getClientY(evt) >= target.top && EventUtil.getClientY(evt) <= target.top + target.height; + }, + + _getShowWindow_handleNorthDrag({$wrpContent, $hov, drag, evt}) { + const diffY = Math.max(drag.startY - EventUtil.getClientY(evt), 80 - drag.baseHeight); + $wrpContent.css("height", drag.baseHeight + diffY); + $hov.css("top", drag.baseTop - diffY); + drag.startY = EventUtil.getClientY(evt); + drag.baseHeight = $wrpContent.height(); + drag.baseTop = parseFloat($hov.css("top")); + }, + + _getShowWindow_handleEastDrag({$wrpContent, $hov, drag, evt}) { + const diffX = drag.startX - EventUtil.getClientX(evt); + $hov.css("width", drag.baseWidth - diffX); + drag.startX = EventUtil.getClientX(evt); + drag.baseWidth = parseFloat($hov.css("width")); + }, + + _getShowWindow_handleSouthDrag({$wrpContent, $hov, drag, evt}) { + const diffY = drag.startY - EventUtil.getClientY(evt); + $wrpContent.css("height", drag.baseHeight - diffY); + drag.startY = EventUtil.getClientY(evt); + drag.baseHeight = $wrpContent.height(); + }, + + _getShowWindow_handleWestDrag({$wrpContent, $hov, drag, evt}) { + const diffX = Math.max(drag.startX - EventUtil.getClientX(evt), 150 - drag.baseWidth); + $hov.css("width", drag.baseWidth + diffX).css("left", drag.baseLeft - diffX); + drag.startX = EventUtil.getClientX(evt); + drag.baseWidth = parseFloat($hov.css("width")); + drag.baseLeft = parseFloat($hov.css("left")); + }, + + _getShowWindow_doToggleMinimizedMaximized({$brdrTop, $hov}) { + const curState = $brdrTop.attr("data-display-title"); + const isNextMinified = curState === "false"; + $brdrTop.attr("data-display-title", isNextMinified); + $brdrTop.attr("data-perm", true); + $hov.toggleClass("hwin--minified", isNextMinified); + }, + + _getShowWindow_doMaximize({$brdrTop, $hov}) { + $brdrTop.attr("data-display-title", false); + $hov.toggleClass("hwin--minified", false); + }, + + async _getShowWindow_pDoPopout({$hov, position, mouseUpId, mouseMoveId, resizeId, hoverId, opts, hoverWindow, $content}, {evt}={}) { + const dimensions = opts.fnGetPopoutSize ? opts.fnGetPopoutSize() : { + width: 600, + height: $content.height() + }; + const win = window.open("", opts.title || "", `width=${dimensions.width},height=${dimensions.height}location=0,menubar=0,status=0,titlebar=0,toolbar=0`, ); + + if (!win._IS_POPOUT) { + win._IS_POPOUT = true; + win.document.write(` + + + + ${opts.title} + ${$(`link[rel="stylesheet"][href]`).map((i,e)=>e.outerHTML).get().join("\n")} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
            + + + + + `); + + win.Renderer = Renderer; + + let ticks = 50; + while (!win.document.body && ticks-- > 0) + await MiscUtil.pDelay(5); + + win.$wrpHoverContent = $(win.document).find(`.hoverbox--popout`); + } + + let $cpyContent; + if (opts.$pFnGetPopoutContent) { + $cpyContent = await opts.$pFnGetPopoutContent(); + } else { + $cpyContent = $content.clone(true, true); + } + + $cpyContent.appendTo(win.$wrpHoverContent.empty()); + + Renderer.hover._getShowWindow_doClose({ + $hov, + position, + mouseUpId, + mouseMoveId, + resizeId, + hoverId, + opts, + hoverWindow + }); + }, + + _getShowWindow_setPosition({$hov, $wrpContent, position}, positionNxt) { + switch (positionNxt.mode) { + case "autoFromElement": + { + const bcr = $hov[0].getBoundingClientRect(); + + if (positionNxt.isFromBottom) + $hov.css("top", positionNxt.bcr.top - (bcr.height + 10)); + else + $hov.css("top", positionNxt.bcr.top + positionNxt.bcr.height + 10); + + if (positionNxt.isFromRight) + $hov.css("left", (positionNxt.clientX || positionNxt.bcr.left) - (bcr.width + 10)); + else + $hov.css("left", (positionNxt.clientX || (positionNxt.bcr.left + positionNxt.bcr.width)) + 10); + + if (position !== positionNxt) { + Renderer.hover._WINDOW_POSITION_PROPS_FROM_EVENT.forEach(prop=>{ + position[prop] = positionNxt[prop]; + } + ); + } + + break; + } + case "exact": + { + $hov.css({ + "left": positionNxt.x, + "top": positionNxt.y, + }); + break; + } + case "exactVisibleBottom": + { + $hov.css({ + "left": positionNxt.x, + "top": positionNxt.y, + "animation": "initial", + }); + + let yPos = positionNxt.y; + + const {bottom: posBottom, height: winHeight} = $hov[0].getBoundingClientRect(); + const height = position.window.innerHeight; + if (posBottom > height) { + yPos = position.window.innerHeight - winHeight; + $hov.css({ + "top": yPos, + "animation": "", + }); + } + + break; + } + default: + throw new Error(`Positiong mode unimplemented: "${positionNxt.mode}"`); + } + + Renderer.hover._getShowWindow_adjustPosition({ + $hov, + $wrpContent, + position + }); + }, + + _getShowWindow_adjustPosition({$hov, $wrpContent, position}) { + const eleHov = $hov[0]; + const wrpContent = $wrpContent[0]; + + const bcr = eleHov.getBoundingClientRect().toJSON(); + const screenHeight = position.window.innerHeight; + const screenWidth = position.window.innerWidth; + + if (bcr.top < 0) { + bcr.top = 0; + bcr.bottom = bcr.top + bcr.height; + eleHov.style.top = `${bcr.top}px`; + } else if (bcr.top >= screenHeight - Renderer.hover._BAR_HEIGHT) { + bcr.top = screenHeight - Renderer.hover._BAR_HEIGHT; + bcr.bottom = bcr.top + bcr.height; + eleHov.style.top = `${bcr.top}px`; + } + + if (bcr.left < 0) { + bcr.left = 0; + bcr.right = bcr.left + bcr.width; + eleHov.style.left = `${bcr.left}px`; + } else if (bcr.left + bcr.width + Renderer.hover._BODY_SCROLLER_WIDTH_PX > screenWidth) { + bcr.left = Math.max(screenWidth - bcr.width - Renderer.hover._BODY_SCROLLER_WIDTH_PX, 0); + bcr.right = bcr.left + bcr.width; + eleHov.style.left = `${bcr.left}px`; + } + + if (position.isPreventFlicker && Renderer.hover._isIntersectRect(bcr, position.bcr)) { + if (position.isFromBottom) { + bcr.height = position.bcr.top - 5; + wrpContent.style.height = `${bcr.height}px`; + } else { + bcr.height = screenHeight - position.bcr.bottom - 5; + wrpContent.style.height = `${bcr.height}px`; + } + } + }, + + _getShowWindow_setIsPermanent({opts, $brdrTop}, isPermanent) { + opts.isPermanent = isPermanent; + $brdrTop.attr("data-perm", isPermanent); + }, + + _getShowWindow_setZIndex({$hov, hoverWindow}, zIndex) { + $hov.css("z-index", zIndex); + hoverWindow.zIndex = zIndex; + }, + + _getShowWindow_doZIndexToFront({$hov, hoverWindow, hoverId}) { + const nxtZIndex = Renderer.hover._getNextZIndex(hoverId); + Renderer.hover._getNextZIndex({ + $hov, + hoverWindow + }, nxtZIndex); + }, + + getMakePredefinedHover(entry, opts) { + opts = opts || {}; + + const id = opts.id ?? Renderer.hover._getNextId(); + Renderer.hover._entryCache[id] = entry; + return { + id, + html: `onmouseover="Renderer.hover.handlePredefinedMouseOver(event, this, ${id}, ${JSON.stringify(opts).escapeQuotes()})" onmousemove="Renderer.hover.handlePredefinedMouseMove(event, this)" onmouseleave="Renderer.hover.handlePredefinedMouseLeave(event, this)" ${Renderer.hover.getPreventTouchString()}`, + mouseOver: (evt,ele)=>Renderer.hover.handlePredefinedMouseOver(evt, ele, id, opts), + mouseMove: (evt,ele)=>Renderer.hover.handlePredefinedMouseMove(evt, ele), + mouseLeave: (evt,ele)=>Renderer.hover.handlePredefinedMouseLeave(evt, ele), + touchStart: (evt,ele)=>Renderer.hover.handleTouchStart(evt, ele), + show: ()=>Renderer.hover.doPredefinedShow(id, opts), + }; + }, + + updatePredefinedHover(id, entry) { + Renderer.hover._entryCache[id] = entry; + }, + + getInlineHover(entry, opts) { + return { + html: `onmouseover="Renderer.hover.handleInlineMouseOver(event, this)" onmouseleave="Renderer.hover.handleLinkMouseLeave(event, this)" onmousemove="Renderer.hover.handleLinkMouseMove(event, this)" data-vet-entry="${JSON.stringify(entry).qq()}" ${opts ? `data-vet-opts="${JSON.stringify(opts).qq()}"` : ""} ${Renderer.hover.getPreventTouchString()}`, + }; + }, + + getPreventTouchString() { + return `ontouchstart="Renderer.hover.handleTouchStart(event, this)"`; + }, + + handleTouchStart(evt, ele) { + if (!Renderer.hover.isSmallScreen(evt)) { + $(ele).data("href", $(ele).data("href") || $(ele).attr("href")); + $(ele).attr("href", "javascript:void(0)"); + setTimeout(()=>{ + const data = $(ele).data("href"); + if (data) { + $(ele).attr("href", data); + $(ele).data("href", null); + } + } + , 100); + } + }, + + getEntityLink(ent, {displayText=null, prop=null, isLowerCase=false, isTitleCase=false, }={}, ) { + if (isLowerCase && isTitleCase) + throw new Error(`"isLowerCase" and "isTitleCase" are mutually exclusive!`); + + const name = isLowerCase ? ent.name.toLowerCase() : isTitleCase ? ent.name.toTitleCase() : ent.name; + + let parts = [name, ent.source, displayText || "", ]; + + switch (prop || ent.__prop) { + case "monster": + { + if (ent._isScaledCr) { + parts.push(`${VeCt.HASH_SCALED}=${Parser.numberToCr(ent._scaledCr)}`); + } + + if (ent._isScaledSpellSummon) { + parts.push(`${VeCt.HASH_SCALED_SPELL_SUMMON}=${ent._scaledSpellSummonLevel}`); + } + + if (ent._isScaledClassSummon) { + parts.push(`${VeCt.HASH_SCALED_CLASS_SUMMON}=${ent._scaledClassSummonLevel}`); + } + + break; + } + + case "deity": + { + parts.splice(1, 0, ent.pantheon); + break; + } + } + + while (parts.length && !parts.last()?.length) + parts.pop(); + + return Renderer.get().render(`{@${Parser.getPropTag(prop || ent.__prop)} ${parts.join("|")}}`); + }, + + getRefMetaFromTag(str) { + str = str.slice(2, -1); + const [tag,...refParts] = str.split(" "); + const ref = refParts.join(" "); + const type = `ref${tag.uppercaseFirst()}`; + return { + type, + [tag]: ref + }; + }, + + async pApplyCustomHashId(page, ent, customHashId) { + switch (page) { + case UrlUtil.PG_BESTIARY: + { + const out = await Renderer.monster.pGetModifiedCreature(ent, customHashId); + Renderer.monster.updateParsed(out); + return out; + } + + case UrlUtil.PG_RECIPES: + return Renderer.recipe.pGetModifiedRecipe(ent, customHashId); + + default: + return ent; + } + }, + + getGenericCompactRenderedString(entry, depth=0) { + return ` + + ${Renderer.get().setFirstSection(true).render(entry, depth)} + + `; + }, + + getFnRenderCompact(page, {isStatic=false}={}) { + switch (page) { + case "generic": + case "hover": + return Renderer.hover.getGenericCompactRenderedString; + case UrlUtil.PG_QUICKREF: + return Renderer.hover.getGenericCompactRenderedString; + case UrlUtil.PG_CLASSES: + return Renderer.class.getCompactRenderedString; + case UrlUtil.PG_SPELLS: + return Renderer.spell.getCompactRenderedString; + case UrlUtil.PG_ITEMS: + return Renderer.item.getCompactRenderedString; + case UrlUtil.PG_BESTIARY: + return it=>Renderer.monster.getCompactRenderedString(it, { + isShowScalers: !isStatic, + isScaledCr: it._originalCr != null, + isScaledSpellSummon: it._isScaledSpellSummon, + isScaledClassSummon: it._isScaledClassSummon + }); + case UrlUtil.PG_CONDITIONS_DISEASES: + return Renderer.condition.getCompactRenderedString; + case UrlUtil.PG_BACKGROUNDS: + return Renderer.background.getCompactRenderedString; + case UrlUtil.PG_FEATS: + return Renderer.feat.getCompactRenderedString; + case UrlUtil.PG_OPT_FEATURES: + return Renderer.optionalfeature.getCompactRenderedString; + case UrlUtil.PG_PSIONICS: + return Renderer.psionic.getCompactRenderedString; + case UrlUtil.PG_REWARDS: + return Renderer.reward.getCompactRenderedString; + case UrlUtil.PG_RACES: + return it=>Renderer.race.getCompactRenderedString(it, { + isStatic + }); + case UrlUtil.PG_DEITIES: + return Renderer.deity.getCompactRenderedString; + case UrlUtil.PG_OBJECTS: + return Renderer.object.getCompactRenderedString; + case UrlUtil.PG_TRAPS_HAZARDS: + return Renderer.traphazard.getCompactRenderedString; + case UrlUtil.PG_VARIANTRULES: + return Renderer.variantrule.getCompactRenderedString; + case UrlUtil.PG_CULTS_BOONS: + return Renderer.cultboon.getCompactRenderedString; + case UrlUtil.PG_TABLES: + return Renderer.table.getCompactRenderedString; + case UrlUtil.PG_VEHICLES: + return Renderer.vehicle.getCompactRenderedString; + case UrlUtil.PG_ACTIONS: + return Renderer.action.getCompactRenderedString; + case UrlUtil.PG_LANGUAGES: + return Renderer.language.getCompactRenderedString; + case UrlUtil.PG_CHAR_CREATION_OPTIONS: + return Renderer.charoption.getCompactRenderedString; + case UrlUtil.PG_RECIPES: + return Renderer.recipe.getCompactRenderedString; + case UrlUtil.PG_CLASS_SUBCLASS_FEATURES: + return Renderer.hover.getGenericCompactRenderedString; + case UrlUtil.PG_CREATURE_FEATURES: + return Renderer.hover.getGenericCompactRenderedString; + case UrlUtil.PG_DECKS: + return Renderer.deck.getCompactRenderedString; + case "classfeature": + case "classFeature": + return Renderer.hover.getGenericCompactRenderedString; + case "subclassfeature": + case "subclassFeature": + return Renderer.hover.getGenericCompactRenderedString; + case "citation": + return Renderer.hover.getGenericCompactRenderedString; + default: + if (Renderer[page]?.getCompactRenderedString) + return Renderer[page].getCompactRenderedString; + return null; + } + }, + + getFnBindListenersCompact(page) { + switch (page) { + case UrlUtil.PG_BESTIARY: + return Renderer.monster.bindListenersCompact; + case UrlUtil.PG_RACES: + return Renderer.race.bindListenersCompact; + default: + return null; + } + }, + + _pageToFluffFn(page) { + switch (page) { + case UrlUtil.PG_BESTIARY: + return Renderer.monster.pGetFluff; + case UrlUtil.PG_ITEMS: + return Renderer.item.pGetFluff; + case UrlUtil.PG_CONDITIONS_DISEASES: + return Renderer.condition.pGetFluff; + case UrlUtil.PG_SPELLS: + return Renderer.spell.pGetFluff; + case UrlUtil.PG_RACES: + return Renderer.race.pGetFluff; + case UrlUtil.PG_BACKGROUNDS: + return Renderer.background.pGetFluff; + case UrlUtil.PG_FEATS: + return Renderer.feat.pGetFluff; + case UrlUtil.PG_LANGUAGES: + return Renderer.language.pGetFluff; + case UrlUtil.PG_VEHICLES: + return Renderer.vehicle.pGetFluff; + case UrlUtil.PG_CHAR_CREATION_OPTIONS: + return Renderer.charoption.pGetFluff; + case UrlUtil.PG_RECIPES: + return Renderer.recipe.pGetFluff; + default: + return null; + } + }, + + isSmallScreen(evt) { + if (typeof window === "undefined") + return false; + + evt = evt || {}; + const win = (evt.view || {}).window || window; + return win.innerWidth <= 768; + }, + + $getHoverContent_stats(page, toRender, opts, renderFnOpts) { + opts = opts || {}; + if (page === UrlUtil.PG_RECIPES) + opts = { + ...MiscUtil.copyFast(opts), + isBookContent: true + }; + + const fnRender = opts.fnRender || Renderer.hover.getFnRenderCompact(page, { + isStatic: opts.isStatic + }); + const $out = $$`${fnRender(toRender, renderFnOpts)}
            `; + + if (!opts.isStatic) { + const fnBind = Renderer.hover.getFnBindListenersCompact(page); + if (fnBind) + fnBind(toRender, $out[0]); + } + + return $out; + }, + + $getHoverContent_fluff(page, toRender, opts, renderFnOpts) { + opts = opts || {}; + if (page === UrlUtil.PG_RECIPES) + opts = { + ...MiscUtil.copyFast(opts), + isBookContent: true + }; + + if (!toRender) { + return $$`
            ${Renderer.utils.HTML_NO_INFO}
            `; + } + + toRender = MiscUtil.copyFast(toRender); + + if (toRender.images && toRender.images.length) { + const cachedImages = MiscUtil.copyFast(toRender.images); + delete toRender.images; + + toRender.entries = toRender.entries || []; + const hasText = toRender.entries.length > 0; + if (hasText) + toRender.entries.unshift({ + type: "hr" + }); + cachedImages[0].maxHeight = 33; + cachedImages[0].maxHeightUnits = "vh"; + toRender.entries.unshift(cachedImages[0]); + + if (cachedImages.length > 1) { + if (hasText) + toRender.entries.push({ + type: "hr" + }); + toRender.entries.push(...cachedImages.slice(1)); + } + } + + return $$`${Renderer.generic.getCompactRenderedString(toRender, renderFnOpts)}
            `; + }, + + $getHoverContent_statsCode(toRender, {isSkipClean=false, title=null}={}) { + const cleanCopy = isSkipClean ? toRender : DataUtil.cleanJson(MiscUtil.copyFast(toRender)); + return Renderer.hover.$getHoverContent_miscCode(title || [cleanCopy.name, "Source Data"].filter(Boolean).join(" \u2014 "), JSON.stringify(cleanCopy, null, "\t"), ); + }, + + $getHoverContent_miscCode(name, code) { + const toRenderCode = { + type: "code", + name, + preformatted: code, + }; + return $$`${Renderer.get().render(toRenderCode)}
            `; + }, + + $getHoverContent_generic(toRender, opts) { + opts = opts || {}; + + return $$`${Renderer.hover.getGenericCompactRenderedString(toRender, opts.depth || 0)}
            `; + }, + + doPopoutCurPage(evt, entity) { + const page = UrlUtil.getCurrentPage(); + const $content = Renderer.hover.$getHoverContent_stats(page, entity); + Renderer.hover.getShowWindow($content, Renderer.hover.getWindowPositionFromEvent(evt), { + pageUrl: `#${UrlUtil.autoEncodeHash(entity)}`, + title: entity._displayName || entity.name, + isPermanent: true, + isBookContent: page === UrlUtil.PG_RECIPES, + sourceData: entity, + }, ); + }, +}; + +Renderer.getNames = function(nameStack, entry, opts) { + opts = opts || {}; + if (opts.maxDepth == null) + opts.maxDepth = false; + if (opts.depth == null) + opts.depth = 0; + + if (opts.typeBlocklist && entry.type && opts.typeBlocklist.has(entry.type)) + return; + + if (opts.maxDepth !== false && opts.depth > opts.maxDepth) + return; + if (entry.name) + nameStack.push(Renderer.stripTags(entry.name)); + if (entry.entries) { + let nextDepth = entry.type === "section" ? -1 : entry.type === "entries" ? opts.depth + 1 : opts.depth; + for (const eX of entry.entries) { + const nxtOpts = { + ...opts + }; + nxtOpts.depth = nextDepth; + Renderer.getNames(nameStack, eX, nxtOpts); + } + } else if (entry.items) { + for (const eX of entry.items) { + Renderer.getNames(nameStack, eX, opts); + } + } +} +; + +Renderer.getNumberedNames = function(entry) { + const renderer = new Renderer().setTrackTitles(true); + renderer.render(entry); + const titles = renderer.getTrackedTitles(); + const out = {}; + Object.entries(titles).forEach(([k,v])=>{ + v = Renderer.stripTags(v); + out[v] = Number(k); + } + ); + return out; +} +; + +Renderer.findName = function(entry) { + return CollectionUtil.dfs(entry, { + prop: "name" + }); +} +; +Renderer.findSource = function(entry) { + return CollectionUtil.dfs(entry, { + prop: "source" + }); +} +; +Renderer.findEntry = function(entry) { + return CollectionUtil.dfs(entry, { + fnMatch: obj=>obj.name && obj?.entries?.length + }); +} +; + +Renderer.stripTags = function(str) { + if (!str) + return str; + let nxtStr = Renderer._stripTagLayer(str); + while (nxtStr.length !== str.length) { + str = nxtStr; + nxtStr = Renderer._stripTagLayer(str); + } + return nxtStr; +} +; + +Renderer._stripTagLayer = function(str) { + if (str.includes("{@")) { + const tagSplit = Renderer.splitByTags(str); + return tagSplit.filter(it=>it).map(it=>{ + if (it.startsWith("{@")) { + let[tag,text] = Renderer.splitFirstSpace(it.slice(1, -1)); + const tagInfo = Renderer.tag.TAG_LOOKUP[tag]; + if (!tagInfo) + throw new Error(`Unhandled tag: "${tag}"`); + return tagInfo.getStripped(tag, text); + } else + return it; + } + ).join(""); + } + return str; +} +; + +Renderer.getRollableRow = function(row, opts) { + opts = opts || {}; + + if (row[0]?.type === "cell" && (row[0]?.roll?.exact != null || (row[0]?.roll?.min != null && row[0]?.roll?.max != null))) + return row; + + row = MiscUtil.copyFast(row); + try { + const cleanRow = String(row[0]).trim(); + + const mLowHigh = /^(\d+) or (lower|higher)$/i.exec(cleanRow); + if (mLowHigh) { + row[0] = { + type: "cell", + entry: cleanRow + }; + if (mLowHigh[2].toLowerCase() === "lower") { + row[0].roll = { + min: -Renderer.dice.POS_INFINITE, + max: Number(mLowHigh[1]), + }; + } else { + row[0].roll = { + min: Number(mLowHigh[1]), + max: Renderer.dice.POS_INFINITE, + }; + } + + return row; + } + + const m = /^(\d+)([-\u2012\u2013](\d+))?$/.exec(cleanRow); + if (m) { + if (m[1] && !m[2]) { + row[0] = { + type: "cell", + roll: { + exact: Number(m[1]), + }, + }; + if (m[1][0] === "0") + row[0].roll.pad = true; + Renderer.getRollableRow._handleInfiniteOpts(row, opts); + } else { + row[0] = { + type: "cell", + roll: { + min: Number(m[1]), + max: Number(m[3]), + }, + }; + if (m[1][0] === "0" || m[3][0] === "0") + row[0].roll.pad = true; + Renderer.getRollableRow._handleInfiniteOpts(row, opts); + } + } else { + const m = /^(\d+)\+$/.exec(row[0]); + row[0] = { + type: "cell", + roll: { + min: Number(m[1]), + max: Renderer.dice.POS_INFINITE, + }, + }; + } + } catch (e) { + if (opts.cbErr) + opts.cbErr(row[0], e); + } + return row; +} +; +Renderer.getRollableRow._handleInfiniteOpts = function(row, opts) { + if (!opts.isForceInfiniteResults) + return; + + const isExact = row[0].roll.exact != null; + + if (opts.isFirstRow) { + if (!isExact) + row[0].roll.displayMin = row[0].roll.min; + row[0].roll.min = -Renderer.dice.POS_INFINITE; + } + + if (opts.isLastRow) { + if (!isExact) + row[0].roll.displayMax = row[0].roll.max; + row[0].roll.max = Renderer.dice.POS_INFINITE; + } +} +; + +Renderer.initLazyImageLoaders = function() { + const images = document.querySelectorAll(`img[data-src]`); + + Renderer.utils.lazy.destroyObserver({ + observerId: "images" + }); + + const observer = Renderer.utils.lazy.getCreateObserver({ + observerId: "images", + fnOnObserve: ({entry})=>{ + const $img = $(entry.target); + $img.attr("src", $img.attr("data-src")).removeAttr("data-src"); + } + , + }); + + images.forEach(img=>observer.track(img)); +} +; + +Renderer.HEAD_NEG_1 = "rd__b--0"; +Renderer.HEAD_0 = "rd__b--1"; +Renderer.HEAD_1 = "rd__b--2"; +Renderer.HEAD_2 = "rd__b--3"; +Renderer.HEAD_2_SUB_VARIANT = "rd__b--4"; +Renderer.DATA_NONE = "data-none"; + +"use strict"; + +Renderer.dice = { + SYSTEM_USER: { + name: "Avandra", + }, + POS_INFINITE: 100000000000000000000, + _SYMBOL_PARSE_FAILED: Symbol("parseFailed"), + + _$wrpRoll: null, + _$minRoll: null, + _$iptRoll: null, + _$outRoll: null, + _$head: null, + _hist: [], + _histIndex: null, + _$lastRolledBy: null, + _storage: null, + + _isManualMode: false, + + DICE: [4, 6, 8, 10, 12, 20, 100], + getNextDice(faces) { + const idx = Renderer.dice.DICE.indexOf(faces); + if (~idx) + return Renderer.dice.DICE[idx + 1]; + else + return null; + }, + + getPreviousDice(faces) { + const idx = Renderer.dice.DICE.indexOf(faces); + if (~idx) + return Renderer.dice.DICE[idx - 1]; + else + return null; + }, + + _panel: null, + bindDmScreenPanel(panel, title) { + if (Renderer.dice._panel) { + Renderer.dice.unbindDmScreenPanel(); + } + Renderer.dice._showBox(); + Renderer.dice._panel = panel; + panel.doPopulate_Rollbox(title); + }, + + unbindDmScreenPanel() { + if (Renderer.dice._panel) { + $(`body`).append(Renderer.dice._$wrpRoll); + Renderer.dice._panel.close$TabContent(); + Renderer.dice._panel = null; + Renderer.dice._hideBox(); + Renderer.dice._$wrpRoll.removeClass("rollbox-panel"); + } + }, + + get$Roller() { + return Renderer.dice._$wrpRoll; + }, + + parseRandomise2(str) { + if (!str || !str.trim()) + return null; + const wrpTree = Renderer.dice.lang.getTree3(str); + if (wrpTree) + return wrpTree.tree.evl({}); + else + return null; + }, + + parseAverage(str) { + if (!str || !str.trim()) + return null; + const wrpTree = Renderer.dice.lang.getTree3(str); + if (wrpTree) + return wrpTree.tree.avg({}); + else + return null; + }, + + _showBox() { + Renderer.dice._$minRoll.hideVe(); + Renderer.dice._$wrpRoll.showVe(); + Renderer.dice._$iptRoll.prop("placeholder", `${Renderer.dice._getRandomPlaceholder()} or "/help"`); + }, + + _hideBox() { + Renderer.dice._$minRoll.showVe(); + Renderer.dice._$wrpRoll.hideVe(); + }, + + _getRandomPlaceholder() { + const count = RollerUtil.randomise(10); + const faces = Renderer.dice.DICE[RollerUtil.randomise(Renderer.dice.DICE.length - 1)]; + const mod = (RollerUtil.randomise(3) - 2) * RollerUtil.randomise(10); + const drop = (count > 1) && RollerUtil.randomise(5) === 5; + const dropDir = drop ? RollerUtil.randomise(2) === 2 ? "h" : "l" : ""; + const dropAmount = drop ? RollerUtil.randomise(count - 1) : null; + return `${count}d${faces}${drop ? `d${dropDir}${dropAmount}` : ""}${mod < 0 ? mod : mod > 0 ? `+${mod}` : ""}`; + }, + + async _pInit() { + const $wrpRoll = $(`
            `).hideVe(); + const $minRoll = $(``).on("click", ()=>{ + Renderer.dice._showBox(); + Renderer.dice._$iptRoll.focus(); + } + ); + const $head = $(`
            Dice Roller
            `).on("click", ()=>{ + if (!Renderer.dice._panel) + Renderer.dice._hideBox(); + } + ); + const $outRoll = $(`
            `); + const $iptRoll = $(``).on("keypress", async evt=>{ + evt.stopPropagation(); + if (evt.key !== "Enter") + return; + + const strDice = $iptRoll.val(); + const result = await Renderer.dice.pRoll2(strDice, { + isUser: true, + name: "Anon", + }, ); + $iptRoll.val(""); + + if (result === Renderer.dice._SYMBOL_PARSE_FAILED) { + Renderer.dice._showInvalid(); + $iptRoll.addClass("form-control--error"); + } + } + ).on("keydown", (evt)=>{ + $iptRoll.removeClass("form-control--error"); + + if (evt.key === "ArrowUp") { + evt.preventDefault(); + Renderer.dice._prevHistory(); + return; + } + + if (evt.key === "ArrowDown") { + evt.preventDefault(); + Renderer.dice._nextHistory(); + } + } + ); + $wrpRoll.append($head).append($outRoll).append($iptRoll); + + Renderer.dice._$wrpRoll = $wrpRoll; + Renderer.dice._$minRoll = $minRoll; + Renderer.dice._$head = $head; + Renderer.dice._$outRoll = $outRoll; + Renderer.dice._$iptRoll = $iptRoll; + + $(`body`).append($minRoll).append($wrpRoll); + + $wrpRoll.on("click", ".out-roll-item-code", (evt)=>Renderer.dice._$iptRoll.val($(evt.target).text()).focus()); + + Renderer.dice.storage = await StorageUtil.pGet(VeCt.STORAGE_ROLLER_MACRO) || {}; + }, + + _prevHistory() { + Renderer.dice._histIndex--; + Renderer.dice._prevNextHistory_load(); + }, + _nextHistory() { + Renderer.dice._histIndex++; + Renderer.dice._prevNextHistory_load(); + }, + + _prevNextHistory_load() { + Renderer.dice._cleanHistoryIndex(); + const nxtVal = Renderer.dice._hist[Renderer.dice._histIndex]; + Renderer.dice._$iptRoll.val(nxtVal); + if (nxtVal) + Renderer.dice._$iptRoll[0].selectionStart = Renderer.dice._$iptRoll[0].selectionEnd = nxtVal.length; + }, + + _cleanHistoryIndex: ()=>{ + if (!Renderer.dice._hist.length) { + Renderer.dice._histIndex = null; + } else { + Renderer.dice._histIndex = Math.min(Renderer.dice._hist.length, Math.max(Renderer.dice._histIndex, 0)); + } + } + , + + _addHistory: (str)=>{ + Renderer.dice._hist.push(str); + Renderer.dice._histIndex = Renderer.dice._hist.length; + } + , + + _scrollBottom: ()=>{ + Renderer.dice._$outRoll.scrollTop(1e10); + } + , + + async pRollerClickUseData(evt, ele) { + evt.stopPropagation(); + evt.preventDefault(); + + const $ele = $(ele); + const rollData = $ele.data("packed-dice"); + let name = $ele.data("roll-name"); + let shiftKey = evt.shiftKey; + let ctrlKey = EventUtil.isCtrlMetaKey(evt); + + const options = rollData.toRoll.split(";").map(it=>it.trim()).filter(Boolean); + + let chosenRollData; + if (options.length > 1) { + const cpyRollData = MiscUtil.copyFast(rollData); + const menu = ContextUtil.getMenu([new ContextUtil.Action("Choose Roll",null,{ + isDisabled: true + },), null, ...options.map(it=>new ContextUtil.Action(`Roll ${it}`,evt=>{ + shiftKey = shiftKey || evt.shiftKey; + ctrlKey = ctrlKey || (EventUtil.isCtrlMetaKey(evt)); + cpyRollData.toRoll = it; + return cpyRollData; + } + ,)), ]); + + chosenRollData = await ContextUtil.pOpenMenu(evt, menu); + } else + chosenRollData = rollData; + + if (!chosenRollData) + return; + + const rePrompt = /#\$prompt_number:?([^$]*)\$#/g; + const results = []; + let m; + while ((m = rePrompt.exec(chosenRollData.toRoll))) { + const optionsRaw = m[1]; + const opts = {}; + if (optionsRaw) { + const spl = optionsRaw.split(","); + spl.map(it=>it.trim()).forEach(part=>{ + const [k,v] = part.split("=").map(it=>it.trim()); + switch (k) { + case "min": + case "max": + opts[k] = Number(v); + break; + default: + opts[k] = v; + break; + } + } + ); + } + + if (opts.min == null) + opts.min = 0; + if (opts.max == null) + opts.max = Renderer.dice.POS_INFINITE; + if (opts.default == null) + opts.default = 0; + + const input = await InputUiUtil.pGetUserNumber(opts); + if (input == null) + return; + results.push(input); + } + + const rollDataCpy = MiscUtil.copyFast(chosenRollData); + rePrompt.lastIndex = 0; + rollDataCpy.toRoll = rollDataCpy.toRoll.replace(rePrompt, ()=>results.shift()); + + let rollDataCpyToRoll; + if (rollData.prompt) { + const sortedKeys = Object.keys(rollDataCpy.prompt.options).sort(SortUtil.ascSortLower); + const menu = ContextUtil.getMenu([new ContextUtil.Action(rollDataCpy.prompt.entry,null,{ + isDisabled: true + }), null, ...sortedKeys.map(it=>{ + const title = rollDataCpy.prompt.mode === "psi" ? `${it} point${it === "1" ? "" : "s"}` : `${Parser.spLevelToFull(it)} level`; + + return new ContextUtil.Action(title,evt=>{ + shiftKey = shiftKey || evt.shiftKey; + ctrlKey = ctrlKey || (EventUtil.isCtrlMetaKey(evt)); + + const fromScaling = rollDataCpy.prompt.options[it]; + if (!fromScaling) { + name = ""; + return rollDataCpy; + } else { + name = rollDataCpy.prompt.mode === "psi" ? `${it} psi activation` : `${Parser.spLevelToFull(it)}-level cast`; + rollDataCpy.toRoll += `+${fromScaling}`; + return rollDataCpy; + } + } + ,); + } + ), ]); + + rollDataCpyToRoll = await ContextUtil.pOpenMenu(evt, menu); + } else + rollDataCpyToRoll = rollDataCpy; + + if (!rollDataCpyToRoll) + return; + await Renderer.dice.pRollerClick({ + shiftKey, + ctrlKey + }, ele, JSON.stringify(rollDataCpyToRoll), name); + }, + + __rerollNextInlineResult(ele) { + const $ele = $(ele); + const $result = $ele.next(`.result`); + const r = Renderer.dice.__rollPackedData($ele); + $result.text(r); + }, + + __rollPackedData($ele) { + const wrpTree = Renderer.dice.lang.getTree3($ele.data("packed-dice").toRoll); + return wrpTree.tree.evl({}); + }, + + $getEleUnknownTableRoll(total) { + return $(Renderer.dice._pRollerClick_getMsgBug(total)); + }, + + _pRollerClick_getMsgBug(total) { + return `No result found matching roll ${total}?! ๐Ÿ›`; + }, + + async pRollerClick(evtMock, ele, packed, name) { + const $ele = $(ele); + const entry = JSON.parse(packed); + const additionalData = { + ...ele.dataset + }; + + const rolledBy = { + name: Renderer.dice._pRollerClick_attemptToGetNameOfRoller({ + $ele + }), + label: name != null ? name : Renderer.dice._pRollerClick_attemptToGetNameOfRoll({ + entry, + $ele + }), + }; + + const modRollMeta = Renderer.dice.getEventModifiedRollMeta(evtMock, entry); + const $parent = $ele.closest("th, p, table"); + + const rollResult = await this._pRollerClick_pGetResult({ + $parent, + $ele, + entry, + modRollMeta, + rolledBy, + additionalData, + }); + + if (!entry.autoRoll) + return; + + const $tgt = $ele.next(`[data-rd-is-autodice-result="true"]`); + const curTxt = $tgt.text(); + $tgt.text(rollResult); + JqueryUtil.showCopiedEffect($tgt, curTxt, true); + }, + + async _pRollerClick_pGetResult({$parent, $ele, entry, modRollMeta, rolledBy, additionalData}) { + const sharedRollOpts = { + rollCount: modRollMeta.rollCount, + additionalData, + isHidden: !!entry.autoRoll, + }; + + if ($parent.is("th") && $parent.attr("data-rd-isroller") === "true") { + if ($parent.attr("data-rd-namegeneratorrolls")) { + return Renderer.dice._pRollerClick_pRollGeneratorTable({ + $parent, + $ele, + rolledBy, + modRollMeta, + rollOpts: sharedRollOpts, + }); + } + + return Renderer.dice.pRollEntry(modRollMeta.entry, rolledBy, { + ...sharedRollOpts, + fnGetMessage: Renderer.dice._pRollerClick_fnGetMessageTable.bind(Renderer.dice, $ele), + }, ); + } + + return Renderer.dice.pRollEntry(modRollMeta.entry, rolledBy, { + ...sharedRollOpts, + }, ); + }, + + _pRollerClick_fnGetMessageTable($ele, total) { + const elesTd = Renderer.dice._pRollerClick_$getTdsFromTotal($ele, total); + if (elesTd) { + const tableRow = elesTd.map(ele=>ele.innerHTML.trim()).filter(it=>it).join(" | "); + const $row = $(`${tableRow}`); + Renderer.dice._pRollerClick_rollInlineRollers($ele); + return $row.html(); + } + return Renderer.dice._pRollerClick_getMsgBug(total); + }, + + _pRollerClick_attemptToGetNameOfRoll({entry, $ele}) { + if (entry.name) + return entry.name; + + let titleMaybe = $ele.closest(`table:not(.stats)`).children(`caption`).text(); + if (titleMaybe) + return titleMaybe.trim(); + + titleMaybe = $ele.parent().children(`.rd__list-item-name`).text(); + if (titleMaybe) + return titleMaybe.trim().replace(/[.,:]$/, ""); + + titleMaybe = $ele.closest(`div`).children(`.rd__h`).first().find(`.entry-title-inner`).text(); + if (titleMaybe) { + titleMaybe = titleMaybe.trim().replace(/[.,:]$/, ""); + return titleMaybe; + } + + titleMaybe = $ele.closest(`table.stats`).children(`tbody`).first().children(`tr`).first().find(`.rnd-name .stats-name`).text(); + if (titleMaybe) + return titleMaybe.trim(); + + if (UrlUtil.getCurrentPage() === UrlUtil.PG_CHARACTERS) { + titleMaybe = ($ele.closest(`.chr-entity__row`).find(".chr-entity__ipt-name").val() || "").trim(); + if (titleMaybe) + return titleMaybe; + } + + return titleMaybe; + }, + + _pRollerClick_attemptToGetNameOfRoller({$ele}) { + const $hov = $ele.closest(`.hwin`); + if ($hov.length) + return $hov.find(`.stats-name`).first().text(); + const $roll = $ele.closest(`.out-roll-wrp`); + if ($roll.length) + return $roll.data("name"); + const $dispPanelTitle = $ele.closest(`.dm-screen-panel`).children(`.panel-control-title`); + if ($dispPanelTitle.length) + return $dispPanelTitle.text().trim(); + let name = document.title.replace("- 5etools", "").trim(); + return name === "DM Screen" ? "Dungeon Master" : name; + }, + + _pRollerClick_$getTdsFromTotal($ele, total) { + const $table = $ele.closest(`table`); + const $tdRoll = $table.find(`td`).filter((i,e)=>{ + const $e = $(e); + if (!$e.closest(`table`).is($table)) + return false; + return total >= Number($e.data("roll-min")) && total <= Number($e.data("roll-max")); + } + ); + if ($tdRoll.length && $tdRoll.nextAll().length) { + return $tdRoll.nextAll().get(); + } + return null; + }, + + _pRollerClick_rollInlineRollers($ele) { + $ele.find(`.render-roller`).each((i,e)=>{ + const $e = $(e); + const r = Renderer.dice.__rollPackedData($e); + $e.attr("onclick", `Renderer.dice.__rerollNextInlineResult(this)`); + $e.after(` (${r})`); + } + ); + }, + + _pRollerClick_fnGetMessageGeneratorTable($ele, ix, total) { + const elesTd = Renderer.dice._pRollerClick_$getTdsFromTotal($ele, total); + if (elesTd) { + const $row = $(`${elesTd[ix].innerHTML.trim()}`); + Renderer.dice._pRollerClick_rollInlineRollers($ele); + return $row.html(); + } + return Renderer.dice._pRollerClick_getMsgBug(total); + }, + + async _pRollerClick_pRollGeneratorTable({$parent, $ele, rolledBy, modRollMeta, rollOpts}) { + Renderer.dice.addElement({ + rolledBy, + html: `${rolledBy.label}:`, + isMessage: true + }); + + let total = 0; + + const out = []; + const numRolls = Number($parent.attr("data-rd-namegeneratorrolls")); + const $ths = $ele.closest(`table`).find(`th`); + for (let i = 0; i < numRolls; ++i) { + const cpyRolledBy = MiscUtil.copyFast(rolledBy); + cpyRolledBy.label = $($ths.get(i + 1)).text().trim(); + + const result = await Renderer.dice.pRollEntry(modRollMeta.entry, cpyRolledBy, { + ...rollOpts, + fnGetMessage: Renderer.dice._pRollerClick_fnGetMessageGeneratorTable.bind(Renderer.dice, $ele, i), + }, ); + total += result; + const elesTd = Renderer.dice._pRollerClick_$getTdsFromTotal($ele, result); + + if (!elesTd) { + out.push(`(no result)`); + continue; + } + + out.push(elesTd[i].innerHTML.trim()); + } + + Renderer.dice.addElement({ + rolledBy, + html: `= ${out.join(" ")}`, + isMessage: true + }); + + return total; + }, + + getEventModifiedRollMeta(evt, entry) { + const out = { + rollCount: 1, + entry + }; + + if (evt.shiftKey) { + if (entry.subType === "damage") { + const dice = []; + entry.toRoll.replace(/\s+/g, "").replace(/\d*?d\d+/gi, m0=>dice.push(m0)); + entry.toRoll = `${entry.toRoll}${dice.length ? `+${dice.join("+")}` : ""}`; + } else if (entry.subType === "d20") { + if (entry.d20mod != null) + entry.toRoll = `2d20dl1${entry.d20mod}`; + else + entry.toRoll = entry.toRoll.replace(/^\s*1?\s*d\s*20/, "2d20dl1"); + } else + out.rollCount = 2; + } + + if (EventUtil.isCtrlMetaKey(evt)) { + if (entry.subType === "damage") { + entry.toRoll = `floor((${entry.toRoll}) / 2)`; + } else if (entry.subType === "d20") { + if (entry.d20mod != null) + entry.toRoll = `2d20dh1${entry.d20mod}`; + else + entry.toRoll = entry.toRoll.replace(/^\s*1?\s*d\s*20/, "2d20dh1"); + } else + out.rollCount = 2; + } + + return out; + }, + + async pRoll2(str, rolledBy, opts) { + opts = opts || {}; + str = str.trim().replace(/\/r(?:oll)? /gi, "").trim(); + if (!str) + return; + if (rolledBy.isUser) + Renderer.dice._addHistory(str); + + if (str.startsWith("/")) + return Renderer.dice._pHandleCommand(str, rolledBy); + if (str.startsWith("#")) + return Renderer.dice._pHandleSavedRoll(str, rolledBy, opts); + + const [head,...tail] = str.split(":"); + if (tail.length) { + str = tail.join(":"); + rolledBy.label = head; + } + const wrpTree = Renderer.dice.lang.getTree3(str); + if (!wrpTree) + return Renderer.dice._SYMBOL_PARSE_FAILED; + return Renderer.dice._pHandleRoll2(wrpTree, rolledBy, opts); + }, + + async pRollEntry(entry, rolledBy, opts) { + opts = opts || {}; + + const rollCount = Math.round(opts.rollCount || 1); + delete opts.rollCount; + if (rollCount <= 0) + throw new Error(`Invalid roll count: ${rollCount} (must be a positive integer)`); + + const wrpTree = Renderer.dice.lang.getTree3(entry.toRoll); + wrpTree.tree.successThresh = entry.successThresh; + wrpTree.tree.successMax = entry.successMax; + wrpTree.tree.chanceSuccessText = entry.chanceSuccessText; + wrpTree.tree.chanceFailureText = entry.chanceFailureText; + wrpTree.tree.isColorSuccessFail = entry.isColorSuccessFail; + + const results = []; + if (rollCount > 1 && !opts.isHidden) + Renderer.dice._showMessage(`Rolling twice...`, rolledBy); + for (let i = 0; i < rollCount; ++i) { + const result = await Renderer.dice._pHandleRoll2(wrpTree, rolledBy, opts); + if (result == null) + return null; + results.push(result); + } + return Math.max(...results); + }, + + async _pHandleRoll2(wrpTree, rolledBy, opts) { + opts = { + ...opts + }; + + if (wrpTree.meta && wrpTree.meta.hasPb) { + const userPb = await InputUiUtil.pGetUserNumber({ + min: 0, + int: true, + title: "Enter Proficiency Bonus", + default: 2, + storageKey_default: "dice.playerProficiencyBonus", + isGlobal_default: true, + }); + if (userPb == null) + return null; + opts.pb = userPb; + } + + if (wrpTree.meta && wrpTree.meta.hasSummonSpellLevel) { + const predefinedSpellLevel = opts.additionalData?.summonedBySpellLevel != null && !isNaN(opts.additionalData?.summonedBySpellLevel) ? Number(opts.additionalData.summonedBySpellLevel) : null; + + const userSummonSpellLevel = await InputUiUtil.pGetUserNumber({ + min: predefinedSpellLevel ?? 0, + int: true, + title: "Enter Spell Level", + default: predefinedSpellLevel ?? 1, + }); + if (userSummonSpellLevel == null) + return null; + opts.summonSpellLevel = userSummonSpellLevel; + } + + if (wrpTree.meta && wrpTree.meta.hasSummonClassLevel) { + const predefinedClassLevel = opts.additionalData?.summonedByClassLevel != null && !isNaN(opts.additionalData?.summonedByClassLevel) ? Number(opts.additionalData.summonedByClassLevel) : null; + + const userSummonClassLevel = await InputUiUtil.pGetUserNumber({ + min: predefinedClassLevel ?? 0, + int: true, + title: "Enter Class Level", + default: predefinedClassLevel ?? 1, + }); + if (userSummonClassLevel == null) + return null; + opts.summonClassLevel = userSummonClassLevel; + } + + if (Renderer.dice._isManualMode) + return Renderer.dice._pHandleRoll2_manual(wrpTree.tree, rolledBy, opts); + else + return Renderer.dice._pHandleRoll2_automatic(wrpTree.tree, rolledBy, opts); + }, + + _pHandleRoll2_automatic(tree, rolledBy, opts) { + opts = opts || {}; + + if (!opts.isHidden) + Renderer.dice._showBox(); + Renderer.dice._checkHandleName(rolledBy.name); + const $out = Renderer.dice._$lastRolledBy; + + if (tree) { + const meta = {}; + if (opts.pb) + meta.pb = opts.pb; + if (opts.summonSpellLevel) + meta.summonSpellLevel = opts.summonSpellLevel; + if (opts.summonClassLevel) + meta.summonClassLevel = opts.summonClassLevel; + + const result = tree.evl(meta); + const fullHtml = (meta.html || []).join(""); + const allMax = meta.allMax && meta.allMax.length && !(meta.allMax.filter(it=>!it).length); + const allMin = meta.allMin && meta.allMin.length && !(meta.allMin.filter(it=>!it).length); + + const lbl = rolledBy.label && (!rolledBy.name || rolledBy.label.trim().toLowerCase() !== rolledBy.name.trim().toLowerCase()) ? rolledBy.label : null; + + const ptTarget = opts.target != null ? result >= opts.target ? ` ≥${opts.target}` : ` <${opts.target}` : ""; + + const isThreshSuccess = tree.successThresh != null && result > (tree.successMax || 100) - tree.successThresh; + const isColorSuccess = tree.isColorSuccessFail || !tree.chanceSuccessText; + const isColorFail = tree.isColorSuccessFail || !tree.chanceFailureText; + const totalPart = tree.successThresh != null ? `${isThreshSuccess ? (tree.chanceSuccessText || "Success!") : (tree.chanceFailureText || "Failure")}` : `${result}`; + + const title = `${rolledBy.name ? `${rolledBy.name} \u2014 ` : ""}${lbl ? `${lbl}: ` : ""}${tree}`; + + const message = opts.fnGetMessage ? opts.fnGetMessage(result) : null; + ExtensionUtil.doSendRoll({ + dice: tree.toString(), + result, + rolledBy: rolledBy.name, + label: [lbl, message].filter(Boolean).join(" \u2013 "), + }); + + if (!opts.isHidden) { + $out.append(` +
            +
            + ${lbl ? `${lbl}: ` : ""} + ${totalPart} + ${ptTarget} + ${fullHtml} + ${message ? `${message}` : ""} +
            +
            + +
            +
            `); + + Renderer.dice._scrollBottom(); + } + + return result; + } else { + if (!opts.isHidden) { + $out.append(`
            Invalid input! Try "/help"
            `); + Renderer.dice._scrollBottom(); + } + return null; + } + }, + + _pHandleRoll2_manual(tree, rolledBy, opts) { + opts = opts || {}; + + if (!tree) + return JqueryUtil.doToast({ + type: "danger", + content: `Invalid roll input!` + }); + + const title = (rolledBy.label || "").toTitleCase() || "Roll Dice"; + const $dispDice = $(`
            ${tree.toString()}
            `); + if (opts.isResultUsed) { + return InputUiUtil.pGetUserNumber({ + title, + $elePre: $dispDice, + }); + } else { + const {$modalInner} = UiUtil.getShowModal({ + title, + isMinHeight0: true, + }); + $dispDice.appendTo($modalInner); + return null; + } + }, + + _showMessage(message, rolledBy) { + Renderer.dice._showBox(); + Renderer.dice._checkHandleName(rolledBy.name); + const $out = Renderer.dice._$lastRolledBy; + $out.append(`
            ${message}
            `); + Renderer.dice._scrollBottom(); + }, + + _showInvalid() { + Renderer.dice._showMessage("Invalid input! Try "/help"", Renderer.dice.SYSTEM_USER); + }, + + _validCommands: new Set(["/c", "/cls", "/clear", "/iterroll"]), + async _pHandleCommand(com, rolledBy) { + Renderer.dice._showMessage(`${com}`, rolledBy); + const comParsed = Renderer.dice._getParsedCommand(com); + const [comOp] = comParsed; + + if (comOp === "/help" || comOp === "/h") { + Renderer.dice._showMessage(`
              +
            • Keep highest; 4d6kh3
            • +
            • Drop lowest; 4d6dl1
            • +
            • Drop highest; 3d4dh1
            • +
            • Keep lowest; 3d4kl1
            • + +
            • Reroll equal; 2d4r1
            • +
            • Reroll less; 2d4r<2
            • +
            • Reroll less or equal; 2d4r<=2
            • +
            • Reroll greater; 2d4r>2
            • +
            • Reroll greater equal; 2d4r>=3
            • + +
            • Explode equal; 2d4x4
            • +
            • Explode less; 2d4x<2
            • +
            • Explode less or equal; 2d4x<=2
            • +
            • Explode greater; 2d4x>2
            • +
            • Explode greater equal; 2d4x>=3
            • + +
            • Count Successes equal; 2d4cs=4
            • +
            • Count Successes less; 2d4cs<2
            • +
            • Count Successes less or equal; 2d4cs<=2
            • +
            • Count Successes greater; 2d4cs>2
            • +
            • Count Successes greater equal; 2d4cs>=3
            • + +
            • Margin of Success; 2d4ms=4
            • + +
            • Dice pools; {2d8, 1d6}
            • +
            • Dice pools with modifiers; {1d20+7, 10}kh1
            • + +
            • Rounding; floor(1.5), ceil(1.5), round(1.5)
            • + +
            • Average; avg(8d6)
            • +
            • Maximize dice; dmax(8d6)
            • +
            • Minimize dice; dmin(8d6)
            • + +
            • Other functions; sign(1d6-3), abs(1d6-3), ...etc.
            • +
            + Up and down arrow keys cycle input history.
            + Anything before a colon is treated as a label (Fireball: 8d6)
            +Use /macro list to list saved macros.
            + Use /macro add myName 1d2+3 to add (or update) a macro. Macro names should not contain spaces or hashes.
            + Use /macro remove myName to remove a macro.
            + Use #myName to roll a macro.
            + Use /iterroll roll count [target] to roll multiple times, optionally against a target. + Use /clear to clear the roller.`, Renderer.dice.SYSTEM_USER, ); + return; + } + + if (comOp === "/macro") { + const [,mode,...others] = comParsed; + + if (!["list", "add", "remove", "clear"].includes(mode)) + Renderer.dice._showInvalid(); + else { + switch (mode) { + case "list": + if (!others.length) { + Object.keys(Renderer.dice.storage).forEach(name=>{ + Renderer.dice._showMessage(`#${name} \u2014 ${Renderer.dice.storage[name]}`, Renderer.dice.SYSTEM_USER); + } + ); + } else { + Renderer.dice._showInvalid(); + } + break; + case "add": + { + if (others.length === 2) { + const [name,macro] = others; + if (name.includes(" ") || name.includes("#")) + Renderer.dice._showInvalid(); + else { + Renderer.dice.storage[name] = macro; + await Renderer.dice._pSaveMacros(); + Renderer.dice._showMessage(`Saved macro #${name}`, Renderer.dice.SYSTEM_USER); + } + } else { + Renderer.dice._showInvalid(); + } + break; + } + case "remove": + if (others.length === 1) { + if (Renderer.dice.storage[others[0]]) { + delete Renderer.dice.storage[others[0]]; + await Renderer.dice._pSaveMacros(); + Renderer.dice._showMessage(`Removed macro #${others[0]}`, Renderer.dice.SYSTEM_USER); + } else { + Renderer.dice._showMessage(`Macro #${others[0]} not found`, Renderer.dice.SYSTEM_USER); + } + } else { + Renderer.dice._showInvalid(); + } + break; + } + } + return; + } + + if (Renderer.dice._validCommands.has(comOp)) { + switch (comOp) { + case "/c": + case "/cls": + case "/clear": + Renderer.dice._$outRoll.empty(); + Renderer.dice._$lastRolledBy.empty(); + Renderer.dice._$lastRolledBy = null; + return; + + case "/iterroll": + { + let[,exp,count,target] = comParsed; + + if (!exp) + return Renderer.dice._showInvalid(); + const wrpTree = Renderer.dice.lang.getTree3(exp); + if (!wrpTree) + return Renderer.dice._showInvalid(); + + count = count && !isNaN(count) ? Number(count) : 1; + target = target && !isNaN(target) ? Number(target) : undefined; + + for (let i = 0; i < count; ++i) { + await Renderer.dice.pRoll2(exp, { + name: "Anon", + }, { + target, + }, ); + } + } + } + return; + } + + Renderer.dice._showInvalid(); + }, + + async _pSaveMacros() { + await StorageUtil.pSet(VeCt.STORAGE_ROLLER_MACRO, Renderer.dice.storage); + }, + + _getParsedCommand(str) { + return str.split(/\s+/); + }, + + _pHandleSavedRoll(id, rolledBy, opts) { + id = id.replace(/^#/, ""); + const macro = Renderer.dice.storage[id]; + if (macro) { + rolledBy.label = id; + const wrpTree = Renderer.dice.lang.getTree3(macro); + return Renderer.dice._pHandleRoll2(wrpTree, rolledBy, opts); + } else + Renderer.dice._showMessage(`Macro #${id} not found`, Renderer.dice.SYSTEM_USER); + }, + + addRoll({rolledBy, html, $ele}) { + if (html && $ele) + throw new Error(`Must specify one of html or $ele!`); + + if (html != null && !html.trim()) + return; + + Renderer.dice._showBox(); + Renderer.dice._checkHandleName(rolledBy.name); + + if (html) { + Renderer.dice._$lastRolledBy.append(`
            ${html}
            `); + } else { + $$`
            ${$ele}
            `.appendTo(Renderer.dice._$lastRolledBy); + } + + Renderer.dice._scrollBottom(); + }, + + addElement({rolledBy, html, $ele}) { + if (html && $ele) + throw new Error(`Must specify one of html or $ele!`); + + if (html != null && !html.trim()) + return; + + Renderer.dice._showBox(); + Renderer.dice._checkHandleName(rolledBy.name); + + if (html) { + Renderer.dice._$lastRolledBy.append(`
            ${html}
            `); + } else { + $$`
            ${$ele}
            `.appendTo(Renderer.dice._$lastRolledBy); + } + + Renderer.dice._scrollBottom(); + }, + + _checkHandleName(name) { + if (!Renderer.dice._$lastRolledBy || Renderer.dice._$lastRolledBy.data("name") !== name) { + Renderer.dice._$outRoll.prepend(`
            ${name}
            `); + Renderer.dice._$lastRolledBy = $(`
            `).data("name", name); + Renderer.dice._$outRoll.prepend(Renderer.dice._$lastRolledBy); + } + }, +}; + +Renderer.dice.util = { + getReducedMeta(meta) { + return { + pb: meta.pb + }; + }, +}; + +Renderer.dice.lang = { + validate3(str) { + str = str.trim(); + + let lexed; + try { + lexed = Renderer.dice.lang._lex3(str).lexed; + } catch (e) { + return e.message; + } + + try { + Renderer.dice.lang._parse3(lexed); + } catch (e) { + return e.message; + } + + return null; + }, + + getTree3(str, isSilent=true) { + str = str.trim(); + if (isSilent) { + try { + const {lexed, lexedMeta} = Renderer.dice.lang._lex3(str); + return { + tree: Renderer.dice.lang._parse3(lexed), + meta: lexedMeta + }; + } catch (e) { + return null; + } + } else { + const {lexed, lexedMeta} = Renderer.dice.lang._lex3(str); + return { + tree: Renderer.dice.lang._parse3(lexed), + meta: lexedMeta + }; + } + }, + + _M_NUMBER_CHAR: /[0-9.]/, + _M_SYMBOL_CHAR: /[-+/*^=>) without opening (`); + this._lex3_outputToken(self); + self.token = ")"; + this._lex3_outputToken(self); + break; + case "{": + self.braceCount++; + this._lex3_outputToken(self); + self.token = "{"; + this._lex3_outputToken(self); + break; + case "}": + self.braceCount--; + if (self.parenCount < 0) + throw new Error(`Syntax error: closing } without opening (`); + this._lex3_outputToken(self); + self.token = "}"; + this._lex3_outputToken(self); + break; + case "+": + case "-": + case "*": + case "/": + case "^": + case ",": + this._lex3_outputToken(self); + self.token += c; + this._lex3_outputToken(self); + break; + default: + { + if (Renderer.dice.lang._M_NUMBER_CHAR.test(c)) { + if (self.mode === "symbol") + this._lex3_outputToken(self); + self.token += c; + self.mode = "text"; + } else if (Renderer.dice.lang._M_SYMBOL_CHAR.test(c)) { + if (self.mode === "text") + this._lex3_outputToken(self); + self.token += c; + self.mode = "symbol"; + } else + throw new Error(`Syntax error: unexpected character ${c}`); + break; + } + } + } + + this._lex3_outputToken(self); + }, + + _lex3_outputToken(self) { + if (!self.token) + return; + + switch (self.token) { + case "(": + self.tokenStack.push(Renderer.dice.tk.PAREN_OPEN); + break; + case ")": + self.tokenStack.push(Renderer.dice.tk.PAREN_CLOSE); + break; + case "{": + self.tokenStack.push(Renderer.dice.tk.BRACE_OPEN); + break; + case "}": + self.tokenStack.push(Renderer.dice.tk.BRACE_CLOSE); + break; + case ",": + self.tokenStack.push(Renderer.dice.tk.COMMA); + break; + case "+": + self.tokenStack.push(Renderer.dice.tk.ADD); + break; + case "-": + self.tokenStack.push(Renderer.dice.tk.SUB); + break; + case "*": + self.tokenStack.push(Renderer.dice.tk.MULT); + break; + case "/": + self.tokenStack.push(Renderer.dice.tk.DIV); + break; + case "^": + self.tokenStack.push(Renderer.dice.tk.POW); + break; + case "pb": + self.tokenStack.push(Renderer.dice.tk.PB); + self.hasPb = true; + break; + case "summonspelllevel": + self.tokenStack.push(Renderer.dice.tk.SUMMON_SPELL_LEVEL); + self.hasSummonSpellLevel = true; + break; + case "summonclasslevel": + self.tokenStack.push(Renderer.dice.tk.SUMMON_CLASS_LEVEL); + self.hasSummonClassLevel = true; + break; + case "floor": + self.tokenStack.push(Renderer.dice.tk.FLOOR); + break; + case "ceil": + self.tokenStack.push(Renderer.dice.tk.CEIL); + break; + case "round": + self.tokenStack.push(Renderer.dice.tk.ROUND); + break; + case "avg": + self.tokenStack.push(Renderer.dice.tk.AVERAGE); + break; + case "dmax": + self.tokenStack.push(Renderer.dice.tk.DMAX); + break; + case "dmin": + self.tokenStack.push(Renderer.dice.tk.DMIN); + break; + case "sign": + self.tokenStack.push(Renderer.dice.tk.SIGN); + break; + case "abs": + self.tokenStack.push(Renderer.dice.tk.ABS); + break; + case "cbrt": + self.tokenStack.push(Renderer.dice.tk.CBRT); + break; + case "sqrt": + self.tokenStack.push(Renderer.dice.tk.SQRT); + break; + case "exp": + self.tokenStack.push(Renderer.dice.tk.EXP); + break; + case "log": + self.tokenStack.push(Renderer.dice.tk.LOG); + break; + case "random": + self.tokenStack.push(Renderer.dice.tk.RANDOM); + break; + case "trunc": + self.tokenStack.push(Renderer.dice.tk.TRUNC); + break; + case "pow": + self.tokenStack.push(Renderer.dice.tk.POW); + break; + case "max": + self.tokenStack.push(Renderer.dice.tk.MAX); + break; + case "min": + self.tokenStack.push(Renderer.dice.tk.MIN); + break; + case "d": + self.tokenStack.push(Renderer.dice.tk.DICE); + break; + case "dh": + self.tokenStack.push(Renderer.dice.tk.DROP_HIGHEST); + break; + case "kh": + self.tokenStack.push(Renderer.dice.tk.KEEP_HIGHEST); + break; + case "dl": + self.tokenStack.push(Renderer.dice.tk.DROP_LOWEST); + break; + case "kl": + self.tokenStack.push(Renderer.dice.tk.KEEP_LOWEST); + break; + case "r": + self.tokenStack.push(Renderer.dice.tk.REROLL_EXACT); + break; + case "r>": + self.tokenStack.push(Renderer.dice.tk.REROLL_GT); + break; + case "r>=": + self.tokenStack.push(Renderer.dice.tk.REROLL_GTEQ); + break; + case "r<": + self.tokenStack.push(Renderer.dice.tk.REROLL_LT); + break; + case "r<=": + self.tokenStack.push(Renderer.dice.tk.REROLL_LTEQ); + break; + case "x": + self.tokenStack.push(Renderer.dice.tk.EXPLODE_EXACT); + break; + case "x>": + self.tokenStack.push(Renderer.dice.tk.EXPLODE_GT); + break; + case "x>=": + self.tokenStack.push(Renderer.dice.tk.EXPLODE_GTEQ); + break; + case "x<": + self.tokenStack.push(Renderer.dice.tk.EXPLODE_LT); + break; + case "x<=": + self.tokenStack.push(Renderer.dice.tk.EXPLODE_LTEQ); + break; + case "cs=": + self.tokenStack.push(Renderer.dice.tk.COUNT_SUCCESS_EXACT); + break; + case "cs>": + self.tokenStack.push(Renderer.dice.tk.COUNT_SUCCESS_GT); + break; + case "cs>=": + self.tokenStack.push(Renderer.dice.tk.COUNT_SUCCESS_GTEQ); + break; + case "cs<": + self.tokenStack.push(Renderer.dice.tk.COUNT_SUCCESS_LT); + break; + case "cs<=": + self.tokenStack.push(Renderer.dice.tk.COUNT_SUCCESS_LTEQ); + break; + case "ms=": + self.tokenStack.push(Renderer.dice.tk.MARGIN_SUCCESS_EXACT); + break; + case "ms>": + self.tokenStack.push(Renderer.dice.tk.MARGIN_SUCCESS_GT); + break; + case "ms>=": + self.tokenStack.push(Renderer.dice.tk.MARGIN_SUCCESS_GTEQ); + break; + case "ms<": + self.tokenStack.push(Renderer.dice.tk.MARGIN_SUCCESS_LT); + break; + case "ms<=": + self.tokenStack.push(Renderer.dice.tk.MARGIN_SUCCESS_LTEQ); + break; + default: + { + if (Renderer.dice.lang._M_NUMBER.test(self.token)) { + if (self.token.split(Parser._decimalSeparator).length > 2) + throw new Error(`Syntax error: too many decimal separators ${self.token}`); + self.tokenStack.push(Renderer.dice.tk.NUMBER(self.token)); + } else + throw new Error(`Syntax error: unexpected token ${self.token}`); + } + } + + self.token = ""; + }, + + _parse3(lexed) { + const self = { + ixSym: -1, + syms: lexed, + sym: null, + lastAccepted: null, + isIgnoreCommas: true, + }; + + this._parse3_nextSym(self); + return this._parse3_expression(self); + }, + + _parse3_nextSym(self) { + const cur = self.syms[self.ixSym]; + self.ixSym++; + self.sym = self.syms[self.ixSym]; + return cur; + }, + + _parse3_match(self, symbol) { + if (self.sym == null) + return false; + if (symbol.type) + symbol = symbol.type; + return self.sym.type === symbol; + }, + + _parse3_accept(self, symbol) { + if (this._parse3_match(self, symbol)) { + const out = self.sym; + this._parse3_nextSym(self); + self.lastAccepted = out; + return out; + } + return false; + }, + + _parse3_expect(self, symbol) { + const accepted = this._parse3_accept(self, symbol); + if (accepted) + return accepted; + if (self.sym) + throw new Error(`Unexpected input: Expected ${symbol} but found ${self.sym}`); + else + throw new Error(`Unexpected end of input: Expected ${symbol}`); + }, + + _parse3_factor(self, {isSilent=false}={}) { + if (this._parse3_accept(self, Renderer.dice.tk.TYP_NUMBER)) { + if (self.isIgnoreCommas) { + const syms = [self.lastAccepted]; + while (this._parse3_accept(self, Renderer.dice.tk.COMMA)) { + const sym = this._parse3_expect(self, Renderer.dice.tk.TYP_NUMBER); + syms.push(sym); + } + const sym = Renderer.dice.tk.NUMBER(syms.map(it=>it.value).join("")); + return new Renderer.dice.parsed.Factor(sym); + } + + return new Renderer.dice.parsed.Factor(self.lastAccepted); + } else if (this._parse3_accept(self, Renderer.dice.tk.PB)) { + return new Renderer.dice.parsed.Factor(Renderer.dice.tk.PB); + } else if (this._parse3_accept(self, Renderer.dice.tk.SUMMON_SPELL_LEVEL)) { + return new Renderer.dice.parsed.Factor(Renderer.dice.tk.SUMMON_SPELL_LEVEL); + } else if (this._parse3_accept(self, Renderer.dice.tk.SUMMON_CLASS_LEVEL)) { + return new Renderer.dice.parsed.Factor(Renderer.dice.tk.SUMMON_CLASS_LEVEL); + } else if (this._parse3_match(self, Renderer.dice.tk.FLOOR) || this._parse3_match(self, Renderer.dice.tk.CEIL) || this._parse3_match(self, Renderer.dice.tk.ROUND) || this._parse3_match(self, Renderer.dice.tk.AVERAGE) || this._parse3_match(self, Renderer.dice.tk.DMAX) || this._parse3_match(self, Renderer.dice.tk.DMIN) || this._parse3_match(self, Renderer.dice.tk.SIGN) || this._parse3_match(self, Renderer.dice.tk.ABS) || this._parse3_match(self, Renderer.dice.tk.CBRT) || this._parse3_match(self, Renderer.dice.tk.SQRT) || this._parse3_match(self, Renderer.dice.tk.EXP) || this._parse3_match(self, Renderer.dice.tk.LOG) || this._parse3_match(self, Renderer.dice.tk.RANDOM) || this._parse3_match(self, Renderer.dice.tk.TRUNC)) { + const children = []; + + children.push(this._parse3_nextSym(self)); + this._parse3_expect(self, Renderer.dice.tk.PAREN_OPEN); + children.push(this._parse3_expression(self)); + this._parse3_expect(self, Renderer.dice.tk.PAREN_CLOSE); + + return new Renderer.dice.parsed.Function(children); + } else if (this._parse3_match(self, Renderer.dice.tk.POW)) { + self.isIgnoreCommas = false; + + const children = []; + + children.push(this._parse3_nextSym(self)); + this._parse3_expect(self, Renderer.dice.tk.PAREN_OPEN); + children.push(this._parse3_expression(self)); + this._parse3_expect(self, Renderer.dice.tk.COMMA); + children.push(this._parse3_expression(self)); + this._parse3_expect(self, Renderer.dice.tk.PAREN_CLOSE); + + self.isIgnoreCommas = true; + + return new Renderer.dice.parsed.Function(children); + } else if (this._parse3_match(self, Renderer.dice.tk.MAX) || this._parse3_match(self, Renderer.dice.tk.MIN)) { + self.isIgnoreCommas = false; + + const children = []; + + children.push(this._parse3_nextSym(self)); + this._parse3_expect(self, Renderer.dice.tk.PAREN_OPEN); + children.push(this._parse3_expression(self)); + while (this._parse3_accept(self, Renderer.dice.tk.COMMA)) + children.push(this._parse3_expression(self)); + this._parse3_expect(self, Renderer.dice.tk.PAREN_CLOSE); + + self.isIgnoreCommas = true; + + return new Renderer.dice.parsed.Function(children); + } else if (this._parse3_accept(self, Renderer.dice.tk.PAREN_OPEN)) { + const exp = this._parse3_expression(self); + this._parse3_expect(self, Renderer.dice.tk.PAREN_CLOSE); + return new Renderer.dice.parsed.Factor(exp,{ + hasParens: true + }); + } else if (this._parse3_accept(self, Renderer.dice.tk.BRACE_OPEN)) { + self.isIgnoreCommas = false; + + const children = []; + + children.push(this._parse3_expression(self)); + while (this._parse3_accept(self, Renderer.dice.tk.COMMA)) + children.push(this._parse3_expression(self)); + + this._parse3_expect(self, Renderer.dice.tk.BRACE_CLOSE); + + self.isIgnoreCommas = true; + + const modPart = []; + this._parse3__dice_modifiers(self, modPart); + + return new Renderer.dice.parsed.Pool(children,modPart[0]); + } else { + if (isSilent) + return null; + + if (self.sym) + throw new Error(`Unexpected input: ${self.sym}`); + else + throw new Error(`Unexpected end of input`); + } + }, + + _parse3_dice(self) { + const children = []; + + if (this._parse3_match(self, Renderer.dice.tk.DICE)) + children.push(new Renderer.dice.parsed.Factor(Renderer.dice.tk.NUMBER(1))); + else + children.push(this._parse3_factor(self)); + + while (this._parse3_match(self, Renderer.dice.tk.DICE)) { + this._parse3_nextSym(self); + children.push(this._parse3_factor(self)); + this._parse3__dice_modifiers(self, children); + } + return new Renderer.dice.parsed.Dice(children); + }, + + _parse3__dice_modifiers(self, children) { + const modsMeta = new Renderer.dice.lang.DiceModMeta(); + + while (this._parse3_match(self, Renderer.dice.tk.DROP_HIGHEST) || this._parse3_match(self, Renderer.dice.tk.KEEP_HIGHEST) || this._parse3_match(self, Renderer.dice.tk.DROP_LOWEST) || this._parse3_match(self, Renderer.dice.tk.KEEP_LOWEST) || this._parse3_match(self, Renderer.dice.tk.REROLL_EXACT) || this._parse3_match(self, Renderer.dice.tk.REROLL_GT) || this._parse3_match(self, Renderer.dice.tk.REROLL_GTEQ) || this._parse3_match(self, Renderer.dice.tk.REROLL_LT) || this._parse3_match(self, Renderer.dice.tk.REROLL_LTEQ) || this._parse3_match(self, Renderer.dice.tk.EXPLODE_EXACT) || this._parse3_match(self, Renderer.dice.tk.EXPLODE_GT) || this._parse3_match(self, Renderer.dice.tk.EXPLODE_GTEQ) || this._parse3_match(self, Renderer.dice.tk.EXPLODE_LT) || this._parse3_match(self, Renderer.dice.tk.EXPLODE_LTEQ) || this._parse3_match(self, Renderer.dice.tk.COUNT_SUCCESS_EXACT) || this._parse3_match(self, Renderer.dice.tk.COUNT_SUCCESS_GT) || this._parse3_match(self, Renderer.dice.tk.COUNT_SUCCESS_GTEQ) || this._parse3_match(self, Renderer.dice.tk.COUNT_SUCCESS_LT) || this._parse3_match(self, Renderer.dice.tk.COUNT_SUCCESS_LTEQ) || this._parse3_match(self, Renderer.dice.tk.MARGIN_SUCCESS_EXACT) || this._parse3_match(self, Renderer.dice.tk.MARGIN_SUCCESS_GT) || this._parse3_match(self, Renderer.dice.tk.MARGIN_SUCCESS_GTEQ) || this._parse3_match(self, Renderer.dice.tk.MARGIN_SUCCESS_LT) || this._parse3_match(self, Renderer.dice.tk.MARGIN_SUCCESS_LTEQ)) { + const nxtSym = this._parse3_nextSym(self); + const nxtFactor = this._parse3__dice_modifiers_nxtFactor(self, nxtSym); + + if (nxtSym.isSuccessMode) + modsMeta.isSuccessMode = true; + modsMeta.mods.push({ + modSym: nxtSym, + numSym: nxtFactor + }); + } + + if (modsMeta.mods.length) + children.push(modsMeta); + }, + + _parse3__dice_modifiers_nxtFactor(self, nxtSym) { + if (nxtSym.diceModifierImplicit == null) + return this._parse3_factor(self, { + isSilent: true + }); + + const fallback = new Renderer.dice.parsed.Factor(Renderer.dice.tk.NUMBER(nxtSym.diceModifierImplicit)); + if (self.sym == null) + return fallback; + + const out = this._parse3_factor(self, { + isSilent: true + }); + if (out) + return out; + + return fallback; + }, + + _parse3_exponent(self) { + const children = []; + children.push(this._parse3_dice(self)); + while (this._parse3_match(self, Renderer.dice.tk.POW)) { + this._parse3_nextSym(self); + children.push(this._parse3_dice(self)); + } + return new Renderer.dice.parsed.Exponent(children); + }, + + _parse3_term(self) { + const children = []; + children.push(this._parse3_exponent(self)); + while (this._parse3_match(self, Renderer.dice.tk.MULT) || this._parse3_match(self, Renderer.dice.tk.DIV)) { + children.push(this._parse3_nextSym(self)); + children.push(this._parse3_exponent(self)); + } + return new Renderer.dice.parsed.Term(children); + }, + + _parse3_expression(self) { + const children = []; + if (this._parse3_match(self, Renderer.dice.tk.ADD) || this._parse3_match(self, Renderer.dice.tk.SUB)) + children.push(this._parse3_nextSym(self)); + children.push(this._parse3_term(self)); + while (this._parse3_match(self, Renderer.dice.tk.ADD) || this._parse3_match(self, Renderer.dice.tk.SUB)) { + children.push(this._parse3_nextSym(self)); + children.push(this._parse3_term(self)); + } + return new Renderer.dice.parsed.Expression(children); + }, + + DiceModMeta: class { + constructor() { + this.isDiceModifierGroup = true; + this.isSuccessMode = false; + this.mods = []; + } + } + , +}; + +Renderer.dice.tk = { + Token: class { + constructor(type, value, asString, opts) { + opts = opts || {}; + this.type = type; + this.value = value; + this._asString = asString; + if (opts.isDiceModifier) + this.isDiceModifier = true; + if (opts.diceModifierImplicit) + this.diceModifierImplicit = true; + if (opts.isSuccessMode) + this.isSuccessMode = true; + } + + eq(other) { + return other && other.type === this.type; + } + + toString() { + if (this._asString) + return this._asString; + return this.toDebugString(); + } + + toDebugString() { + return `${this.type}${this.value ? ` :: ${this.value}` : ""}`; + } + } + , + + _new(type, asString, opts) { + return new Renderer.dice.tk.Token(type,null,asString,opts); + }, + + TYP_NUMBER: "NUMBER", + TYP_DICE: "DICE", + TYP_SYMBOL: "SYMBOL", + NUMBER(val) { + return new Renderer.dice.tk.Token(Renderer.dice.tk.TYP_NUMBER,val); + }, +}; +Renderer.dice.tk.PAREN_OPEN = Renderer.dice.tk._new("PAREN_OPEN", "("); +Renderer.dice.tk.PAREN_CLOSE = Renderer.dice.tk._new("PAREN_CLOSE", ")"); +Renderer.dice.tk.BRACE_OPEN = Renderer.dice.tk._new("BRACE_OPEN", "{"); +Renderer.dice.tk.BRACE_CLOSE = Renderer.dice.tk._new("BRACE_CLOSE", "}"); +Renderer.dice.tk.COMMA = Renderer.dice.tk._new("COMMA", ","); +Renderer.dice.tk.ADD = Renderer.dice.tk._new("ADD", "+"); +Renderer.dice.tk.SUB = Renderer.dice.tk._new("SUB", "-"); +Renderer.dice.tk.MULT = Renderer.dice.tk._new("MULT", "*"); +Renderer.dice.tk.DIV = Renderer.dice.tk._new("DIV", "/"); +Renderer.dice.tk.POW = Renderer.dice.tk._new("POW", "^"); +Renderer.dice.tk.PB = Renderer.dice.tk._new("PB", "pb"); +Renderer.dice.tk.SUMMON_SPELL_LEVEL = Renderer.dice.tk._new("SUMMON_SPELL_LEVEL", "summonspelllevel"); +Renderer.dice.tk.SUMMON_CLASS_LEVEL = Renderer.dice.tk._new("SUMMON_CLASS_LEVEL", "summonclasslevel"); +Renderer.dice.tk.FLOOR = Renderer.dice.tk._new("FLOOR", "floor"); +Renderer.dice.tk.CEIL = Renderer.dice.tk._new("CEIL", "ceil"); +Renderer.dice.tk.ROUND = Renderer.dice.tk._new("ROUND", "round"); +Renderer.dice.tk.AVERAGE = Renderer.dice.tk._new("AVERAGE", "avg"); +Renderer.dice.tk.DMAX = Renderer.dice.tk._new("DMAX", "avg"); +Renderer.dice.tk.DMIN = Renderer.dice.tk._new("DMIN", "avg"); +Renderer.dice.tk.SIGN = Renderer.dice.tk._new("SIGN", "sign"); +Renderer.dice.tk.ABS = Renderer.dice.tk._new("ABS", "abs"); +Renderer.dice.tk.CBRT = Renderer.dice.tk._new("CBRT", "cbrt"); +Renderer.dice.tk.SQRT = Renderer.dice.tk._new("SQRT", "sqrt"); +Renderer.dice.tk.EXP = Renderer.dice.tk._new("EXP", "exp"); +Renderer.dice.tk.LOG = Renderer.dice.tk._new("LOG", "log"); +Renderer.dice.tk.RANDOM = Renderer.dice.tk._new("RANDOM", "random"); +Renderer.dice.tk.TRUNC = Renderer.dice.tk._new("TRUNC", "trunc"); +Renderer.dice.tk.POW = Renderer.dice.tk._new("POW", "pow"); +Renderer.dice.tk.MAX = Renderer.dice.tk._new("MAX", "max"); +Renderer.dice.tk.MIN = Renderer.dice.tk._new("MIN", "min"); +Renderer.dice.tk.DICE = Renderer.dice.tk._new("DICE", "d"); +Renderer.dice.tk.DROP_HIGHEST = Renderer.dice.tk._new("DH", "dh", { + isDiceModifier: true, + diceModifierImplicit: 1 +}); +Renderer.dice.tk.KEEP_HIGHEST = Renderer.dice.tk._new("KH", "kh", { + isDiceModifier: true, + diceModifierImplicit: 1 +}); +Renderer.dice.tk.DROP_LOWEST = Renderer.dice.tk._new("DL", "dl", { + isDiceModifier: true, + diceModifierImplicit: 1 +}); +Renderer.dice.tk.KEEP_LOWEST = Renderer.dice.tk._new("KL", "kl", { + isDiceModifier: true, + diceModifierImplicit: 1 +}); +Renderer.dice.tk.REROLL_EXACT = Renderer.dice.tk._new("REROLL", "r", { + isDiceModifier: true +}); +Renderer.dice.tk.REROLL_GT = Renderer.dice.tk._new("REROLL_GT", "r>", { + isDiceModifier: true +}); +Renderer.dice.tk.REROLL_GTEQ = Renderer.dice.tk._new("REROLL_GTEQ", "r>=", { + isDiceModifier: true +}); +Renderer.dice.tk.REROLL_LT = Renderer.dice.tk._new("REROLL_LT", "r<", { + isDiceModifier: true +}); +Renderer.dice.tk.REROLL_LTEQ = Renderer.dice.tk._new("REROLL_LTEQ", "r<=", { + isDiceModifier: true +}); +Renderer.dice.tk.EXPLODE_EXACT = Renderer.dice.tk._new("EXPLODE", "x", { + isDiceModifier: true +}); +Renderer.dice.tk.EXPLODE_GT = Renderer.dice.tk._new("EXPLODE_GT", "x>", { + isDiceModifier: true +}); +Renderer.dice.tk.EXPLODE_GTEQ = Renderer.dice.tk._new("EXPLODE_GTEQ", "x>=", { + isDiceModifier: true +}); +Renderer.dice.tk.EXPLODE_LT = Renderer.dice.tk._new("EXPLODE_LT", "x<", { + isDiceModifier: true +}); +Renderer.dice.tk.EXPLODE_LTEQ = Renderer.dice.tk._new("EXPLODE_LTEQ", "x<=", { + isDiceModifier: true +}); +Renderer.dice.tk.COUNT_SUCCESS_EXACT = Renderer.dice.tk._new("COUNT_SUCCESS_EXACT", "cs=", { + isDiceModifier: true, + isSuccessMode: true +}); +Renderer.dice.tk.COUNT_SUCCESS_GT = Renderer.dice.tk._new("COUNT_SUCCESS_GT", "cs>", { + isDiceModifier: true, + isSuccessMode: true +}); +Renderer.dice.tk.COUNT_SUCCESS_GTEQ = Renderer.dice.tk._new("COUNT_SUCCESS_GTEQ", "cs>=", { + isDiceModifier: true, + isSuccessMode: true +}); +Renderer.dice.tk.COUNT_SUCCESS_LT = Renderer.dice.tk._new("COUNT_SUCCESS_LT", "cs<", { + isDiceModifier: true, + isSuccessMode: true +}); +Renderer.dice.tk.COUNT_SUCCESS_LTEQ = Renderer.dice.tk._new("COUNT_SUCCESS_LTEQ", "cs<=", { + isDiceModifier: true, + isSuccessMode: true +}); +Renderer.dice.tk.MARGIN_SUCCESS_EXACT = Renderer.dice.tk._new("MARGIN_SUCCESS_EXACT", "ms=", { + isDiceModifier: true +}); +Renderer.dice.tk.MARGIN_SUCCESS_GT = Renderer.dice.tk._new("MARGIN_SUCCESS_GT", "ms>", { + isDiceModifier: true +}); +Renderer.dice.tk.MARGIN_SUCCESS_GTEQ = Renderer.dice.tk._new("MARGIN_SUCCESS_GTEQ", "ms>=", { + isDiceModifier: true +}); +Renderer.dice.tk.MARGIN_SUCCESS_LT = Renderer.dice.tk._new("MARGIN_SUCCESS_LT", "ms<", { + isDiceModifier: true +}); +Renderer.dice.tk.MARGIN_SUCCESS_LTEQ = Renderer.dice.tk._new("MARGIN_SUCCESS_LTEQ", "ms<=", { + isDiceModifier: true +}); + +Renderer.dice.AbstractSymbol = class { + constructor() { + this.type = Renderer.dice.tk.TYP_SYMBOL; + } + eq(symbol) { + return symbol && this.type === symbol.type; + } + evl(meta) { + this.meta = meta; + return this._evl(meta); + } + avg(meta) { + this.meta = meta; + return this._avg(meta); + } + min(meta) { + this.meta = meta; + return this._min(meta); + } + max(meta) { + this.meta = meta; + return this._max(meta); + } + _evl() { + throw new Error("Unimplemented!"); + } + _avg() { + throw new Error("Unimplemented!"); + } + _min() { + throw new Error("Unimplemented!"); + } + _max() { + throw new Error("Unimplemented!"); + } + toString() { + throw new Error("Unimplemented!"); + } + addToMeta(meta, {text, html=null, md=null}={}) { + if (!meta) + return; + html = html || text; + md = md || text; + (meta.html = meta.html || []).push(html); + (meta.text = meta.text || []).push(text); + (meta.md = meta.md || []).push(md); + } +} +; + +Renderer.dice.parsed = { + _PARTITION_EQ: (r,compareTo)=>r === compareTo, + _PARTITION_GT: (r,compareTo)=>r > compareTo, + _PARTITION_GTEQ: (r,compareTo)=>r >= compareTo, + _PARTITION_LT: (r,compareTo)=>r < compareTo, + _PARTITION_LTEQ: (r,compareTo)=>r <= compareTo, + + _handleModifiers(fnName, meta, vals, nodeMod, opts) { + opts = opts || {}; + + const displayVals = vals.slice(); + const {mods} = nodeMod; + + for (const mod of mods) { + vals.sort(SortUtil.ascSortProp.bind(null, "val")).reverse(); + const valsAlive = vals.filter(it=>!it.isDropped); + + const modNum = mod.numSym[fnName](); + + switch (mod.modSym.type) { + case Renderer.dice.tk.DROP_HIGHEST.type: + case Renderer.dice.tk.KEEP_HIGHEST.type: + case Renderer.dice.tk.DROP_LOWEST.type: + case Renderer.dice.tk.KEEP_LOWEST.type: + { + const isHighest = mod.modSym.type.endsWith("H"); + + const splitPoint = isHighest ? modNum : valsAlive.length - modNum; + + const highSlice = valsAlive.slice(0, splitPoint); + const lowSlice = valsAlive.slice(splitPoint, valsAlive.length); + + switch (mod.modSym.type) { + case Renderer.dice.tk.DROP_HIGHEST.type: + case Renderer.dice.tk.KEEP_LOWEST.type: + highSlice.forEach(val=>val.isDropped = true); + break; + case Renderer.dice.tk.KEEP_HIGHEST.type: + case Renderer.dice.tk.DROP_LOWEST.type: + lowSlice.forEach(val=>val.isDropped = true); + break; + default: + throw new Error(`Unimplemented!`); + } + break; + } + + case Renderer.dice.tk.REROLL_EXACT.type: + case Renderer.dice.tk.REROLL_GT.type: + case Renderer.dice.tk.REROLL_GTEQ.type: + case Renderer.dice.tk.REROLL_LT.type: + case Renderer.dice.tk.REROLL_LTEQ.type: + { + let fnPartition; + switch (mod.modSym.type) { + case Renderer.dice.tk.REROLL_EXACT.type: + fnPartition = Renderer.dice.parsed._PARTITION_EQ; + break; + case Renderer.dice.tk.REROLL_GT.type: + fnPartition = Renderer.dice.parsed._PARTITION_GT; + break; + case Renderer.dice.tk.REROLL_GTEQ.type: + fnPartition = Renderer.dice.parsed._PARTITION_GTEQ; + break; + case Renderer.dice.tk.REROLL_LT.type: + fnPartition = Renderer.dice.parsed._PARTITION_LT; + break; + case Renderer.dice.tk.REROLL_LTEQ.type: + fnPartition = Renderer.dice.parsed._PARTITION_LTEQ; + break; + default: + throw new Error(`Unimplemented!`); + } + + const toReroll = valsAlive.filter(val=>fnPartition(val.val, modNum)); + toReroll.forEach(val=>val.isDropped = true); + + const nuVals = opts.fnGetRerolls(toReroll); + + vals.push(...nuVals); + displayVals.push(...nuVals); + break; + } + + case Renderer.dice.tk.EXPLODE_EXACT.type: + case Renderer.dice.tk.EXPLODE_GT.type: + case Renderer.dice.tk.EXPLODE_GTEQ.type: + case Renderer.dice.tk.EXPLODE_LT.type: + case Renderer.dice.tk.EXPLODE_LTEQ.type: + { + let fnPartition; + switch (mod.modSym.type) { + case Renderer.dice.tk.EXPLODE_EXACT.type: + fnPartition = Renderer.dice.parsed._PARTITION_EQ; + break; + case Renderer.dice.tk.EXPLODE_GT.type: + fnPartition = Renderer.dice.parsed._PARTITION_GT; + break; + case Renderer.dice.tk.EXPLODE_GTEQ.type: + fnPartition = Renderer.dice.parsed._PARTITION_GTEQ; + break; + case Renderer.dice.tk.EXPLODE_LT.type: + fnPartition = Renderer.dice.parsed._PARTITION_LT; + break; + case Renderer.dice.tk.EXPLODE_LTEQ.type: + fnPartition = Renderer.dice.parsed._PARTITION_LTEQ; + break; + default: + throw new Error(`Unimplemented!`); + } + + let tries = 999; + let lastLen; + let toExplodeNext = valsAlive; + do { + lastLen = vals.length; + + const [toExplode] = toExplodeNext.partition(roll=>!roll.isExploded && fnPartition(roll.val, modNum)); + toExplode.forEach(roll=>roll.isExploded = true); + + const nuVals = opts.fnGetExplosions(toExplode); + + toExplodeNext = nuVals; + + vals.push(...nuVals); + displayVals.push(...nuVals); + } while (tries-- > 0 && vals.length !== lastLen); + + if (!~tries) + JqueryUtil.doToast({ + type: "warning", + content: `Stopped exploding after 999 additional rolls.` + }); + + break; + } + + case Renderer.dice.tk.COUNT_SUCCESS_EXACT.type: + case Renderer.dice.tk.COUNT_SUCCESS_GT.type: + case Renderer.dice.tk.COUNT_SUCCESS_GTEQ.type: + case Renderer.dice.tk.COUNT_SUCCESS_LT.type: + case Renderer.dice.tk.COUNT_SUCCESS_LTEQ.type: + { + let fnPartition; + switch (mod.modSym.type) { + case Renderer.dice.tk.COUNT_SUCCESS_EXACT.type: + fnPartition = Renderer.dice.parsed._PARTITION_EQ; + break; + case Renderer.dice.tk.COUNT_SUCCESS_GT.type: + fnPartition = Renderer.dice.parsed._PARTITION_GT; + break; + case Renderer.dice.tk.COUNT_SUCCESS_GTEQ.type: + fnPartition = Renderer.dice.parsed._PARTITION_GTEQ; + break; + case Renderer.dice.tk.COUNT_SUCCESS_LT.type: + fnPartition = Renderer.dice.parsed._PARTITION_LT; + break; + case Renderer.dice.tk.COUNT_SUCCESS_LTEQ.type: + fnPartition = Renderer.dice.parsed._PARTITION_LTEQ; + break; + default: + throw new Error(`Unimplemented!`); + } + + const successes = valsAlive.filter(val=>fnPartition(val.val, modNum)); + successes.forEach(val=>val.isSuccess = true); + + break; + } + + case Renderer.dice.tk.MARGIN_SUCCESS_EXACT.type: + case Renderer.dice.tk.MARGIN_SUCCESS_GT.type: + case Renderer.dice.tk.MARGIN_SUCCESS_GTEQ.type: + case Renderer.dice.tk.MARGIN_SUCCESS_LT.type: + case Renderer.dice.tk.MARGIN_SUCCESS_LTEQ.type: + { + const total = valsAlive.map(it=>it.val).reduce((valA,valB)=>valA + valB, 0); + + const subDisplayDice = displayVals.map(r=>`[${Renderer.dice.parsed._rollToNumPart_html(r, opts.faces)}]`).join("+"); + + let delta; + let subDisplay; + switch (mod.modSym.type) { + case Renderer.dice.tk.MARGIN_SUCCESS_EXACT.type: + case Renderer.dice.tk.MARGIN_SUCCESS_GT.type: + case Renderer.dice.tk.MARGIN_SUCCESS_GTEQ.type: + { + delta = total - modNum; + + subDisplay = `(${subDisplayDice})-${modNum}`; + + break; + } + case Renderer.dice.tk.MARGIN_SUCCESS_LT.type: + case Renderer.dice.tk.MARGIN_SUCCESS_LTEQ.type: + { + delta = modNum - total; + + subDisplay = `${modNum}-(${subDisplayDice})`; + + break; + } + default: + throw new Error(`Unimplemented!`); + } + + while (vals.length) { + vals.pop(); + displayVals.pop(); + } + + vals.push({ + val: delta + }); + displayVals.push({ + val: delta, + htmlDisplay: subDisplay + }); + + break; + } + + default: + throw new Error(`Unimplemented!`); + } + } + + return displayVals; + }, + + _rollToNumPart_html(r, faces) { + if (faces == null) + return r.val; + return r.val === faces ? `${r.val}` : r.val === 1 ? `${r.val}` : r.val; + }, + + Function: class extends Renderer.dice.AbstractSymbol { + constructor(nodes) { + super(); + this._nodes = nodes; + } + + _evl(meta) { + return this._invoke("evl", meta); + } + _avg(meta) { + return this._invoke("avg", meta); + } + _min(meta) { + return this._invoke("min", meta); + } + _max(meta) { + return this._invoke("max", meta); + } + + _invoke(fnName, meta) { + const [symFunc] = this._nodes; + switch (symFunc.type) { + case Renderer.dice.tk.FLOOR.type: + case Renderer.dice.tk.CEIL.type: + case Renderer.dice.tk.ROUND.type: + case Renderer.dice.tk.SIGN.type: + case Renderer.dice.tk.CBRT.type: + case Renderer.dice.tk.SQRT.type: + case Renderer.dice.tk.EXP.type: + case Renderer.dice.tk.LOG.type: + case Renderer.dice.tk.RANDOM.type: + case Renderer.dice.tk.TRUNC.type: + case Renderer.dice.tk.POW.type: + case Renderer.dice.tk.MAX.type: + case Renderer.dice.tk.MIN.type: + { + const [,...symExps] = this._nodes; + this.addToMeta(meta, { + text: `${symFunc.toString()}(` + }); + const args = []; + symExps.forEach((symExp,i)=>{ + if (i !== 0) + this.addToMeta(meta, { + text: `, ` + }); + args.push(symExp[fnName](meta)); + } + ); + const out = Math[symFunc.toString()](...args); + this.addToMeta(meta, { + text: ")" + }); + return out; + } + case Renderer.dice.tk.AVERAGE.type: + { + const [,symExp] = this._nodes; + return symExp.avg(meta); + } + case Renderer.dice.tk.DMAX.type: + { + const [,symExp] = this._nodes; + return symExp.max(meta); + } + case Renderer.dice.tk.DMIN.type: + { + const [,symExp] = this._nodes; + return symExp.min(meta); + } + default: + throw new Error(`Unimplemented!`); + } + } + + toString() { + let out; + const [symFunc,symExp] = this._nodes; + switch (symFunc.type) { + case Renderer.dice.tk.FLOOR.type: + case Renderer.dice.tk.CEIL.type: + case Renderer.dice.tk.ROUND.type: + case Renderer.dice.tk.AVERAGE.type: + case Renderer.dice.tk.DMAX.type: + case Renderer.dice.tk.DMIN.type: + case Renderer.dice.tk.SIGN.type: + case Renderer.dice.tk.ABS.type: + case Renderer.dice.tk.CBRT.type: + case Renderer.dice.tk.SQRT.type: + case Renderer.dice.tk.EXP.type: + case Renderer.dice.tk.LOG.type: + case Renderer.dice.tk.RANDOM.type: + case Renderer.dice.tk.TRUNC.type: + case Renderer.dice.tk.POW.type: + case Renderer.dice.tk.MAX.type: + case Renderer.dice.tk.MIN.type: + out = symFunc.toString(); + break; + default: + throw new Error(`Unimplemented!`); + } + out += `(${symExp.toString()})`; + return out; + } + } + , + + Pool: class extends Renderer.dice.AbstractSymbol { + constructor(nodesPool, nodeMod) { + super(); + this._nodesPool = nodesPool; + this._nodeMod = nodeMod; + } + + _evl(meta) { + return this._invoke("evl", meta); + } + _avg(meta) { + return this._invoke("avg", meta); + } + _min(meta) { + return this._invoke("min", meta); + } + _max(meta) { + return this._invoke("max", meta); + } + + _invoke(fnName, meta) { + const vals = this._nodesPool.map(it=>{ + const subMeta = {}; + return { + node: it, + val: it[fnName](subMeta), + meta: subMeta + }; + } + ); + + if (this._nodeMod && vals.length) { + const isSuccessMode = this._nodeMod.isSuccessMode; + + const modOpts = { + fnGetRerolls: toReroll=>toReroll.map(val=>{ + const subMeta = {}; + return { + node: val.node, + val: val.node[fnName](subMeta), + meta: subMeta + }; + } + ), + fnGetExplosions: toExplode=>toExplode.map(val=>{ + const subMeta = {}; + return { + node: val.node, + val: val.node[fnName](subMeta), + meta: subMeta + }; + } + ), + }; + + const displayVals = Renderer.dice.parsed._handleModifiers(fnName, meta, vals, this._nodeMod, modOpts); + + const asHtml = displayVals.map(v=>{ + const html = v.meta.html.join(""); + if (v.isDropped) + return `(${html})`; + else if (v.isExploded) + return `(${html})`; + else if (v.isSuccess) + return `(${html})`; + else + return `(${html})`; + } + ).join("+"); + + const asText = displayVals.map(v=>`(${v.meta.text.join("")})`).join("+"); + const asMd = displayVals.map(v=>`(${v.meta.md.join("")})`).join("+"); + + this.addToMeta(meta, { + html: asHtml, + text: asText, + md: asMd + }); + + if (isSuccessMode) { + return vals.filter(it=>!it.isDropped && it.isSuccess).length; + } else { + return vals.filter(it=>!it.isDropped).map(it=>it.val).sum(); + } + } else { + this.addToMeta(meta, ["html", "text", "md"].mergeMap(prop=>({ + [prop]: `${vals.map(it=>`(${it.meta[prop].join("")})`).join("+")}`, + })), ); + return vals.map(it=>it.val).sum(); + } + } + + toString() { + return `{${this._nodesPool.map(it=>it.toString()).join(", ")}}${this._nodeMod ? this._nodeMod.toString() : ""}`; + } + } + , + + Factor: class extends Renderer.dice.AbstractSymbol { + constructor(node, opts) { + super(); + opts = opts || {}; + this._node = node; + this._hasParens = !!opts.hasParens; + } + + _evl(meta) { + return this._invoke("evl", meta); + } + _avg(meta) { + return this._invoke("avg", meta); + } + _min(meta) { + return this._invoke("min", meta); + } + _max(meta) { + return this._invoke("max", meta); + } + + _invoke(fnName, meta) { + switch (this._node.type) { + case Renderer.dice.tk.TYP_NUMBER: + { + this.addToMeta(meta, { + text: this.toString() + }); + return Number(this._node.value); + } + case Renderer.dice.tk.TYP_SYMBOL: + { + if (this._hasParens) + this.addToMeta(meta, { + text: "(" + }); + const out = this._node[fnName](meta); + if (this._hasParens) + this.addToMeta(meta, { + text: ")" + }); + return out; + } + case Renderer.dice.tk.PB.type: + { + this.addToMeta(meta, { + text: this.toString(meta) + }); + return meta.pb == null ? 0 : meta.pb; + } + case Renderer.dice.tk.SUMMON_SPELL_LEVEL.type: + { + this.addToMeta(meta, { + text: this.toString(meta) + }); + return meta.summonSpellLevel == null ? 0 : meta.summonSpellLevel; + } + case Renderer.dice.tk.SUMMON_CLASS_LEVEL.type: + { + this.addToMeta(meta, { + text: this.toString(meta) + }); + return meta.summonClassLevel == null ? 0 : meta.summonClassLevel; + } + default: + throw new Error(`Unimplemented!`); + } + } + + toString(indent) { + let out; + switch (this._node.type) { + case Renderer.dice.tk.TYP_NUMBER: + out = this._node.value; + break; + case Renderer.dice.tk.TYP_SYMBOL: + out = this._node.toString(); + break; + case Renderer.dice.tk.PB.type: + out = this.meta ? (this.meta.pb || 0) : "PB"; + break; + case Renderer.dice.tk.SUMMON_SPELL_LEVEL.type: + out = this.meta ? (this.meta.summonSpellLevel || 0) : "the spell's level"; + break; + case Renderer.dice.tk.SUMMON_CLASS_LEVEL.type: + out = this.meta ? (this.meta.summonClassLevel || 0) : "your class level"; + break; + default: + throw new Error(`Unimplemented!`); + } + return this._hasParens ? `(${out})` : out; + } + } + , + + Dice: class extends Renderer.dice.AbstractSymbol { + static _facesToValue(faces, fnName) { + switch (fnName) { + case "evl": + return RollerUtil.randomise(faces); + case "avg": + return (faces + 1) / 2; + case "min": + return 1; + case "max": + return faces; + } + } + + constructor(nodes) { + super(); + this._nodes = nodes; + } + + _evl(meta) { + return this._invoke("evl", meta); + } + _avg(meta) { + return this._invoke("avg", meta); + } + _min(meta) { + return this._invoke("min", meta); + } + _max(meta) { + return this._invoke("max", meta); + } + + _invoke(fnName, meta) { + if (this._nodes.length === 1) + return this._nodes[0][fnName](meta); + + const view = this._nodes.slice(); + const numSym = view.shift(); + let tmp = numSym[fnName](Renderer.dice.util.getReducedMeta(meta)); + + while (view.length) { + if (Math.round(tmp) !== tmp) + throw new Error(`Number of dice to roll (${tmp}) was not an integer!`); + + const facesSym = view.shift(); + const faces = facesSym[fnName](); + if (Math.round(faces) !== faces) + throw new Error(`Dice face count (${faces}) was not an integer!`); + + const isLast = view.length === 0 || (view.length === 1 && view.last().isDiceModifierGroup); + tmp = this._invoke_handlePart(fnName, meta, view, tmp, faces, isLast); + } + + return tmp; + } + + _invoke_handlePart(fnName, meta, view, num, faces, isLast) { + const rolls = [...new Array(num)].map(()=>({ + val: Renderer.dice.parsed.Dice._facesToValue(faces, fnName) + })); + let displayRolls; + let isSuccessMode = false; + + if (view.length && view[0].isDiceModifierGroup) { + const nodeMod = view[0]; + + if (fnName === "evl" || fnName === "min" || fnName === "max") { + isSuccessMode = nodeMod.isSuccessMode; + + const modOpts = { + faces, + fnGetRerolls: toReroll=>[...new Array(toReroll.length)].map(()=>({ + val: Renderer.dice.parsed.Dice._facesToValue(faces, fnName) + })), + fnGetExplosions: toExplode=>[...new Array(toExplode.length)].map(()=>({ + val: Renderer.dice.parsed.Dice._facesToValue(faces, fnName) + })), + }; + + displayRolls = Renderer.dice.parsed._handleModifiers(fnName, meta, rolls, nodeMod, modOpts); + } + + view.shift(); + } else + displayRolls = rolls; + + if (isLast) { + const asHtml = displayRolls.map(r=>{ + if (r.htmlDisplay) + return r.htmlDisplay; + + const numPart = Renderer.dice.parsed._rollToNumPart_html(r, faces); + + if (r.isDropped) + return `[${numPart}]`; + else if (r.isExploded) + return `[${numPart}]`; + else if (r.isSuccess) + return `[${numPart}]`; + else + return `[${numPart}]`; + } + ).join("+"); + + const asText = displayRolls.map(r=>`[${r.val}]`).join("+"); + + const asMd = displayRolls.map(r=>{ + if (r.isDropped) + return `~~[${r.val}]~~`; + else if (r.isExploded) + return `_[${r.val}]_`; + else if (r.isSuccess) + return `**[${r.val}]**`; + else + return `[${r.val}]`; + } + ).join("+"); + + this.addToMeta(meta, { + html: asHtml, + text: asText, + md: asMd, + }, ); + } + + if (fnName === "evl") { + const maxRolls = rolls.filter(it=>it.val === faces && !it.isDropped); + const minRolls = rolls.filter(it=>it.val === 1 && !it.isDropped); + meta.allMax = meta.allMax || []; + meta.allMin = meta.allMin || []; + meta.allMax.push(maxRolls.length && maxRolls.length === rolls.length); + meta.allMin.push(minRolls.length && minRolls.length === rolls.length); + } + + if (isSuccessMode) { + return rolls.filter(it=>!it.isDropped && it.isSuccess).length; + } else { + return rolls.filter(it=>!it.isDropped).map(it=>it.val).sum(); + } + } + + toString() { + if (this._nodes.length === 1) + return this._nodes[0].toString(); + const [numSym,facesSym] = this._nodes; + let out = `${numSym.toString()}d${facesSym.toString()}`; + + for (let i = 2; i < this._nodes.length; ++i) { + const n = this._nodes[i]; + if (n.isDiceModifierGroup) + out += n.mods.map(it=>`${it.modSym.toString()}${it.numSym.toString()}`).join(""); + else + out += `d${n.toString()}`; + } + + return out; + } + } + , + + Exponent: class extends Renderer.dice.AbstractSymbol { + constructor(nodes) { + super(); + this._nodes = nodes; + } + + _evl(meta) { + return this._invoke("evl", meta); + } + _avg(meta) { + return this._invoke("avg", meta); + } + _min(meta) { + return this._invoke("min", meta); + } + _max(meta) { + return this._invoke("max", meta); + } + + _invoke(fnName, meta) { + const view = this._nodes.slice(); + let val = view.pop()[fnName](meta); + while (view.length) { + this.addToMeta(meta, { + text: "^" + }); + val = view.pop()[fnName](meta) ** val; + } + return val; + } + + toString() { + const view = this._nodes.slice(); + let out = view.pop().toString(); + while (view.length) + out = `${view.pop().toString()}^${out}`; + return out; + } + } + , + + Term: class extends Renderer.dice.AbstractSymbol { + constructor(nodes) { + super(); + this._nodes = nodes; + } + + _evl(meta) { + return this._invoke("evl", meta); + } + _avg(meta) { + return this._invoke("avg", meta); + } + _min(meta) { + return this._invoke("min", meta); + } + _max(meta) { + return this._invoke("max", meta); + } + + _invoke(fnName, meta) { + let out = this._nodes[0][fnName](meta); + + for (let i = 1; i < this._nodes.length; i += 2) { + if (this._nodes[i].eq(Renderer.dice.tk.MULT)) { + this.addToMeta(meta, { + text: " ร— " + }); + out *= this._nodes[i + 1][fnName](meta); + } else if (this._nodes[i].eq(Renderer.dice.tk.DIV)) { + this.addToMeta(meta, { + text: " รท " + }); + out /= this._nodes[i + 1][fnName](meta); + } else + throw new Error(`Unimplemented!`); + } + + return out; + } + + toString() { + let out = this._nodes[0].toString(); + for (let i = 1; i < this._nodes.length; i += 2) { + if (this._nodes[i].eq(Renderer.dice.tk.MULT)) + out += ` * ${this._nodes[i + 1].toString()}`; + else if (this._nodes[i].eq(Renderer.dice.tk.DIV)) + out += ` / ${this._nodes[i + 1].toString()}`; + else + throw new Error(`Unimplemented!`); + } + return out; + } + } + , + + Expression: class extends Renderer.dice.AbstractSymbol { + constructor(nodes) { + super(); + this._nodes = nodes; + } + + _evl(meta) { + return this._invoke("evl", meta); + } + _avg(meta) { + return this._invoke("avg", meta); + } + _min(meta) { + return this._invoke("min", meta); + } + _max(meta) { + return this._invoke("max", meta); + } + + _invoke(fnName, meta) { + const view = this._nodes.slice(); + + let isNeg = false; + if (view[0].eq(Renderer.dice.tk.ADD) || view[0].eq(Renderer.dice.tk.SUB)) { + isNeg = view.shift().eq(Renderer.dice.tk.SUB); + if (isNeg) + this.addToMeta(meta, { + text: "-" + }); + } + + let out = view[0][fnName](meta); + if (isNeg) + out = -out; + + for (let i = 1; i < view.length; i += 2) { + if (view[i].eq(Renderer.dice.tk.ADD)) { + this.addToMeta(meta, { + text: " + " + }); + out += view[i + 1][fnName](meta); + } else if (view[i].eq(Renderer.dice.tk.SUB)) { + this.addToMeta(meta, { + text: " - " + }); + out -= view[i + 1][fnName](meta); + } else + throw new Error(`Unimplemented!`); + } + + return out; + } + + toString(indent=0) { + let out = ""; + const view = this._nodes.slice(); + + let isNeg = false; + if (view[0].eq(Renderer.dice.tk.ADD) || view[0].eq(Renderer.dice.tk.SUB)) { + isNeg = view.shift().eq(Renderer.dice.tk.SUB); + if (isNeg) + out += "-"; + } + + out += view[0].toString(indent); + for (let i = 1; i < view.length; i += 2) { + if (view[i].eq(Renderer.dice.tk.ADD)) + out += ` + ${view[i + 1].toString(indent)}`; + else if (view[i].eq(Renderer.dice.tk.SUB)) + out += ` - ${view[i + 1].toString(indent)}`; + else + throw new Error(`Unimplemented!`); + } + return out; + } + } + , +}; + +if (!IS_VTT && typeof window !== "undefined") { + window.addEventListener("load", Renderer.dice._pInit); +} + +Renderer.getRollableRow = function(row, opts) { + opts = opts || {}; + + if (row[0]?.type === "cell" && (row[0]?.roll?.exact != null || (row[0]?.roll?.min != null && row[0]?.roll?.max != null))) + return row; + + row = MiscUtil.copyFast(row); + try { + const cleanRow = String(row[0]).trim(); + + const mLowHigh = /^(\d+) or (lower|higher)$/i.exec(cleanRow); + if (mLowHigh) { + row[0] = { + type: "cell", + entry: cleanRow + }; + if (mLowHigh[2].toLowerCase() === "lower") { + row[0].roll = { + min: -Renderer.dice.POS_INFINITE, + max: Number(mLowHigh[1]), + }; + } else { + row[0].roll = { + min: Number(mLowHigh[1]), + max: Renderer.dice.POS_INFINITE, + }; + } + + return row; + } + + const m = /^(\d+)([-\u2012\u2013](\d+))?$/.exec(cleanRow); + if (m) { + if (m[1] && !m[2]) { + row[0] = { + type: "cell", + roll: { + exact: Number(m[1]), + }, + }; + if (m[1][0] === "0") + row[0].roll.pad = true; + Renderer.getRollableRow._handleInfiniteOpts(row, opts); + } else { + row[0] = { + type: "cell", + roll: { + min: Number(m[1]), + max: Number(m[3]), + }, + }; + if (m[1][0] === "0" || m[3][0] === "0") + row[0].roll.pad = true; + Renderer.getRollableRow._handleInfiniteOpts(row, opts); + } + } else { + const m = /^(\d+)\+$/.exec(row[0]); + row[0] = { + type: "cell", + roll: { + min: Number(m[1]), + max: Renderer.dice.POS_INFINITE, + }, + }; + } + } catch (e) { + if (opts.cbErr) + opts.cbErr(row[0], e); + } + return row; +} +; +Renderer.getRollableRow._handleInfiniteOpts = function(row, opts) { + if (!opts.isForceInfiniteResults) + return; + + const isExact = row[0].roll.exact != null; + + if (opts.isFirstRow) { + if (!isExact) + row[0].roll.displayMin = row[0].roll.min; + row[0].roll.min = -Renderer.dice.POS_INFINITE; + } + + if (opts.isLastRow) { + if (!isExact) + row[0].roll.displayMax = row[0].roll.max; + row[0].roll.max = Renderer.dice.POS_INFINITE; + } +} +; + +//#endregion diff --git a/charbuilder/js/vetools/utils-dataloader.js b/charbuilder/js/vetools/utils-dataloader.js new file mode 100644 index 0000000..e99299b --- /dev/null +++ b/charbuilder/js/vetools/utils-dataloader.js @@ -0,0 +1,2187 @@ +"use strict"; + +/** + * General notes: + * - Raw/`raw_` data *should* be left as-is from `DataUtil`, such that we match anything returned by a prop-specific + * `.loadRawJSON` in `DataUtil`. Note that this is generally *not* the same as the result of `DataUtil.loadRawJSON`, + * which is instead JSON prior to the application of `_copy`/etc.! + * - Other cached data (without `raw_`) should be ready for use, with all references resolved to the best of our + * capabilities. + */ + +// region Utilities + +class _DataLoaderConst { + static SOURCE_SITE_ALL = Symbol("SOURCE_SITE_ALL"); + static SOURCE_PRERELEASE_ALL_CURRENT = Symbol("SOURCE_PRERELEASE_ALL_CURRENT"); + static SOURCE_BREW_ALL_CURRENT = Symbol("SOURCE_BREW_ALL_CURRENT"); + + static ENTITY_NULL = Symbol("ENTITY_NULL"); +} + +class _DataLoaderInternalUtil { + static getCleanPageSourceHash ({page, source, hash}) { + return { + page: this.getCleanPage({page}), + source: this.getCleanSource({source}), + hash: this.getCleanHash({hash}), + }; + } + + static getCleanPage ({page}) { return page.toLowerCase(); } + static getCleanSource ({source}) { return source.toLowerCase(); } + static getCleanHash ({hash}) { return hash.toLowerCase(); } + + /* -------------------------------------------- */ + + static getCleanPageFluff ({page}) { return `${this.getCleanPage({page})}fluff`; } + + /* -------------------------------------------- */ + + static _NOTIFIED_FAILED_DEREFERENCES = new Set(); + + static doNotifyFailedDereferences ({missingRefSets, diagnostics}) { + // region Avoid repeatedly throwing errors for the same missing references + const missingRefSetsUnseen = Object.entries(missingRefSets) + .mergeMap(([prop, set]) => ({ + [prop]: new Set( + [...set] + .filter(ref => { + const refLower = ref.toLowerCase(); + const out = !this._NOTIFIED_FAILED_DEREFERENCES.has(refLower); + this._NOTIFIED_FAILED_DEREFERENCES.add(refLower); + return out; + }), + ), + })); + // endregion + + const cntMissingRefs = Object.values(missingRefSetsUnseen).map(({size}) => size).sum(); + if (!cntMissingRefs) return; + + const notificationRefs = Object.entries(missingRefSetsUnseen) + .map(([k, v]) => `${k}: ${[...v].sort(SortUtil.ascSortLower).join(", ")}`) + .join("; "); + + const ptDiagnostics = DataLoader.getDiagnosticsSummary(diagnostics); + const msgStart = `Failed to load references for ${cntMissingRefs} entr${cntMissingRefs === 1 ? "y" : "ies"}!`; + + JqueryUtil.doToast({ + type: "danger", + content: `${msgStart} Reference types and values were: ${[notificationRefs, ptDiagnostics].join(" ")}`, + isAutoHide: false, + }); + + const cnslRefs = [ + ...Object.entries(missingRefSetsUnseen) + .map(([k, v]) => `${k}:\n\t${[...v].sort(SortUtil.ascSortLower).join("\n\t")}`), + ptDiagnostics, + ] + .filter(Boolean) + .join("\n"); + + setTimeout(() => { throw new Error(`${msgStart}\nReference types and values were:\n${cnslRefs}`); }); + } +} + +// endregion + +/* -------------------------------------------- */ + +// region Dereferencer + +class _DataLoaderDereferencerBase { + static _DereferenceMeta = class { + constructor ({cntReplaces = 0, offsetIx = 0}) { + this.cntReplaces = cntReplaces; + this.offsetIx = offsetIx; + } + }; + + static _WALKER_MOD = MiscUtil.getWalker({ + keyBlocklist: MiscUtil.GENERIC_WALKER_ENTRIES_KEY_BLOCKLIST, + }); + + /* -------------------------------------------- */ + + _pPreloadingRefContentSite = null; + _pPreloadingRefContentPrerelease = null; + _pPreloadingRefContentBrew = null; + + _preloadingPrereleaseLastIdent = null; + _preloadingBrewLastIdent = null; + + async pPreloadRefContent () { + await (this._pPreloadingRefContentSite = this._pPreloadingRefContentSite || this._pPreloadRefContentSite()); + + if (typeof PrereleaseUtil !== "undefined") { + const identPrerelease = PrereleaseUtil.getCacheIteration(); + if (identPrerelease !== this._preloadingPrereleaseLastIdent) this._pPreloadingRefContentPrerelease = null; + this._preloadingPrereleaseLastIdent = identPrerelease; + await (this._pPreloadingRefContentPrerelease = this._pPreloadingRefContentPrerelease || this._pPreloadRefContentPrerelease()); + } + + if (typeof BrewUtil2 !== "undefined") { + const identBrew = BrewUtil2.getCacheIteration(); + if (identBrew !== this._preloadingBrewLastIdent) this._pPreloadingRefContentBrew = null; + this._preloadingBrewLastIdent = identBrew; + await (this._pPreloadingRefContentBrew = this._pPreloadingRefContentBrew || this._pPreloadRefContentBrew()); + } + } + + async _pPreloadRefContentSite () { /* Implement as required */ } + async _pPreloadRefContentPrerelease () { /* Implement as required */ } + async _pPreloadRefContentBrew () { /* Implement as required */ } + + /* -------------------------------------------- */ + + dereference ({ent, entriesWithoutRefs, toReplaceMeta, ixReplace}) { throw new Error("Unimplemented!"); } + + _getCopyFromCache ({page, entriesWithoutRefs, refUnpacked, refHash}) { + if (page.toLowerCase().endsWith(".html")) throw new Error(`Could not dereference "${page}" content. Dereferencing is only supported for props!`); + + // Prefer content from our active load, where available + return entriesWithoutRefs[page]?.[refHash] + ? MiscUtil.copyFast(entriesWithoutRefs[page]?.[refHash]) + : DataLoader.getFromCache(page, refUnpacked.source, refHash, {isCopy: true}); + } +} + +class _DataLoaderDereferencerClassSubclassFeatures extends _DataLoaderDereferencerBase { + dereference ({ent, entriesWithoutRefs, toReplaceMeta, ixReplace}) { + const prop = toReplaceMeta.type === "refClassFeature" ? "classFeature" : "subclassFeature"; + const refUnpacked = toReplaceMeta.type === "refClassFeature" + ? DataUtil.class.unpackUidClassFeature(toReplaceMeta.classFeature) + : DataUtil.class.unpackUidSubclassFeature(toReplaceMeta.subclassFeature); + const refHash = UrlUtil.URL_TO_HASH_BUILDER[prop](refUnpacked); + + // Skip blocklisted + if (ExcludeUtil.isInitialised && ExcludeUtil.isExcluded(refHash, prop, refUnpacked.source, {isNoCount: true})) { + toReplaceMeta.array[toReplaceMeta.ix] = {}; + return new this.constructor._DereferenceMeta({cntReplaces: 1}); + } + + const cpy = this._getCopyFromCache({page: prop, entriesWithoutRefs, refUnpacked, refHash}); + if (!cpy) return new this.constructor._DereferenceMeta({cntReplaces: 0}); + + delete cpy.header; + if (toReplaceMeta.name) cpy.name = toReplaceMeta.name; + toReplaceMeta.array[toReplaceMeta.ix] = cpy; + return new this.constructor._DereferenceMeta({cntReplaces: 1}); + } +} + +class _DataLoaderDereferencerOptionalfeatures extends _DataLoaderDereferencerBase { + async _pPreloadRefContentSite () { await DataLoader.pCacheAndGetAllSite(UrlUtil.PG_OPT_FEATURES); } + async _pPreloadRefContentPrerelease () { await DataLoader.pCacheAndGetAllPrerelease(UrlUtil.PG_OPT_FEATURES); } + async _pPreloadRefContentBrew () { await DataLoader.pCacheAndGetAllBrew(UrlUtil.PG_OPT_FEATURES); } + + dereference ({ent, entriesWithoutRefs, toReplaceMeta, ixReplace}) { + const refUnpacked = DataUtil.generic.unpackUid(toReplaceMeta.optionalfeature, "optfeature"); + const refHash = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_OPT_FEATURES](refUnpacked); + + // Skip blocklisted + if (ExcludeUtil.isInitialised && ExcludeUtil.isExcluded(refHash, "optionalfeature", refUnpacked.source, {isNoCount: true})) { + toReplaceMeta.array[toReplaceMeta.ix] = {}; + return new this.constructor._DereferenceMeta({cntReplaces: 1}); + } + + const cpy = this._getCopyFromCache({page: "optionalfeature", entriesWithoutRefs, refUnpacked, refHash}); + if (!cpy) return new this.constructor._DereferenceMeta({cntReplaces: 0}); + + delete cpy.featureType; + delete cpy.prerequisite; + if (toReplaceMeta.name) cpy.name = toReplaceMeta.name; + toReplaceMeta.array[toReplaceMeta.ix] = cpy; + + return new this.constructor._DereferenceMeta({cntReplaces: 1}); + } +} + +class _DataLoaderDereferencerItemEntries extends _DataLoaderDereferencerBase { + async _pPreloadRefContentSite () { await DataLoader.pCacheAndGetAllSite(UrlUtil.PG_ITEMS); } + async _pPreloadRefContentPrerelease () { await DataLoader.pCacheAndGetAllPrerelease(UrlUtil.PG_ITEMS); } + async _pPreloadRefContentBrew () { await DataLoader.pCacheAndGetAllBrew(UrlUtil.PG_ITEMS); } + + dereference ({ent, entriesWithoutRefs, toReplaceMeta, ixReplace}) { + const refUnpacked = DataUtil.generic.unpackUid(toReplaceMeta.itemEntry, "itemEntry"); + const refHash = UrlUtil.URL_TO_HASH_BUILDER["itemEntry"](refUnpacked); + + const cpy = this._getCopyFromCache({page: "itemEntry", entriesWithoutRefs, refUnpacked, refHash}); + if (!cpy) return new this.constructor._DereferenceMeta({cntReplaces: 0}); + + cpy.entriesTemplate = this.constructor._WALKER_MOD.walk( + cpy.entriesTemplate, + { + string: (str) => { + return Renderer.utils.applyTemplate( + ent, + str, + ); + }, + }, + ); + + toReplaceMeta.array.splice(toReplaceMeta.ix, 1, ...cpy.entriesTemplate); + + return new this.constructor._DereferenceMeta({ + cntReplaces: 1, + // Offset by the length of the array we just merged in (minus one, since we replaced an + // element) + offsetIx: cpy.entriesTemplate.length - 1, + }); + } +} + +class _DataLoaderDereferencer { + static _REF_TYPE_TO_DEREFERENCER = {}; + + static _init () { + this._REF_TYPE_TO_DEREFERENCER["refClassFeature"] = + this._REF_TYPE_TO_DEREFERENCER["refSubclassFeature"] = + new _DataLoaderDereferencerClassSubclassFeatures(); + + this._REF_TYPE_TO_DEREFERENCER["refOptionalfeature"] = + new _DataLoaderDereferencerOptionalfeatures(); + + this._REF_TYPE_TO_DEREFERENCER["refItemEntry"] = + new _DataLoaderDereferencerItemEntries(); + + return null; + } + + static _ = this._init(); + + static _WALKER_READ = MiscUtil.getWalker({ + keyBlocklist: MiscUtil.GENERIC_WALKER_ENTRIES_KEY_BLOCKLIST, + isNoModification: true, + isBreakOnReturn: true, + }); + + /** + * Build an object of the form `{page: [...entities...]}` and return it. + * @param entities + * @param {string} page + * @param {string} propEntries + * @param {string} propIsRef + */ + static async pGetDereferenced ( + entities, + page, + { + propEntries = "entries", + propIsRef = null, + } = {}, + ) { + if (page.toLowerCase().endsWith(".html")) throw new Error(`Could not dereference "${page}" content. Dereferencing is only supported for props!`); + + if (!entities || !entities.length) return {}; + + const out = {}; + const entriesWithRefs = {}; + const entriesWithoutRefs = {}; + + this._pGetDereferenced_doSegregateWithWithoutRefs({ + entities, + page, + propEntries, + propIsRef, + entriesWithRefs, + entriesWithoutRefs, + }); + + await this._pGetDereferenced_pDoDereference({propEntries, entriesWithRefs, entriesWithoutRefs}); + this._pGetDereferenced_doNotifyFailed({entriesWithRefs, entities}); + this._pGetDereferenced_doPopulateOutput({page, out, entriesWithoutRefs, entriesWithRefs}); + + return out; + } + + /* -------------------------------------------- */ + + static _pGetDereferenced_doSegregateWithWithoutRefs ({entities, page, propEntries, propIsRef, entriesWithRefs, entriesWithoutRefs}) { + const hashBuilder = UrlUtil.URL_TO_HASH_BUILDER[page]; + entities + .forEach(ent => { + const hash = hashBuilder(ent); + const hasRefs = this._pGetDereferenced_hasRefs({ent, propEntries, propIsRef}); + + ( + (hasRefs ? entriesWithRefs : entriesWithoutRefs)[page] = (hasRefs ? entriesWithRefs : entriesWithoutRefs)[page] || {} + )[hash] = hasRefs ? MiscUtil.copyFast(ent) : ent; + }); + } + + static _pGetDereferenced_hasRefs ({ent, propEntries, propIsRef}) { + if (propIsRef != null) return !!ent[propIsRef]; + + const ptrHasRef = {_: false}; + this._WALKER_READ.walk(ent[propEntries], this._pGetDereferenced_doPopulateRaw_getHandlers({ptrHasRef})); + return ptrHasRef._; + } + + static _pGetDereferenced_doPopulateRaw_getHandlers ({ptrHasRef}) { + return { + object: (obj) => { + if (this._REF_TYPE_TO_DEREFERENCER[obj.type]) return ptrHasRef._ = true; + }, + string: (str) => { + if (str.startsWith("{#") && str.endsWith("}")) return ptrHasRef._ = true; + }, + }; + } + + /* -------------------------------------------- */ + + static _MAX_DEREFERENCE_LOOPS = 25; // conservatively avoid infinite looping + + static async _pGetDereferenced_pDoDereference ({propEntries, entriesWithRefs, entriesWithoutRefs}) { + for (let i = 0; i < this._MAX_DEREFERENCE_LOOPS; ++i) { + if (!Object.keys(entriesWithRefs).length) break; + + for (const [page, pageEntries] of Object.entries(entriesWithRefs)) { + for (const [hash, ent] of Object.entries(pageEntries)) { + const toReplaceMetas = []; + this._WALKER_READ.walk( + ent[propEntries], + this._pGetDereferenced_doDereference_getHandlers({toReplaceMetas}), + ); + + for (const {type} of toReplaceMetas) { + if (!this._REF_TYPE_TO_DEREFERENCER[type]) continue; + await this._REF_TYPE_TO_DEREFERENCER[type].pPreloadRefContent(); + } + + let cntReplaces = 0; + for (let ixReplace = 0; ixReplace < toReplaceMetas.length; ++ixReplace) { + const toReplaceMeta = this._pGetDereferenced_doDereference_getToReplaceMeta(toReplaceMetas[ixReplace]); + + const derefMeta = this._REF_TYPE_TO_DEREFERENCER[toReplaceMeta.type].dereference({ + ent, + entriesWithoutRefs, + toReplaceMeta, + ixReplace, + }); + cntReplaces += derefMeta.cntReplaces; + + if (!derefMeta.offsetIx) continue; + + toReplaceMetas.slice(ixReplace + 1).forEach(it => it.ix += derefMeta.offsetIx); + } + + if (cntReplaces === toReplaceMetas.length) { + delete pageEntries[hash]; + (entriesWithoutRefs[page] = entriesWithoutRefs[page] || {})[hash] = ent; + } + } + + if (!Object.keys(pageEntries).length) delete entriesWithRefs[page]; + } + } + } + + static _pGetDereferenced_doDereference_getHandlers ({toReplaceMetas}) { + return { + array: (arr) => { + arr.forEach((it, i) => { + if (this._REF_TYPE_TO_DEREFERENCER[it.type]) { + toReplaceMetas.push({ + ...it, + array: arr, + ix: i, + }); + return; + } + + if (typeof it === "string" && it.startsWith("{#") && it.endsWith("}")) { + toReplaceMetas.push({ + string: it, + array: arr, + ix: i, + }); + } + }); + }, + }; + } + + static _pGetDereferenced_doDereference_getToReplaceMeta (toReplaceMetaRaw) { + if (toReplaceMetaRaw.string == null) return toReplaceMetaRaw; + + const str = toReplaceMetaRaw.string; + delete toReplaceMetaRaw.string; + return {...toReplaceMetaRaw, ...Renderer.hover.getRefMetaFromTag(str)}; + } + + /* -------------------------------------------- */ + + static _pGetDereferenced_doNotifyFailed ({entriesWithRefs, entities}) { + const entriesWithRefsVals = Object.values(entriesWithRefs) + .map(hashToEntry => Object.values(hashToEntry)) + .flat(); + + if (!entriesWithRefsVals.length) return; + + const missingRefSets = {}; + this._WALKER_READ.walk( + entriesWithRefsVals, + { + object: (obj) => { + switch (obj.type) { + case "refClassFeature": (missingRefSets["classFeature"] = missingRefSets["classFeature"] || new Set()).add(obj.classFeature); break; + case "refSubclassFeature": (missingRefSets["subclassFeature"] = missingRefSets["subclassFeature"] || new Set()).add(obj.subclassFeature); break; + case "refOptionalfeature": (missingRefSets["optionalfeature"] = missingRefSets["optionalfeature"] || new Set()).add(obj.optionalfeature); break; + case "refItemEntry": (missingRefSets["itemEntry"] = missingRefSets["itemEntry"] || new Set()).add(obj.itemEntry); break; + } + }, + }, + ); + + _DataLoaderInternalUtil.doNotifyFailedDereferences({ + missingRefSets, + diagnostics: entities + .map(ent => ent.__diagnostic) + .filter(Boolean), + }); + } + + /* -------------------------------------------- */ + + static _pGetDereferenced_doPopulateOutput ({isOverwrite, out, entriesWithoutRefs, entriesWithRefs}) { + [ + ...Object.entries(entriesWithoutRefs), + // Add the failed-to-resolve entities to the cache; the missing refs will simply not be rendered + ...Object.entries(entriesWithRefs), + ] + .forEach(([page, hashToEnt]) => { + Object.entries(hashToEnt) + .forEach(([hash, ent]) => { + if (!isOverwrite && DataLoader.getFromCache(page, ent.source, hash)) return; + (out[page] = out[page] || []).push(ent); + }); + }); + } +} + +// endregion + +/* -------------------------------------------- */ + +// region Cache + +class _DataLoaderCache { + static _PARTITION_UNKNOWN = 0; + static _PARTITION_SITE = 1; + static _PARTITION_PRERELEASE = 2; + static _PARTITION_BREW = 3; + + _cache = {}; + _cacheSiteLists = {}; + _cachePrereleaseLists = {}; + _cacheBrewLists = {}; + + get (pageClean, sourceClean, hashClean) { + return this._cache[pageClean]?.[sourceClean]?.[hashClean]; + } + + getAllSite (pageClean) { + return Object.values(this._cacheSiteLists[pageClean] || {}); + } + + getAllPrerelease (pageClean) { + return Object.values(this._cachePrereleaseLists[pageClean] || {}); + } + + getAllBrew (pageClean) { + return Object.values(this._cacheBrewLists[pageClean] || {}); + } + + set (pageClean, sourceClean, hashClean, ent) { + // region Set primary cache + let pageCache = this._cache[pageClean]; + if (!pageCache) { + pageCache = {}; + this._cache[pageClean] = pageCache; + } + + let sourceCache = pageCache[sourceClean]; + if (!sourceCache) { + sourceCache = {}; + pageCache[sourceClean] = sourceCache; + } + + sourceCache[hashClean] = ent; + // endregion + + if (ent === _DataLoaderConst.ENTITY_NULL) return; + + // region Set site/prerelease/brew list cache + switch (this._set_getPartition(ent)) { + case this.constructor._PARTITION_SITE: { + return this._set_addToPartition({ + cache: this._cacheSiteLists, + pageClean, + hashClean, + ent, + }); + } + + case this.constructor._PARTITION_PRERELEASE: { + return this._set_addToPartition({ + cache: this._cachePrereleaseLists, + pageClean, + hashClean, + ent, + }); + } + + case this.constructor._PARTITION_BREW: { + return this._set_addToPartition({ + cache: this._cacheBrewLists, + pageClean, + hashClean, + ent, + }); + } + + // Skip by default + } + // endregion + } + + _set_getPartition (ent) { + if (ent.adventure) return this._set_getPartition_fromSource(SourceUtil.getEntitySource(ent.adventure)); + if (ent.book) return this._set_getPartition_fromSource(SourceUtil.getEntitySource(ent.book)); + + if (ent.__prop !== "item" || ent._category !== "Specific Variant") return this._set_getPartition_fromSource(SourceUtil.getEntitySource(ent)); + + // "Specific Variant" items have a dual source. For the purposes of partitioning: + // - only items with both `baseitem` source and `magicvariant` source both "site" sources + // - items which include any brew are treated as brew + // - items which include any prerelease (and no brew) are treated as prerelease + const entitySource = SourceUtil.getEntitySource(ent); + const partitionBaseitem = this._set_getPartition_fromSource(entitySource); + const partitionMagicvariant = this._set_getPartition_fromSource(ent._baseSource ?? entitySource); + + if (partitionBaseitem === partitionMagicvariant && partitionBaseitem === this.constructor._PARTITION_SITE) return this.constructor._PARTITION_SITE; + if (partitionBaseitem === this.constructor._PARTITION_BREW || partitionMagicvariant === this.constructor._PARTITION_BREW) return this.constructor._PARTITION_BREW; + return this.constructor._PARTITION_PRERELEASE; + } + + _set_getPartition_fromSource (partitionSource) { + if (SourceUtil.isSiteSource(partitionSource)) return this.constructor._PARTITION_SITE; + if (PrereleaseUtil.hasSourceJson(partitionSource)) return this.constructor._PARTITION_PRERELEASE; + if (BrewUtil2.hasSourceJson(partitionSource)) return this.constructor._PARTITION_BREW; + return this.constructor._PARTITION_UNKNOWN; + } + + _set_addToPartition ({cache, pageClean, hashClean, ent}) { + let siteListCache = cache[pageClean]; + if (!siteListCache) { + siteListCache = {}; + cache[pageClean] = siteListCache; + } + siteListCache[hashClean] = ent; + } +} + +// endregion + +/* -------------------------------------------- */ + +// region Data type loading + +class _DataTypeLoader { + static PROPS = []; + static PAGE = null; + static IS_FLUFF = false; + + static register ({fnRegister}) { + fnRegister({ + loader: new this(), + props: this.PROPS, + page: this.PAGE, + isFluff: this.IS_FLUFF, + }); + } + + static _getAsRawPrefixed (json, {propsRaw}) { + return { + ...propsRaw.mergeMap(prop => ({[`raw_${prop}`]: json[prop]})), + }; + } + + /* -------------------------------------------- */ + + /** Used to reduce phase 1 caching for a loader where phase 2 is the primary caching step. */ + phase1CachePropAllowlist; + + /** (Unused) */ + phase2CachePropAllowlist; + + hasPhase2Cache = false; + + _cache_pSiteData = {}; + _cache_pPostCaches = {}; + + /** + * @param pageClean + * @param sourceClean + * @return {string} + */ + _getSiteIdent ({pageClean, sourceClean}) { throw new Error("Unimplemented!"); } + + _isPrereleaseAvailable () { return typeof PrereleaseUtil !== "undefined"; } + + _isBrewAvailable () { return typeof BrewUtil2 !== "undefined"; } + + async _pPrePopulate ({data, isPrerelease, isBrew}) { /* Implement as required */ } + + async pGetSiteData ({pageClean, sourceClean}) { + const propCache = this._getSiteIdent({pageClean, sourceClean}); + this._cache_pSiteData[propCache] = this._cache_pSiteData[propCache] || this._pGetSiteData({pageClean, sourceClean}); + return this._cache_pSiteData[propCache]; + } + + async _pGetSiteData ({pageClean, sourceClean}) { throw new Error("Unimplemented!"); } + + async pGetStoredPrereleaseData () { + if (!this._isPrereleaseAvailable()) return {}; + return this._pGetStoredPrereleaseData(); + } + + async pGetStoredBrewData () { + if (!this._isBrewAvailable()) return {}; + return this._pGetStoredBrewData(); + } + + async _pGetStoredPrereleaseData () { + return this._pGetStoredPrereleaseBrewData({brewUtil: PrereleaseUtil, isPrerelease: true}); + } + + async _pGetStoredBrewData () { + return this._pGetStoredPrereleaseBrewData({brewUtil: BrewUtil2, isBrew: true}); + } + + async _pGetStoredPrereleaseBrewData ({brewUtil, isPrerelease, isBrew}) { + const prereleaseBrewData = await brewUtil.pGetBrewProcessed(); + await this._pPrePopulate({data: prereleaseBrewData, isPrerelease, isBrew}); + return prereleaseBrewData; + } + + async pGetPostCacheData ({siteData = null, prereleaseData = null, brewData = null, lockToken2}) { /* Implement as required */ } + + async _pGetPostCacheData_obj_withCache ({obj, propCache, lockToken2}) { + this._cache_pPostCaches[propCache] = this._cache_pPostCaches[propCache] || this._pGetPostCacheData_obj({obj, lockToken2}); + return this._cache_pPostCaches[propCache]; + } + + async _pGetPostCacheData_obj ({obj, lockToken2}) { throw new Error("Unimplemented!"); } + + hasCustomCacheStrategy ({obj}) { return false; } + + addToCacheCustom ({cache, obj}) { /* Implement as required */ } +} + +class _DataTypeLoaderSingleSource extends _DataTypeLoader { + _filename; + + _getSiteIdent ({pageClean, sourceClean}) { return this._filename; } + + async _pGetSiteData ({pageClean, sourceClean}) { + return DataUtil.loadJSON(`${Renderer.get().baseUrl}data/${this._filename}`); + } +} + +class _DataTypeLoaderBackground extends _DataTypeLoaderSingleSource { + static PROPS = ["background"]; + static PAGE = UrlUtil.PG_BACKGROUNDS; + + _filename = "backgrounds.json"; +} + +class _DataTypeLoaderPsionic extends _DataTypeLoaderSingleSource { + static PROPS = ["psionic"]; + static PAGE = UrlUtil.PG_PSIONICS; + + _filename = "psionics.json"; +} + +class _DataTypeLoaderObject extends _DataTypeLoaderSingleSource { + static PROPS = ["object"]; + static PAGE = UrlUtil.PG_OBJECTS; + + _filename = "objects.json"; +} + +class _DataTypeLoaderAction extends _DataTypeLoaderSingleSource { + static PROPS = ["action"]; + static PAGE = UrlUtil.PG_ACTIONS; + + _filename = "actions.json"; +} + +class _DataTypeLoaderFeat extends _DataTypeLoaderSingleSource { + static PROPS = ["feat"]; + static PAGE = UrlUtil.PG_FEATS; + + _filename = "feats.json"; +} + +class _DataTypeLoaderOptionalfeature extends _DataTypeLoaderSingleSource { + static PROPS = ["optionalfeature"]; + static PAGE = UrlUtil.PG_OPT_FEATURES; + + _filename = "optionalfeatures.json"; +} + +class _DataTypeLoaderReward extends _DataTypeLoaderSingleSource { + static PROPS = ["reward"]; + static PAGE = UrlUtil.PG_REWARDS; + + _filename = "rewards.json"; +} + +class _DataTypeLoaderCharoption extends _DataTypeLoaderSingleSource { + static PROPS = ["charoption"]; + static PAGE = UrlUtil.PG_CHAR_CREATION_OPTIONS; + + _filename = "charcreationoptions.json"; +} + +class _DataTypeLoaderTrapHazard extends _DataTypeLoaderSingleSource { + static PROPS = ["trap", "hazard"]; + static PAGE = UrlUtil.PG_TRAPS_HAZARDS; + + _filename = "trapshazards.json"; +} + +class _DataTypeLoaderCultBoon extends _DataTypeLoaderSingleSource { + static PROPS = ["cult", "boon"]; + static PAGE = UrlUtil.PG_CULTS_BOONS; + + _filename = "cultsboons.json"; +} + +class _DataTypeLoaderVehicle extends _DataTypeLoaderSingleSource { + static PROPS = ["vehicle", "vehicleUpgrade"]; + static PAGE = UrlUtil.PG_VEHICLES; + + _filename = "vehicles.json"; +} + +class _DataTypeLoaderConditionDisease extends _DataTypeLoaderSingleSource { + static PROPS = ["condition", "disease", "status"]; + static PAGE = UrlUtil.PG_CONDITIONS_DISEASES; + + _filename = "conditionsdiseases.json"; +} + +class _DataTypeLoaderSkill extends _DataTypeLoaderSingleSource { + static PROPS = ["skill"]; + + _filename = "skills.json"; +} + +class _DataTypeLoaderSense extends _DataTypeLoaderSingleSource { + static PROPS = ["sense"]; + + _filename = "senses.json"; +} + +class _DataTypeLoaderLegendaryGroup extends _DataTypeLoaderSingleSource { + static PROPS = ["legendaryGroup"]; + + _filename = "bestiary/legendarygroups.json"; +} + +class _DataTypeLoaderItemEntry extends _DataTypeLoaderSingleSource { + static PROPS = ["itemEntry"]; + + _filename = "items-base.json"; +} + +class _DataTypeLoaderItemMastery extends _DataTypeLoaderSingleSource { + static PROPS = ["itemMastery"]; + + _filename = "items-base.json"; + + async _pPrePopulate ({data, isPrerelease, isBrew}) { + // Ensure properties are loaded + await Renderer.item.pGetSiteUnresolvedRefItems(); + Renderer.item.addPrereleaseBrewPropertiesAndTypesFrom({data}); + } +} + +class _DataTypeLoaderBackgroundFluff extends _DataTypeLoaderSingleSource { + static PROPS = ["backgroundFluff"]; + static PAGE = UrlUtil.PG_BACKGROUNDS; + static IS_FLUFF = true; + + _filename = "fluff-backgrounds.json"; +} + +class _DataTypeLoaderFeatFluff extends _DataTypeLoaderSingleSource { + static PROPS = ["featFluff"]; + static PAGE = UrlUtil.PG_FEATS; + static IS_FLUFF = true; + + _filename = "fluff-feats.json"; +} + +class _DataTypeLoaderOptionalfeatureFluff extends _DataTypeLoaderSingleSource { + static PROPS = ["optionalfeatureFluff"]; + static PAGE = UrlUtil.PG_OPT_FEATURES; + static IS_FLUFF = true; + + _filename = "fluff-optionalfeatures.json"; +} + +class _DataTypeLoaderItemFluff extends _DataTypeLoaderSingleSource { + static PROPS = ["itemFluff"]; + static PAGE = UrlUtil.PG_ITEMS; + static IS_FLUFF = true; + + _filename = "fluff-items.json"; +} + +class _DataTypeLoaderRaceFluff extends _DataTypeLoaderSingleSource { + static PROPS = ["raceFluff"]; + static PAGE = UrlUtil.PG_RACES; + static IS_FLUFF = true; + + _filename = "fluff-races.json"; +} + +class _DataTypeLoaderLanguageFluff extends _DataTypeLoaderSingleSource { + static PROPS = ["languageFluff"]; + static PAGE = UrlUtil.PG_LANGUAGES; + static IS_FLUFF = true; + + _filename = "fluff-languages.json"; +} + +class _DataTypeLoaderVehicleFluff extends _DataTypeLoaderSingleSource { + static PROPS = ["vehicleFluff"]; + static PAGE = UrlUtil.PG_VEHICLES; + static IS_FLUFF = true; + + _filename = "fluff-vehicles.json"; +} + +class _DataTypeLoaderObjectFluff extends _DataTypeLoaderSingleSource { + static PROPS = ["objectFluff"]; + static PAGE = UrlUtil.PG_OBJECTS; + static IS_FLUFF = true; + + _filename = "fluff-objects.json"; +} + +class _DataTypeLoaderCharoptionFluff extends _DataTypeLoaderSingleSource { + static PROPS = ["charoptionFluff"]; + static PAGE = UrlUtil.PG_CHAR_CREATION_OPTIONS; + static IS_FLUFF = true; + + _filename = "fluff-charcreationoptions.json"; +} + +class _DataTypeLoaderRecipeFluff extends _DataTypeLoaderSingleSource { + static PROPS = ["recipeFluff"]; + static PAGE = UrlUtil.PG_RECIPES; + static IS_FLUFF = true; + + _filename = "fluff-recipes.json"; +} + +class _DataTypeLoaderConditionDiseaseFluff extends _DataTypeLoaderSingleSource { + static PROPS = ["conditionFluff", "diseaseFluff", "statusFluff"]; + static PAGE = UrlUtil.PG_CONDITIONS_DISEASES; + static IS_FLUFF = true; + + _filename = "fluff-conditionsdiseases.json"; +} + +class _DataTypeLoaderTrapHazardFluff extends _DataTypeLoaderSingleSource { + static PROPS = ["trapFluff", "hazardFluff"]; + static PAGE = UrlUtil.PG_TRAPS_HAZARDS; + static IS_FLUFF = true; + + _filename = "fluff-trapshazards.json"; +} + +class _DataTypeLoaderPredefined extends _DataTypeLoader { + _loader; + _loadJsonArgs = null; + _loadPrereleaseArgs = null; + _loadBrewArgs = null; + + _getSiteIdent ({pageClean, sourceClean}) { return this._loader; } + + async _pGetSiteData ({pageClean, sourceClean}) { + return DataUtil[this._loader].loadJSON(this._loadJsonArgs); + } + + async _pGetStoredPrereleaseData () { + if (!DataUtil[this._loader].loadPrerelease) return super._pGetStoredPrereleaseData(); + return DataUtil[this._loader].loadPrerelease(this._loadPrereleaseArgs); + } + + async _pGetStoredBrewData () { + if (!DataUtil[this._loader].loadBrew) return super._pGetStoredBrewData(); + return DataUtil[this._loader].loadBrew(this._loadBrewArgs); + } +} + +class _DataTypeLoaderRace extends _DataTypeLoaderPredefined { + static PROPS = [...UrlUtil.PAGE_TO_PROPS[UrlUtil.PG_RACES]]; + static PAGE = UrlUtil.PG_RACES; + + _loader = "race"; + _loadJsonArgs = {isAddBaseRaces: true}; + _loadPrereleaseArgs = {isAddBaseRaces: true}; + _loadBrewArgs = {isAddBaseRaces: true}; +} + +class _DataTypeLoaderDeity extends _DataTypeLoaderPredefined { + static PROPS = ["deity"]; + static PAGE = UrlUtil.PG_DEITIES; + + _loader = "deity"; +} + +class _DataTypeLoaderVariantrule extends _DataTypeLoaderPredefined { + static PROPS = ["variantrule"]; + static PAGE = UrlUtil.PG_VARIANTRULES; + + _loader = "variantrule"; +} + +class _DataTypeLoaderTable extends _DataTypeLoaderPredefined { + static PROPS = ["table", "tableGroup"]; + static PAGE = UrlUtil.PG_TABLES; + + _loader = "table"; +} + +class _DataTypeLoaderLanguage extends _DataTypeLoaderPredefined { + static PROPS = ["language"]; + static PAGE = UrlUtil.PG_LANGUAGES; + + _loader = "language"; +} + +class _DataTypeLoaderRecipe extends _DataTypeLoaderPredefined { + static PROPS = ["recipe"]; + static PAGE = UrlUtil.PG_RECIPES; + + _loader = "recipe"; +} + +class _DataTypeLoaderMultiSource extends _DataTypeLoader { + _prop; + + _getSiteIdent ({pageClean, sourceClean}) { + // use `.toString()` in case `sourceClean` is a `Symbol` + return `${this._prop}__${sourceClean.toString()}`; + } + + async _pGetSiteData ({pageClean, sourceClean}) { + const data = await this._pGetSiteData_data({sourceClean}); + + if (data == null) return {}; + + await this._pPrePopulate({data}); + + return data; + } + + async _pGetSiteData_data ({sourceClean}) { + if (sourceClean === _DataLoaderConst.SOURCE_SITE_ALL) return this._pGetSiteDataAll(); + + const source = Parser.sourceJsonToJson(sourceClean); + return DataUtil[this._prop].pLoadSingleSource(source); + } + + async _pGetSiteDataAll () { + return DataUtil[this._prop].loadJSON(); + } +} + +class _DataTypeLoaderCustomMonster extends _DataTypeLoaderMultiSource { + static PROPS = ["monster"]; + static PAGE = UrlUtil.PG_BESTIARY; + + _prop = "monster"; + + async _pGetSiteData ({pageClean, sourceClean}) { + await DataUtil.monster.pPreloadMeta(); + return super._pGetSiteData({pageClean, sourceClean}); + } + + async _pPrePopulate ({data, isPrerelease, isBrew}) { + DataUtil.monster.populateMetaReference(data); + } +} + +class _DataTypeLoaderCustomMonsterFluff extends _DataTypeLoaderMultiSource { + static PROPS = ["monsterFluff"]; + static PAGE = UrlUtil.PG_BESTIARY; + static IS_FLUFF = true; + + _prop = "monsterFluff"; +} + +class _DataTypeLoaderCustomSpell extends _DataTypeLoaderMultiSource { + static PROPS = [...UrlUtil.PAGE_TO_PROPS[UrlUtil.PG_SPELLS]]; + static PAGE = UrlUtil.PG_SPELLS; + + _prop = "spell"; + + async _pPrePopulate ({data, isPrerelease, isBrew}) { + Renderer.spell.prePopulateHover(data); + if (isPrerelease) Renderer.spell.prePopulateHoverPrerelease(data); + if (isBrew) Renderer.spell.prePopulateHoverBrew(data); + } +} + +class _DataTypeLoaderCustomSpellFluff extends _DataTypeLoaderMultiSource { + static PROPS = ["spellFluff"]; + static PAGE = UrlUtil.PG_SPELLS; + static IS_FLUFF = true; + + _prop = "spellFluff"; +} + +/** @abstract */ +class _DataTypeLoaderCustomRawable extends _DataTypeLoader { + static _PROPS_RAWABLE; + + hasPhase2Cache = true; + + _getSiteIdent ({pageClean, sourceClean}) { return `${pageClean}__${this.constructor.name}`; } + + async _pGetSiteData ({pageClean, sourceClean}) { + const json = await this._pGetRawSiteData(); + return this.constructor._getAsRawPrefixed(json, {propsRaw: this.constructor._PROPS_RAWABLE}); + } + + /** @abstract */ + async _pGetRawSiteData () { throw new Error("Unimplemented!"); } + + async _pGetStoredPrereleaseBrewData ({brewUtil, isPrerelease, isBrew}) { + const prereleaseBrew = await brewUtil.pGetBrewProcessed(); + return this.constructor._getAsRawPrefixed(prereleaseBrew, {propsRaw: this.constructor._PROPS_RAWABLE}); + } + + static _pGetDereferencedData_doNotifyFailed ({ent, uids, prop}) { + const missingRefSets = { + [prop]: new Set(uids), + }; + + _DataLoaderInternalUtil.doNotifyFailedDereferences({ + missingRefSets, + diagnostics: [ent.__diagnostic].filter(Boolean), + }); + } +} + +class _DataTypeLoaderCustomClassesSubclass extends _DataTypeLoaderCustomRawable { + static PROPS = ["raw_class", "raw_subclass", "class", "subclass"]; + static PAGE = UrlUtil.PG_CLASSES; + + // Note that this only loads these specific props, to avoid deadlock incurred by dereferencing class/subclass features + static _PROPS_RAWABLE = ["class", "subclass"]; + + async _pGetRawSiteData () { return DataUtil.class.loadRawJSON(); } + + async _pGetPostCacheData_obj ({obj, lockToken2}) { + if (!obj) return null; + + const out = {}; + + if (obj.raw_class?.length) out.class = await obj.raw_class.pSerialAwaitMap(cls => this.constructor._pGetDereferencedClassData(cls, {lockToken2})); + if (obj.raw_subclass?.length) out.subclass = await obj.raw_subclass.pSerialAwaitMap(sc => this.constructor._pGetDereferencedSubclassData(sc, {lockToken2})); + + return out; + } + + static _mutEntryNestLevel (feature) { + const depth = (feature.header == null ? 1 : feature.header) - 1; + for (let i = 0; i < depth; ++i) { + const nxt = MiscUtil.copyFast(feature); + feature.entries = [nxt]; + delete feature.name; + delete feature.page; + delete feature.source; + } + } + + static async _pGetDereferencedClassData (cls, {lockToken2}) { + // Gracefully handle legacy class data + if (cls.classFeatures && cls.classFeatures.every(it => typeof it !== "string" && !it.classFeature)) return cls; + + cls = MiscUtil.copyFast(cls); + + const byLevel = await this._pGetDereferencedClassSubclassData( + cls, + { + lockToken2, + propFeatures: "classFeatures", + propFeature: "classFeature", + fnUnpackUid: DataUtil.class.unpackUidClassFeature.bind(DataUtil.class), + fnIsInvalidUnpackedUid: ({name, className, level}) => !name || !className || !level || isNaN(level), + }, + ); + + cls.classFeatures = [...new Array(Math.max(0, ...Object.keys(byLevel).map(Number)))] + .map((_, i) => byLevel[i + 1] || []); + + return cls; + } + + static async _pGetDereferencedSubclassData (sc, {lockToken2}) { + // Gracefully handle legacy class data + if (sc.subclassFeatures && sc.subclassFeatures.every(it => typeof it !== "string" && !it.subclassFeature)) return sc; + + sc = MiscUtil.copyFast(sc); + + const byLevel = await this._pGetDereferencedClassSubclassData( + sc, + { + lockToken2, + propFeatures: "subclassFeatures", + propFeature: "subclassFeature", + fnUnpackUid: DataUtil.class.unpackUidSubclassFeature.bind(DataUtil.class), + fnIsInvalidUnpackedUid: ({name, className, subclassShortName, level}) => !name || !className || !subclassShortName || !level || isNaN(level), + }, + ); + + sc.subclassFeatures = Object.keys(byLevel) + .map(Number) + .sort(SortUtil.ascSort) + .map(k => byLevel[k]); + + return sc; + } + + static async _pGetDereferencedClassSubclassData ( + clsOrSc, + { + lockToken2, + propFeatures, + propFeature, + fnUnpackUid, + fnIsInvalidUnpackedUid, + }, + ) { + // Gracefully handle legacy data + if (clsOrSc[propFeatures] && clsOrSc[propFeatures].every(it => typeof it !== "string" && !it[propFeature])) return clsOrSc; + + clsOrSc = MiscUtil.copyFast(clsOrSc); + + const byLevel = {}; // Build a map of `level: [ ...feature... ]` + const notFoundUids = []; + + await (clsOrSc[propFeatures] || []) + .pSerialAwaitMap(async featureRef => { + const uid = featureRef[propFeature] ? featureRef[propFeature] : featureRef; + const unpackedUid = fnUnpackUid(uid); + const {source, displayText} = unpackedUid; + + // Skip over broken links + if (fnIsInvalidUnpackedUid(unpackedUid)) return; + + // Skip over temp/nonexistent links + if (source === Parser.SRC_5ETOOLS_TMP) return; + + const hash = UrlUtil.URL_TO_HASH_BUILDER[propFeature](unpackedUid); + + // Skip blocklisted + if (ExcludeUtil.isInitialised && ExcludeUtil.isExcluded(hash, propFeature, source, {isNoCount: true})) return; + + const feature = await DataLoader.pCacheAndGet(propFeature, source, hash, {isCopy: true, lockToken2}); + // Skip over missing links + if (!feature) return notFoundUids.push(uid); + + if (displayText) feature._displayName = displayText; + if (featureRef.tableDisplayName) feature._displayNameTable = featureRef.tableDisplayName; + + if (featureRef.gainSubclassFeature) feature.gainSubclassFeature = true; + if (featureRef.gainSubclassFeatureHasContent) feature.gainSubclassFeatureHasContent = true; + + if (clsOrSc.otherSources && clsOrSc.source === feature.source) feature.otherSources = MiscUtil.copyFast(clsOrSc.otherSources); + + this._mutEntryNestLevel(feature); + + (byLevel[feature.level || 1] = byLevel[feature.level || 1] || []).push(feature); + }); + + this._pGetDereferencedData_doNotifyFailed({ent: clsOrSc, uids: notFoundUids, prop: propFeature}); + + return byLevel; + } + + async pGetPostCacheData ({siteData = null, prereleaseData = null, brewData = null, lockToken2}) { + return { + siteDataPostCache: await this._pGetPostCacheData_obj_withCache({obj: siteData, lockToken2, propCache: "site"}), + prereleaseDataPostCache: await this._pGetPostCacheData_obj({obj: prereleaseData, lockToken2}), + brewDataPostCache: await this._pGetPostCacheData_obj({obj: brewData, lockToken2}), + }; + } +} + +class _DataTypeLoaderCustomClassSubclassFeature extends _DataTypeLoader { + static PROPS = ["raw_classFeature", "raw_subclassFeature", "classFeature", "subclassFeature"]; + static PAGE = UrlUtil.PG_CLASS_SUBCLASS_FEATURES; + + static _PROPS_RAWABLE = ["classFeature", "subclassFeature"]; + + hasPhase2Cache = true; + + _getSiteIdent ({pageClean, sourceClean}) { return `${pageClean}__${this.constructor.name}`; } + + async _pGetSiteData ({pageClean, sourceClean}) { + const json = await DataUtil.class.loadRawJSON(); + return this.constructor._getAsRawPrefixed(json, {propsRaw: this.constructor._PROPS_RAWABLE}); + } + + async _pGetStoredPrereleaseBrewData ({brewUtil, isPrerelease, isBrew}) { + const prereleaseBrew = await brewUtil.pGetBrewProcessed(); + return this.constructor._getAsRawPrefixed(prereleaseBrew, {propsRaw: this.constructor._PROPS_RAWABLE}); + } + + async _pGetPostCacheData_obj ({obj, lockToken2}) { + if (!obj) return null; + + const out = {}; + + if (obj.raw_classFeature?.length) out.classFeature = (await _DataLoaderDereferencer.pGetDereferenced(obj.raw_classFeature, "classFeature"))?.classFeature || []; + if (obj.raw_subclassFeature?.length) out.subclassFeature = (await _DataLoaderDereferencer.pGetDereferenced(obj.raw_subclassFeature, "subclassFeature"))?.subclassFeature || []; + + return out; + } + + async pGetPostCacheData ({siteData = null, prereleaseData = null, brewData = null, lockToken2}) { + return { + siteDataPostCache: await this._pGetPostCacheData_obj_withCache({obj: siteData, lockToken2, propCache: "site"}), + prereleaseDataPostCache: await this._pGetPostCacheData_obj({obj: prereleaseData, lockToken2}), + brewDataPostCache: await this._pGetPostCacheData_obj({obj: brewData, lockToken2}), + }; + } +} + +class _DataTypeLoaderCustomItem extends _DataTypeLoader { + static PROPS = [...UrlUtil.PAGE_TO_PROPS[UrlUtil.PG_ITEMS]]; + static PAGE = UrlUtil.PG_ITEMS; + + /** + * Avoid adding phase 1 items to the cache. Adding them as `raw_item` is inaccurate, as we have already e.g. merged + * generic variants, and enhanced the items. + * Adding them as `item` is also inaccurate, as we have yet to run our phase 2 post-processing to remove any + * `itemEntry` references. + * We could cache them under, say, `phase1_item`, but this would mean supporting `phase1_item` everywhere (has + * builders, etc.), polluting other areas with our implementation details. + * Therefore, cache only the essentials in phase 1. + */ + phase1CachePropAllowlist = new Set(["itemEntry"]); + + hasPhase2Cache = true; + + _getSiteIdent ({pageClean, sourceClean}) { return this.constructor.name; } + + async _pGetSiteData ({pageClean, sourceClean}) { + return Renderer.item.pGetSiteUnresolvedRefItems(); + } + + async _pGetStoredPrereleaseBrewData ({brewUtil, isPrerelease, isBrew}) { + const prereleaseBrewData = await brewUtil.pGetBrewProcessed(); + await this._pPrePopulate({data: prereleaseBrewData, isPrerelease, isBrew}); + return { + item: await Renderer.item.pGetSiteUnresolvedRefItemsFromPrereleaseBrew({brewUtil, brew: prereleaseBrewData}), + itemEntry: prereleaseBrewData.itemEntry || [], + }; + } + + async _pPrePopulate ({data, isPrerelease, isBrew}) { + Renderer.item.addPrereleaseBrewPropertiesAndTypesFrom({data}); + } + + async _pGetPostCacheData_obj ({siteData, obj, lockToken2}) { + if (!obj) return null; + + const out = {}; + + if (obj.item?.length) { + out.item = (await _DataLoaderDereferencer.pGetDereferenced(obj.item, "item", {propEntries: "entries", propIsRef: "hasRefs"}))?.item || []; + out.item = (await _DataLoaderDereferencer.pGetDereferenced(out.item, "item", {propEntries: "_fullEntries", propIsRef: "hasRefs"}))?.item || []; + } + + return out; + } + + async pGetPostCacheData ({siteData = null, prereleaseData = null, brewData = null, lockToken2}) { + return { + siteDataPostCache: await this._pGetPostCacheData_obj_withCache({obj: siteData, lockToken2, propCache: "site"}), + prereleaseDataPostCache: await this._pGetPostCacheData_obj({obj: prereleaseData, lockToken2}), + brewDataPostCache: await this._pGetPostCacheData_obj({obj: brewData, lockToken2}), + }; + } +} + +class _DataTypeLoaderCustomCard extends _DataTypeLoader { + static PROPS = ["card"]; + static PAGE = UrlUtil.PG_DECKS; + + _getSiteIdent ({pageClean, sourceClean}) { return `${pageClean}__${this.constructor.name}`; } + + async _pGetSiteData ({pageClean, sourceClean}) { + const json = await DataUtil.deck.loadRawJSON(); + return {card: json.card}; + } +} + +class _DataTypeLoaderCustomDeck extends _DataTypeLoaderCustomRawable { + static PROPS = ["raw_deck", "deck"]; + static PAGE = UrlUtil.PG_DECKS; + + static _PROPS_RAWABLE = ["deck"]; + + async _pGetRawSiteData () { return DataUtil.deck.loadRawJSON(); } + + async _pGetPostCacheData_obj ({obj, lockToken2}) { + if (!obj) return null; + + const out = {}; + + if (obj.raw_deck?.length) out.deck = await obj.raw_deck.pSerialAwaitMap(ent => this.constructor._pGetDereferencedDeckData(ent, {lockToken2})); + + return out; + } + + static async _pGetDereferencedDeckData (deck, {lockToken2}) { + deck = MiscUtil.copyFast(deck); + + deck.cards = await this._pGetDereferencedCardData(deck, {lockToken2}); + + return deck; + } + + static async _pGetDereferencedCardData (deck, {lockToken2}) { + const notFoundUids = []; + + const out = (await (deck.cards || []) + .pSerialAwaitMap(async cardMeta => { + const uid = typeof cardMeta === "string" ? cardMeta : cardMeta.uid; + const count = typeof cardMeta === "string" ? 1 : cardMeta.count ?? 1; + const isReplacement = typeof cardMeta === "string" ? false : cardMeta.replacement ?? false; + + const unpackedUid = DataUtil.deck.unpackUidCard(uid); + const {source} = unpackedUid; + + // Skip over broken links + if (unpackedUid.name == null || unpackedUid.set == null || unpackedUid.source == null) return; + + const hash = UrlUtil.URL_TO_HASH_BUILDER["card"](unpackedUid); + + // Skip blocklisted + if (ExcludeUtil.isInitialised && ExcludeUtil.isExcluded(hash, "card", source, {isNoCount: true})) return; + + const card = await DataLoader.pCacheAndGet("card", source, hash, {isCopy: true, lockToken2}); + // Skip over missing links + if (!card) return notFoundUids.push(uid); + + if (deck.otherSources && deck.source === card.source) card.otherSources = MiscUtil.copyFast(deck.otherSources); + if (isReplacement) card._isReplacement = true; + + return [...new Array(count)].map(() => MiscUtil.copyFast(card)); + })) + .flat() + .filter(Boolean); + + this._pGetDereferencedData_doNotifyFailed({ent: deck, uids: notFoundUids, prop: "card"}); + + return out; + } + + async pGetPostCacheData ({siteData = null, prereleaseData = null, brewData = null, lockToken2}) { + return { + siteDataPostCache: await this._pGetPostCacheData_obj_withCache({obj: siteData, lockToken2, propCache: "site"}), + prereleaseDataPostCache: await this._pGetPostCacheData_obj({obj: prereleaseData, lockToken2}), + brewDataPostCache: await this._pGetPostCacheData_obj({obj: brewData, lockToken2}), + }; + } +} + +class _DataTypeLoaderCustomQuickref extends _DataTypeLoader { + static PROPS = ["reference", "referenceData"]; + static PAGE = UrlUtil.PG_QUICKREF; + + _getSiteIdent ({pageClean, sourceClean}) { return this.constructor.name; } + + _isPrereleaseAvailable () { return false; } + + _isBrewAvailable () { return false; } + + async _pGetSiteData ({pageClean, sourceClean}) { + const json = await DataUtil.loadJSON(`${Renderer.get().baseUrl}data/generated/bookref-quick.json`); + return { + reference: json.reference["bookref-quick"], + referenceData: json.data["bookref-quick"], + }; + } + + hasCustomCacheStrategy ({obj}) { return this.constructor.PROPS.some(prop => obj[prop]?.length); } + + addToCacheCustom ({cache, obj}) { + obj.referenceData.forEach((chapter, ixChapter) => this._addToCacheCustom_chapter({cache, chapter, ixChapter})); + return [...this.constructor.PROPS]; + } + + _addToCacheCustom_chapter ({cache, chapter, ixChapter}) { + const metas = IndexableFileQuickReference.getChapterNameMetas(chapter, {isRequireQuickrefFlag: false}); + + metas.forEach(nameMeta => { + const hashParts = [ + "bookref-quick", + ixChapter, + UrlUtil.encodeForHash(nameMeta.name.toLowerCase()), + ]; + if (nameMeta.ixBook) hashParts.push(nameMeta.ixBook); + + const hash = hashParts.join(HASH_PART_SEP); + + const {page: pageClean, source: sourceClean, hash: hashClean} = _DataLoaderInternalUtil.getCleanPageSourceHash({ + page: UrlUtil.PG_QUICKREF, + source: nameMeta.source, + hash, + }); + cache.set(pageClean, sourceClean, hashClean, nameMeta.entry); + + if (nameMeta.ixBook) return; + + // region Add the hash with the redundant `0` header included + hashParts.push(nameMeta.ixBook); + const hashAlt = hashParts.join(HASH_PART_SEP); + const hashAltClean = _DataLoaderInternalUtil.getCleanHash({hash: hashAlt}); + cache.set(pageClean, sourceClean, hashAltClean, nameMeta.entry); + // endregion + }); + } +} + +class _DataTypeLoaderCustomAdventureBook extends _DataTypeLoader { + _filename; + + _getSiteIdent ({pageClean, sourceClean}) { return `${pageClean}__${sourceClean}`; } + + hasCustomCacheStrategy ({obj}) { return this.constructor.PROPS.some(prop => obj[prop]?.length); } + + addToCacheCustom ({cache, obj}) { + const [prop, propData] = this.constructor.PROPS; + + // Get only the ids that exist in both data + contents + const dataIds = (obj[propData] || []).filter(it => it.id).map(it => it.id); + const contentsIds = new Set((obj[prop] || []).filter(it => it.id).map(it => it.id)); + const matchingIds = dataIds.filter(id => contentsIds.has(id)); + + matchingIds.forEach(id => { + const data = (obj[propData] || []).find(it => it.id === id); + const contents = (obj[prop] || []).find(it => it.id === id); + + const hash = UrlUtil.URL_TO_HASH_BUILDER[this.constructor.PAGE](contents); + this._addImageBackReferences(data, this.constructor.PAGE, contents.source, hash); + + const {page: pageClean, source: sourceClean, hash: hashClean} = _DataLoaderInternalUtil.getCleanPageSourceHash({ + page: this.constructor.PAGE, + source: contents.source, + hash, + }); + + const pack = { + [prop]: contents, + [propData]: data, + }; + + cache.set(pageClean, sourceClean, hashClean, pack); + }); + + return [prop, propData]; + } + + async _pGetSiteData ({pageClean, sourceClean}) { + const [prop, propData] = this.constructor.PROPS; + + const index = await DataUtil.loadJSON(`${Renderer.get().baseUrl}data/${this._filename}`); + const contents = index[prop].find(contents => _DataLoaderInternalUtil.getCleanSource({source: contents.source}) === sourceClean); + + if (!contents) return {}; + + const json = await DataUtil.loadJSON(`${Renderer.get().baseUrl}data/${prop}/${prop}-${UrlUtil.encodeForHash(contents.id.toLowerCase())}.json`); + + return { + [prop]: [contents], + [propData]: [ + { + source: contents.source, + id: contents.id, + ...json, + }, + ], + }; + } + + _addImageBackReferences (json, page, source, hash) { + if (!json) return; + + const walker = MiscUtil.getWalker({keyBlocklist: MiscUtil.GENERIC_WALKER_ENTRIES_KEY_BLOCKLIST, isNoModification: true}); + walker.walk( + json, + { + object: (obj) => { + if (obj.type === "image" && obj.mapRegions) { + obj.page = obj.page || page; + obj.source = obj.source || source; + obj.hash = obj.hash || hash; + } + }, + }, + ); + } +} + +class _DataTypeLoaderCustomAdventure extends _DataTypeLoaderCustomAdventureBook { + static PROPS = ["adventure", "adventureData"]; + static PAGE = UrlUtil.PG_ADVENTURE; + + _filename = "adventures.json"; +} + +class _DataTypeLoaderCustomBook extends _DataTypeLoaderCustomAdventureBook { + static PROPS = ["book", "bookData"]; + static PAGE = UrlUtil.PG_BOOK; + + _filename = "books.json"; +} + +class _DataTypeLoaderCitation extends _DataTypeLoader { + static PROPS = ["citation"]; + + _getSiteIdent ({pageClean, sourceClean}) { return this.constructor.name; } + + async _pGetSiteData ({pageClean, sourceClean}) { + return {citation: []}; + } +} + +// endregion + +/* -------------------------------------------- */ + +// region Data loader + +class DataLoader { + static _PROP_TO_HASH_PAGE = { + "monster": UrlUtil.PG_BESTIARY, + "spell": UrlUtil.PG_SPELLS, + "class": UrlUtil.PG_CLASSES, + "subclass": UrlUtil.PG_CLASSES, + "item": UrlUtil.PG_ITEMS, + "background": UrlUtil.PG_BACKGROUNDS, + "psionic": UrlUtil.PG_PSIONICS, + "object": UrlUtil.PG_OBJECTS, + "action": UrlUtil.PG_ACTIONS, + "trap": UrlUtil.PG_TRAPS_HAZARDS, + "hazard": UrlUtil.PG_TRAPS_HAZARDS, + "cult": UrlUtil.PG_CULTS_BOONS, + "boon": UrlUtil.PG_CULTS_BOONS, + "condition": UrlUtil.PG_CONDITIONS_DISEASES, + "deck": UrlUtil.PG_DECKS, + "disease": UrlUtil.PG_CONDITIONS_DISEASES, + "status": UrlUtil.PG_CONDITIONS_DISEASES, + "vehicle": UrlUtil.PG_VEHICLES, + "vehicleUpgrade": UrlUtil.PG_VEHICLES, + "feat": UrlUtil.PG_FEATS, + "optionalfeature": UrlUtil.PG_OPT_FEATURES, + "reward": UrlUtil.PG_REWARDS, + "charoption": UrlUtil.PG_CHAR_CREATION_OPTIONS, + "race": UrlUtil.PG_RACES, + "subrace": UrlUtil.PG_RACES, + "deity": UrlUtil.PG_DEITIES, + "variantrule": UrlUtil.PG_VARIANTRULES, + "table": UrlUtil.PG_TABLES, + "tableGroup": UrlUtil.PG_TABLES, + "language": UrlUtil.PG_LANGUAGES, + "recipe": UrlUtil.PG_RECIPES, + "classFeature": UrlUtil.PG_CLASS_SUBCLASS_FEATURES, + "subclassFeature": UrlUtil.PG_CLASS_SUBCLASS_FEATURES, + "reference": UrlUtil.PG_QUICKREF, + "referenceData": UrlUtil.PG_QUICKREF, + "adventure": UrlUtil.PG_ADVENTURE, + "adventureData": UrlUtil.PG_ADVENTURE, + "book": UrlUtil.PG_BOOK, + "bookData": UrlUtil.PG_BOOK, + }; + + static _DATA_TYPE_LOADERS = {}; + static _DATA_TYPE_LOADER_LIST = []; + + static _init () { + this._registerPropToHashPages(); + this._registerDataTypeLoaders(); + return null; + } + + static _registerPropToHashPages () { + Object.entries(this._PROP_TO_HASH_PAGE) + .forEach(([k, v]) => this._PROP_TO_HASH_PAGE[`${k}Fluff`] = _DataLoaderInternalUtil.getCleanPageFluff({page: v})); + } + + static _registerDataTypeLoader ({loader, props, page, isFluff}) { + this._DATA_TYPE_LOADER_LIST.push(loader); + + if (!props?.length) throw new Error(`No "props" specified for loader "${loader.constructor.name}"!`); + + props.forEach(prop => this._DATA_TYPE_LOADERS[_DataLoaderInternalUtil.getCleanPage({page: prop})] = loader); + + if (!page) return; + + this._DATA_TYPE_LOADERS[ + isFluff + ? _DataLoaderInternalUtil.getCleanPageFluff({page}) + : _DataLoaderInternalUtil.getCleanPage({page}) + ] = loader; + } + + static _registerDataTypeLoaders () { + const fnRegister = this._registerDataTypeLoader.bind(this); + + // region Multi-file + _DataTypeLoaderCustomMonster.register({fnRegister}); + _DataTypeLoaderCustomMonsterFluff.register({fnRegister}); + _DataTypeLoaderCustomSpell.register({fnRegister}); + _DataTypeLoaderCustomSpellFluff.register({fnRegister}); + // endregion + + // region Predefined + _DataTypeLoaderRace.register({fnRegister}); + _DataTypeLoaderDeity.register({fnRegister}); + _DataTypeLoaderVariantrule.register({fnRegister}); + _DataTypeLoaderTable.register({fnRegister}); + _DataTypeLoaderLanguage.register({fnRegister}); + _DataTypeLoaderRecipe.register({fnRegister}); + // endregion + + // region Special + _DataTypeLoaderCustomClassesSubclass.register({fnRegister}); + _DataTypeLoaderCustomClassSubclassFeature.register({fnRegister}); + _DataTypeLoaderCustomItem.register({fnRegister}); + _DataTypeLoaderCustomCard.register({fnRegister}); + _DataTypeLoaderCustomDeck.register({fnRegister}); + _DataTypeLoaderCustomQuickref.register({fnRegister}); + _DataTypeLoaderCustomAdventure.register({fnRegister}); + _DataTypeLoaderCustomBook.register({fnRegister}); + // endregion + + // region Single file + _DataTypeLoaderBackground.register({fnRegister}); + _DataTypeLoaderPsionic.register({fnRegister}); + _DataTypeLoaderObject.register({fnRegister}); + _DataTypeLoaderAction.register({fnRegister}); + _DataTypeLoaderFeat.register({fnRegister}); + _DataTypeLoaderOptionalfeature.register({fnRegister}); + _DataTypeLoaderReward.register({fnRegister}); + _DataTypeLoaderCharoption.register({fnRegister}); + + _DataTypeLoaderTrapHazard.register({fnRegister}); + _DataTypeLoaderCultBoon.register({fnRegister}); + _DataTypeLoaderVehicle.register({fnRegister}); + + _DataTypeLoaderConditionDisease.register({fnRegister}); + + _DataTypeLoaderSkill.register({fnRegister}); + _DataTypeLoaderSense.register({fnRegister}); + _DataTypeLoaderLegendaryGroup.register({fnRegister}); + _DataTypeLoaderItemEntry.register({fnRegister}); + _DataTypeLoaderItemMastery.register({fnRegister}); + _DataTypeLoaderCitation.register({fnRegister}); + // endregion + + // region Fluff + _DataTypeLoaderBackgroundFluff.register({fnRegister}); + _DataTypeLoaderFeatFluff.register({fnRegister}); + _DataTypeLoaderOptionalfeatureFluff.register({fnRegister}); + _DataTypeLoaderItemFluff.register({fnRegister}); + _DataTypeLoaderRaceFluff.register({fnRegister}); + _DataTypeLoaderLanguageFluff.register({fnRegister}); + _DataTypeLoaderVehicleFluff.register({fnRegister}); + _DataTypeLoaderObjectFluff.register({fnRegister}); + _DataTypeLoaderCharoptionFluff.register({fnRegister}); + _DataTypeLoaderRecipeFluff.register({fnRegister}); + + _DataTypeLoaderConditionDiseaseFluff.register({fnRegister}); + _DataTypeLoaderTrapHazardFluff.register({fnRegister}); + // endregion + } + + static _ = this._init(); + + static _CACHE = new _DataLoaderCache(); + static _LOCK_1 = new VeLock({isDbg: false, name: "loader-lock-1"}); + static _LOCK_2 = new VeLock({isDbg: false, name: "loader-lock-2"}); + + /* -------------------------------------------- */ + + /** + * @param page + * @param source + * @param hash + * @param [isCopy] If a copy, rather than the original entity, should be returned. + * @param [isRequired] If an error should be thrown on a missing entity. + * @param [_isReturnSentinel] If a null sentinel should be returned, if it exists. + * @param [_isInsertSentinelOnMiss] If a null sentinel should be inserted on cache miss. + */ + static getFromCache ( + page, + source, + hash, + { + isCopy = false, + isRequired = false, + _isReturnSentinel = false, + _isInsertSentinelOnMiss = false, + } = {}, + ) { + const {page: pageClean, source: sourceClean, hash: hashClean} = _DataLoaderInternalUtil.getCleanPageSourceHash({page, source, hash}); + const ent = this._getFromCache({pageClean, sourceClean, hashClean, isCopy, _isReturnSentinel, _isInsertSentinelOnMiss}); + return this._getVerifiedRequiredEntity({pageClean, sourceClean, hashClean, ent, isRequired}); + } + + static _getFromCache ( + { + pageClean, + sourceClean, + hashClean, + isCopy = false, + _isInsertSentinelOnMiss = false, + _isReturnSentinel = false, + }, + ) { + const out = this._CACHE.get(pageClean, sourceClean, hashClean); + + if (out === _DataLoaderConst.ENTITY_NULL) { + if (_isReturnSentinel) return out; + if (!_isReturnSentinel) return null; + } + + if (out == null && _isInsertSentinelOnMiss) { + this._CACHE.set(pageClean, sourceClean, hashClean, _DataLoaderConst.ENTITY_NULL); + } + + if (!isCopy || out == null) return out; + return MiscUtil.copyFast(out); + } + + /* -------------------------------------------- */ + + static _getVerifiedRequiredEntity ({pageClean, sourceClean, hashClean, ent, isRequired}) { + if (ent || !isRequired) return ent; + throw new Error(`Could not find entity for page/prop "${pageClean}" with source "${sourceClean}" and hash "${hashClean}"`); + } + + /* -------------------------------------------- */ + + static async pCacheAndGetAllSite (page, {isSilent = false} = {}) { + const pageClean = _DataLoaderInternalUtil.getCleanPage({page}); + + if (this._PAGES_NO_CONTENT.has(pageClean)) return null; + + const dataLoader = this._pCache_getDataTypeLoader({pageClean, isSilent}); + if (!dataLoader) return null; + + // (Avoid preloading missing brew here, as we only return site data.) + + const {siteData} = await this._pCacheAndGet_getCacheMeta({pageClean, sourceClean: _DataLoaderConst.SOURCE_SITE_ALL, dataLoader}); + await this._pCacheAndGet_processCacheMeta({dataLoader, siteData}); + + return this._CACHE.getAllSite(pageClean); + } + + static async pCacheAndGetAllPrerelease (page, {isSilent = false} = {}) { + return this._CacheAndGetAllPrerelease.pCacheAndGetAll({parent: this, page, isSilent}); + } + + static async pCacheAndGetAllBrew (page, {isSilent = false} = {}) { + return this._CacheAndGetAllBrew.pCacheAndGetAll({parent: this, page, isSilent}); + } + + static _CacheAndGetAllPrereleaseBrew = class { + static _SOURCE_ALL; + static _PROP_DATA; + + static async pCacheAndGetAll ( + { + parent, + page, + isSilent, + }, + ) { + const pageClean = _DataLoaderInternalUtil.getCleanPage({page}); + + if (parent._PAGES_NO_CONTENT.has(pageClean)) return null; + + const dataLoader = parent._pCache_getDataTypeLoader({pageClean, isSilent}); + if (!dataLoader) return null; + + // (Avoid preloading missing prerelease/homebrew here, as we only return currently-loaded prerelease/homebrew.) + + const cacheMeta = await parent._pCacheAndGet_getCacheMeta({pageClean, sourceClean: this._SOURCE_ALL, dataLoader}); + await parent._pCacheAndGet_processCacheMeta({dataLoader, [this._PROP_DATA]: cacheMeta[this._PROP_DATA]}); + + return this._getAllCached({parent, pageClean}); + } + + /** @abstract */ + static _getAllCached ({parent, pageClean}) { throw new Error("Unimplemented!"); } + }; + + static _CacheAndGetAllPrerelease = class extends this._CacheAndGetAllPrereleaseBrew { + static _SOURCE_ALL = _DataLoaderConst.SOURCE_PRERELEASE_ALL_CURRENT; + static _PROP_DATA = "prereleaseData"; + + static _getAllCached ({parent, pageClean}) { return parent._CACHE.getAllPrerelease(pageClean); } + }; + + static _CacheAndGetAllBrew = class extends this._CacheAndGetAllPrereleaseBrew { + static _SOURCE_ALL = _DataLoaderConst.SOURCE_BREW_ALL_CURRENT; + static _PROP_DATA = "brewData"; + + static _getAllCached ({parent, pageClean}) { return parent._CACHE.getAllBrew(pageClean); } + }; + + /* -------------------------------------------- */ + + static _PAGES_NO_CONTENT = new Set([ + _DataLoaderInternalUtil.getCleanPage({page: "generic"}), + _DataLoaderInternalUtil.getCleanPage({page: "hover"}), + ]); + + /** + * @param page + * @param source + * @param hash + * @param [isCopy] If a copy, rather than the original entity, should be returned. + * @param [isRequired] If an error should be thrown on a missing entity. + * @param [isSilent] If errors should not be thrown on a missing implementation. + * @param [lockToken2] Post-process lock token for recursive calls. + */ + static async pCacheAndGet (page, source, hash, {isCopy = false, isRequired = false, isSilent = false, lockToken2} = {}) { + const fromCache = this.getFromCache(page, source, hash, {isCopy, _isReturnSentinel: true}); + if (fromCache === _DataLoaderConst.ENTITY_NULL) return null; + if (fromCache) return fromCache; + + const {page: pageClean, source: sourceClean, hash: hashClean} = _DataLoaderInternalUtil.getCleanPageSourceHash({page, source, hash}); + + if (this._PAGES_NO_CONTENT.has(pageClean)) return this._getVerifiedRequiredEntity({pageClean, sourceClean, hashClean, ent: null, isRequired}); + + const dataLoader = this._pCache_getDataTypeLoader({pageClean, isSilent}); + if (!dataLoader) return this._getVerifiedRequiredEntity({pageClean, sourceClean, hashClean, ent: null, isRequired}); + + const isUnavailablePrerelease = await this._PrereleasePreloader._pPreloadMissing({parent: this, sourceClean}); + if (isUnavailablePrerelease) return this._getVerifiedRequiredEntity({pageClean, sourceClean, hashClean, ent: null, isRequired}); + + const isUnavailableBrew = await this._BrewPreloader._pPreloadMissing({parent: this, sourceClean}); + if (isUnavailableBrew) return this._getVerifiedRequiredEntity({pageClean, sourceClean, hashClean, ent: null, isRequired}); + + const {siteData = null, prereleaseData = null, brewData = null} = await this._pCacheAndGet_getCacheMeta({pageClean, sourceClean, dataLoader}); + await this._pCacheAndGet_processCacheMeta({dataLoader, siteData, prereleaseData, brewData, lockToken2}); + + return this.getFromCache(page, source, hash, {isCopy, _isInsertSentinelOnMiss: true}); + } + + static async pCacheAndGetHash (page, hash, opts) { + const source = UrlUtil.decodeHash(hash).last(); + return DataLoader.pCacheAndGet(page, source, hash, opts); + } + + static _PrereleaseBrewPreloader = class { + static _LOCK_0; + static _SOURCES_ATTEMPTED; + /** Cache of clean (lowercase) source -> URL. */ + static _CACHE_SOURCE_CLEAN_TO_URL; + static _SOURCE_ALL; + + /** + * Phase 0: check if prerelease/homebrew, and if so, check/load the source (if available). + * Track failures (i.e., there is no available JSON for the source requested), and skip repeated failures. + * This allows us to avoid an expensive mass re-cache, if a source which does not exist is requested for + * loading multiple times. + */ + static async pPreloadMissing ({parent, sourceClean}) { + try { + await this._LOCK_0.pLock(); + return (await this._pPreloadMissing({parent, sourceClean})); + } finally { + this._LOCK_0.unlock(); + } + } + + /** + * @param parent + * @param sourceClean + * @return {Promise} `true` if the source does not exist and could not be loaded, false otherwise. + */ + static async _pPreloadMissing ({parent, sourceClean}) { + if (this._isExistingMiss({parent, sourceClean})) return true; + + if (!this._isPossibleSource({parent, sourceClean})) return false; + if (sourceClean === this._SOURCE_ALL) return false; + + const brewUtil = this._getBrewUtil(); + if (!brewUtil) { + this._setExistingMiss({parent, sourceClean}); + return true; + } + + if (brewUtil.hasSourceJson(sourceClean)) return false; + + const urlBrew = await this._pGetSourceUrl({parent, sourceClean}); + if (!urlBrew) { + this._setExistingMiss({parent, sourceClean}); + return true; + } + + await brewUtil.pAddBrewFromUrl(urlBrew); + return false; + } + + static _isExistingMiss ({sourceClean}) { + return this._SOURCES_ATTEMPTED.has(sourceClean); + } + + static _setExistingMiss ({sourceClean}) { + this._SOURCES_ATTEMPTED.add(sourceClean); + } + + /* -------------------------------------------- */ + + static async _pInitCacheSourceToUrl () { + if (this._CACHE_SOURCE_CLEAN_TO_URL) return; + + const index = await this._pGetUrlIndex(); + if (!index) return this._CACHE_SOURCE_CLEAN_TO_URL = {}; + + const brewUtil = this._getBrewUtil(); + const urlRoot = await brewUtil.pGetCustomUrl(); + + this._CACHE_SOURCE_CLEAN_TO_URL = Object.entries(index) + .mergeMap(([src, url]) => ({[_DataLoaderInternalUtil.getCleanSource({source: src})]: brewUtil.getFileUrl(url, urlRoot)})); + } + + static async _pGetUrlIndex () { + try { + return (await this._pGetSourceIndex()); + } catch (e) { + setTimeout(() => { throw e; }); + return null; + } + } + + static async _pGetSourceUrl ({sourceClean}) { + await this._pInitCacheSourceToUrl(); + return this._CACHE_SOURCE_CLEAN_TO_URL[sourceClean]; + } + + /** @abstract */ + static _isPossibleSource ({parent, sourceClean}) { throw new Error("Unimplemented!"); } + /** @abstract */ + static _getBrewUtil () { throw new Error("Unimplemented!"); } + /** @abstract */ + static _pGetSourceIndex () { throw new Error("Unimplemented!"); } + }; + + static _PrereleasePreloader = class extends this._PrereleaseBrewPreloader { + static _LOCK_0 = new VeLock({isDbg: false, name: "loader-lock-0--prerelease"}); + static _SOURCE_ALL = _DataLoaderConst.SOURCE_BREW_ALL_CURRENT; + static _SOURCES_ATTEMPTED = new Set(); + static _CACHE_SOURCE_CLEAN_TO_URL = null; + + static _isPossibleSource ({parent, sourceClean}) { return parent._isPrereleaseSource({sourceClean}) && !Parser.SOURCE_JSON_TO_FULL[Parser.sourceJsonToJson(sourceClean)]; } + static _getBrewUtil () { return typeof PrereleaseUtil !== "undefined" ? PrereleaseUtil : null; } + static async _pGetSourceIndex () { return DataUtil.prerelease.pLoadSourceIndex(await PrereleaseUtil.pGetCustomUrl()); } + }; + + static _BrewPreloader = class extends this._PrereleaseBrewPreloader { + static _LOCK_0 = new VeLock({isDbg: false, name: "loader-lock-0--brew"}); + static _SOURCE_ALL = _DataLoaderConst.SOURCE_PRERELEASE_ALL_CURRENT; + static _SOURCES_ATTEMPTED = new Set(); + static _CACHE_SOURCE_CLEAN_TO_URL = null; + + static _isPossibleSource ({parent, sourceClean}) { return !parent._isSiteSource({sourceClean}) && !parent._isPrereleaseSource({sourceClean}); } + static _getBrewUtil () { return typeof BrewUtil2 !== "undefined" ? BrewUtil2 : null; } + static async _pGetSourceIndex () { return DataUtil.brew.pLoadSourceIndex(await BrewUtil2.pGetCustomUrl()); } + }; + + static async _pCacheAndGet_getCacheMeta ({pageClean, sourceClean, dataLoader}) { + try { + await this._LOCK_1.pLock(); + return (await this._pCache({pageClean, sourceClean, dataLoader})); + } finally { + this._LOCK_1.unlock(); + } + } + + static async _pCache ({pageClean, sourceClean, dataLoader}) { + // region Fetch from site data + const siteData = await dataLoader.pGetSiteData({pageClean, sourceClean}); + this._pCache_addToCache({allDataMerged: siteData, propAllowlist: dataLoader.phase1CachePropAllowlist || new Set(dataLoader.constructor.PROPS)}); + // Always early-exit, regardless of whether the entity was found in the cache, if we know this is a site source + if (this._isSiteSource({sourceClean})) return {siteData}; + // endregion + + const out = {siteData}; + + // region Fetch from already-stored prerelease/brew data + // As we have preloaded missing prerelease/brew earlier in the flow, we know that a prerelease/brew is either + // present, or unavailable + if (typeof PrereleaseUtil !== "undefined") { + const prereleaseData = await dataLoader.pGetStoredPrereleaseData(); + this._pCache_addToCache({allDataMerged: prereleaseData, propAllowlist: dataLoader.phase1CachePropAllowlist || new Set(dataLoader.constructor.PROPS)}); + out.prereleaseData = prereleaseData; + } + + if (typeof BrewUtil2 !== "undefined") { + const brewData = await dataLoader.pGetStoredBrewData(); + this._pCache_addToCache({allDataMerged: brewData, propAllowlist: dataLoader.phase1CachePropAllowlist || new Set(dataLoader.constructor.PROPS)}); + out.brewData = brewData; + } + // endregion + + return out; + } + + static async _pCacheAndGet_processCacheMeta ({dataLoader, siteData = null, prereleaseData = null, brewData = null, lockToken2 = null}) { + if (!dataLoader.hasPhase2Cache) return; + + try { + lockToken2 = await this._LOCK_2.pLock({token: lockToken2}); + await this._pCacheAndGet_processCacheMeta_({dataLoader, siteData, prereleaseData, brewData, lockToken2}); + } finally { + this._LOCK_2.unlock(); + } + } + + static async _pCacheAndGet_processCacheMeta_ ({dataLoader, siteData = null, prereleaseData = null, brewData = null, lockToken2 = null}) { + const {siteDataPostCache, prereleaseDataPostCache, brewDataPostCache} = await dataLoader.pGetPostCacheData({siteData, prereleaseData, brewData, lockToken2}); + + this._pCache_addToCache({allDataMerged: siteDataPostCache, propAllowlist: dataLoader.phase2CachePropAllowlist || new Set(dataLoader.constructor.PROPS)}); + this._pCache_addToCache({allDataMerged: prereleaseDataPostCache, propAllowlist: dataLoader.phase2CachePropAllowlist || new Set(dataLoader.constructor.PROPS)}); + this._pCache_addToCache({allDataMerged: brewDataPostCache, propAllowlist: dataLoader.phase2CachePropAllowlist || new Set(dataLoader.constructor.PROPS)}); + } + + static _pCache_getDataTypeLoader ({pageClean, isSilent}) { + const dataLoader = this._DATA_TYPE_LOADERS[pageClean]; + if (!dataLoader && !isSilent) throw new Error(`No loading strategy found for page "${pageClean}"!`); + return dataLoader; + } + + static _pCache_addToCache ({allDataMerged, propAllowlist}) { + if (!allDataMerged) return; + + allDataMerged = {...allDataMerged}; + + this._DATA_TYPE_LOADER_LIST + .filter(loader => loader.hasCustomCacheStrategy({obj: allDataMerged})) + .forEach(loader => { + const propsToRemove = loader.addToCacheCustom({cache: this._CACHE, obj: allDataMerged}); + propsToRemove.forEach(prop => delete allDataMerged[prop]); + }); + + Object.keys(allDataMerged) + .forEach(prop => { + if (!propAllowlist.has(prop)) return; + + const arr = allDataMerged[prop]; + if (!arr?.length || !(arr instanceof Array)) return; + + const hashBuilder = UrlUtil.URL_TO_HASH_BUILDER[prop]; + if (!hashBuilder) return; + + arr.forEach(ent => { + this._pCache_addEntityToCache({prop, hashBuilder, ent}); + DataUtil.proxy.getVersions(prop, ent) + .forEach(entVer => this._pCache_addEntityToCache({prop, hashBuilder, ent: entVer})); + }); + }); + } + + static _pCache_addEntityToCache ({prop, hashBuilder, ent}) { + ent.__prop = ent.__prop || prop; + + const page = this._PROP_TO_HASH_PAGE[prop]; + const source = SourceUtil.getEntitySource(ent); // + const hash = hashBuilder(ent); + + const {page: propClean, source: sourceClean, hash: hashClean} = _DataLoaderInternalUtil.getCleanPageSourceHash({page: prop, source, hash}); + const pageClean = page ? _DataLoaderInternalUtil.getCleanPage({page}) : null; + + this._CACHE.set(propClean, sourceClean, hashClean, ent); + if (pageClean) this._CACHE.set(pageClean, sourceClean, hashClean, ent); + } + + /* -------------------------------------------- */ + + static _CACHE_SITE_SOURCE_CLEAN = null; + + static _doBuildSourceCaches () { + this._CACHE_SITE_SOURCE_CLEAN = this._CACHE_SITE_SOURCE_CLEAN || new Set(Object.keys(Parser.SOURCE_JSON_TO_FULL) + .map(src => _DataLoaderInternalUtil.getCleanSource({source: src}))); + } + + static _isSiteSource ({sourceClean}) { + if (sourceClean === _DataLoaderConst.SOURCE_SITE_ALL) return true; + if (sourceClean === _DataLoaderConst.SOURCE_BREW_ALL_CURRENT) return false; + if (sourceClean === _DataLoaderConst.SOURCE_PRERELEASE_ALL_CURRENT) return false; + + this._doBuildSourceCaches(); + + return this._CACHE_SITE_SOURCE_CLEAN.has(sourceClean); + } + + static _isPrereleaseSource ({sourceClean}) { + if (sourceClean === _DataLoaderConst.SOURCE_SITE_ALL) return false; + if (sourceClean === _DataLoaderConst.SOURCE_BREW_ALL_CURRENT) return false; + if (sourceClean === _DataLoaderConst.SOURCE_PRERELEASE_ALL_CURRENT) return true; + + this._doBuildSourceCaches(); + + return sourceClean.startsWith(_DataLoaderInternalUtil.getCleanSource({source: Parser.SRC_UA_PREFIX})) + || sourceClean.startsWith(_DataLoaderInternalUtil.getCleanSource({source: Parser.SRC_UA_ONE_PREFIX})); + } + + /* -------------------------------------------- */ + + static getDiagnosticsSummary (diagnostics) { + diagnostics = diagnostics.filter(Boolean); + if (!diagnostics.length) return ""; + + const filenames = diagnostics + .map(it => it.filename) + .filter(Boolean) + .unique() + .sort(SortUtil.ascSortLower); + + if (!filenames.length) return ""; + + return `Filename${filenames.length === 1 ? " was" : "s were"}: ${filenames.map(it => `"${it}"`).join("; ")}.`; + } +} + +// endregion + +/* -------------------------------------------- */ + +// region Exports + +globalThis.DataLoader = DataLoader; + +// endregion diff --git a/charbuilder/js/vetools/utils.js b/charbuilder/js/vetools/utils.js new file mode 100644 index 0000000..c3fa60e --- /dev/null +++ b/charbuilder/js/vetools/utils.js @@ -0,0 +1,7570 @@ +"use strict"; + +// in deployment, `IS_DEPLOYED = "";` should be set below. +globalThis.IS_DEPLOYED = undefined; +globalThis.VERSION_NUMBER = /* 5ETOOLS_VERSION__OPEN */"1.197.4"/* 5ETOOLS_VERSION__CLOSE */; +globalThis.DEPLOYED_IMG_ROOT = undefined; +// for the roll20 script to set +globalThis.IS_VTT = false; + +globalThis.IMGUR_CLIENT_ID = `abdea4de492d3b0`; + +// TODO refactor into VeCt +globalThis.HASH_PART_SEP = ","; +globalThis.HASH_LIST_SEP = "_"; +globalThis.HASH_SUB_LIST_SEP = "~"; +globalThis.HASH_SUB_KV_SEP = ":"; +globalThis.HASH_BLANK = "blankhash"; +globalThis.HASH_SUB_NONE = "null"; + +globalThis.VeCt = { + STR_NONE: "None", + STR_SEE_CONSOLE: "See the console (CTRL+SHIFT+J) for details.", + + HASH_SCALED: "scaled", + HASH_SCALED_SPELL_SUMMON: "scaledspellsummon", + HASH_SCALED_CLASS_SUMMON: "scaledclasssummon", + + FILTER_BOX_SUB_HASH_SEARCH_PREFIX: "fbsr", + + JSON_PRERELEASE_INDEX: `prerelease/index.json`, + JSON_BREW_INDEX: `homebrew/index.json`, + + STORAGE_HOMEBREW: "HOMEBREW_STORAGE", + STORAGE_HOMEBREW_META: "HOMEBREW_META_STORAGE", + STORAGE_EXCLUDES: "EXCLUDES_STORAGE", + STORAGE_DMSCREEN: "DMSCREEN_STORAGE", + STORAGE_DMSCREEN_TEMP_SUBLIST: "DMSCREEN_TEMP_SUBLIST", + STORAGE_ROLLER_MACRO: "ROLLER_MACRO_STORAGE", + STORAGE_ENCOUNTER: "ENCOUNTER_STORAGE", + STORAGE_POINTBUY: "POINTBUY_STORAGE", + STORAGE_GLOBAL_COMPONENT_STATE: "GLOBAL_COMPONENT_STATE", + + DUR_INLINE_NOTIFY: 500, + + PG_NONE: "NO_PAGE", + STR_GENERIC: "Generic", + + SYM_UI_SKIP: Symbol("uiSkip"), + + SYM_WALKER_BREAK: Symbol("walkerBreak"), + + SYM_UTIL_TIMEOUT: Symbol("timeout"), + + LOC_ORIGIN_CANCER: "https://5e.tools", + + URL_BREW: `https://github.com/TheGiddyLimit/homebrew`, + URL_ROOT_BREW: `https://raw.githubusercontent.com/TheGiddyLimit/homebrew/master/`, // N.b. must end with a slash + URL_PRERELEASE: `https://github.com/TheGiddyLimit/unearthed-arcana`, + URL_ROOT_PRERELEASE: `https://raw.githubusercontent.com/TheGiddyLimit/unearthed-arcana/master/`, // As above + + STR_NO_ATTUNEMENT: "No Attunement Required", + + CR_UNKNOWN: 100001, + CR_CUSTOM: 100000, + + SPELL_LEVEL_MAX: 9, + LEVEL_MAX: 20, + + ENTDATA_TABLE_INCLUDE: "tableInclude", + ENTDATA_ITEM_MERGED_ENTRY_TAG: "item__mergedEntryTag", + + DRAG_TYPE_IMPORT: "ve-Import", + DRAG_TYPE_LOOT: "ve-Loot", + + Z_INDEX_BENEATH_HOVER: 199, +}; + +// STRING ============================================================================================================== +String.prototype.uppercaseFirst = String.prototype.uppercaseFirst || function () { + const str = this.toString(); + if (str.length === 0) return str; + if (str.length === 1) return str.charAt(0).toUpperCase(); + return str.charAt(0).toUpperCase() + str.slice(1); +}; + +String.prototype.lowercaseFirst = String.prototype.lowercaseFirst || function () { + const str = this.toString(); + if (str.length === 0) return str; + if (str.length === 1) return str.charAt(0).toLowerCase(); + return str.charAt(0).toLowerCase() + str.slice(1); +}; + +String.prototype.toTitleCase = String.prototype.toTitleCase || function () { + let str = this.replace(/([^\W_]+[^-\u2014\s/]*) */g, m0 => m0.charAt(0).toUpperCase() + m0.substring(1).toLowerCase()); + + // Require space surrounded, as title-case requires a full word on either side + StrUtil._TITLE_LOWER_WORDS_RE = StrUtil._TITLE_LOWER_WORDS_RE || StrUtil.TITLE_LOWER_WORDS.map(it => new RegExp(`\\s${it}\\s`, "gi")); + StrUtil._TITLE_UPPER_WORDS_RE = StrUtil._TITLE_UPPER_WORDS_RE || StrUtil.TITLE_UPPER_WORDS.map(it => new RegExp(`\\b${it}\\b`, "g")); + StrUtil._TITLE_UPPER_WORDS_PLURAL_RE = StrUtil._TITLE_UPPER_WORDS_PLURAL_RE || StrUtil.TITLE_UPPER_WORDS_PLURAL.map(it => new RegExp(`\\b${it}\\b`, "g")); + + const len = StrUtil.TITLE_LOWER_WORDS.length; + for (let i = 0; i < len; i++) { + str = str.replace( + StrUtil._TITLE_LOWER_WORDS_RE[i], + txt => txt.toLowerCase(), + ); + } + + const len1 = StrUtil.TITLE_UPPER_WORDS.length; + for (let i = 0; i < len1; i++) { + str = str.replace( + StrUtil._TITLE_UPPER_WORDS_RE[i], + StrUtil.TITLE_UPPER_WORDS[i].toUpperCase(), + ); + } + + for (let i = 0; i < len1; i++) { + str = str.replace( + StrUtil._TITLE_UPPER_WORDS_PLURAL_RE[i], + `${StrUtil.TITLE_UPPER_WORDS_PLURAL[i].slice(0, -1).toUpperCase()}${StrUtil.TITLE_UPPER_WORDS_PLURAL[i].slice(-1).toLowerCase()}`, + ); + } + + str = str + .split(/([;:?!.])/g) + .map(pt => pt.replace(/^(\s*)([^\s])/, (...m) => `${m[1]}${m[2].toUpperCase()}`)) + .join(""); + + return str; +}; + +String.prototype.toSentenceCase = String.prototype.toSentenceCase || function () { + const out = []; + const re = /([^.!?]+)([.!?]\s*|$)/gi; + let m; + do { + m = re.exec(this); + if (m) { + out.push(m[0].toLowerCase().uppercaseFirst()); + } + } while (m); + return out.join(""); +}; + +String.prototype.toSpellCase = String.prototype.toSpellCase || function () { + return this.toLowerCase().replace(/(^|of )(bigby|otiluke|mordenkainen|evard|hadar|agathys|abi-dalzim|aganazzar|drawmij|leomund|maximilian|melf|nystul|otto|rary|snilloc|tasha|tenser|jim)('s|$| )/g, (...m) => `${m[1]}${m[2].toTitleCase()}${m[3]}`); +}; + +String.prototype.toCamelCase = String.prototype.toCamelCase || function () { + return this.split(" ").map((word, index) => { + if (index === 0) return word.toLowerCase(); + return `${word.charAt(0).toUpperCase()}${word.slice(1).toLowerCase()}`; + }).join(""); +}; + +String.prototype.toPlural = String.prototype.toPlural || function () { + let plural; + if (StrUtil.IRREGULAR_PLURAL_WORDS[this.toLowerCase()]) plural = StrUtil.IRREGULAR_PLURAL_WORDS[this.toLowerCase()]; + else if (/(s|x|z|ch|sh)$/i.test(this)) plural = `${this}es`; + else if (/[bcdfghjklmnpqrstvwxyz]y$/i.test(this)) plural = this.replace(/y$/i, "ies"); + else plural = `${this}s`; + + if (this.toLowerCase() === this) return plural; + if (this.toUpperCase() === this) return plural.toUpperCase(); + if (this.toTitleCase() === this) return plural.toTitleCase(); + return plural; +}; + +String.prototype.escapeQuotes = String.prototype.escapeQuotes || function () { + return this.replace(/'/g, `'`).replace(/"/g, `"`).replace(//g, `>`); +}; + +String.prototype.qq = String.prototype.qq || function () { + return this.escapeQuotes(); +}; + +String.prototype.unescapeQuotes = String.prototype.unescapeQuotes || function () { + return this.replace(/'/g, `'`).replace(/"/g, `"`).replace(/</g, `<`).replace(/>/g, `>`); +}; + +String.prototype.uq = String.prototype.uq || function () { + return this.unescapeQuotes(); +}; + +String.prototype.encodeApos = String.prototype.encodeApos || function () { + return this.replace(/'/g, `%27`); +}; + +/** + * Calculates the Damerau-Levenshtein distance between two strings. + * https://gist.github.com/IceCreamYou/8396172 + */ +String.prototype.distance = String.prototype.distance || function (target) { + let source = this; let i; let j; + if (!source) return target ? target.length : 0; + else if (!target) return source.length; + + const m = source.length; const n = target.length; const INF = m + n; const score = new Array(m + 2); const sd = {}; + for (i = 0; i < m + 2; i++) score[i] = new Array(n + 2); + score[0][0] = INF; + for (i = 0; i <= m; i++) { + score[i + 1][1] = i; + score[i + 1][0] = INF; + sd[source[i]] = 0; + } + for (j = 0; j <= n; j++) { + score[1][j + 1] = j; + score[0][j + 1] = INF; + sd[target[j]] = 0; + } + + for (i = 1; i <= m; i++) { + let DB = 0; + for (j = 1; j <= n; j++) { + const i1 = sd[target[j - 1]]; const j1 = DB; + if (source[i - 1] === target[j - 1]) { + score[i + 1][j + 1] = score[i][j]; + DB = j; + } else { + score[i + 1][j + 1] = Math.min(score[i][j], Math.min(score[i + 1][j], score[i][j + 1])) + 1; + } + score[i + 1][j + 1] = Math.min(score[i + 1][j + 1], score[i1] ? score[i1][j1] + (i - i1 - 1) + 1 + (j - j1 - 1) : Infinity); + } + sd[source[i - 1]] = i; + } + return score[m + 1][n + 1]; +}; + +String.prototype.isNumeric = String.prototype.isNumeric || function () { + return !isNaN(parseFloat(this)) && isFinite(this); +}; + +String.prototype.last = String.prototype.last || function () { + return this[this.length - 1]; +}; + +String.prototype.escapeRegexp = String.prototype.escapeRegexp || function () { + return this.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&"); +}; + +String.prototype.toUrlified = String.prototype.toUrlified || function () { + return encodeURIComponent(this.toLowerCase()).toLowerCase(); +}; + +String.prototype.toChunks = String.prototype.toChunks || function (size) { + // https://stackoverflow.com/a/29202760/5987433 + const numChunks = Math.ceil(this.length / size); + const chunks = new Array(numChunks); + for (let i = 0, o = 0; i < numChunks; ++i, o += size) chunks[i] = this.substr(o, size); + return chunks; +}; + +String.prototype.toAscii = String.prototype.toAscii || function () { + return this + .normalize("NFD") // replace diacritics with their individual graphemes + .replace(/[\u0300-\u036f]/g, "") // remove accent graphemes + .replace(/ร†/g, "AE").replace(/รฆ/g, "ae"); +}; + +String.prototype.trimChar = String.prototype.trimChar || function (ch) { + let start = 0; let end = this.length; + while (start < end && this[start] === ch) ++start; + while (end > start && this[end - 1] === ch) --end; + return (start > 0 || end < this.length) ? this.substring(start, end) : this; +}; + +String.prototype.trimAnyChar = String.prototype.trimAnyChar || function (chars) { + let start = 0; let end = this.length; + while (start < end && chars.indexOf(this[start]) >= 0) ++start; + while (end > start && chars.indexOf(this[end - 1]) >= 0) --end; + return (start > 0 || end < this.length) ? this.substring(start, end) : this; +}; + +Array.prototype.joinConjunct || Object.defineProperty(Array.prototype, "joinConjunct", { + enumerable: false, + writable: true, + value: function (joiner, lastJoiner, nonOxford) { + if (this.length === 0) return ""; + if (this.length === 1) return this[0]; + if (this.length === 2) return this.join(lastJoiner); + else { + let outStr = ""; + for (let i = 0; i < this.length; ++i) { + outStr += this[i]; + if (i < this.length - 2) outStr += joiner; + else if (i === this.length - 2) outStr += `${(!nonOxford && this.length > 2 ? joiner.trim() : "")}${lastJoiner}`; + } + return outStr; + } + }, +}); + +globalThis.StrUtil = { + COMMAS_NOT_IN_PARENTHESES_REGEX: /,\s?(?![^(]*\))/g, + COMMA_SPACE_NOT_IN_PARENTHESES_REGEX: /, (?![^(]*\))/g, + + uppercaseFirst: function (string) { + return string.uppercaseFirst(); + }, + // Certain minor words should be left lowercase unless they are the first or last words in the string + TITLE_LOWER_WORDS: ["a", "an", "the", "and", "but", "or", "for", "nor", "as", "at", "by", "for", "from", "in", "into", "near", "of", "on", "onto", "to", "with", "over", "von"], + // Certain words such as initialisms or acronyms should be left uppercase + TITLE_UPPER_WORDS: ["Id", "Tv", "Dm", "Ok", "Npc", "Pc", "Tpk", "Wip", "Dc"], + TITLE_UPPER_WORDS_PLURAL: ["Ids", "Tvs", "Dms", "Oks", "Npcs", "Pcs", "Tpks", "Wips", "Dcs"], // (Manually pluralize, to avoid infinite loop) + + IRREGULAR_PLURAL_WORDS: { + "cactus": "cacti", + "child": "children", + "die": "dice", + "djinni": "djinn", + "dwarf": "dwarves", + "efreeti": "efreet", + "elf": "elves", + "fey": "fey", + "foot": "feet", + "goose": "geese", + "ki": "ki", + "man": "men", + "mouse": "mice", + "ox": "oxen", + "person": "people", + "sheep": "sheep", + "slaad": "slaadi", + "tooth": "teeth", + "undead": "undead", + "woman": "women", + }, + + padNumber: (n, len, padder) => { + return String(n).padStart(len, padder); + }, + + elipsisTruncate (str, atLeastPre = 5, atLeastSuff = 0, maxLen = 20) { + if (maxLen >= str.length) return str; + + maxLen = Math.max(atLeastPre + atLeastSuff + 3, maxLen); + let out = ""; + let remain = maxLen - (3 + atLeastPre + atLeastSuff); + for (let i = 0; i < str.length - atLeastSuff; ++i) { + const c = str[i]; + if (i < atLeastPre) out += c; + else if ((remain--) > 0) out += c; + } + if (remain < 0) out += "..."; + out += str.substring(str.length - atLeastSuff, str.length); + return out; + }, + + toTitleCase (str) { return str.toTitleCase(); }, + qq (str) { return (str = str || "").qq(); }, +}; + +globalThis.NumberUtil = class { + static toFixedNumber (num, toFixed) { + if (num == null || isNaN(num)) return num; + + num = Number(num); + if (!num) return num; + + return Number(num.toFixed(toFixed)); + } +}; + +globalThis.CleanUtil = { + getCleanJson (data, {isMinify = false, isFast = true} = {}) { + data = MiscUtil.copy(data); + data = MiscUtil.getWalker().walk(data, {string: (str) => CleanUtil.getCleanString(str, {isFast})}); + let str = isMinify ? JSON.stringify(data) : `${JSON.stringify(data, null, "\t")}\n`; + return str.replace(CleanUtil.STR_REPLACEMENTS_REGEX, (match) => CleanUtil.STR_REPLACEMENTS[match]); + }, + + getCleanString (str, {isFast = true} = {}) { + str = str + .replace(CleanUtil.SHARED_REPLACEMENTS_REGEX, (match) => CleanUtil.SHARED_REPLACEMENTS[match]) + .replace(CleanUtil._SOFT_HYPHEN_REMOVE_REGEX, "") + ; + + if (isFast) return str; + + const ptrStack = {_: ""}; + CleanUtil._getCleanString_walkerStringHandler(ptrStack, 0, str); + return ptrStack._; + }, + + _getCleanString_walkerStringHandler (ptrStack, tagCount, str) { + const tagSplit = Renderer.splitByTags(str); + const len = tagSplit.length; + for (let i = 0; i < len; ++i) { + const s = tagSplit[i]; + if (!s) continue; + if (s.startsWith("{@")) { + const [tag, text] = Renderer.splitFirstSpace(s.slice(1, -1)); + + ptrStack._ += `{${tag}${text.length ? " " : ""}`; + this._getCleanString_walkerStringHandler(ptrStack, tagCount + 1, text); + ptrStack._ += `}`; + } else { + // avoid tagging things wrapped in existing tags + if (tagCount) { + ptrStack._ += s; + } else { + ptrStack._ += s + .replace(CleanUtil._DASH_COLLAPSE_REGEX, "$1") + .replace(CleanUtil._ELLIPSIS_COLLAPSE_REGEX, "$1"); + } + } + } + }, +}; +CleanUtil.SHARED_REPLACEMENTS = { + "โ€™": "'", + "โ€˜": "'", + "ย’": "'", + "โ€ฆ": "...", + "\u200B": "", // zero-width space + "\u2002": " ", // em space + "๏ฌ€": "ff", + "๏ฌƒ": "ffi", + "๏ฌ„": "ffl", + "๏ฌ": "fi", + "๏ฌ‚": "fl", + "ฤฒ": "IJ", + "ฤณ": "ij", + "ว‡": "LJ", + "วˆ": "Lj", + "ว‰": "lj", + "วŠ": "NJ", + "ว‹": "Nj", + "วŒ": "nj", + "๏ฌ…": "ft", + "โ€œ": `"`, + "โ€": `"`, + "\u201a": ",", +}; +CleanUtil.STR_REPLACEMENTS = { + "โ€”": "\\u2014", + "โ€“": "\\u2013", + "โ€‘": "\\u2011", + "โˆ’": "\\u2212", + " ": "\\u00A0", + "โ€‡": "\\u2007", +}; +CleanUtil.SHARED_REPLACEMENTS_REGEX = new RegExp(Object.keys(CleanUtil.SHARED_REPLACEMENTS).join("|"), "g"); +CleanUtil.STR_REPLACEMENTS_REGEX = new RegExp(Object.keys(CleanUtil.STR_REPLACEMENTS).join("|"), "g"); +CleanUtil._SOFT_HYPHEN_REMOVE_REGEX = /\u00AD *\r?\n?\r?/g; +CleanUtil._ELLIPSIS_COLLAPSE_REGEX = /\s*(\.\s*\.\s*\.)/g; +CleanUtil._DASH_COLLAPSE_REGEX = /[ ]*([\u2014\u2013])[ ]*/g; + +// SOURCES ============================================================================================================= +globalThis.SourceUtil = class { + static ADV_BOOK_GROUPS = [ + {group: "core", displayName: "Core"}, + {group: "supplement", displayName: "Supplements"}, + {group: "setting", displayName: "Settings"}, + {group: "setting-alt", displayName: "Additional Settings"}, + {group: "supplement-alt", displayName: "Extras"}, + {group: "prerelease", displayName: "Prerelease"}, + {group: "homebrew", displayName: "Homebrew"}, + {group: "screen", displayName: "Screens"}, + {group: "recipe", displayName: "Recipes"}, + {group: "other", displayName: "Miscellaneous"}, + ]; + + static _subclassReprintLookup = {}; + static async pInitSubclassReprintLookup () { + SourceUtil._subclassReprintLookup = await DataUtil.loadJSON(`${Renderer.get().baseUrl}data/generated/gendata-subclass-lookup.json`); + } + + static isSubclassReprinted (className, classSource, subclassShortName, subclassSource) { + const fromLookup = MiscUtil.get(SourceUtil._subclassReprintLookup, classSource, className, subclassSource, subclassShortName); + return fromLookup ? fromLookup.isReprinted : false; + } + + static isKnownSource (source) { + return SourceUtil.isSiteSource(source) + || (typeof PrereleaseUtil !== "undefined" && PrereleaseUtil.hasSourceJson(source)) + || (typeof BrewUtil2 !== "undefined" && BrewUtil2.hasSourceJson(source)); + } + + /** I.e., not homebrew. */ + static isSiteSource (source) { return !!Parser.SOURCE_JSON_TO_FULL[source]; } + + static isAdventure (source) { + if (source instanceof FilterItem) source = source.item; + return Parser.SOURCES_ADVENTURES.has(source); + } + + static isCoreOrSupplement (source) { + if (source instanceof FilterItem) source = source.item; + return Parser.SOURCES_CORE_SUPPLEMENTS.has(source); + } + + static isNonstandardSource (source) { + if (source == null) return false; + return ( + (typeof BrewUtil2 === "undefined" || !BrewUtil2.hasSourceJson(source)) + && SourceUtil.isNonstandardSourceWotc(source) + ) + || SourceUtil.isPrereleaseSource(source); + } + + static isPartneredSourceWotc (source) { + if (source == null) return false; + return Parser.SOURCES_PARTNERED_WOTC.has(source); + } + + static isLegacySourceWotc (source) { + if (source == null) return false; + return source === Parser.SRC_VGM || source === Parser.SRC_MTF; + } + + // TODO(Future) remove this in favor of simply checking existence in `PrereleaseUtil` + // TODO(Future) cleanup uses of `PrereleaseUtil.hasSourceJson` to match + static isPrereleaseSource (source) { + if (source == null) return false; + if (typeof PrereleaseUtil !== "undefined" && PrereleaseUtil.hasSourceJson(source)) return true; + return source.startsWith(Parser.SRC_UA_PREFIX) + || source.startsWith(Parser.SRC_UA_ONE_PREFIX); + } + + static isNonstandardSourceWotc (source) { + return SourceUtil.isPrereleaseSource(source) + || source.startsWith(Parser.SRC_PS_PREFIX) + || source.startsWith(Parser.SRC_AL_PREFIX) + || source.startsWith(Parser.SRC_MCVX_PREFIX) + || Parser.SOURCES_NON_STANDARD_WOTC.has(source); + } + + static FILTER_GROUP_STANDARD = 0; + static FILTER_GROUP_PARTNERED = 1; + static FILTER_GROUP_NON_STANDARD = 2; + static FILTER_GROUP_HOMEBREW = 3; + + static getFilterGroup (source) { + if (source instanceof FilterItem) source = source.item; + if ( + (typeof PrereleaseUtil !== "undefined" && PrereleaseUtil.hasSourceJson(source)) + || SourceUtil.isNonstandardSource(source) + ) return SourceUtil.FILTER_GROUP_NON_STANDARD; + if (typeof BrewUtil2 !== "undefined" && BrewUtil2.hasSourceJson(source)) return SourceUtil.FILTER_GROUP_HOMEBREW; + if (SourceUtil.isPartneredSourceWotc(source)) return SourceUtil.FILTER_GROUP_PARTNERED; + return SourceUtil.FILTER_GROUP_STANDARD; + } + + static getAdventureBookSourceHref (source, page) { + if (!source) return null; + source = source.toLowerCase(); + + // TODO this could be made to work with homebrew + let docPage, mappedSource; + if (Parser.SOURCES_AVAILABLE_DOCS_BOOK[source]) { + docPage = UrlUtil.PG_BOOK; + mappedSource = Parser.SOURCES_AVAILABLE_DOCS_BOOK[source]; + } else if (Parser.SOURCES_AVAILABLE_DOCS_ADVENTURE[source]) { + docPage = UrlUtil.PG_ADVENTURE; + mappedSource = Parser.SOURCES_AVAILABLE_DOCS_ADVENTURE[source]; + } + if (!docPage) return null; + + mappedSource = mappedSource.toLowerCase(); + + return `${docPage}#${[mappedSource, page ? `page:${page}` : null].filter(Boolean).join(HASH_PART_SEP)}`; + } + + static getEntitySource (it) { return it.source || it.inherits?.source; } +}; + +// CURRENCY ============================================================================================================ +globalThis.CurrencyUtil = class { + /** + * Convert 10 gold -> 1 platinum, etc. + * @param obj Object of the form {cp: 123, sp: 456, ...} (values optional) + * @param [opts] + * @param [opts.currencyConversionId] Currency conversion table ID. + * @param [opts.currencyConversionTable] Currency conversion table. + * @param [opts.originalCurrency] Original currency object, if the current currency object is after spending coin. + * @param [opts.isPopulateAllValues] If all currency properties should be be populated, even if no currency of that + * type is being returned (i.e. zero out unused coins). + */ + static doSimplifyCoins (obj, opts) { + opts = opts || {}; + + const conversionTable = opts.currencyConversionTable || Parser.getCurrencyConversionTable(opts.currencyConversionId); + if (!conversionTable.length) return obj; + + const normalized = conversionTable + .map(it => { + return { + ...it, + normalizedMult: 1 / it.mult, + }; + }) + .sort((a, b) => SortUtil.ascSort(a.normalizedMult, b.normalizedMult)); + + // Simplify currencies + for (let i = 0; i < normalized.length - 1; ++i) { + const coinCur = normalized[i].coin; + const coinNxt = normalized[i + 1].coin; + const coinRatio = normalized[i + 1].normalizedMult / normalized[i].normalizedMult; + + if (obj[coinCur] && Math.abs(obj[coinCur]) >= coinRatio) { + const nxtVal = obj[coinCur] >= 0 ? Math.floor(obj[coinCur] / coinRatio) : Math.ceil(obj[coinCur] / coinRatio); + obj[coinCur] = obj[coinCur] % coinRatio; + obj[coinNxt] = (obj[coinNxt] || 0) + nxtVal; + } + } + + // Note: this assumes that we, overall, lost money. + if (opts.originalCurrency) { + const normalizedHighToLow = MiscUtil.copyFast(normalized).reverse(); + + // For each currency, look at the previous coin's diff. Say, for gp, that it is -1pp. That means we could have + // gained up to 10gp as change. So we can have + <10gp> max gold; the rest is converted + // to sp. Repeat to the end. + // Never allow more highest-value currency (i.e. pp) than we originally had. + normalizedHighToLow + .forEach((coinMeta, i) => { + const valOld = opts.originalCurrency[coinMeta.coin] || 0; + const valNew = obj[coinMeta.coin] || 0; + + const prevCoinMeta = normalizedHighToLow[i - 1]; + const nxtCoinMeta = normalizedHighToLow[i + 1]; + + if (!prevCoinMeta) { // Handle the biggest currency, e.g. platinum--never allow it to increase + if (nxtCoinMeta) { + const diff = valNew - valOld; + if (diff > 0) { + obj[coinMeta.coin] = valOld; + const coinRatio = coinMeta.normalizedMult / nxtCoinMeta.normalizedMult; + obj[nxtCoinMeta.coin] = (obj[nxtCoinMeta.coin] || 0) + (diff * coinRatio); + } + } + } else { + if (nxtCoinMeta) { + const diffPrevCoin = (opts.originalCurrency[prevCoinMeta.coin] || 0) - (obj[prevCoinMeta.coin] || 0); + const coinRatio = prevCoinMeta.normalizedMult / coinMeta.normalizedMult; + const capFromOld = valOld + (diffPrevCoin > 0 ? diffPrevCoin * coinRatio : 0); + const diff = valNew - capFromOld; + if (diff > 0) { + obj[coinMeta.coin] = capFromOld; + const coinRatio = coinMeta.normalizedMult / nxtCoinMeta.normalizedMult; + obj[nxtCoinMeta.coin] = (obj[nxtCoinMeta.coin] || 0) + (diff * coinRatio); + } + } + } + }); + } + + normalized + .filter(coinMeta => obj[coinMeta.coin] === 0 || obj[coinMeta.coin] == null) + .forEach(coinMeta => { + // First set the value to null, in case we're dealing with a class instance that has setters + obj[coinMeta.coin] = null; + delete obj[coinMeta.coin]; + }); + + if (opts.isPopulateAllValues) normalized.forEach(coinMeta => obj[coinMeta.coin] = obj[coinMeta.coin] || 0); + + return obj; + } + + /** + * Convert a collection of coins into an equivalent value in copper. + * @param obj Object of the form {cp: 123, sp: 456, ...} (values optional) + */ + static getAsCopper (obj) { + return Parser.FULL_CURRENCY_CONVERSION_TABLE + .map(currencyMeta => (obj[currencyMeta.coin] || 0) * (1 / currencyMeta.mult)) + .reduce((a, b) => a + b, 0); + } + + /** + * Convert a collection of coins into an equivalent number of coins of the highest denomination. + * @param obj Object of the form {cp: 123, sp: 456, ...} (values optional) + */ + static getAsSingleCurrency (obj) { + const simplified = CurrencyUtil.doSimplifyCoins({...obj}); + + if (Object.keys(simplified).length === 1) return simplified; + + const out = {}; + + const targetDemonination = Parser.FULL_CURRENCY_CONVERSION_TABLE.find(it => simplified[it.coin]); + + out[targetDemonination.coin] = simplified[targetDemonination.coin]; + delete simplified[targetDemonination.coin]; + + Object.entries(simplified) + .forEach(([coin, amt]) => { + const denom = Parser.FULL_CURRENCY_CONVERSION_TABLE.find(it => it.coin === coin); + out[targetDemonination.coin] = (out[targetDemonination.coin] || 0) + (amt / denom.mult) * targetDemonination.mult; + }); + + return out; + } + + static getCombinedCurrency (currencyA, currencyB) { + const out = {}; + + [currencyA, currencyB] + .forEach(currency => { + Object.entries(currency) + .forEach(([coin, cnt]) => { + if (cnt == null) return; + if (isNaN(cnt)) throw new Error(`Unexpected non-numerical value "${JSON.stringify(cnt)}" for currency key "${coin}"`); + + out[coin] = (out[coin] || 0) + cnt; + }); + }); + + return out; + } +}; + +// CONVENIENCE/ELEMENTS ================================================================================================ +Math.seed = Math.seed || function (s) { + return function () { + s = Math.sin(s) * 10000; + return s - Math.floor(s); + }; +}; + +globalThis.JqueryUtil = { + _isEnhancementsInit: false, + initEnhancements () { + if (JqueryUtil._isEnhancementsInit) return; + JqueryUtil._isEnhancementsInit = true; + + JqueryUtil.addSelectors(); + + /** + * Template strings which can contain jQuery objects. + * Usage: $$`
            Press this button: ${$btn}
            ` + * @return jQuery + */ + window.$$ = function (parts, ...args) { + if (parts instanceof jQuery || parts instanceof HTMLElement) { + return (...passed) => { + const parts2 = [...passed[0]]; + const args2 = passed.slice(1); + parts2[0] = `
            ${parts2[0]}`; + parts2.last(`${parts2.last()}
            `); + + const $temp = $$(parts2, ...args2); + $temp.children().each((i, e) => $(e).appendTo(parts)); + return parts; + }; + } else { + const $eles = []; + let ixArg = 0; + + const handleArg = (arg) => { + if (arg instanceof $) { + $eles.push(arg); + return `<${arg.tag()} data-r="true">`; + } else if (arg instanceof HTMLElement) { + return handleArg($(arg)); + } else return arg; + }; + + const raw = parts.reduce((html, p) => { + const myIxArg = ixArg++; + if (args[myIxArg] == null) return `${html}${p}`; + if (args[myIxArg] instanceof Array) return `${html}${args[myIxArg].map(arg => handleArg(arg)).join("")}${p}`; + else return `${html}${handleArg(args[myIxArg])}${p}`; + }); + const $res = $(raw); + + if ($res.length === 1) { + if ($res.attr("data-r") === "true") return $eles[0]; + else $res.find(`[data-r=true]`).replaceWith(i => $eles[i]); + } else { + // Handle case where user has passed in a bunch of elements with no outer wrapper + const $tmp = $(`
            `); + $tmp.append($res); + $tmp.find(`[data-r=true]`).replaceWith(i => $eles[i]); + return $tmp.children(); + } + + return $res; + } + }; + + $.fn.extend({ + // avoid setting input type to "search" as it visually offsets the contents of the input + disableSpellcheck: function () { return this.attr("autocomplete", "new-password").attr("autocapitalize", "off").attr("spellcheck", "false"); }, + tag: function () { return this.prop("tagName").toLowerCase(); }, + title: function (...args) { return this.attr("title", ...args); }, + placeholder: function (...args) { return this.attr("placeholder", ...args); }, + disable: function () { return this.attr("disabled", true); }, + + /** + * Quickly set the innerHTML of the innermost element, without parsing the whole thing with jQuery. + * Useful for populating e.g. a table row. + */ + fastSetHtml: function (html) { + if (!this.length) return this; + let tgt = this[0]; + while (tgt.children.length) { + tgt = tgt.children[0]; + } + tgt.innerHTML = html; + return this; + }, + + blurOnEsc: function () { + return this.keydown(evt => { + if (evt.which === 27) this.blur(); // escape + }); + }, + + hideVe: function () { return this.addClass("ve-hidden"); }, + showVe: function () { return this.removeClass("ve-hidden"); }, + toggleVe: function (val) { + if (val === undefined) return this.toggleClass("ve-hidden", !this.hasClass("ve-hidden")); + else return this.toggleClass("ve-hidden", !val); + }, + }); + + $.event.special.destroyed = { + remove: function (o) { + if (o.handler) o.handler(); + }, + }; + }, + + addSelectors () { + // Add a selector to match exact text (case insensitive) to jQuery's arsenal + // Note that the search text should be `trim().toLowerCase()`'d before being passed in + $.expr[":"].textEquals = (el, i, m) => $(el).text().toLowerCase().trim() === m[3].unescapeQuotes(); + + // Add a selector to match contained text (case insensitive) + $.expr[":"].containsInsensitive = (el, i, m) => { + const searchText = m[3]; + const textNode = $(el).contents().filter((i, e) => e.nodeType === 3)[0]; + if (!textNode) return false; + const match = textNode.nodeValue.toLowerCase().trim().match(`${searchText.toLowerCase().trim().escapeRegexp()}`); + return match && match.length > 0; + }; + }, + + showCopiedEffect (eleOr$Ele, text = "Copied!", bubble) { + const $ele = eleOr$Ele instanceof $ ? eleOr$Ele : $(eleOr$Ele); + + const top = $(window).scrollTop(); + const pos = $ele.offset(); + + const animationOptions = { + top: "-=8", + opacity: 0, + }; + if (bubble) { + animationOptions.left = `${Math.random() > 0.5 ? "-" : "+"}=${~~(Math.random() * 17)}`; + } + const seed = Math.random(); + const duration = bubble ? 250 + seed * 200 : 250; + const offsetY = bubble ? 16 : 0; + + const $dispCopied = $(`
            `); + $dispCopied + .html(text) + .css({ + top: (pos.top - 24) + offsetY - top, + left: pos.left + ($ele.width() / 2), + }) + .appendTo(document.body) + .animate( + animationOptions, + { + duration, + complete: () => $dispCopied.remove(), + progress: (_, progress) => { // progress is 0..1 + if (bubble) { + const diffProgress = 0.5 - progress; + animationOptions.top = `${diffProgress > 0 ? "-" : "+"}=40`; + $dispCopied.css("transform", `rotate(${seed > 0.5 ? "-" : ""}${seed * 500 * progress}deg)`); + } + }, + }, + ); + }, + + _dropdownInit: false, + bindDropdownButton ($ele) { + if (!JqueryUtil._dropdownInit) { + JqueryUtil._dropdownInit = true; + document.addEventListener("click", () => [...document.querySelectorAll(`.open`)].filter(ele => !(ele.className || "").split(" ").includes(`dropdown--navbar`)).forEach(ele => ele.classList.remove("open"))); + } + $ele.click(() => setTimeout(() => $ele.parent().addClass("open"), 1)); // defer to allow the above to complete + }, + + _WRP_TOAST: null, + _ACTIVE_TOAST: [], + /** + * @param {{content: jQuery|string, type?: string, autoHideTime?: boolean} | string} options The options for the toast. + * @param {(jQuery|string)} options.content Toast contents. Supports jQuery objects. + * @param {string} options.type Toast type. Can be any Bootstrap alert type ("success", "info", "warning", or "danger"). + * @param {number} options.autoHideTime The time in ms before the toast will be automatically hidden. + * Defaults to 5000 ms. + * @param {boolean} options.isAutoHide + */ + doToast (options) { + if (typeof window === "undefined") return; + + if (JqueryUtil._WRP_TOAST == null) { + JqueryUtil._WRP_TOAST = e_({ + tag: "div", + clazz: "toast__container no-events w-100 overflow-y-hidden ve-flex-col", + }); + document.body.appendChild(JqueryUtil._WRP_TOAST); + } + + if (typeof options === "string") { + options = { + content: options, + type: "info", + }; + } + options.type = options.type || "info"; + + options.isAutoHide = options.isAutoHide ?? true; + options.autoHideTime = options.autoHideTime ?? 5000; + + const eleToast = e_({ + tag: "div", + clazz: `toast toast--type-${options.type} events-initial relative my-2 mx-auto`, + children: [ + e_({ + tag: "div", + clazz: "toast__wrp-content", + children: [ + options.content instanceof $ ? options.content[0] : options.content, + ], + }), + e_({ + tag: "div", + clazz: "toast__wrp-control", + children: [ + e_({ + tag: "button", + clazz: "btn toast__btn-close", + children: [ + e_({ + tag: "span", + clazz: "glyphicon glyphicon-remove", + }), + ], + }), + ], + }), + ], + mousedown: evt => { + evt.preventDefault(); + }, + click: evt => { + evt.preventDefault(); + JqueryUtil._doToastCleanup(toastMeta); + + // Close all on SHIFT-click + if (!evt.shiftKey) return; + [...JqueryUtil._ACTIVE_TOAST].forEach(toastMeta => JqueryUtil._doToastCleanup(toastMeta)); + }, + }); + + // FIXME(future) this could be smoother; when stacking multiple tooltips, the incoming tooltip bumps old tooltips + // down instantly (should be animated). + // See e.g.: + // `[...new Array(10)].forEach((_, i) => MiscUtil.pDelay(i * 50).then(() => JqueryUtil.doToast(`test ${i}`)))` + eleToast.prependTo(JqueryUtil._WRP_TOAST); + + const toastMeta = {isAutoHide: !!options.isAutoHide, eleToast}; + JqueryUtil._ACTIVE_TOAST.push(toastMeta); + + AnimationUtil.pRecomputeStyles() + .then(() => { + eleToast.addClass(`toast--animate`); + + if (options.isAutoHide) { + setTimeout(() => { + JqueryUtil._doToastCleanup(toastMeta); + }, options.autoHideTime); + } + + if (JqueryUtil._ACTIVE_TOAST.length >= 3) { + JqueryUtil._ACTIVE_TOAST + .filter(({isAutoHide}) => !isAutoHide) + .forEach(toastMeta => { + JqueryUtil._doToastCleanup(toastMeta); + }); + } + }); + }, + + _doToastCleanup (toastMeta) { + toastMeta.eleToast.removeClass("toast--animate"); + JqueryUtil._ACTIVE_TOAST.splice(JqueryUtil._ACTIVE_TOAST.indexOf(toastMeta), 1); + setTimeout(() => toastMeta.eleToast.parentElement && toastMeta.eleToast.remove(), 85); + }, + + isMobile () { + if (navigator?.userAgentData?.mobile) return true; + // Equivalent to `$width-screen-sm` + return window.matchMedia("(max-width: 768px)").matches; + }, +}; + +if (typeof window !== "undefined") window.addEventListener("load", JqueryUtil.initEnhancements); + +globalThis.ElementUtil = { + _ATTRS_NO_FALSY: new Set([ + "checked", + "disabled", + ]), + + getOrModify ({ + tag, + clazz, + style, + click, + contextmenu, + change, + mousedown, + mouseup, + mousemove, + pointerdown, + pointerup, + keydown, + html, + text, + txt, + ele, + children, + outer, + + id, + name, + title, + val, + href, + type, + tabindex, + value, + placeholder, + attrs, + data, + }) { + ele = ele || (outer ? (new DOMParser()).parseFromString(outer, "text/html").body.childNodes[0] : document.createElement(tag)); + + if (clazz) ele.className = clazz; + if (style) ele.setAttribute("style", style); + if (click) ele.addEventListener("click", click); + if (contextmenu) ele.addEventListener("contextmenu", contextmenu); + if (change) ele.addEventListener("change", change); + if (mousedown) ele.addEventListener("mousedown", mousedown); + if (mouseup) ele.addEventListener("mouseup", mouseup); + if (mousemove) ele.addEventListener("mousemove", mousemove); + if (pointerdown) ele.addEventListener("pointerdown", pointerdown); + if (pointerup) ele.addEventListener("pointerup", pointerup); + if (keydown) ele.addEventListener("keydown", keydown); + if (html != null) ele.innerHTML = html; + if (text != null || txt != null) ele.textContent = text; + if (id != null) ele.setAttribute("id", id); + if (name != null) ele.setAttribute("name", name); + if (title != null) ele.setAttribute("title", title); + if (href != null) ele.setAttribute("href", href); + if (val != null) ele.setAttribute("value", val); + if (type != null) ele.setAttribute("type", type); + if (tabindex != null) ele.setAttribute("tabindex", tabindex); + if (value != null) ele.setAttribute("value", value); + if (placeholder != null) ele.setAttribute("placeholder", placeholder); + + if (attrs != null) { + for (const k in attrs) { + if (attrs[k] === undefined) continue; + if (!attrs[k] && ElementUtil._ATTRS_NO_FALSY.has(k)) continue; + ele.setAttribute(k, attrs[k]); + } + } + + if (data != null) { for (const k in data) { if (data[k] === undefined) continue; ele.dataset[k] = data[k]; } } + + if (children) for (let i = 0, len = children.length; i < len; ++i) if (children[i] != null) ele.append(children[i]); + + ele.appends = ele.appends || ElementUtil._appends.bind(ele); + ele.appendTo = ele.appendTo || ElementUtil._appendTo.bind(ele); + ele.prependTo = ele.prependTo || ElementUtil._prependTo.bind(ele); + ele.insertAfter = ele.insertAfter || ElementUtil._insertAfter.bind(ele); + ele.addClass = ele.addClass || ElementUtil._addClass.bind(ele); + ele.removeClass = ele.removeClass || ElementUtil._removeClass.bind(ele); + ele.toggleClass = ele.toggleClass || ElementUtil._toggleClass.bind(ele); + ele.showVe = ele.showVe || ElementUtil._showVe.bind(ele); + ele.hideVe = ele.hideVe || ElementUtil._hideVe.bind(ele); + ele.toggleVe = ele.toggleVe || ElementUtil._toggleVe.bind(ele); + ele.empty = ele.empty || ElementUtil._empty.bind(ele); + ele.detach = ele.detach || ElementUtil._detach.bind(ele); + ele.attr = ele.attr || ElementUtil._attr.bind(ele); + ele.val = ele.val || ElementUtil._val.bind(ele); + ele.html = ele.html || ElementUtil._html.bind(ele); + ele.txt = ele.txt || ElementUtil._txt.bind(ele); + ele.tooltip = ele.tooltip || ElementUtil._tooltip.bind(ele); + ele.disableSpellcheck = ele.disableSpellcheck || ElementUtil._disableSpellcheck.bind(ele); + ele.on = ele.on || ElementUtil._onX.bind(ele); + ele.onClick = ele.onClick || ElementUtil._onX.bind(ele, "click"); + ele.onContextmenu = ele.onContextmenu || ElementUtil._onX.bind(ele, "contextmenu"); + ele.onChange = ele.onChange || ElementUtil._onX.bind(ele, "change"); + ele.onKeydown = ele.onKeydown || ElementUtil._onX.bind(ele, "keydown"); + ele.onKeyup = ele.onKeyup || ElementUtil._onX.bind(ele, "keyup"); + + return ele; + }, + + _appends (child) { + this.appendChild(child); + return this; + }, + + _appendTo (parent) { + parent.appendChild(this); + return this; + }, + + _prependTo (parent) { + parent.prepend(this); + return this; + }, + + _insertAfter (parent) { + parent.after(this); + return this; + }, + + _addClass (clazz) { + this.classList.add(clazz); + return this; + }, + + _removeClass (clazz) { + this.classList.remove(clazz); + return this; + }, + + _toggleClass (clazz, isActive) { + if (isActive == null) this.classList.toggle(clazz); + else if (isActive) this.classList.add(clazz); + else this.classList.remove(clazz); + return this; + }, + + _showVe () { + this.classList.remove("ve-hidden"); + return this; + }, + + _hideVe () { + this.classList.add("ve-hidden"); + return this; + }, + + _toggleVe (isActive) { + this.toggleClass("ve-hidden", isActive == null ? isActive : !isActive); + return this; + }, + + _empty () { + this.innerHTML = ""; + return this; + }, + + _detach () { + if (this.parentElement) this.parentElement.removeChild(this); + return this; + }, + + _attr (name, value) { + this.setAttribute(name, value); + return this; + }, + + _html (html) { + if (html === undefined) return this.innerHTML; + this.innerHTML = html; + return this; + }, + + _txt (txt) { + if (txt === undefined) return this.innerText; + this.innerText = txt; + return this; + }, + + _tooltip (title) { + return this.attr("title", title); + }, + + _disableSpellcheck () { + // avoid setting input type to "search" as it visually offsets the contents of the input + return this + .attr("autocomplete", "new-password") + .attr("autocapitalize", "off") + .attr("spellcheck", "false"); + }, + + _onX (evtName, fn) { + this.addEventListener(evtName, fn); + return this; + }, + + _val (val) { + if (val !== undefined) { + switch (this.tagName) { + case "SELECT": { + let selectedIndexNxt = -1; + for (let i = 0, len = this.options.length; i < len; ++i) { + if (this.options[i]?.value === val) { selectedIndexNxt = i; break; } + } + this.selectedIndex = selectedIndexNxt; + return this; + } + + default: { + this.value = val; + return this; + } + } + } + + switch (this.tagName) { + case "SELECT": return this.options[this.selectedIndex]?.value; + + default: return this.value; + } + }, + + // region "Static" + getIndexPathToParent (parent, child) { + if (!parent.contains(child)) return null; // Should never occur + + const path = []; + + while (child !== parent) { + if (!child.parentElement) return null; // Should never occur + + const ix = [...child.parentElement.children].indexOf(child); + if (!~ix) return null; // Should never occur + + path.push(ix); + + child = child.parentElement; + } + + return path.reverse(); + }, + + getChildByIndexPath (parent, indexPath) { + for (let i = 0; i < indexPath.length; ++i) { + const ix = indexPath[i]; + parent = parent.children[ix]; + if (!parent) return null; + } + return parent; + }, + // endregion +}; + +if (typeof window !== "undefined") window.e_ = ElementUtil.getOrModify; + +globalThis.ObjUtil = { + async pForEachDeep (source, pCallback, options = {depth: Infinity, callEachLevel: false}) { + const path = []; + const pDiveDeep = async function (val, path, depth = 0) { + if (options.callEachLevel || typeof val !== "object" || options.depth === depth) { + await pCallback(val, path, depth); + } + if (options.depth !== depth && typeof val === "object") { + for (const key of Object.keys(val)) { + path.push(key); + await pDiveDeep(val[key], path, depth + 1); + } + } + path.pop(); + }; + await pDiveDeep(source, path); + }, +}; + +// TODO refactor other misc utils into this +globalThis.MiscUtil = { + COLOR_HEALTHY: "#00bb20", + COLOR_HURT: "#c5ca00", + COLOR_BLOODIED: "#f7a100", + COLOR_DEFEATED: "#cc0000", + + /** + * @param obj + * @param isSafe + * @param isPreserveUndefinedValueKeys Otherwise, drops the keys of `undefined` values + * (e.g. `{a: undefined}` -> `{}`). + */ + copy (obj, {isSafe = false, isPreserveUndefinedValueKeys = false} = {}) { + if (isSafe && obj === undefined) return undefined; // Generally use "unsafe," as this helps identify bugs. + return JSON.parse(JSON.stringify(obj)); + }, + + copyFast (obj) { + if ((typeof obj !== "object") || obj == null) return obj; + + if (obj instanceof Array) return obj.map(MiscUtil.copyFast); + + const cpy = {}; + for (const k of Object.keys(obj)) cpy[k] = MiscUtil.copyFast(obj[k]); + return cpy; + }, + + async pCopyTextToClipboard (text) { + function doCompatibilityCopy () { + const $iptTemp = $(``) + .appendTo(document.body) + .val(text) + .select(); + document.execCommand("Copy"); + $iptTemp.remove(); + } + + if (navigator && navigator.permissions) { + try { + const access = await navigator.permissions.query({name: "clipboard-write"}); + if (access.state === "granted" || access.state === "prompt") { + await navigator.clipboard.writeText(text); + } else doCompatibilityCopy(); + } catch (e) { doCompatibilityCopy(); } + } else doCompatibilityCopy(); + }, + + checkProperty (object, ...path) { + for (let i = 0; i < path.length; ++i) { + object = object[path[i]]; + if (object == null) return false; + } + return true; + }, + + get (object, ...path) { + if (object == null) return null; + for (let i = 0; i < path.length; ++i) { + object = object[path[i]]; + if (object == null) return object; + } + return object; + }, + + set (object, ...pathAndVal) { + if (object == null) return null; + + const val = pathAndVal.pop(); + if (!pathAndVal.length) return null; + + const len = pathAndVal.length; + for (let i = 0; i < len; ++i) { + const pathPart = pathAndVal[i]; + if (i === len - 1) object[pathPart] = val; + else object = (object[pathPart] = object[pathPart] || {}); + } + + return val; + }, + + getOrSet (object, ...pathAndVal) { + if (pathAndVal.length < 2) return null; + const existing = MiscUtil.get(object, ...pathAndVal.slice(0, -1)); + if (existing != null) return existing; + return MiscUtil.set(object, ...pathAndVal); + }, + + getThenSetCopy (object1, object2, ...path) { + const val = MiscUtil.get(object1, ...path); + return MiscUtil.set(object2, ...path, MiscUtil.copyFast(val, {isSafe: true})); + }, + + delete (object, ...path) { + if (object == null) return object; + for (let i = 0; i < path.length - 1; ++i) { + object = object[path[i]]; + if (object == null) return object; + } + return delete object[path.last()]; + }, + + /** Delete a prop from a nested object, then all now-empty objects backwards from that point. */ + deleteObjectPath (object, ...path) { + const stack = [object]; + + if (object == null) return object; + for (let i = 0; i < path.length - 1; ++i) { + object = object[path[i]]; + stack.push(object); + if (object === undefined) return object; + } + const out = delete object[path.last()]; + + for (let i = path.length - 1; i > 0; --i) { + if (!Object.keys(stack[i]).length) delete stack[i - 1][path[i - 1]]; + } + + return out; + }, + + merge (obj1, obj2) { + obj2 = MiscUtil.copyFast(obj2); + + Object.entries(obj2) + .forEach(([k, v]) => { + if (obj1[k] == null) { + obj1[k] = v; + return; + } + + if ( + typeof obj1[k] === "object" + && typeof v === "object" + && !(obj1[k] instanceof Array) + && !(v instanceof Array) + ) { + MiscUtil.merge(obj1[k], v); + return; + } + + obj1[k] = v; + }); + + return obj1; + }, + + /** + * @deprecated + */ + mix: (superclass) => new MiscUtil._MixinBuilder(superclass), + _MixinBuilder: function (superclass) { + this.superclass = superclass; + + this.with = function (...mixins) { + return mixins.reduce((c, mixin) => mixin(c), this.superclass); + }; + }, + + clearSelection () { + if (document.getSelection) { + document.getSelection().removeAllRanges(); + document.getSelection().addRange(document.createRange()); + } else if (window.getSelection) { + if (window.getSelection().removeAllRanges) { + window.getSelection().removeAllRanges(); + window.getSelection().addRange(document.createRange()); + } else if (window.getSelection().empty) { + window.getSelection().empty(); + } + } else if (document.selection) { + document.selection.empty(); + } + }, + + randomColor () { + let r; let g; let b; + const h = RollerUtil.randomise(30, 0) / 30; + const i = ~~(h * 6); + const f = h * 6 - i; + const q = 1 - f; + switch (i % 6) { + case 0: r = 1; g = f; b = 0; break; + case 1: r = q; g = 1; b = 0; break; + case 2: r = 0; g = 1; b = f; break; + case 3: r = 0; g = q; b = 1; break; + case 4: r = f; g = 0; b = 1; break; + case 5: r = 1; g = 0; b = q; break; + } + return `#${`00${(~~(r * 255)).toString(16)}`.slice(-2)}${`00${(~~(g * 255)).toString(16)}`.slice(-2)}${`00${(~~(b * 255)).toString(16)}`.slice(-2)}`; + }, + + /** + * @param hex Original hex color. + * @param [opts] Options object. + * @param [opts.bw] True if the color should be returnes as black/white depending on contrast ratio. + * @param [opts.dark] Color to return if a "dark" color would contrast best. + * @param [opts.light] Color to return if a "light" color would contrast best. + */ + invertColor (hex, opts) { + opts = opts || {}; + + hex = hex.slice(1); // remove # + + let r = parseInt(hex.slice(0, 2), 16); + let g = parseInt(hex.slice(2, 4), 16); + let b = parseInt(hex.slice(4, 6), 16); + + // http://stackoverflow.com/a/3943023/112731 + const isDark = (r * 0.299 + g * 0.587 + b * 0.114) > 186; + if (opts.dark && opts.light) return isDark ? opts.dark : opts.light; + else if (opts.bw) return isDark ? "#000000" : "#FFFFFF"; + + r = (255 - r).toString(16); g = (255 - g).toString(16); b = (255 - b).toString(16); + return `#${[r, g, b].map(it => it.padStart(2, "0")).join("")}`; + }, + + scrollPageTop () { + document.body.scrollTop = document.documentElement.scrollTop = 0; + }, + + expEval (str) { + // eslint-disable-next-line no-new-func + return new Function(`return ${str.replace(/[^-()\d/*+.]/g, "")}`)(); + }, + + parseNumberRange (input, min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER) { + if (!input || !input.trim()) return null; + + const errInvalid = input => { throw new Error(`Could not parse range input "${input}"`); }; + + const errOutOfRange = () => { throw new Error(`Number was out of range! Range was ${min}-${max} (inclusive).`); }; + + const isOutOfRange = (num) => num < min || num > max; + + const addToRangeVal = (range, num) => range.add(num); + + const addToRangeLoHi = (range, lo, hi) => { + for (let i = lo; i <= hi; ++i) range.add(i); + }; + + const clean = input.replace(/\s*/g, ""); + if (!/^((\d+-\d+|\d+),)*(\d+-\d+|\d+)$/.exec(clean)) errInvalid(); + + const parts = clean.split(","); + const out = new Set(); + + for (const part of parts) { + if (part.includes("-")) { + const spl = part.split("-"); + const numLo = Number(spl[0]); + const numHi = Number(spl[1]); + + if (isNaN(numLo) || isNaN(numHi) || numLo === 0 || numHi === 0 || numLo > numHi) errInvalid(); + + if (isOutOfRange(numLo) || isOutOfRange(numHi)) errOutOfRange(); + + if (numLo === numHi) addToRangeVal(out, numLo); + else addToRangeLoHi(out, numLo, numHi); + continue; + } + + const num = Number(part); + if (isNaN(num) || num === 0) errInvalid(); + + if (isOutOfRange(num)) errOutOfRange(); + addToRangeVal(out, num); + } + + return out; + }, + + findCommonPrefix (strArr, {isRespectWordBoundaries} = {}) { + if (isRespectWordBoundaries) { + return MiscUtil._findCommonPrefixSuffixWords({strArr}); + } + + let prefix = null; + strArr.forEach(s => { + if (prefix == null) { + prefix = s; + return; + } + + const minLen = Math.min(s.length, prefix.length); + for (let i = 0; i < minLen; ++i) { + const cp = prefix[i]; + const cs = s[i]; + if (cp !== cs) { + prefix = prefix.substring(0, i); + break; + } + } + }); + return prefix; + }, + + findCommonSuffix (strArr, {isRespectWordBoundaries} = {}) { + if (!isRespectWordBoundaries) throw new Error(`Unimplemented!`); + + return MiscUtil._findCommonPrefixSuffixWords({strArr, isSuffix: true}); + }, + + _findCommonPrefixSuffixWords ({strArr, isSuffix}) { + let prefixTks = null; + let lenMax = -1; + + strArr + .map(str => { + lenMax = Math.max(lenMax, str.length); + return str.split(" "); + }) + .forEach(tks => { + if (isSuffix) tks.reverse(); + + if (prefixTks == null) return prefixTks = [...tks]; + + const minLen = Math.min(tks.length, prefixTks.length); + while (prefixTks.length > minLen) prefixTks.pop(); + + for (let i = 0; i < minLen; ++i) { + const cp = prefixTks[i]; + const cs = tks[i]; + if (cp !== cs) { + prefixTks = prefixTks.slice(0, i); + break; + } + } + }); + + if (isSuffix) prefixTks.reverse(); + + if (!prefixTks.length) return ""; + + const out = prefixTks.join(" "); + if (out.length === lenMax) return out; + + return isSuffix + ? ` ${prefixTks.join(" ")}` + : `${prefixTks.join(" ")} `; + }, + + /** + * @param fgHexTarget Target/resultant color for the foreground item + * @param fgOpacity Desired foreground transparency (0-1 inclusive) + * @param bgHex Background color + */ + calculateBlendedColor (fgHexTarget, fgOpacity, bgHex) { + const fgDcTarget = CryptUtil.hex2Dec(fgHexTarget); + const bgDc = CryptUtil.hex2Dec(bgHex); + return ((fgDcTarget - ((1 - fgOpacity) * bgDc)) / fgOpacity).toString(16); + }, + + /** + * Borrowed from lodash. + * + * @param func The function to debounce. + * @param wait Minimum duration between calls. + * @param options Options object. + * @return {Function} The debounced function. + */ + debounce (func, wait, options) { + let lastArgs; let lastThis; let maxWait; let result; let timerId; let lastCallTime; let lastInvokeTime = 0; let leading = false; let maxing = false; let trailing = true; + + wait = Number(wait) || 0; + if (typeof options === "object") { + leading = !!options.leading; + maxing = "maxWait" in options; + maxWait = maxing ? Math.max(Number(options.maxWait) || 0, wait) : maxWait; + trailing = "trailing" in options ? !!options.trailing : trailing; + } + + function invokeFunc (time) { + let args = lastArgs; let thisArg = lastThis; + + lastArgs = lastThis = undefined; + lastInvokeTime = time; + result = func.apply(thisArg, args); + return result; + } + + function leadingEdge (time) { + lastInvokeTime = time; + timerId = setTimeout(timerExpired, wait); + return leading ? invokeFunc(time) : result; + } + + function remainingWait (time) { + let timeSinceLastCall = time - lastCallTime; let timeSinceLastInvoke = time - lastInvokeTime; let result = wait - timeSinceLastCall; + return maxing ? Math.min(result, maxWait - timeSinceLastInvoke) : result; + } + + function shouldInvoke (time) { + let timeSinceLastCall = time - lastCallTime; let timeSinceLastInvoke = time - lastInvokeTime; + + return (lastCallTime === undefined || (timeSinceLastCall >= wait) || (timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait)); + } + + function timerExpired () { + const time = Date.now(); + if (shouldInvoke(time)) { + return trailingEdge(time); + } + // Restart the timer. + timerId = setTimeout(timerExpired, remainingWait(time)); + } + + function trailingEdge (time) { + timerId = undefined; + + if (trailing && lastArgs) return invokeFunc(time); + lastArgs = lastThis = undefined; + return result; + } + + function cancel () { + if (timerId !== undefined) clearTimeout(timerId); + lastInvokeTime = 0; + lastArgs = lastCallTime = lastThis = timerId = undefined; + } + + function flush () { + return timerId === undefined ? result : trailingEdge(Date.now()); + } + + function debounced () { + let time = Date.now(); let isInvoking = shouldInvoke(time); + lastArgs = arguments; + lastThis = this; + lastCallTime = time; + + if (isInvoking) { + if (timerId === undefined) return leadingEdge(lastCallTime); + if (maxing) { + // Handle invocations in a tight loop. + timerId = setTimeout(timerExpired, wait); + return invokeFunc(lastCallTime); + } + } + if (timerId === undefined) timerId = setTimeout(timerExpired, wait); + return result; + } + + debounced.cancel = cancel; + debounced.flush = flush; + return debounced; + }, + + // from lodash + throttle (func, wait, options) { + let leading = true; let trailing = true; + + if (typeof options === "object") { + leading = "leading" in options ? !!options.leading : leading; + trailing = "trailing" in options ? !!options.trailing : trailing; + } + + return this.debounce(func, wait, {leading, maxWait: wait, trailing}); + }, + + pDelay (msecs, resolveAs) { + return new Promise(resolve => setTimeout(() => resolve(resolveAs), msecs)); + }, + + GENERIC_WALKER_ENTRIES_KEY_BLOCKLIST: new Set(["caption", "type", "colLabels", "colLabelGroups", "name", "colStyles", "style", "shortName", "subclassShortName", "id", "path"]), + + /** + * @param [opts] + * @param [opts.keyBlocklist] + * @param [opts.isAllowDeleteObjects] If returning `undefined` from an object handler should be treated as a delete. + * @param [opts.isAllowDeleteArrays] If returning `undefined` from an array handler should be treated as a delete. + * @param [opts.isAllowDeleteBooleans] (Unimplemented) // TODO + * @param [opts.isAllowDeleteNumbers] (Unimplemented) // TODO + * @param [opts.isAllowDeleteStrings] (Unimplemented) // TODO + * @param [opts.isDepthFirst] If array/object recursion should occur before array/object primitive handling. + * @param [opts.isNoModification] If the walker should not attempt to modify the data. + * @param [opts.isBreakOnReturn] If the walker should fast-exist on any handler returning a value. + */ + getWalker (opts) { + opts = opts || {}; + + if (opts.isBreakOnReturn && !opts.isNoModification) throw new Error(`"isBreakOnReturn" may only be used in "isNoModification" mode!`); + + const keyBlocklist = opts.keyBlocklist || new Set(); + + const getMappedPrimitive = (obj, primitiveHandlers, lastKey, stack, prop, propPre, propPost) => { + if (primitiveHandlers[propPre]) MiscUtil._getWalker_runHandlers({handlers: primitiveHandlers[propPre], obj, lastKey, stack}); + if (primitiveHandlers[prop]) { + const out = MiscUtil._getWalker_applyHandlers({opts, handlers: primitiveHandlers[prop], obj, lastKey, stack}); + if (out === VeCt.SYM_WALKER_BREAK) return out; + if (!opts.isNoModification) obj = out; + } + if (primitiveHandlers[propPost]) MiscUtil._getWalker_runHandlers({handlers: primitiveHandlers[propPost], obj, lastKey, stack}); + return obj; + }; + + const doObjectRecurse = (obj, primitiveHandlers, stack) => { + for (const k of Object.keys(obj)) { + if (keyBlocklist.has(k)) continue; + + const out = fn(obj[k], primitiveHandlers, k, stack); + if (out === VeCt.SYM_WALKER_BREAK) return VeCt.SYM_WALKER_BREAK; + if (!opts.isNoModification) obj[k] = out; + } + }; + + const fn = (obj, primitiveHandlers, lastKey, stack) => { + if (obj === null) return getMappedPrimitive(obj, primitiveHandlers, lastKey, stack, "null", "preNull", "postNull"); + + switch (typeof obj) { + case "undefined": return getMappedPrimitive(obj, primitiveHandlers, lastKey, stack, "undefined", "preUndefined", "postUndefined"); + case "boolean": return getMappedPrimitive(obj, primitiveHandlers, lastKey, stack, "boolean", "preBoolean", "postBoolean"); + case "number": return getMappedPrimitive(obj, primitiveHandlers, lastKey, stack, "number", "preNumber", "postNumber"); + case "string": return getMappedPrimitive(obj, primitiveHandlers, lastKey, stack, "string", "preString", "postString"); + case "object": { + // region Array + if (obj instanceof Array) { + if (primitiveHandlers.preArray) MiscUtil._getWalker_runHandlers({handlers: primitiveHandlers.preArray, obj, lastKey, stack}); + if (opts.isDepthFirst) { + if (stack) stack.push(obj); + const out = new Array(obj.length); + for (let i = 0, len = out.length; i < len; ++i) { + out[i] = fn(obj[i], primitiveHandlers, lastKey, stack); + if (out[i] === VeCt.SYM_WALKER_BREAK) return out[i]; + } + if (!opts.isNoModification) obj = out; + if (stack) stack.pop(); + + if (primitiveHandlers.array) { + const out = MiscUtil._getWalker_applyHandlers({opts, handlers: primitiveHandlers.array, obj, lastKey, stack}); + if (out === VeCt.SYM_WALKER_BREAK) return out; + if (!opts.isNoModification) obj = out; + } + if (obj == null) { + if (!opts.isAllowDeleteArrays) throw new Error(`Array handler(s) returned null!`); + } + } else { + if (primitiveHandlers.array) { + const out = MiscUtil._getWalker_applyHandlers({opts, handlers: primitiveHandlers.array, obj, lastKey, stack}); + if (out === VeCt.SYM_WALKER_BREAK) return out; + if (!opts.isNoModification) obj = out; + } + if (obj != null) { + const out = new Array(obj.length); + for (let i = 0, len = out.length; i < len; ++i) { + if (stack) stack.push(obj); + out[i] = fn(obj[i], primitiveHandlers, lastKey, stack); + if (stack) stack.pop(); + if (out[i] === VeCt.SYM_WALKER_BREAK) return out[i]; + } + if (!opts.isNoModification) obj = out; + } else { + if (!opts.isAllowDeleteArrays) throw new Error(`Array handler(s) returned null!`); + } + } + if (primitiveHandlers.postArray) MiscUtil._getWalker_runHandlers({handlers: primitiveHandlers.postArray, obj, lastKey, stack}); + return obj; + } + // endregion + + // region Object + if (primitiveHandlers.preObject) MiscUtil._getWalker_runHandlers({handlers: primitiveHandlers.preObject, obj, lastKey, stack}); + if (opts.isDepthFirst) { + if (stack) stack.push(obj); + const flag = doObjectRecurse(obj, primitiveHandlers, stack); + if (stack) stack.pop(); + if (flag === VeCt.SYM_WALKER_BREAK) return flag; + + if (primitiveHandlers.object) { + const out = MiscUtil._getWalker_applyHandlers({opts, handlers: primitiveHandlers.object, obj, lastKey, stack}); + if (out === VeCt.SYM_WALKER_BREAK) return out; + if (!opts.isNoModification) obj = out; + } + if (obj == null) { + if (!opts.isAllowDeleteObjects) throw new Error(`Object handler(s) returned null!`); + } + } else { + if (primitiveHandlers.object) { + const out = MiscUtil._getWalker_applyHandlers({opts, handlers: primitiveHandlers.object, obj, lastKey, stack}); + if (out === VeCt.SYM_WALKER_BREAK) return out; + if (!opts.isNoModification) obj = out; + } + if (obj == null) { + if (!opts.isAllowDeleteObjects) throw new Error(`Object handler(s) returned null!`); + } else { + if (stack) stack.push(obj); + const flag = doObjectRecurse(obj, primitiveHandlers, stack); + if (stack) stack.pop(); + if (flag === VeCt.SYM_WALKER_BREAK) return flag; + } + } + if (primitiveHandlers.postObject) MiscUtil._getWalker_runHandlers({handlers: primitiveHandlers.postObject, obj, lastKey, stack}); + return obj; + // endregion + } + default: throw new Error(`Unhandled type "${typeof obj}"`); + } + }; + + return {walk: fn}; + }, + + _getWalker_applyHandlers ({opts, handlers, obj, lastKey, stack}) { + handlers = handlers instanceof Array ? handlers : [handlers]; + const didBreak = handlers.some(h => { + const out = h(obj, lastKey, stack); + if (opts.isBreakOnReturn && out) return true; + if (!opts.isNoModification) obj = out; + }); + if (didBreak) return VeCt.SYM_WALKER_BREAK; + return obj; + }, + + _getWalker_runHandlers ({handlers, obj, lastKey, stack}) { + handlers = handlers instanceof Array ? handlers : [handlers]; + handlers.forEach(h => h(obj, lastKey, stack)); + }, + + /** + * TODO refresh to match sync version + * @param [opts] + * @param [opts.keyBlocklist] + * @param [opts.isAllowDeleteObjects] If returning `undefined` from an object handler should be treated as a delete. + * @param [opts.isAllowDeleteArrays] If returning `undefined` from an array handler should be treated as a delete. + * @param [opts.isAllowDeleteBooleans] (Unimplemented) // TODO + * @param [opts.isAllowDeleteNumbers] (Unimplemented) // TODO + * @param [opts.isAllowDeleteStrings] (Unimplemented) // TODO + * @param [opts.isDepthFirst] If array/object recursion should occur before array/object primitive handling. + * @param [opts.isNoModification] If the walker should not attempt to modify the data. + */ + getAsyncWalker (opts) { + opts = opts || {}; + const keyBlocklist = opts.keyBlocklist || new Set(); + + const pFn = async (obj, primitiveHandlers, lastKey, stack) => { + if (obj == null) { + if (primitiveHandlers.null) return MiscUtil._getAsyncWalker_pApplyHandlers({opts, handlers: primitiveHandlers.null, obj, lastKey, stack}); + return obj; + } + + const pDoObjectRecurse = async () => { + await Object.keys(obj).pSerialAwaitMap(async k => { + const v = obj[k]; + if (keyBlocklist.has(k)) return; + const out = await pFn(v, primitiveHandlers, k, stack); + if (!opts.isNoModification) obj[k] = out; + }); + }; + + const to = typeof obj; + switch (to) { + case undefined: + if (primitiveHandlers.preUndefined) await MiscUtil._getAsyncWalker_pRunHandlers({handlers: primitiveHandlers.preUndefined, obj, lastKey, stack}); + if (primitiveHandlers.undefined) { + const out = await MiscUtil._getAsyncWalker_pApplyHandlers({opts, handlers: primitiveHandlers.undefined, obj, lastKey, stack}); + if (!opts.isNoModification) obj = out; + } + if (primitiveHandlers.postUndefined) await MiscUtil._getAsyncWalker_pRunHandlers({handlers: primitiveHandlers.postUndefined, obj, lastKey, stack}); + return obj; + case "boolean": + if (primitiveHandlers.preBoolean) await MiscUtil._getAsyncWalker_pRunHandlers({handlers: primitiveHandlers.preBoolean, obj, lastKey, stack}); + if (primitiveHandlers.boolean) { + const out = await MiscUtil._getAsyncWalker_pApplyHandlers({opts, handlers: primitiveHandlers.boolean, obj, lastKey, stack}); + if (!opts.isNoModification) obj = out; + } + if (primitiveHandlers.postBoolean) await MiscUtil._getAsyncWalker_pRunHandlers({handlers: primitiveHandlers.postBoolean, obj, lastKey, stack}); + return obj; + case "number": + if (primitiveHandlers.preNumber) await MiscUtil._getAsyncWalker_pRunHandlers({handlers: primitiveHandlers.preNumber, obj, lastKey, stack}); + if (primitiveHandlers.number) { + const out = await MiscUtil._getAsyncWalker_pApplyHandlers({opts, handlers: primitiveHandlers.number, obj, lastKey, stack}); + if (!opts.isNoModification) obj = out; + } + if (primitiveHandlers.postNumber) await MiscUtil._getAsyncWalker_pRunHandlers({handlers: primitiveHandlers.postNumber, obj, lastKey, stack}); + return obj; + case "string": + if (primitiveHandlers.preString) await MiscUtil._getAsyncWalker_pRunHandlers({handlers: primitiveHandlers.preString, obj, lastKey, stack}); + if (primitiveHandlers.string) { + const out = await MiscUtil._getAsyncWalker_pApplyHandlers({opts, handlers: primitiveHandlers.string, obj, lastKey, stack}); + if (!opts.isNoModification) obj = out; + } + if (primitiveHandlers.postString) await MiscUtil._getAsyncWalker_pRunHandlers({handlers: primitiveHandlers.postString, obj, lastKey, stack}); + return obj; + case "object": { + if (obj instanceof Array) { + if (primitiveHandlers.preArray) await MiscUtil._getAsyncWalker_pRunHandlers({handlers: primitiveHandlers.preArray, obj, lastKey, stack}); + if (opts.isDepthFirst) { + if (stack) stack.push(obj); + const out = await obj.pSerialAwaitMap(it => pFn(it, primitiveHandlers, lastKey, stack)); + if (!opts.isNoModification) obj = out; + if (stack) stack.pop(); + + if (primitiveHandlers.array) { + const out = await MiscUtil._getAsyncWalker_pApplyHandlers({opts, handlers: primitiveHandlers.array, obj, lastKey, stack}); + if (!opts.isNoModification) obj = out; + } + if (obj == null) { + if (!opts.isAllowDeleteArrays) throw new Error(`Array handler(s) returned null!`); + } + } else { + if (primitiveHandlers.array) { + const out = await MiscUtil._getAsyncWalker_pApplyHandlers({opts, handlers: primitiveHandlers.array, obj, lastKey, stack}); + if (!opts.isNoModification) obj = out; + } + if (obj != null) { + const out = await obj.pSerialAwaitMap(it => pFn(it, primitiveHandlers, lastKey, stack)); + if (!opts.isNoModification) obj = out; + } else { + if (!opts.isAllowDeleteArrays) throw new Error(`Array handler(s) returned null!`); + } + } + if (primitiveHandlers.postArray) await MiscUtil._getAsyncWalker_pRunHandlers({handlers: primitiveHandlers.postArray, obj, lastKey, stack}); + return obj; + } else { + if (primitiveHandlers.preObject) await MiscUtil._getAsyncWalker_pRunHandlers({handlers: primitiveHandlers.preObject, obj, lastKey, stack}); + if (opts.isDepthFirst) { + if (stack) stack.push(obj); + await pDoObjectRecurse(); + if (stack) stack.pop(); + + if (primitiveHandlers.object) { + const out = await MiscUtil._getAsyncWalker_pApplyHandlers({opts, handlers: primitiveHandlers.object, obj, lastKey, stack}); + if (!opts.isNoModification) obj = out; + } + if (obj == null) { + if (!opts.isAllowDeleteObjects) throw new Error(`Object handler(s) returned null!`); + } + } else { + if (primitiveHandlers.object) { + const out = await MiscUtil._getAsyncWalker_pApplyHandlers({opts, handlers: primitiveHandlers.object, obj, lastKey, stack}); + if (!opts.isNoModification) obj = out; + } + if (obj == null) { + if (!opts.isAllowDeleteObjects) throw new Error(`Object handler(s) returned null!`); + } else { + await pDoObjectRecurse(); + } + } + if (primitiveHandlers.postObject) await MiscUtil._getAsyncWalker_pRunHandlers({handlers: primitiveHandlers.postObject, obj, lastKey, stack}); + return obj; + } + } + default: throw new Error(`Unhandled type "${to}"`); + } + }; + + return {pWalk: pFn}; + }, + + async _getAsyncWalker_pApplyHandlers ({opts, handlers, obj, lastKey, stack}) { + handlers = handlers instanceof Array ? handlers : [handlers]; + await handlers.pSerialAwaitMap(async pH => { + const out = await pH(obj, lastKey, stack); + if (!opts.isNoModification) obj = out; + }); + return obj; + }, + + async _getAsyncWalker_pRunHandlers ({handlers, obj, lastKey, stack}) { + handlers = handlers instanceof Array ? handlers : [handlers]; + await handlers.pSerialAwaitMap(pH => pH(obj, lastKey, stack)); + }, + + pDefer (fn) { + return (async () => fn())(); + }, +}; + +// EVENT HANDLERS ====================================================================================================== +globalThis.EventUtil = { + _mouseX: 0, + _mouseY: 0, + _isUsingTouch: false, + _isSetCssVars: false, + + init () { + document.addEventListener("mousemove", evt => { + EventUtil._mouseX = evt.clientX; + EventUtil._mouseY = evt.clientY; + EventUtil._onMouseMove_setCssVars(); + }); + document.addEventListener("touchstart", () => { + EventUtil._isUsingTouch = true; + }); + }, + + _eleDocRoot: null, + _onMouseMove_setCssVars () { + if (!EventUtil._isSetCssVars) return; + + EventUtil._eleDocRoot = EventUtil._eleDocRoot || document.querySelector(":root"); + + EventUtil._eleDocRoot.style.setProperty("--mouse-position-x", EventUtil._mouseX); + EventUtil._eleDocRoot.style.setProperty("--mouse-position-y", EventUtil._mouseY); + }, + + getClientX (evt) { return evt.touches && evt.touches.length ? evt.touches[0].clientX : evt.clientX; }, + getClientY (evt) { return evt.touches && evt.touches.length ? evt.touches[0].clientY : evt.clientY; }, + + getOffsetY (evt) { + if (!evt.touches?.length) return evt.offsetY; + + const bounds = evt.target.getBoundingClientRect(); + return evt.targetTouches[0].clientY - bounds.y; + }, + + getMousePos () { + return {x: EventUtil._mouseX, y: EventUtil._mouseY}; + }, + + isUsingTouch () { return !!EventUtil._isUsingTouch; }, + + isInInput (evt) { + return evt.target.nodeName === "INPUT" || evt.target.nodeName === "TEXTAREA" + || evt.target.getAttribute("contenteditable") === "true"; + }, + + isCtrlMetaKey (evt) { + return evt.ctrlKey || evt.metaKey; + }, + + noModifierKeys (evt) { return !evt.ctrlKey && !evt.altKey && !evt.metaKey; }, + + getKeyIgnoreCapsLock (evt) { + if (!evt.key) return null; + if (evt.key.length !== 1) return evt.key; + const isCaps = (evt.originalEvent || evt).getModifierState("CapsLock"); + if (!isCaps) return evt.key; + const asciiCode = evt.key.charCodeAt(0); + const isUpperCase = asciiCode >= 65 && asciiCode <= 90; + const isLowerCase = asciiCode >= 97 && asciiCode <= 122; + if (!isUpperCase && !isLowerCase) return evt.key; + return isUpperCase ? evt.key.toLowerCase() : evt.key.toUpperCase(); + }, +}; + +if (typeof window !== "undefined") window.addEventListener("load", EventUtil.init); + +// ANIMATIONS ========================================================================================================== +globalThis.AnimationUtil = class { + /** + * See: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Animations/Tips + * + * requestAnimationFrame() [...] gets executed just before the next repaint of the document. [...] because it's + * before the repaint, the style recomputation hasn't actually happened yet! + * [...] calls requestAnimationFrame() a second time! This time, the callback is run before the next repaint, + * which is after the style recomputation has occurred. + */ + static async pRecomputeStyles () { + return new Promise(resolve => { + requestAnimationFrame(() => { + requestAnimationFrame(() => { + resolve(); + }); + }); + }); + } + + static pLoadImage (uri) { + return new Promise((resolve, reject) => { + const img = new Image(); + img.onerror = err => reject(err); + img.onload = () => resolve(img); + img.src = uri; + }); + } +}; + +// CONTEXT MENUS ======================================================================================================= +globalThis.ContextUtil = { + _isInit: false, + _menus: [], + + _init () { + if (ContextUtil._isInit) return; + ContextUtil._isInit = true; + + document.body.addEventListener("click", () => ContextUtil.closeAllMenus()); + }, + + getMenu (actions) { + ContextUtil._init(); + + const menu = new ContextUtil.Menu(actions); + ContextUtil._menus.push(menu); + return menu; + }, + + deleteMenu (menu) { + if (!menu) return; + + menu.remove(); + const ix = ContextUtil._menus.findIndex(it => it === menu); + if (~ix) ContextUtil._menus.splice(ix, 1); + }, + + /** + * @param evt + * @param menu + * @param {?object} userData + * @return {Promise<*>} + */ + pOpenMenu (evt, menu, {userData = null} = {}) { + evt.preventDefault(); + evt.stopPropagation(); + + ContextUtil._init(); + + // Close any other open menus + ContextUtil._menus.filter(it => it !== menu).forEach(it => it.close()); + + return menu.pOpen(evt, {userData}); + }, + + closeAllMenus () { + ContextUtil._menus.forEach(menu => menu.close()); + }, + + Menu: class { + constructor (actions) { + this._actions = actions; + this._pResult = null; + this.resolveResult_ = null; + + this.userData = null; + + this._$ele = null; + this._metasActions = []; + + this._menusSub = []; + } + + remove () { + if (!this._$ele) return; + this._$ele.remove(); + this._$ele = null; + } + + width () { return this._$ele ? this._$ele.width() : undefined; } + height () { return this._$ele ? this._$ele.height() : undefined; } + + pOpen (evt, {userData = null, offsetY = null, boundsX = null} = {}) { + evt.stopPropagation(); + evt.preventDefault(); + + this._initLazy(); + + if (this.resolveResult_) this.resolveResult_(null); + this._pResult = new Promise(resolve => { + this.resolveResult_ = resolve; + }); + this.userData = userData; + + this._$ele + // Show as transparent/non-clickable first, so we can get an accurate width/height + .css({ + left: 0, + top: 0, + opacity: 0, + pointerEvents: "none", + }) + .showVe() + // Use the accurate width/height to set the final position, and remove our temp styling + .css({ + left: this._getMenuPosition(evt, "x", {bounds: boundsX}), + top: this._getMenuPosition(evt, "y", {offset: offsetY}), + opacity: "", + pointerEvents: "", + }); + + this._metasActions[0].$eleRow.focus(); + + return this._pResult; + } + + close () { + if (!this._$ele) return; + this._$ele.hideVe(); + + this.closeSubMenus(); + } + + isOpen () { + if (!this._$ele) return false; + return !this._$ele.hasClass("ve-hidden"); + } + + _initLazy () { + if (this._$ele) { + this._metasActions.forEach(meta => meta.action.update()); + return; + } + + const $elesAction = this._actions.map(it => { + if (it == null) return $(`
            `); + + const rdMeta = it.render({menu: this}); + this._metasActions.push(rdMeta); + return rdMeta.$eleRow; + }); + + this._$ele = $$`
            ${$elesAction}
            ` + .hideVe() + .appendTo(document.body); + } + + _getMenuPosition (evt, axis, {bounds = null, offset = null} = {}) { + const {fnMenuSize, fnGetEventPos, fnWindowSize, fnScrollDir} = axis === "x" + ? {fnMenuSize: "width", fnGetEventPos: "getClientX", fnWindowSize: "width", fnScrollDir: "scrollLeft"} + : {fnMenuSize: "height", fnGetEventPos: "getClientY", fnWindowSize: "height", fnScrollDir: "scrollTop"}; + + const posMouse = EventUtil[fnGetEventPos](evt); + const szWin = $(window)[fnWindowSize](); + const posScroll = $(window)[fnScrollDir](); + let position = posMouse + posScroll; + + if (offset) position += offset; + + const szMenu = this[fnMenuSize](); + + // region opening menu would violate bounds + if (bounds != null) { + const {trailingLower, leadingUpper} = bounds; + + const posTrailing = position; + const posLeading = position + szMenu; + + if (posTrailing < trailingLower) { + position += trailingLower - posTrailing; + } else if (posLeading > leadingUpper) { + position -= posLeading - leadingUpper; + } + } + // endregion + + // opening menu would pass the side of the page + if (position + szMenu > szWin && szMenu < position) position -= szMenu; + + return position; + } + + addSubMenu (menu) { + this._menusSub.push(menu); + } + + closeSubMenus (menuSubExclude = null) { + this._menusSub + .filter(menuSub => menuSubExclude == null || menuSub !== menuSubExclude) + .forEach(menuSub => menuSub.close()); + } + }, + + /** + * @param text + * @param fnAction Action, which is passed its triggering click event as an argument. + * @param [opts] Options object. + * @param [opts.isDisabled] If this action is disabled. + * @param [opts.title] Help (title) text. + * @param [opts.style] Additional CSS classes to add (e.g. `ctx-danger`). + * @param [opts.fnActionAlt] Alternate action, which can be accessed by clicking a secondary "settings"-esque button. + * @param [opts.textAlt] Text for the alt-action button + * @param [opts.titleAlt] Title for the alt-action button + */ + Action: function (text, fnAction, opts) { + opts = opts || {}; + + this.text = text; + this.fnAction = fnAction; + + this.isDisabled = opts.isDisabled; + this.title = opts.title; + this.style = opts.style; + + this.fnActionAlt = opts.fnActionAlt; + this.textAlt = opts.textAlt; + this.titleAlt = opts.titleAlt; + + this.render = function ({menu}) { + const $btnAction = this._render_$btnAction({menu}); + const $btnActionAlt = this._render_$btnActionAlt({menu}); + + return { + action: this, + $eleRow: $$`
            ${$btnAction}${$btnActionAlt}
            `, + $eleBtn: $btnAction, + }; + }; + + this._render_$btnAction = function ({menu}) { + const $btnAction = $(`
            ${this.text}
            `) + .on("click", async evt => { + if (this.isDisabled) return; + + evt.preventDefault(); + evt.stopPropagation(); + + menu.close(); + + const result = await this.fnAction(evt, {userData: menu.userData}); + if (menu.resolveResult_) menu.resolveResult_(result); + }) + .keydown(evt => { + if (evt.key !== "Enter") return; + $btnAction.click(); + }); + if (this.title) $btnAction.title(this.title); + + return $btnAction; + }; + + this._render_$btnActionAlt = function ({menu}) { + if (!this.fnActionAlt) return null; + + const $btnActionAlt = $(`
            ${this.textAlt ?? ``}
            `) + .on("click", async evt => { + if (this.isDisabled) return; + + evt.preventDefault(); + evt.stopPropagation(); + + menu.close(); + + const result = await this.fnActionAlt(evt, {userData: menu.userData}); + if (menu.resolveResult_) menu.resolveResult_(result); + }); + if (this.titleAlt) $btnActionAlt.title(this.titleAlt); + + return $btnActionAlt; + }; + + this.update = function () { /* Implement as required */ }; + }, + + ActionLink: function (text, fnHref, opts) { + ContextUtil.Action.call(this, text, null, opts); + + this.fnHref = fnHref; + this._$btnAction = null; + + this._render_$btnAction = function () { + this._$btnAction = $(`${this.text}`); + if (this.title) this._$btnAction.title(this.title); + + return this._$btnAction; + }; + + this.update = function () { + this._$btnAction.attr("href", this.fnHref()); + }; + }, + + ActionSelect: function ( + { + values, + fnOnChange = null, + fnGetDisplayValue = null, + }, + ) { + this._values = values; + this._fnOnChange = fnOnChange; + this._fnGetDisplayValue = fnGetDisplayValue; + + this._sel = null; + + this._ixInitial = null; + + this.render = function ({menu}) { + this._sel = this._render_sel({menu}); + + if (this._ixInitial != null) { + this._sel.val(`${this._ixInitial}`); + this._ixInitial = null; + } + + return { + action: this, + $eleRow: $$`
            ${this._sel}
            `, + }; + }; + + this._render_sel = function ({menu}) { + const sel = e_({ + tag: "select", + clazz: "w-100 min-w-0 mx-5 py-1", + tabindex: 0, + children: this._values + .map((val, i) => { + return e_({ + tag: "option", + value: i, + text: this._fnGetDisplayValue ? this._fnGetDisplayValue(val) : val, + }); + }), + click: async evt => { + evt.preventDefault(); + evt.stopPropagation(); + }, + keydown: evt => { + if (evt.key !== "Enter") return; + sel.click(); + }, + change: () => { + menu.close(); + + const ix = Number(sel.val() || 0); + const val = this._values[ix]; + + if (this._fnOnChange) this._fnOnChange(val); + if (menu.resolveResult_) menu.resolveResult_(val); + }, + }); + + return sel; + }; + + this.setValue = function (val) { + const ix = this._values.indexOf(val); + if (!this._sel) return this._ixInitial = ix; + this._sel.val(`${ix}`); + }; + + this.update = function () { /* Implement as required */ }; + }, + + ActionSubMenu: class { + constructor (name, actions) { + this._name = name; + this._actions = actions; + } + + render ({menu}) { + const menuSub = ContextUtil.getMenu(this._actions); + menu.addSubMenu(menuSub); + + const $eleRow = $$`
            +
            ${this._name}
            +
            +
            ` + .on("click", async evt => { + evt.stopPropagation(); + if (menuSub.isOpen()) return menuSub.close(); + + menu.closeSubMenus(menuSub); + + const bcr = $eleRow[0].getBoundingClientRect(); + + await menuSub.pOpen( + evt, + { + offsetY: bcr.top - EventUtil.getClientY(evt), + boundsX: { + trailingLower: bcr.right, + leadingUpper: bcr.left, + }, + }, + ); + + menu.close(); + }); + + return { + action: this, + $eleRow, + }; + } + + update () { /* Implement as required */ } + }, +}; + +// LIST AND SEARCH ===================================================================================================== +globalThis.SearchUtil = { + removeStemmer (elasticSearch) { + const stemmer = elasticlunr.Pipeline.getRegisteredFunction("stemmer"); + elasticSearch.pipeline.remove(stemmer); + }, +}; + +// ENCODING/DECODING =================================================================================================== +globalThis.UrlUtil = { + encodeForHash (toEncode) { + if (toEncode instanceof Array) return toEncode.map(it => `${it}`.toUrlified()).join(HASH_LIST_SEP); + else return `${toEncode}`.toUrlified(); + }, + + encodeArrayForHash (...toEncodes) { + return toEncodes.map(UrlUtil.encodeForHash).join(HASH_LIST_SEP); + }, + + autoEncodeHash (obj) { + const curPage = UrlUtil.getCurrentPage(); + const encoder = UrlUtil.URL_TO_HASH_BUILDER[curPage]; + if (!encoder) throw new Error(`No encoder found for page ${curPage}`); + return encoder(obj); + }, + + decodeHash (hash) { + return hash.split(HASH_LIST_SEP).map(it => decodeURIComponent(it)); + }, + + getSluggedHash (hash) { + return Parser.stringToSlug(decodeURIComponent(hash)).replace(/_/g, "-"); + }, + + getCurrentPage () { + if (typeof window === "undefined") return VeCt.PG_NONE; + const pSplit = window.location.pathname.split("/"); + let out = pSplit[pSplit.length - 1]; + if (!out.toLowerCase().endsWith(".html")) out += ".html"; + return out; + }, + + /** + * All internal URL construction should pass through here, to ensure `static.5etools.com` is used when required. + * + * @param href the link + * @param isBustCache If a cache-busting parameter should always be added. + */ + link (href, {isBustCache = false} = {}) { + if (isBustCache) return UrlUtil._link_getWithParam(href, {param: `t=${Date.now()}`}); + return href; + }, + + _link_getWithParam (href, {param = `v=${VERSION_NUMBER}`} = {}) { + if (href.includes("?")) return `${href}&${param}`; + return `${href}?${param}`; + }, + + unpackSubHash (subHash, unencode) { + // format is "key:value~list~sep~with~tilde" + if (subHash.includes(HASH_SUB_KV_SEP)) { + const keyValArr = subHash.split(HASH_SUB_KV_SEP).map(s => s.trim()); + const out = {}; + let k = keyValArr[0].toLowerCase(); + if (unencode) k = decodeURIComponent(k); + let v = keyValArr[1].toLowerCase(); + if (unencode) v = decodeURIComponent(v); + out[k] = v.split(HASH_SUB_LIST_SEP).map(s => s.trim()); + if (out[k].length === 1 && out[k] === HASH_SUB_NONE) out[k] = []; + return out; + } else { + throw new Error(`Badly formatted subhash ${subHash}`); + } + }, + + /** + * @param key The subhash key. + * @param values The subhash values. + * @param [opts] Options object. + * @param [opts.isEncodeBoth] If both the key and values should be URl encoded. + * @param [opts.isEncodeKey] If the key should be URL encoded. + * @param [opts.isEncodeValues] If the values should be URL encoded. + * @returns {string} + */ + packSubHash (key, values, opts) { + opts = opts || {}; + if (opts.isEncodeBoth || opts.isEncodeKey) key = key.toUrlified(); + if (opts.isEncodeBoth || opts.isEncodeValues) values = values.map(it => it.toUrlified()); + return `${key}${HASH_SUB_KV_SEP}${values.join(HASH_SUB_LIST_SEP)}`; + }, + + categoryToPage (category) { return UrlUtil.CAT_TO_PAGE[category]; }, + categoryToHoverPage (category) { return UrlUtil.CAT_TO_HOVER_PAGE[category] || UrlUtil.categoryToPage(category); }, + + pageToDisplayPage (page) { return UrlUtil.PG_TO_NAME[page] || page; }, + + getFilename (url) { return url.slice(url.lastIndexOf("/") + 1); }, + + isFullUrl (url) { return url && /^.*?:\/\//.test(url); }, + + mini: { + compress (primitive) { + const type = typeof primitive; + if (primitive === undefined) return "u"; + if (primitive === null) return "x"; + switch (type) { + case "boolean": return `b${Number(primitive)}`; + case "number": return `n${primitive}`; + case "string": return `s${primitive.toUrlified()}`; + default: throw new Error(`Unhandled type "${type}"`); + } + }, + + decompress (raw) { + const [type, data] = [raw.slice(0, 1), raw.slice(1)]; + switch (type) { + case "u": return undefined; + case "x": return null; + case "b": return !!Number(data); + case "n": return Number(data); + case "s": return decodeURIComponent(String(data)); + default: throw new Error(`Unhandled type "${type}"`); + } + }, + }, + + class: { + getIndexedClassEntries (cls) { + const out = []; + + (cls.classFeatures || []).forEach((lvlFeatureList, ixLvl) => { + lvlFeatureList + // don't add "you gain a subclass feature" or ASI's + .filter(feature => (!feature.gainSubclassFeature || feature.gainSubclassFeatureHasContent) + && feature.name !== "Ability Score Improvement" + && feature.name !== "Proficiency Versatility") + .forEach((feature, ixFeature) => { + const name = Renderer.findName(feature); + if (!name) { // tolerate missing names in homebrew + if (BrewUtil2.hasSourceJson(cls.source)) return; + else throw new Error("Class feature had no name!"); + } + out.push({ + _type: "classFeature", + source: cls.source.source || cls.source, + name, + hash: `${UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_CLASSES](cls)}${HASH_PART_SEP}${UrlUtil.getClassesPageStatePart({feature: {ixLevel: ixLvl, ixFeature: ixFeature}})}`, + entry: feature, + level: ixLvl + 1, + }); + }); + }); + + return out; + }, + + getIndexedSubclassEntries (sc) { + const out = []; + + const lvlFeatures = sc.subclassFeatures || []; + sc.source = sc.source || sc.classSource; // default to class source if required + + lvlFeatures.forEach(lvlFeature => { + lvlFeature.forEach((feature, ixFeature) => { + const subclassFeatureHash = `${UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_CLASSES]({name: sc.className, source: sc.classSource})}${HASH_PART_SEP}${UrlUtil.getClassesPageStatePart({subclass: sc, feature: {ixLevel: feature.level - 1, ixFeature: ixFeature}})}`; + + const name = Renderer.findName(feature); + if (!name) { // tolerate missing names in homebrew + if (BrewUtil2.hasSourceJson(sc.source)) return; + else throw new Error("Subclass feature had no name!"); + } + out.push({ + _type: "subclassFeature", + name, + subclassName: sc.name, + subclassShortName: sc.shortName, + source: sc.source.source || sc.source, + hash: subclassFeatureHash, + entry: feature, + level: feature.level, + }); + + if (feature.entries) { + const namedFeatureParts = feature.entries.filter(it => it.name); + namedFeatureParts.forEach(it => { + if (out.find(existing => it.name === existing.name && feature.level === existing.level)) return; + out.push({ + _type: "subclassFeaturePart", + name: it.name, + subclassName: sc.name, + subclassShortName: sc.shortName, + source: sc.source.source || sc.source, + hash: subclassFeatureHash, + entry: feature, + level: feature.level, + }); + }); + } + }); + }); + + return out; + }, + }, + + getStateKeySubclass (sc) { return Parser.stringToSlug(`sub ${sc.shortName || sc.name} ${sc.source}`); }, + + /** + * @param opts Options object. + * @param [opts.subclass] Subclass (or object of the form `{shortName: "str", source: "str"}`) + * @param [opts.feature] Object of the form `{ixLevel: 0, ixFeature: 0}` + */ + getClassesPageStatePart (opts) { + if (!opts.subclass && !opts.feature) return ""; + + if (!opts.feature) return UrlUtil.packSubHash("state", [UrlUtil._getClassesPageStatePart_subclass(opts.subclass)]); + if (!opts.subclass) return UrlUtil.packSubHash("state", [UrlUtil._getClassesPageStatePart_feature(opts.feature)]); + + return UrlUtil.packSubHash( + "state", + [ + UrlUtil._getClassesPageStatePart_subclass(opts.subclass), + UrlUtil._getClassesPageStatePart_feature(opts.feature), + ], + ); + }, + + _getClassesPageStatePart_subclass (sc) { return `${UrlUtil.getStateKeySubclass(sc)}=${UrlUtil.mini.compress(true)}`; }, + _getClassesPageStatePart_feature (feature) { return `feature=${UrlUtil.mini.compress(`${feature.ixLevel}-${feature.ixFeature}`)}`; }, +}; + +UrlUtil.PG_BESTIARY = "bestiary.html"; +UrlUtil.PG_SPELLS = "spells.html"; +UrlUtil.PG_BACKGROUNDS = "backgrounds.html"; +UrlUtil.PG_ITEMS = "items.html"; +UrlUtil.PG_CLASSES = "classes.html"; +UrlUtil.PG_CONDITIONS_DISEASES = "conditionsdiseases.html"; +UrlUtil.PG_FEATS = "feats.html"; +UrlUtil.PG_OPT_FEATURES = "optionalfeatures.html"; +UrlUtil.PG_PSIONICS = "psionics.html"; +UrlUtil.PG_RACES = "races.html"; +UrlUtil.PG_REWARDS = "rewards.html"; +UrlUtil.PG_VARIANTRULES = "variantrules.html"; +UrlUtil.PG_ADVENTURE = "adventure.html"; +UrlUtil.PG_ADVENTURES = "adventures.html"; +UrlUtil.PG_BOOK = "book.html"; +UrlUtil.PG_BOOKS = "books.html"; +UrlUtil.PG_DEITIES = "deities.html"; +UrlUtil.PG_CULTS_BOONS = "cultsboons.html"; +UrlUtil.PG_OBJECTS = "objects.html"; +UrlUtil.PG_TRAPS_HAZARDS = "trapshazards.html"; +UrlUtil.PG_QUICKREF = "quickreference.html"; +UrlUtil.PG_MANAGE_BREW = "managebrew.html"; +UrlUtil.PG_MANAGE_PRERELEASE = "manageprerelease.html"; +UrlUtil.PG_MAKE_BREW = "makebrew.html"; +UrlUtil.PG_DEMO_RENDER = "renderdemo.html"; +UrlUtil.PG_TABLES = "tables.html"; +UrlUtil.PG_VEHICLES = "vehicles.html"; +UrlUtil.PG_CHARACTERS = "characters.html"; +UrlUtil.PG_ACTIONS = "actions.html"; +UrlUtil.PG_LANGUAGES = "languages.html"; +UrlUtil.PG_STATGEN = "statgen.html"; +UrlUtil.PG_LIFEGEN = "lifegen.html"; +UrlUtil.PG_NAMES = "names.html"; +UrlUtil.PG_DM_SCREEN = "dmscreen.html"; +UrlUtil.PG_CR_CALCULATOR = "crcalculator.html"; +UrlUtil.PG_ENCOUNTERGEN = "encountergen.html"; +UrlUtil.PG_LOOTGEN = "lootgen.html"; +UrlUtil.PG_TEXT_CONVERTER = "converter.html"; +UrlUtil.PG_CHANGELOG = "changelog.html"; +UrlUtil.PG_CHAR_CREATION_OPTIONS = "charcreationoptions.html"; +UrlUtil.PG_RECIPES = "recipes.html"; +UrlUtil.PG_CLASS_SUBCLASS_FEATURES = "classfeatures.html"; +UrlUtil.PG_CREATURE_FEATURES = "creaturefeatures.html"; +UrlUtil.PG_VEHICLE_FEATURES = "vehiclefeatures.html"; +UrlUtil.PG_OBJECT_FEATURES = "objectfeatures.html"; +UrlUtil.PG_TRAP_FEATURES = "trapfeatures.html"; +UrlUtil.PG_MAPS = "maps.html"; +UrlUtil.PG_SEARCH = "search.html"; +UrlUtil.PG_DECKS = "decks.html"; + +UrlUtil.URL_TO_HASH_GENERIC = (it) => UrlUtil.encodeArrayForHash(it.name, it.source); + +UrlUtil.URL_TO_HASH_BUILDER = {}; +UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_BESTIARY] = UrlUtil.URL_TO_HASH_GENERIC; +UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_SPELLS] = UrlUtil.URL_TO_HASH_GENERIC; +UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_BACKGROUNDS] = UrlUtil.URL_TO_HASH_GENERIC; +UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_ITEMS] = UrlUtil.URL_TO_HASH_GENERIC; +UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_CLASSES] = UrlUtil.URL_TO_HASH_GENERIC; +UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_CONDITIONS_DISEASES] = UrlUtil.URL_TO_HASH_GENERIC; +UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_FEATS] = UrlUtil.URL_TO_HASH_GENERIC; +UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_OPT_FEATURES] = UrlUtil.URL_TO_HASH_GENERIC; +UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_PSIONICS] = UrlUtil.URL_TO_HASH_GENERIC; +UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_RACES] = UrlUtil.URL_TO_HASH_GENERIC; +UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_REWARDS] = UrlUtil.URL_TO_HASH_GENERIC; +UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_VARIANTRULES] = UrlUtil.URL_TO_HASH_GENERIC; +UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_ADVENTURE] = (it) => UrlUtil.encodeForHash(it.id); +UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_ADVENTURES] = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_ADVENTURE]; +UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_BOOK] = (it) => UrlUtil.encodeForHash(it.id); +UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_BOOKS] = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_BOOK]; +UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_DEITIES] = (it) => UrlUtil.encodeArrayForHash(it.name, it.pantheon, it.source); +UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_CULTS_BOONS] = UrlUtil.URL_TO_HASH_GENERIC; +UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_OBJECTS] = UrlUtil.URL_TO_HASH_GENERIC; +UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_TRAPS_HAZARDS] = UrlUtil.URL_TO_HASH_GENERIC; +UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_TABLES] = UrlUtil.URL_TO_HASH_GENERIC; +UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_VEHICLES] = UrlUtil.URL_TO_HASH_GENERIC; +UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_ACTIONS] = UrlUtil.URL_TO_HASH_GENERIC; +UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_LANGUAGES] = UrlUtil.URL_TO_HASH_GENERIC; +UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_CHAR_CREATION_OPTIONS] = UrlUtil.URL_TO_HASH_GENERIC; +UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_RECIPES] = (it) => `${UrlUtil.encodeArrayForHash(it.name, it.source)}${it._scaleFactor ? `${HASH_PART_SEP}${VeCt.HASH_SCALED}${HASH_SUB_KV_SEP}${it._scaleFactor}` : ""}`; +UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_DECKS] = UrlUtil.URL_TO_HASH_GENERIC; +UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_CLASS_SUBCLASS_FEATURES] = (it) => (it.__prop === "subclassFeature" || it.subclassSource) ? UrlUtil.URL_TO_HASH_BUILDER["subclassFeature"](it) : UrlUtil.URL_TO_HASH_BUILDER["classFeature"](it); +UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_CREATURE_FEATURES] = UrlUtil.URL_TO_HASH_GENERIC; +UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_VEHICLE_FEATURES] = UrlUtil.URL_TO_HASH_GENERIC; +UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_OBJECT_FEATURES] = UrlUtil.URL_TO_HASH_GENERIC; +UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_TRAP_FEATURES] = UrlUtil.URL_TO_HASH_GENERIC; +UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_QUICKREF] = ({name, ixChapter, ixHeader}) => { + const hashParts = ["bookref-quick", ixChapter, UrlUtil.encodeForHash(name.toLowerCase())]; + if (ixHeader) hashParts.push(ixHeader); + return hashParts.join(HASH_PART_SEP); +}; + +// region Fake pages (props) +UrlUtil.URL_TO_HASH_BUILDER["monster"] = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_BESTIARY]; +UrlUtil.URL_TO_HASH_BUILDER["spell"] = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_SPELLS]; +UrlUtil.URL_TO_HASH_BUILDER["background"] = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_BACKGROUNDS]; +UrlUtil.URL_TO_HASH_BUILDER["item"] = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_ITEMS]; +UrlUtil.URL_TO_HASH_BUILDER["itemGroup"] = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_ITEMS]; +UrlUtil.URL_TO_HASH_BUILDER["baseitem"] = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_ITEMS]; +UrlUtil.URL_TO_HASH_BUILDER["magicvariant"] = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_ITEMS]; +UrlUtil.URL_TO_HASH_BUILDER["class"] = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_CLASSES]; +UrlUtil.URL_TO_HASH_BUILDER["condition"] = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_CONDITIONS_DISEASES]; +UrlUtil.URL_TO_HASH_BUILDER["disease"] = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_CONDITIONS_DISEASES]; +UrlUtil.URL_TO_HASH_BUILDER["status"] = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_CONDITIONS_DISEASES]; +UrlUtil.URL_TO_HASH_BUILDER["feat"] = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_FEATS]; +UrlUtil.URL_TO_HASH_BUILDER["optionalfeature"] = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_OPT_FEATURES]; +UrlUtil.URL_TO_HASH_BUILDER["psionic"] = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_PSIONICS]; +UrlUtil.URL_TO_HASH_BUILDER["race"] = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_RACES]; +UrlUtil.URL_TO_HASH_BUILDER["subrace"] = (it) => UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_RACES]({name: `${it.name} (${it.raceName})`, source: it.source}); +UrlUtil.URL_TO_HASH_BUILDER["reward"] = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_REWARDS]; +UrlUtil.URL_TO_HASH_BUILDER["variantrule"] = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_VARIANTRULES]; +UrlUtil.URL_TO_HASH_BUILDER["adventure"] = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_ADVENTURES]; +UrlUtil.URL_TO_HASH_BUILDER["adventureData"] = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_ADVENTURES]; +UrlUtil.URL_TO_HASH_BUILDER["book"] = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_BOOKS]; +UrlUtil.URL_TO_HASH_BUILDER["bookData"] = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_BOOKS]; +UrlUtil.URL_TO_HASH_BUILDER["deity"] = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_DEITIES]; +UrlUtil.URL_TO_HASH_BUILDER["cult"] = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_CULTS_BOONS]; +UrlUtil.URL_TO_HASH_BUILDER["boon"] = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_CULTS_BOONS]; +UrlUtil.URL_TO_HASH_BUILDER["object"] = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_OBJECTS]; +UrlUtil.URL_TO_HASH_BUILDER["trap"] = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_TRAPS_HAZARDS]; +UrlUtil.URL_TO_HASH_BUILDER["hazard"] = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_TRAPS_HAZARDS]; +UrlUtil.URL_TO_HASH_BUILDER["table"] = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_TABLES]; +UrlUtil.URL_TO_HASH_BUILDER["tableGroup"] = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_TABLES]; +UrlUtil.URL_TO_HASH_BUILDER["vehicle"] = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_VEHICLES]; +UrlUtil.URL_TO_HASH_BUILDER["vehicleUpgrade"] = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_VEHICLES]; +UrlUtil.URL_TO_HASH_BUILDER["action"] = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_ACTIONS]; +UrlUtil.URL_TO_HASH_BUILDER["language"] = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_LANGUAGES]; +UrlUtil.URL_TO_HASH_BUILDER["charoption"] = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_CHAR_CREATION_OPTIONS]; +UrlUtil.URL_TO_HASH_BUILDER["recipe"] = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_RECIPES]; +UrlUtil.URL_TO_HASH_BUILDER["deck"] = UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_DECKS]; + +UrlUtil.URL_TO_HASH_BUILDER["subclass"] = it => { + return Hist.util.getCleanHash( + `${UrlUtil.URL_TO_HASH_BUILDER[UrlUtil.PG_CLASSES]({name: it.className, source: it.classSource})}${HASH_PART_SEP}${UrlUtil.getClassesPageStatePart({subclass: it})}`, + ); +}; +UrlUtil.URL_TO_HASH_BUILDER["classFeature"] = (it) => UrlUtil.encodeArrayForHash(it.name, it.className, it.classSource, it.level, it.source); +UrlUtil.URL_TO_HASH_BUILDER["subclassFeature"] = (it) => UrlUtil.encodeArrayForHash(it.name, it.className, it.classSource, it.subclassShortName, it.subclassSource, it.level, it.source); +UrlUtil.URL_TO_HASH_BUILDER["card"] = (it) => UrlUtil.encodeArrayForHash(it.name, it.set, it.source); +UrlUtil.URL_TO_HASH_BUILDER["legendaryGroup"] = UrlUtil.URL_TO_HASH_GENERIC; +UrlUtil.URL_TO_HASH_BUILDER["itemEntry"] = UrlUtil.URL_TO_HASH_GENERIC; +UrlUtil.URL_TO_HASH_BUILDER["itemProperty"] = (it) => UrlUtil.encodeArrayForHash(it.abbreviation, it.source); +UrlUtil.URL_TO_HASH_BUILDER["itemType"] = (it) => UrlUtil.encodeArrayForHash(it.abbreviation, it.source); +UrlUtil.URL_TO_HASH_BUILDER["itemTypeAdditionalEntries"] = (it) => UrlUtil.encodeArrayForHash(it.appliesTo, it.source); +UrlUtil.URL_TO_HASH_BUILDER["itemMastery"] = UrlUtil.URL_TO_HASH_GENERIC; +UrlUtil.URL_TO_HASH_BUILDER["skill"] = UrlUtil.URL_TO_HASH_GENERIC; +UrlUtil.URL_TO_HASH_BUILDER["sense"] = UrlUtil.URL_TO_HASH_GENERIC; +UrlUtil.URL_TO_HASH_BUILDER["raceFeature"] = (it) => UrlUtil.encodeArrayForHash(it.name, it.raceName, it.raceSource, it.source); +UrlUtil.URL_TO_HASH_BUILDER["citation"] = UrlUtil.URL_TO_HASH_GENERIC; + +// Add lowercase aliases +Object.keys(UrlUtil.URL_TO_HASH_BUILDER) + .filter(k => !k.endsWith(".html") && k.toLowerCase() !== k) + .forEach(k => UrlUtil.URL_TO_HASH_BUILDER[k.toLowerCase()] = UrlUtil.URL_TO_HASH_BUILDER[k]); + +// Add raw aliases +Object.keys(UrlUtil.URL_TO_HASH_BUILDER) + .filter(k => !k.endsWith(".html")) + .forEach(k => UrlUtil.URL_TO_HASH_BUILDER[`raw_${k}`] = UrlUtil.URL_TO_HASH_BUILDER[k]); + +// Add fluff aliases; template aliases +Object.keys(UrlUtil.URL_TO_HASH_BUILDER) + .filter(k => !k.endsWith(".html")) + .forEach(k => { + UrlUtil.URL_TO_HASH_BUILDER[`${k}Fluff`] = UrlUtil.URL_TO_HASH_BUILDER[k]; + UrlUtil.URL_TO_HASH_BUILDER[`${k}Template`] = UrlUtil.URL_TO_HASH_BUILDER[k]; + }); +// endregion + +UrlUtil.PG_TO_NAME = {}; +UrlUtil.PG_TO_NAME[UrlUtil.PG_BESTIARY] = "Bestiary"; +UrlUtil.PG_TO_NAME[UrlUtil.PG_SPELLS] = "Spells"; +UrlUtil.PG_TO_NAME[UrlUtil.PG_BACKGROUNDS] = "Backgrounds"; +UrlUtil.PG_TO_NAME[UrlUtil.PG_ITEMS] = "Items"; +UrlUtil.PG_TO_NAME[UrlUtil.PG_CLASSES] = "Classes"; +UrlUtil.PG_TO_NAME[UrlUtil.PG_CONDITIONS_DISEASES] = "Conditions & Diseases"; +UrlUtil.PG_TO_NAME[UrlUtil.PG_FEATS] = "Feats"; +UrlUtil.PG_TO_NAME[UrlUtil.PG_OPT_FEATURES] = "Other Options and Features"; +UrlUtil.PG_TO_NAME[UrlUtil.PG_PSIONICS] = "Psionics"; +UrlUtil.PG_TO_NAME[UrlUtil.PG_RACES] = "Races"; +UrlUtil.PG_TO_NAME[UrlUtil.PG_REWARDS] = "Supernatural Gifts & Rewards"; +UrlUtil.PG_TO_NAME[UrlUtil.PG_VARIANTRULES] = "Optional, Variant, and Expanded Rules"; +UrlUtil.PG_TO_NAME[UrlUtil.PG_ADVENTURES] = "Adventures"; +UrlUtil.PG_TO_NAME[UrlUtil.PG_BOOKS] = "Books"; +UrlUtil.PG_TO_NAME[UrlUtil.PG_DEITIES] = "Deities"; +UrlUtil.PG_TO_NAME[UrlUtil.PG_CULTS_BOONS] = "Cults & Supernatural Boons"; +UrlUtil.PG_TO_NAME[UrlUtil.PG_OBJECTS] = "Objects"; +UrlUtil.PG_TO_NAME[UrlUtil.PG_TRAPS_HAZARDS] = "Traps & Hazards"; +UrlUtil.PG_TO_NAME[UrlUtil.PG_QUICKREF] = "Quick Reference"; +UrlUtil.PG_TO_NAME[UrlUtil.PG_MANAGE_BREW] = "Homebrew Manager"; +UrlUtil.PG_TO_NAME[UrlUtil.PG_MANAGE_PRERELEASE] = "Prerelease Content Manager"; +UrlUtil.PG_TO_NAME[UrlUtil.PG_MAKE_BREW] = "Homebrew Builder"; +UrlUtil.PG_TO_NAME[UrlUtil.PG_DEMO_RENDER] = "Renderer Demo"; +UrlUtil.PG_TO_NAME[UrlUtil.PG_TABLES] = "Tables"; +UrlUtil.PG_TO_NAME[UrlUtil.PG_VEHICLES] = "Vehicles"; +// UrlUtil.PG_TO_NAME[UrlUtil.PG_CHARACTERS] = ""; +UrlUtil.PG_TO_NAME[UrlUtil.PG_ACTIONS] = "Actions"; +UrlUtil.PG_TO_NAME[UrlUtil.PG_LANGUAGES] = "Languages"; +UrlUtil.PG_TO_NAME[UrlUtil.PG_STATGEN] = "Stat Generator"; +UrlUtil.PG_TO_NAME[UrlUtil.PG_LIFEGEN] = "This Is Your Life"; +UrlUtil.PG_TO_NAME[UrlUtil.PG_NAMES] = "Names"; +UrlUtil.PG_TO_NAME[UrlUtil.PG_DM_SCREEN] = "DM Screen"; +UrlUtil.PG_TO_NAME[UrlUtil.PG_CR_CALCULATOR] = "CR Calculator"; +UrlUtil.PG_TO_NAME[UrlUtil.PG_ENCOUNTERGEN] = "Encounter Generator"; +UrlUtil.PG_TO_NAME[UrlUtil.PG_LOOTGEN] = "Loot Generator"; +UrlUtil.PG_TO_NAME[UrlUtil.PG_TEXT_CONVERTER] = "Text Converter"; +UrlUtil.PG_TO_NAME[UrlUtil.PG_CHANGELOG] = "Changelog"; +UrlUtil.PG_TO_NAME[UrlUtil.PG_CHAR_CREATION_OPTIONS] = "Other Character Creation Options"; +UrlUtil.PG_TO_NAME[UrlUtil.PG_RECIPES] = "Recipes"; +UrlUtil.PG_TO_NAME[UrlUtil.PG_CREATURE_FEATURES] = "Creature Features"; +UrlUtil.PG_TO_NAME[UrlUtil.PG_VEHICLE_FEATURES] = "Vehicle Features"; +UrlUtil.PG_TO_NAME[UrlUtil.PG_OBJECT_FEATURES] = "Object Features"; +UrlUtil.PG_TO_NAME[UrlUtil.PG_TRAP_FEATURES] = "Trap Features"; +UrlUtil.PG_TO_NAME[UrlUtil.PG_MAPS] = "Maps"; +UrlUtil.PG_TO_NAME[UrlUtil.PG_DECKS] = "Decks"; + +UrlUtil.CAT_TO_PAGE = {}; +UrlUtil.CAT_TO_PAGE[Parser.CAT_ID_CREATURE] = UrlUtil.PG_BESTIARY; +UrlUtil.CAT_TO_PAGE[Parser.CAT_ID_SPELL] = UrlUtil.PG_SPELLS; +UrlUtil.CAT_TO_PAGE[Parser.CAT_ID_BACKGROUND] = UrlUtil.PG_BACKGROUNDS; +UrlUtil.CAT_TO_PAGE[Parser.CAT_ID_ITEM] = UrlUtil.PG_ITEMS; +UrlUtil.CAT_TO_PAGE[Parser.CAT_ID_CLASS] = UrlUtil.PG_CLASSES; +UrlUtil.CAT_TO_PAGE[Parser.CAT_ID_CLASS_FEATURE] = UrlUtil.PG_CLASSES; +UrlUtil.CAT_TO_PAGE[Parser.CAT_ID_SUBCLASS] = UrlUtil.PG_CLASSES; +UrlUtil.CAT_TO_PAGE[Parser.CAT_ID_SUBCLASS_FEATURE] = UrlUtil.PG_CLASSES; +UrlUtil.CAT_TO_PAGE[Parser.CAT_ID_CONDITION] = UrlUtil.PG_CONDITIONS_DISEASES; +UrlUtil.CAT_TO_PAGE[Parser.CAT_ID_FEAT] = UrlUtil.PG_FEATS; +UrlUtil.CAT_TO_PAGE[Parser.CAT_ID_ELDRITCH_INVOCATION] = UrlUtil.PG_OPT_FEATURES; +UrlUtil.CAT_TO_PAGE[Parser.CAT_ID_METAMAGIC] = UrlUtil.PG_OPT_FEATURES; +UrlUtil.CAT_TO_PAGE[Parser.CAT_ID_MANEUVER_BATTLEMASTER] = UrlUtil.PG_OPT_FEATURES; +UrlUtil.CAT_TO_PAGE[Parser.CAT_ID_MANEUVER_CAVALIER] = UrlUtil.PG_OPT_FEATURES; +UrlUtil.CAT_TO_PAGE[Parser.CAT_ID_ARCANE_SHOT] = UrlUtil.PG_OPT_FEATURES; +UrlUtil.CAT_TO_PAGE[Parser.CAT_ID_OPTIONAL_FEATURE_OTHER] = UrlUtil.PG_OPT_FEATURES; +UrlUtil.CAT_TO_PAGE[Parser.CAT_ID_FIGHTING_STYLE] = UrlUtil.PG_OPT_FEATURES; +UrlUtil.CAT_TO_PAGE[Parser.CAT_ID_PSIONIC] = UrlUtil.PG_PSIONICS; +UrlUtil.CAT_TO_PAGE[Parser.CAT_ID_RACE] = UrlUtil.PG_RACES; +UrlUtil.CAT_TO_PAGE[Parser.CAT_ID_OTHER_REWARD] = UrlUtil.PG_REWARDS; +UrlUtil.CAT_TO_PAGE[Parser.CAT_ID_VARIANT_OPTIONAL_RULE] = UrlUtil.PG_VARIANTRULES; +UrlUtil.CAT_TO_PAGE[Parser.CAT_ID_ADVENTURE] = UrlUtil.PG_ADVENTURE; +UrlUtil.CAT_TO_PAGE[Parser.CAT_ID_DEITY] = UrlUtil.PG_DEITIES; +UrlUtil.CAT_TO_PAGE[Parser.CAT_ID_OBJECT] = UrlUtil.PG_OBJECTS; +UrlUtil.CAT_TO_PAGE[Parser.CAT_ID_TRAP] = UrlUtil.PG_TRAPS_HAZARDS; +UrlUtil.CAT_TO_PAGE[Parser.CAT_ID_HAZARD] = UrlUtil.PG_TRAPS_HAZARDS; +UrlUtil.CAT_TO_PAGE[Parser.CAT_ID_QUICKREF] = UrlUtil.PG_QUICKREF; +UrlUtil.CAT_TO_PAGE[Parser.CAT_ID_CULT] = UrlUtil.PG_CULTS_BOONS; +UrlUtil.CAT_TO_PAGE[Parser.CAT_ID_BOON] = UrlUtil.PG_CULTS_BOONS; +UrlUtil.CAT_TO_PAGE[Parser.CAT_ID_DISEASE] = UrlUtil.PG_CONDITIONS_DISEASES; +UrlUtil.CAT_TO_PAGE[Parser.CAT_ID_TABLE] = UrlUtil.PG_TABLES; +UrlUtil.CAT_TO_PAGE[Parser.CAT_ID_TABLE_GROUP] = UrlUtil.PG_TABLES; +UrlUtil.CAT_TO_PAGE[Parser.CAT_ID_VEHICLE] = UrlUtil.PG_VEHICLES; +UrlUtil.CAT_TO_PAGE[Parser.CAT_ID_PACT_BOON] = UrlUtil.PG_OPT_FEATURES; +UrlUtil.CAT_TO_PAGE[Parser.CAT_ID_ELEMENTAL_DISCIPLINE] = UrlUtil.PG_OPT_FEATURES; +UrlUtil.CAT_TO_PAGE[Parser.CAT_ID_ARTIFICER_INFUSION] = UrlUtil.PG_OPT_FEATURES; +UrlUtil.CAT_TO_PAGE[Parser.CAT_ID_SHIP_UPGRADE] = UrlUtil.PG_VEHICLES; +UrlUtil.CAT_TO_PAGE[Parser.CAT_ID_INFERNAL_WAR_MACHINE_UPGRADE] = UrlUtil.PG_VEHICLES; +UrlUtil.CAT_TO_PAGE[Parser.CAT_ID_ONOMANCY_RESONANT] = UrlUtil.PG_OPT_FEATURES; +UrlUtil.CAT_TO_PAGE[Parser.CAT_ID_RUNE_KNIGHT_RUNE] = UrlUtil.PG_OPT_FEATURES; +UrlUtil.CAT_TO_PAGE[Parser.CAT_ID_ALCHEMICAL_FORMULA] = UrlUtil.PG_OPT_FEATURES; +UrlUtil.CAT_TO_PAGE[Parser.CAT_ID_MANEUVER] = UrlUtil.PG_OPT_FEATURES; +UrlUtil.CAT_TO_PAGE[Parser.CAT_ID_ACTION] = UrlUtil.PG_ACTIONS; +UrlUtil.CAT_TO_PAGE[Parser.CAT_ID_LANGUAGE] = UrlUtil.PG_LANGUAGES; +UrlUtil.CAT_TO_PAGE[Parser.CAT_ID_BOOK] = UrlUtil.PG_BOOK; +UrlUtil.CAT_TO_PAGE[Parser.CAT_ID_PAGE] = null; +UrlUtil.CAT_TO_PAGE[Parser.CAT_ID_LEGENDARY_GROUP] = null; +UrlUtil.CAT_TO_PAGE[Parser.CAT_ID_CHAR_CREATION_OPTIONS] = UrlUtil.PG_CHAR_CREATION_OPTIONS; +UrlUtil.CAT_TO_PAGE[Parser.CAT_ID_RECIPES] = UrlUtil.PG_RECIPES; +UrlUtil.CAT_TO_PAGE[Parser.CAT_ID_STATUS] = UrlUtil.PG_CONDITIONS_DISEASES; +UrlUtil.CAT_TO_PAGE[Parser.CAT_ID_DECK] = UrlUtil.PG_DECKS; +UrlUtil.CAT_TO_PAGE[Parser.CAT_ID_CARD] = "card"; +UrlUtil.CAT_TO_PAGE[Parser.CAT_ID_SKILLS] = "skill"; +UrlUtil.CAT_TO_PAGE[Parser.CAT_ID_SENSES] = "sense"; +UrlUtil.CAT_TO_PAGE[Parser.CAT_ID_LEGENDARY_GROUP] = "legendaryGroup"; + +UrlUtil.CAT_TO_HOVER_PAGE = {}; +UrlUtil.CAT_TO_HOVER_PAGE[Parser.CAT_ID_CLASS_FEATURE] = "classfeature"; +UrlUtil.CAT_TO_HOVER_PAGE[Parser.CAT_ID_SUBCLASS_FEATURE] = "subclassfeature"; +UrlUtil.CAT_TO_HOVER_PAGE[Parser.CAT_ID_CARD] = "card"; +UrlUtil.CAT_TO_HOVER_PAGE[Parser.CAT_ID_SKILLS] = "skill"; +UrlUtil.CAT_TO_HOVER_PAGE[Parser.CAT_ID_SENSES] = "sense"; +UrlUtil.CAT_TO_HOVER_PAGE[Parser.CAT_ID_LEGENDARY_GROUP] = "legendaryGroup"; + +UrlUtil.HASH_START_CREATURE_SCALED = `${VeCt.HASH_SCALED}${HASH_SUB_KV_SEP}`; +UrlUtil.HASH_START_CREATURE_SCALED_SPELL_SUMMON = `${VeCt.HASH_SCALED_SPELL_SUMMON}${HASH_SUB_KV_SEP}`; +UrlUtil.HASH_START_CREATURE_SCALED_CLASS_SUMMON = `${VeCt.HASH_SCALED_CLASS_SUMMON}${HASH_SUB_KV_SEP}`; + +UrlUtil.SUBLIST_PAGES = { + [UrlUtil.PG_BESTIARY]: true, + [UrlUtil.PG_SPELLS]: true, + [UrlUtil.PG_BACKGROUNDS]: true, + [UrlUtil.PG_ITEMS]: true, + [UrlUtil.PG_CONDITIONS_DISEASES]: true, + [UrlUtil.PG_FEATS]: true, + [UrlUtil.PG_OPT_FEATURES]: true, + [UrlUtil.PG_PSIONICS]: true, + [UrlUtil.PG_RACES]: true, + [UrlUtil.PG_REWARDS]: true, + [UrlUtil.PG_VARIANTRULES]: true, + [UrlUtil.PG_DEITIES]: true, + [UrlUtil.PG_CULTS_BOONS]: true, + [UrlUtil.PG_OBJECTS]: true, + [UrlUtil.PG_TRAPS_HAZARDS]: true, + [UrlUtil.PG_TABLES]: true, + [UrlUtil.PG_VEHICLES]: true, + [UrlUtil.PG_ACTIONS]: true, + [UrlUtil.PG_LANGUAGES]: true, + [UrlUtil.PG_CHAR_CREATION_OPTIONS]: true, + [UrlUtil.PG_RECIPES]: true, + [UrlUtil.PG_DECKS]: true, +}; + +UrlUtil.PAGE_TO_PROPS = {}; +UrlUtil.PAGE_TO_PROPS[UrlUtil.PG_SPELLS] = ["spell"]; +UrlUtil.PAGE_TO_PROPS[UrlUtil.PG_ITEMS] = ["item", "itemGroup", "itemType", "itemEntry", "itemProperty", "itemTypeAdditionalEntries", "itemMastery", "baseitem", "magicvariant"]; +UrlUtil.PAGE_TO_PROPS[UrlUtil.PG_RACES] = ["race", "subrace"]; + +if (!IS_DEPLOYED && !IS_VTT && typeof window !== "undefined") { + // for local testing, hotkey to get a link to the current page on the main site + window.addEventListener("keypress", (e) => { + if (EventUtil.noModifierKeys(e) && typeof d20 === "undefined") { + if (e.key === "#") { + const spl = window.location.href.split("/"); + window.prompt("Copy to clipboard: Ctrl+C, Enter", `${Vetools._BASE_SITE_URL}/${spl[spl.length - 1]}`); + } + } + }); +} + +// SORTING ============================================================================================================= +globalThis.SortUtil = { + ascSort: (a, b) => { + if (typeof FilterItem !== "undefined") { + if (a instanceof FilterItem) a = a.item; + if (b instanceof FilterItem) b = b.item; + } + + return SortUtil._ascSort(a, b); + }, + + ascSortProp: (prop, a, b) => { return SortUtil.ascSort(a[prop], b[prop]); }, + + ascSortLower: (a, b) => { + if (typeof FilterItem !== "undefined") { + if (a instanceof FilterItem) a = a.item; + if (b instanceof FilterItem) b = b.item; + } + + a = a ? a.toLowerCase() : a; + b = b ? b.toLowerCase() : b; + + return SortUtil._ascSort(a, b); + }, + + ascSortLowerProp: (prop, a, b) => { return SortUtil.ascSortLower(a[prop], b[prop]); }, + + // warning: slow + ascSortNumericalSuffix (a, b) { + if (typeof FilterItem !== "undefined") { + if (a instanceof FilterItem) a = a.item; + if (b instanceof FilterItem) b = b.item; + } + + function popEndNumber (str) { + const spl = str.split(" "); + return spl.last().isNumeric() ? [spl.slice(0, -1).join(" "), Number(spl.last().replace(Parser._numberCleanRegexp, ""))] : [spl.join(" "), 0]; + } + + const [aStr, aNum] = popEndNumber(a.item || a); + const [bStr, bNum] = popEndNumber(b.item || b); + const initialSort = SortUtil.ascSort(aStr, bStr); + if (initialSort) return initialSort; + return SortUtil.ascSort(aNum, bNum); + }, + + _ascSort: (a, b) => { + if (b === a) return 0; + return b < a ? 1 : -1; + }, + + ascSortDate (a, b) { + return b.getTime() - a.getTime(); + }, + + ascSortDateString (a, b) { + return SortUtil.ascSortDate(new Date(a || "1970-01-0"), new Date(b || "1970-01-0")); + }, + + compareListNames (a, b) { return SortUtil._ascSort(a.name.toLowerCase(), b.name.toLowerCase()); }, + + listSort (a, b, opts) { + opts = opts || {sortBy: "name"}; + if (opts.sortBy === "name") return SortUtil.compareListNames(a, b); + else return SortUtil._compareByOrDefault_compareByOrDefault(a, b, opts.sortBy); + }, + + _listSort_compareBy (a, b, sortBy) { + const aValue = typeof a.values[sortBy] === "string" ? a.values[sortBy].toLowerCase() : a.values[sortBy]; + const bValue = typeof b.values[sortBy] === "string" ? b.values[sortBy].toLowerCase() : b.values[sortBy]; + + return SortUtil._ascSort(aValue, bValue); + }, + + _compareByOrDefault_compareByOrDefault (a, b, sortBy) { + return SortUtil._listSort_compareBy(a, b, sortBy) || SortUtil.compareListNames(a, b); + }, + + /** + * "Special Equipment" first, then alphabetical + */ + _MON_TRAIT_ORDER: [ + "special equipment", + "shapechanger", + ], + monTraitSort: (a, b) => { + if (a.sort != null && b.sort != null) return a.sort - b.sort; + if (a.sort != null && b.sort == null) return -1; + if (a.sort == null && b.sort != null) return 1; + + if (!a.name && !b.name) return 0; + const aClean = Renderer.stripTags(a.name).toLowerCase().trim(); + const bClean = Renderer.stripTags(b.name).toLowerCase().trim(); + + const isOnlyA = a.name.endsWith(" Only)"); + const isOnlyB = b.name.endsWith(" Only)"); + if (!isOnlyA && isOnlyB) return -1; + if (isOnlyA && !isOnlyB) return 1; + + const ixA = SortUtil._MON_TRAIT_ORDER.indexOf(aClean); + const ixB = SortUtil._MON_TRAIT_ORDER.indexOf(bClean); + if (~ixA && ~ixB) return ixA - ixB; + else if (~ixA) return -1; + else if (~ixB) return 1; + else return SortUtil.ascSort(aClean, bClean); + }, + + _alignFirst: ["L", "C"], + _alignSecond: ["G", "E"], + alignmentSort: (a, b) => { + if (a === b) return 0; + if (SortUtil._alignFirst.includes(a)) return -1; + if (SortUtil._alignSecond.includes(a)) return 1; + if (SortUtil._alignFirst.includes(b)) return 1; + if (SortUtil._alignSecond.includes(b)) return -1; + return 0; + }, + + ascSortCr (a, b) { + if (typeof FilterItem !== "undefined") { + if (a instanceof FilterItem) a = a.item; + if (b instanceof FilterItem) b = b.item; + } + // always put unknown values last + if (a === "Unknown") a = "998"; + if (b === "Unknown") b = "998"; + if (a === "\u2014" || a == null) a = "999"; + if (b === "\u2014" || b == null) b = "999"; + return SortUtil.ascSort(Parser.crToNumber(a), Parser.crToNumber(b)); + }, + + ascSortAtts (a, b) { + const aSpecial = a === "special"; + const bSpecial = b === "special"; + return aSpecial && bSpecial ? 0 : aSpecial ? 1 : bSpecial ? -1 : Parser.ABIL_ABVS.indexOf(a) - Parser.ABIL_ABVS.indexOf(b); + }, + + ascSortSize (a, b) { return Parser.SIZE_ABVS.indexOf(a) - Parser.SIZE_ABVS.indexOf(b); }, + + initBtnSortHandlers ($wrpBtnsSort, list) { + let dispCaretInitial = null; + + const dispCarets = [...$wrpBtnsSort[0].querySelectorAll(`[data-sort]`)] + .map(btnSort => { + const dispCaret = e_({ + tag: "span", + clazz: "lst__caret", + }) + .appendTo(btnSort); + + const btnSortField = btnSort.dataset.sort; + + if (btnSortField === list.sortBy) dispCaretInitial = dispCaret; + + e_({ + ele: btnSort, + click: evt => { + evt.stopPropagation(); + const direction = list.sortDir === "asc" ? "desc" : "asc"; + SortUtil._initBtnSortHandlers_showCaret({dispCarets, dispCaret, direction}); + list.sort(btnSortField, direction); + }, + }); + + return dispCaret; + }); + + dispCaretInitial = dispCaretInitial || dispCarets[0]; // Fall back on displaying the first caret + + SortUtil._initBtnSortHandlers_showCaret({dispCaret: dispCaretInitial, dispCarets, direction: list.sortDir}); + }, + + _initBtnSortHandlers_showCaret ( + { + dispCaret, + dispCarets, + direction, + }, + ) { + dispCarets.forEach($it => $it.removeClass("lst__caret--active")); + dispCaret.addClass("lst__caret--active").toggleClass("lst__caret--reverse", direction === "asc"); + }, + + /** Add more list sort on-clicks to existing sort buttons. */ + initBtnSortHandlersAdditional ($wrpBtnsSort, list) { + [...$wrpBtnsSort[0].querySelectorAll(".sort")] + .map(btnSort => { + const btnSortField = btnSort.dataset.sort; + + e_({ + ele: btnSort, + click: evt => { + evt.stopPropagation(); + const direction = list.sortDir === "asc" ? "desc" : "asc"; + list.sort(btnSortField, direction); + }, + }); + }); + }, + + ascSortSourceGroup (a, b) { + const grpA = a.group || "other"; + const grpB = b.group || "other"; + const ixA = SourceUtil.ADV_BOOK_GROUPS.findIndex(it => it.group === grpA); + const ixB = SourceUtil.ADV_BOOK_GROUPS.findIndex(it => it.group === grpB); + return SortUtil.ascSort(ixA, ixB); + }, + + ascSortAdventure (a, b) { + return SortUtil.ascSortDateString(b.published, a.published) + || SortUtil.ascSortLower(a.parentSource || "", b.parentSource || "") + || SortUtil.ascSort(a.publishedOrder ?? 0, b.publishedOrder ?? 0) + || SortUtil.ascSortLower(a.storyline, b.storyline) + || SortUtil.ascSort(a.level?.start ?? 20, b.level?.start ?? 20) + || SortUtil.ascSortLower(a.name, b.name); + }, + + ascSortBook (a, b) { + return SortUtil.ascSortDateString(b.published, a.published) + || SortUtil.ascSortLower(a.parentSource || "", b.parentSource || "") + || SortUtil.ascSortLower(a.name, b.name); + }, + + ascSortBookData (a, b) { + return SortUtil.ascSortLower(a.id || "", b.id || ""); + }, + + ascSortGenericEntity (a, b) { + return SortUtil.ascSortLower(a.name, b.name) || SortUtil.ascSortLower(a.source, b.source); + }, + + ascSortDeity (a, b) { + return SortUtil.ascSortLower(a.name, b.name) || SortUtil.ascSortLower(a.source, b.source) || SortUtil.ascSortLower(a.pantheon, b.pantheon); + }, + + ascSortCard (a, b) { + return SortUtil.ascSortLower(a.set, b.set) || SortUtil.ascSortLower(a.source, b.source) || SortUtil.ascSortLower(a.name, b.name); + }, + + ascSortEncounter (a, b) { + return SortUtil.ascSortLower(a.name, b.name) || SortUtil.ascSortLower(a.caption || "", b.caption || "") || SortUtil.ascSort(a.minlvl || 0, b.minlvl || 0) || SortUtil.ascSort(a.maxlvl || Number.MAX_SAFE_INTEGER, b.maxlvl || Number.MAX_SAFE_INTEGER); + }, + + _ITEM_RARITY_ORDER: ["none", "common", "uncommon", "rare", "very rare", "legendary", "artifact", "varies", "unknown (magic)", "unknown"], + ascSortItemRarity (a, b) { + const ixA = SortUtil._ITEM_RARITY_ORDER.indexOf(a); + const ixB = SortUtil._ITEM_RARITY_ORDER.indexOf(b); + return (~ixA ? ixA : Number.MAX_SAFE_INTEGER) - (~ixB ? ixB : Number.MAX_SAFE_INTEGER); + }, +}; + +// JSON LOADING ======================================================================================================== +class _DataUtilPropConfig { + static _MERGE_REQUIRES_PRESERVE = {}; + static _PAGE = null; + + static get PAGE () { return this._PAGE; } + + static async pMergeCopy (lst, ent, options) { + return DataUtil.generic._pMergeCopy(this, this._PAGE, lst, ent, options); + } +} + +class _DataUtilPropConfigSingleSource extends _DataUtilPropConfig { + static _FILENAME = null; + + static getDataUrl () { return `${Renderer.get().baseUrl}data/${this._FILENAME}`; } + + static async loadJSON () { return this.loadRawJSON(); } + static async loadRawJSON () { return DataUtil.loadJSON(this.getDataUrl()); } + static async loadUnmergedJSON () { return DataUtil.loadRawJSON(this.getDataUrl()); } +} + +class _DataUtilPropConfigMultiSource extends _DataUtilPropConfig { + static _DIR = null; + static _PROP = null; + static _IS_MUT_ENTITIES = false; + + static get _isFluff () { return this._PROP.endsWith("Fluff"); } + + static _P_INDEX = null; + + static pLoadIndex () { + this._P_INDEX = this._P_INDEX || DataUtil.loadJSON(`${Renderer.get().baseUrl}data/${this._DIR}/${this._isFluff ? `fluff-` : ""}index.json`); + return this._P_INDEX; + } + + static async pLoadAll () { + const json = await this.loadJSON(); + return json[this._PROP]; + } + + static async loadJSON () { return this._loadJSON({isUnmerged: false}); } + static async loadUnmergedJSON () { return this._loadJSON({isUnmerged: true}); } + + static async _loadJSON ({isUnmerged = false} = {}) { + const index = await this.pLoadIndex(); + + const allData = await Object.entries(index) + .pMap(async ([source, file]) => this._pLoadSourceEntities({source, isUnmerged, file})); + + return {[this._PROP]: allData.flat()}; + } + + static async pLoadSingleSource (source) { + const index = await this.pLoadIndex(); + + const file = index[source]; + if (!file) return null; + + return {[this._PROP]: await this._pLoadSourceEntities({source, file})}; + } + + static async _pLoadSourceEntities ({source, isUnmerged = false, file}) { + await this._pInitPreData(); + + const fnLoad = isUnmerged ? DataUtil.loadRawJSON.bind(DataUtil) : DataUtil.loadJSON.bind(DataUtil); + + let data = await fnLoad(`${Renderer.get().baseUrl}data/${this._DIR}/${file}`); + data = data[this._PROP].filter(it => it.source === source); + + if (!this._IS_MUT_ENTITIES) return data; + + return data.map(ent => this._mutEntity(ent)); + } + + static _P_INIT_PRE_DATA = null; + + static async _pInitPreData () { + return (this._P_INIT_PRE_DATA = this._P_INIT_PRE_DATA || this._pInitPreData_()); + } + + static async _pInitPreData_ () { /* Implement as required */ } + + static _mutEntity (ent) { return ent; } +} + +class _DataUtilPropConfigCustom extends _DataUtilPropConfig { + static async loadJSON () { throw new Error("Unimplemented!"); } + static async loadUnmergedJSON () { throw new Error("Unimplemented!"); } +} + +class _DataUtilBrewHelper { + constructor ({defaultUrlRoot}) { + this._defaultUrlRoot = defaultUrlRoot; + } + + _getCleanUrlRoot (urlRoot) { + if (urlRoot && urlRoot.trim()) { + urlRoot = urlRoot.trim(); + if (!urlRoot.endsWith("/")) urlRoot = `${urlRoot}/`; + return urlRoot; + } + return this._defaultUrlRoot; + } + + async pLoadTimestamps (urlRoot) { + urlRoot = this._getCleanUrlRoot(urlRoot); + return DataUtil.loadJSON(`${urlRoot}_generated/index-timestamps.json`); + } + + async pLoadPropIndex (urlRoot) { + urlRoot = this._getCleanUrlRoot(urlRoot); + return DataUtil.loadJSON(`${urlRoot}_generated/index-props.json`); + } + + async pLoadMetaIndex (urlRoot) { + urlRoot = this._getCleanUrlRoot(urlRoot); + return DataUtil.loadJSON(`${urlRoot}_generated/index-meta.json`); + } + + async pLoadSourceIndex (urlRoot) { + urlRoot = this._getCleanUrlRoot(urlRoot); + return DataUtil.loadJSON(`${urlRoot}_generated/index-sources.json`); + } + + getFileUrl (path, urlRoot) { + urlRoot = this._getCleanUrlRoot(urlRoot); + return `${urlRoot}${path}`; + } +} + +globalThis.DataUtil = { + _loading: {}, + _loaded: {}, + _merging: {}, + _merged: {}, + + async _pLoad ({url, id, isBustCache = false}) { + if (DataUtil._loading[id] && !isBustCache) { + await DataUtil._loading[id]; + return DataUtil._loaded[id]; + } + + DataUtil._loading[id] = new Promise((resolve, reject) => { + const request = new XMLHttpRequest(); + + request.open("GET", url, true); + /* + // These would be nice to have, but kill CORS when e.g. hitting GitHub `raw.`s. + // This may be why `fetch` dies horribly here, too. Prefer `XMLHttpRequest` for now, as it seems to have a + // higher innate tolerance to CORS nonsense. + if (isBustCache) request.setRequestHeader("Cache-Control", "no-cache, no-store"); + request.setRequestHeader("Content-Type", "application/json"); + request.setRequestHeader("Referrer-Policy", "no-referrer"); + */ + request.overrideMimeType("application/json"); + + request.onload = function () { + try { + DataUtil._loaded[id] = JSON.parse(this.response); + resolve(); + } catch (e) { + reject(new Error(`Could not parse JSON from ${url}: ${e.message}`)); + } + }; + request.onerror = (e) => { + const ptDetail = [ + "status", + "statusText", + "readyState", + "response", + "responseType", + ] + .map(prop => `${prop}=${JSON.stringify(e.target[prop])}`) + .join(" "); + reject(new Error(`Error during JSON request: ${ptDetail}`)); + }; + + request.send(); + }); + + await DataUtil._loading[id]; + return DataUtil._loaded[id]; + }, + + _mutAddProps (data) { + if (data && typeof data === "object") { + for (const k in data) { + if (data[k] instanceof Array) { + for (const it of data[k]) { + if (typeof it !== "object") continue; + it.__prop = k; + } + } + } + } + }, + + async loadJSON (url) { + return DataUtil._loadJson(url, {isDoDataMerge: true}); + }, + + async loadRawJSON (url, {isBustCache} = {}) { + return DataUtil._loadJson(url, {isBustCache}); + }, + + async _loadJson (url, {isDoDataMerge = false, isBustCache = false} = {}) { + const procUrl = UrlUtil.link(url, {isBustCache}); + + let data; + try { + data = await DataUtil._pLoad({url: procUrl, id: url, isBustCache}); + } catch (e) { + setTimeout(() => { throw e; }); + } + + // Fallback to the un-processed URL + if (!data) data = await DataUtil._pLoad({url: url, id: url, isBustCache}); + + if (isDoDataMerge) await DataUtil.pDoMetaMerge(url, data); + + return data; + }, + + /* -------------------------------------------- */ + + async pDoMetaMerge (ident, data, options) { + DataUtil._mutAddProps(data); + DataUtil._merging[ident] = DataUtil._merging[ident] || DataUtil._pDoMetaMerge(ident, data, options); + await DataUtil._merging[ident]; + const out = DataUtil._merged[ident]; + + // Cache the result, but immediately flush it. + // We do this because the cache is both a cache and a locking mechanism. + if (options?.isSkipMetaMergeCache) { + delete DataUtil._merging[ident]; + delete DataUtil._merged[ident]; + } + + return out; + }, + + _pDoMetaMerge_handleCopyProp (prop, arr, entry, options) { + if (!entry._copy) return; + let fnMergeCopy = DataUtil[prop]?.pMergeCopy; + if (!fnMergeCopy) throw new Error(`No dependency _copy merge strategy specified for property "${prop}"`); + fnMergeCopy = fnMergeCopy.bind(DataUtil[prop]); + return fnMergeCopy(arr, entry, options); + }, + + async _pDoMetaMerge (ident, data, options) { + if (data._meta) { + const loadedSourceIds = new Set(); + + if (data._meta.dependencies) { + await Promise.all(Object.entries(data._meta.dependencies).map(async ([dataProp, sourceIds]) => { + sourceIds.forEach(sourceId => loadedSourceIds.add(sourceId)); + + if (!data[dataProp]) return; // if e.g. monster dependencies are declared, but there are no monsters to merge with, bail out + + const isHasInternalCopies = (data._meta.internalCopies || []).includes(dataProp); + + const dependencyData = await Promise.all(sourceIds.map(sourceId => DataUtil.pLoadByMeta(dataProp, sourceId))); + + const flatDependencyData = dependencyData.map(dd => dd[dataProp]).flat().filter(Boolean); + await Promise.all(data[dataProp].map(entry => DataUtil._pDoMetaMerge_handleCopyProp(dataProp, flatDependencyData, entry, {...options, isErrorOnMissing: !isHasInternalCopies}))); + })); + delete data._meta.dependencies; + } + + if (data._meta.internalCopies) { + for (const prop of data._meta.internalCopies) { + if (!data[prop]) continue; + for (const entry of data[prop]) { + await DataUtil._pDoMetaMerge_handleCopyProp(prop, data[prop], entry, {...options, isErrorOnMissing: true}); + } + } + delete data._meta.internalCopies; + } + + // Load any other included data + if (data._meta.includes) { + const includesData = await Promise.all(Object.entries(data._meta.includes).map(async ([dataProp, sourceIds]) => { + // Avoid re-loading any sources we already loaded as dependencies + sourceIds = sourceIds.filter(it => !loadedSourceIds.has(it)); + + sourceIds.forEach(sourceId => loadedSourceIds.add(sourceId)); + + const includesData = await Promise.all(sourceIds.map(sourceId => DataUtil.pLoadByMeta(dataProp, sourceId))); + + const flatIncludesData = includesData.map(dd => dd[dataProp]).flat().filter(Boolean); + return {dataProp, flatIncludesData}; + })); + delete data._meta.includes; + + // Add the includes data to our current data + includesData.forEach(({dataProp, flatIncludesData}) => { + data[dataProp] = [...data[dataProp] || [], ...flatIncludesData]; + }); + } + } + + if (data._meta && data._meta.otherSources) { + await Promise.all(Object.entries(data._meta.otherSources).map(async ([dataProp, sourceIds]) => { + const additionalData = await Promise.all(Object.entries(sourceIds).map(async ([sourceId, findWith]) => ({ + findWith, + dataOther: await DataUtil.pLoadByMeta(dataProp, sourceId), + }))); + + additionalData.forEach(({findWith, dataOther}) => { + const toAppend = dataOther[dataProp].filter(it => it.otherSources && it.otherSources.find(os => os.source === findWith)); + if (toAppend.length) data[dataProp] = (data[dataProp] || []).concat(toAppend); + }); + })); + delete data._meta.otherSources; + } + + if (data._meta && !Object.keys(data._meta).length) delete data._meta; + + DataUtil._merged[ident] = data; + }, + + /* -------------------------------------------- */ + + async pDoMetaMergeSingle (prop, meta, ent) { + return (await DataUtil.pDoMetaMerge( + CryptUtil.uid(), + { + _meta: meta, + [prop]: [ent], + }, + { + isSkipMetaMergeCache: true, + }, + ))[prop][0]; + }, + + /* -------------------------------------------- */ + + getCleanFilename (filename) { + return filename.replace(/[^-_a-zA-Z0-9]/g, "_"); + }, + + getCsv (headers, rows) { + function escapeCsv (str) { + return `"${str.replace(/"/g, `""`).replace(/ +/g, " ").replace(/\n\n+/gi, "\n\n")}"`; + } + + function toCsv (row) { + return row.map(str => escapeCsv(str)).join(","); + } + + return `${toCsv(headers)}\n${rows.map(r => toCsv(r)).join("\n")}`; + }, + + userDownload (filename, data, {fileType = null, isSkipAdditionalMetadata = false, propVersion = "siteVersion", valVersion = VERSION_NUMBER} = {}) { + filename = `${filename}.json`; + if (isSkipAdditionalMetadata || data instanceof Array) return DataUtil._userDownload(filename, JSON.stringify(data, null, "\t"), "text/json"); + + data = {[propVersion]: valVersion, ...data}; + if (fileType != null) data = {fileType, ...data}; + return DataUtil._userDownload(filename, JSON.stringify(data, null, "\t"), "text/json"); + }, + + userDownloadText (filename, string) { + return DataUtil._userDownload(filename, string, "text/plain"); + }, + + _userDownload (filename, data, mimeType) { + const a = document.createElement("a"); + const t = new Blob([data], {type: mimeType}); + a.href = window.URL.createObjectURL(t); + a.download = filename; + a.dispatchEvent(new MouseEvent("click", {bubbles: true, cancelable: true, view: window})); + setTimeout(() => window.URL.revokeObjectURL(a.href), 100); + }, + + /** Always returns an array of files, even in "single" mode. */ + pUserUpload ( + { + isMultiple = false, + expectedFileTypes = null, + propVersion = "siteVersion", + } = {}, + ) { + return new Promise(resolve => { + const $iptAdd = $(``) + .on("change", (evt) => { + const input = evt.target; + + const reader = new FileReader(); + let readIndex = 0; + const out = []; + const errs = []; + + reader.onload = async () => { + const name = input.files[readIndex - 1].name; + const text = reader.result; + + try { + const json = JSON.parse(text); + + const isSkipFile = expectedFileTypes != null + && json.fileType + && !expectedFileTypes.includes(json.fileType) + && !(await InputUiUtil.pGetUserBoolean({ + textYes: "Yes", + textNo: "Cancel", + title: "File Type Mismatch", + htmlDescription: `The file "${name}" has the type "${json.fileType}" when the expected file type was "${expectedFileTypes.join("/")}".
            Are you sure you want to upload this file?`, + })); + + if (!isSkipFile) { + delete json.fileType; + delete json[propVersion]; + + out.push({name, json}); + } + } catch (e) { + errs.push({filename: name, message: e.message}); + } + + if (input.files[readIndex]) { + reader.readAsText(input.files[readIndex++]); + return; + } + + resolve({ + files: out, + errors: errs, + jsons: out.map(({json}) => json), + }); + }; + + reader.readAsText(input.files[readIndex++]); + }) + .appendTo(document.body); + + $iptAdd.click(); + }); + }, + + doHandleFileLoadErrorsGeneric (errors) { + if (!errors) return; + errors.forEach(err => { + JqueryUtil.doToast({ + content: `Could not load file "${err.filename}": ${err.message}. ${VeCt.STR_SEE_CONSOLE}`, + type: "danger", + }); + }); + }, + + cleanJson (cpy, {isDeleteUniqueId = true} = {}) { + if (!cpy) return cpy; + cpy.name = cpy._displayName || cpy.name; + if (isDeleteUniqueId) delete cpy.uniqueId; + DataUtil.__cleanJsonObject(cpy); + return cpy; + }, + + _CLEAN_JSON_ALLOWED_UNDER_KEYS: [ + "_copy", + "_versions", + "_version", + ], + __cleanJsonObject (obj) { + if (obj == null) return obj; + if (typeof obj !== "object") return obj; + + if (obj instanceof Array) { + return obj.forEach(it => DataUtil.__cleanJsonObject(it)); + } + + Object.entries(obj).forEach(([k, v]) => { + if (DataUtil._CLEAN_JSON_ALLOWED_UNDER_KEYS.includes(k)) return; + // TODO(Future) use "__" prefix for temp data, instead of "_" + if ((k.startsWith("_") && k !== "_") || k === "customHashId") delete obj[k]; + else DataUtil.__cleanJsonObject(v); + }); + }, + + _MULTI_SOURCE_PROP_TO_DIR: { + "monster": "bestiary", + "monsterFluff": "bestiary", + "spell": "spells", + "spellFluff": "spells", + "class": "class", + "subclass": "class", + "classFeature": "class", + "subclassFeature": "class", + }, + _MULTI_SOURCE_PROP_TO_INDEX_NAME: { + "class": "index.json", + "subclass": "index.json", + "classFeature": "index.json", + "subclassFeature": "index.json", + }, + async pLoadByMeta (prop, source) { + // TODO(future) expand support + + switch (prop) { + // region Predefined multi-source + case "monster": + case "spell": + case "monsterFluff": + case "spellFluff": { + const data = await DataUtil[prop].pLoadSingleSource(source); + if (data) return data; + + return DataUtil._pLoadByMeta_pGetPrereleaseBrew(source); + } + // endregion + + // region Multi-source + case "class": + case "subclass": + case "classFeature": + case "subclassFeature": { + const baseUrlPart = `${Renderer.get().baseUrl}data/${DataUtil._MULTI_SOURCE_PROP_TO_DIR[prop]}`; + const index = await DataUtil.loadJSON(`${baseUrlPart}/${DataUtil._MULTI_SOURCE_PROP_TO_INDEX_NAME[prop]}`); + if (index[source]) return DataUtil.loadJSON(`${baseUrlPart}/${index[source]}`); + + return DataUtil._pLoadByMeta_pGetPrereleaseBrew(source); + } + // endregion + + // region Special + case "item": + case "itemGroup": + case "baseitem": { + const data = await DataUtil.item.loadRawJSON(); + if (data[prop] && data[prop].some(it => it.source === source)) return data; + return DataUtil._pLoadByMeta_pGetPrereleaseBrew(source); + } + case "race": { + const data = await DataUtil.race.loadJSON({isAddBaseRaces: true}); + if (data[prop] && data[prop].some(it => it.source === source)) return data; + return DataUtil._pLoadByMeta_pGetPrereleaseBrew(source); + } + // endregion + + // region Standard + default: { + const impl = DataUtil[prop]; + if (impl && (impl.getDataUrl || impl.loadJSON)) { + const data = await (impl.loadJSON ? impl.loadJSON() : DataUtil.loadJSON(impl.getDataUrl())); + if (data[prop] && data[prop].some(it => it.source === source)) return data; + + return DataUtil._pLoadByMeta_pGetPrereleaseBrew(source); + } + + throw new Error(`Could not get loadable URL for \`${JSON.stringify({key: prop, value: source})}\``); + } + // endregion + } + }, + + async _pLoadByMeta_pGetPrereleaseBrew (source) { + const fromPrerelease = await DataUtil.pLoadPrereleaseBySource(source); + if (fromPrerelease) return fromPrerelease; + + const fromBrew = await DataUtil.pLoadBrewBySource(source); + if (fromBrew) return fromBrew; + + throw new Error(`Could not find prerelease/brew URL for source "${source}"`); + }, + + /* -------------------------------------------- */ + + async pLoadPrereleaseBySource (source) { + if (typeof PrereleaseUtil === "undefined") return null; + return this._pLoadPrereleaseBrewBySource({source, brewUtil: PrereleaseUtil}); + }, + + async pLoadBrewBySource (source) { + if (typeof BrewUtil2 === "undefined") return null; + return this._pLoadPrereleaseBrewBySource({source, brewUtil: BrewUtil2}); + }, + + async _pLoadPrereleaseBrewBySource ({source, brewUtil}) { + // Load from existing first + const fromExisting = await brewUtil.pGetBrewBySource(source); + if (fromExisting) return MiscUtil.copyFast(fromExisting.body); + + // Load from remote + const url = await brewUtil.pGetSourceUrl(source); + if (!url) return null; + + return DataUtil.loadJSON(url); + }, + + /* -------------------------------------------- */ + + // region Dbg + dbg: { + isTrackCopied: false, + }, + // endregion + + generic: { + _MERGE_REQUIRES_PRESERVE_BASE: { + page: true, + otherSources: true, + srd: true, + basicRules: true, + reprintedAs: true, + hasFluff: true, + hasFluffImages: true, + hasToken: true, + _versions: true, + }, + + _walker_replaceTxt: null, + + /** + * @param uid + * @param tag + * @param [opts] + * @param [opts.isLower] If the returned values should be lowercase. + */ + unpackUid (uid, tag, opts) { + opts = opts || {}; + if (opts.isLower) uid = uid.toLowerCase(); + let [name, source, displayText, ...others] = uid.split("|").map(Function.prototype.call.bind(String.prototype.trim)); + + source = source || Parser.getTagSource(tag, source); + if (opts.isLower) source = source.toLowerCase(); + + return { + name, + source, + displayText, + others, + }; + }, + + packUid (ent, tag) { + // | + const sourceDefault = Parser.getTagSource(tag); + return [ + ent.name, + (ent.source || "").toLowerCase() === sourceDefault.toLowerCase() ? "" : ent.source, + ].join("|").replace(/\|+$/, ""); // Trim trailing pipes + }, + + getNormalizedUid (uid, tag) { + const {name, source} = DataUtil.generic.unpackUid(uid, tag, {isLower: true}); + return [name, source].join("|"); + }, + + getUid (ent, {isMaintainCase = false} = {}) { + const {name} = ent; + const source = SourceUtil.getEntitySource(ent); + if (!name || !source) throw new Error(`Entity did not have a name and source!`); + const out = [name, source].join("|"); + if (isMaintainCase) return out; + return out.toLowerCase(); + }, + + async _pMergeCopy (impl, page, entryList, entry, options) { + if (!entry._copy) return; + + const hashCurrent = UrlUtil.URL_TO_HASH_BUILDER[page](entry); + const hash = UrlUtil.URL_TO_HASH_BUILDER[page](entry._copy); + + if (hashCurrent === hash) throw new Error(`${hashCurrent} _copy self-references! This is a bug!`); + + const it = (impl._mergeCache = impl._mergeCache || {})[hash] || DataUtil.generic._pMergeCopy_search(impl, page, entryList, entry, options); + + if (!it) { + if (options.isErrorOnMissing) { + // In development/script mode, throw an exception + if (!IS_DEPLOYED && !IS_VTT) throw new Error(`Could not find "${page}" entity "${entry._copy.name}" ("${entry._copy.source}") to copy in copier "${entry.name}" ("${entry.source}")`); + } + return; + } + + if (DataUtil.dbg.isTrackCopied) it.dbg_isCopied = true; + // Handle recursive copy + if (it._copy) await DataUtil.generic._pMergeCopy(impl, page, entryList, it, options); + + // Preload templates, if required + const templateData = entry._copy?._trait + ? (await DataUtil.loadJSON(`${Renderer.get().baseUrl}data/bestiary/template.json`)) + : null; + return DataUtil.generic.copyApplier.getCopy(impl, MiscUtil.copyFast(it), entry, templateData, options); + }, + + _pMergeCopy_search (impl, page, entryList, entry, options) { + const entryHash = UrlUtil.URL_TO_HASH_BUILDER[page](entry._copy); + return entryList.find(it => { + const hash = UrlUtil.URL_TO_HASH_BUILDER[page](it); + impl._mergeCache[hash] = it; + return hash === entryHash; + }); + }, + + COPY_ENTRY_PROPS: [ + "action", "bonus", "reaction", "trait", "legendary", "mythic", "variant", "spellcasting", + "actionHeader", "bonusHeader", "reactionHeader", "legendaryHeader", "mythicHeader", + ], + + copyApplier: class { + // convert everything to arrays + static _normaliseMods (obj) { + Object.entries(obj._mod).forEach(([k, v]) => { + if (!(v instanceof Array)) obj._mod[k] = [v]; + }); + } + + // mod helpers ///////////////// + static _doEnsureArray ({obj, prop}) { + if (!(obj[prop] instanceof Array)) obj[prop] = [obj[prop]]; + } + + static _getRegexFromReplaceModInfo ({replace, flags}) { + return new RegExp(replace, `g${flags || ""}`); + } + + static _doReplaceStringHandler ({re, withStr}, str) { + // TODO(Future) may need to have this handle replaces inside _some_ tags + const split = Renderer.splitByTags(str); + const len = split.length; + for (let i = 0; i < len; ++i) { + if (split[i].startsWith("{@")) continue; + split[i] = split[i].replace(re, withStr); + } + return split.join(""); + } + + static _doMod_appendStr ({copyTo, copyFrom, modInfo, msgPtFailed, prop}) { + if (copyTo[prop]) copyTo[prop] = `${copyTo[prop]}${modInfo.joiner || ""}${modInfo.str}`; + else copyTo[prop] = modInfo.str; + } + + static _doMod_replaceName ({copyTo, copyFrom, modInfo, msgPtFailed, prop}) { + if (!copyTo[prop]) return; + + DataUtil.generic._walker_replaceTxt = DataUtil.generic._walker_replaceTxt || MiscUtil.getWalker(); + const re = this._getRegexFromReplaceModInfo({replace: modInfo.replace, flags: modInfo.flags}); + const handlers = {string: this._doReplaceStringHandler.bind(null, {re: re, withStr: modInfo.with})}; + + copyTo[prop].forEach(it => { + if (it.name) it.name = DataUtil.generic._walker_replaceTxt.walk(it.name, handlers); + }); + } + + static _doMod_replaceTxt ({copyTo, copyFrom, modInfo, msgPtFailed, prop}) { + if (!copyTo[prop]) return; + + DataUtil.generic._walker_replaceTxt = DataUtil.generic._walker_replaceTxt || MiscUtil.getWalker(); + const re = this._getRegexFromReplaceModInfo({replace: modInfo.replace, flags: modInfo.flags}); + const handlers = {string: this._doReplaceStringHandler.bind(null, {re: re, withStr: modInfo.with})}; + + const props = modInfo.props || [null, "entries", "headerEntries", "footerEntries"]; + if (!props.length) return; + + if (props.includes(null)) { + // Handle any pure strings, e.g. `"legendaryHeader"` + copyTo[prop] = copyTo[prop].map(it => { + if (typeof it !== "string") return it; + return DataUtil.generic._walker_replaceTxt.walk(it, handlers); + }); + } + + copyTo[prop].forEach(it => { + props.forEach(prop => { + if (prop == null) return; + if (it[prop]) it[prop] = DataUtil.generic._walker_replaceTxt.walk(it[prop], handlers); + }); + }); + } + + static _doMod_prependArr ({copyTo, copyFrom, modInfo, msgPtFailed, prop}) { + this._doEnsureArray({obj: modInfo, prop: "items"}); + copyTo[prop] = copyTo[prop] ? modInfo.items.concat(copyTo[prop]) : modInfo.items; + } + + static _doMod_appendArr ({copyTo, copyFrom, modInfo, msgPtFailed, prop}) { + this._doEnsureArray({obj: modInfo, prop: "items"}); + copyTo[prop] = copyTo[prop] ? copyTo[prop].concat(modInfo.items) : modInfo.items; + } + + static _doMod_appendIfNotExistsArr ({copyTo, copyFrom, modInfo, msgPtFailed, prop}) { + this._doEnsureArray({obj: modInfo, prop: "items"}); + if (!copyTo[prop]) return copyTo[prop] = modInfo.items; + copyTo[prop] = copyTo[prop].concat(modInfo.items.filter(it => !copyTo[prop].some(x => CollectionUtil.deepEquals(it, x)))); + } + + static _doMod_replaceArr ({copyTo, copyFrom, modInfo, msgPtFailed, prop, isThrow = true}) { + this._doEnsureArray({obj: modInfo, prop: "items"}); + + if (!copyTo[prop]) { + if (isThrow) throw new Error(`${msgPtFailed} Could not find "${prop}" array`); + return false; + } + + let ixOld; + if (modInfo.replace.regex) { + const re = new RegExp(modInfo.replace.regex, modInfo.replace.flags || ""); + ixOld = copyTo[prop].findIndex(it => it.name ? re.test(it.name) : typeof it === "string" ? re.test(it) : false); + } else if (modInfo.replace.index != null) { + ixOld = modInfo.replace.index; + } else { + ixOld = copyTo[prop].findIndex(it => it.name ? it.name === modInfo.replace : it === modInfo.replace); + } + + if (~ixOld) { + copyTo[prop].splice(ixOld, 1, ...modInfo.items); + return true; + } else if (isThrow) throw new Error(`${msgPtFailed} Could not find "${prop}" item with name "${modInfo.replace}" to replace`); + return false; + } + + static _doMod_replaceOrAppendArr ({copyTo, copyFrom, modInfo, msgPtFailed, prop}) { + const didReplace = this._doMod_replaceArr({copyTo, copyFrom, modInfo, msgPtFailed, prop, isThrow: false}); + if (!didReplace) this._doMod_appendArr({copyTo, copyFrom, modInfo, msgPtFailed, prop}); + } + + static _doMod_insertArr ({copyTo, copyFrom, modInfo, msgPtFailed, prop}) { + this._doEnsureArray({obj: modInfo, prop: "items"}); + if (!copyTo[prop]) throw new Error(`${msgPtFailed} Could not find "${prop}" array`); + copyTo[prop].splice(~modInfo.index ? modInfo.index : copyTo[prop].length, 0, ...modInfo.items); + } + + static _doMod_removeArr ({copyTo, copyFrom, modInfo, msgPtFailed, prop}) { + if (modInfo.names) { + this._doEnsureArray({obj: modInfo, prop: "names"}); + modInfo.names.forEach(nameToRemove => { + const ixOld = copyTo[prop].findIndex(it => it.name === nameToRemove); + if (~ixOld) copyTo[prop].splice(ixOld, 1); + else { + if (!modInfo.force) throw new Error(`${msgPtFailed} Could not find "${prop}" item with name "${nameToRemove}" to remove`); + } + }); + } else if (modInfo.items) { + this._doEnsureArray({obj: modInfo, prop: "items"}); + modInfo.items.forEach(itemToRemove => { + const ixOld = copyTo[prop].findIndex(it => it === itemToRemove); + if (~ixOld) copyTo[prop].splice(ixOld, 1); + else throw new Error(`${msgPtFailed} Could not find "${prop}" item "${itemToRemove}" to remove`); + }); + } else throw new Error(`${msgPtFailed} One of "names" or "items" must be provided!`); + } + + static _doMod_calculateProp ({copyTo, copyFrom, modInfo, msgPtFailed, prop}) { + copyTo[prop] = copyTo[prop] || {}; + const toExec = modInfo.formula.replace(/<\$([^$]+)\$>/g, (...m) => { + switch (m[1]) { + case "prof_bonus": return Parser.crToPb(copyTo.cr); + case "dex_mod": return Parser.getAbilityModNumber(copyTo.dex); + default: throw new Error(`${msgPtFailed} Unknown variable "${m[1]}"`); + } + }); + // eslint-disable-next-line no-eval + copyTo[prop][modInfo.prop] = eval(toExec); + } + + static _doMod_scalarAddProp ({copyTo, copyFrom, modInfo, msgPtFailed, prop}) { + const applyTo = (k) => { + const out = Number(copyTo[prop][k]) + modInfo.scalar; + const isString = typeof copyTo[prop][k] === "string"; + copyTo[prop][k] = isString ? `${out >= 0 ? "+" : ""}${out}` : out; + }; + + if (!copyTo[prop]) return; + if (modInfo.prop === "*") Object.keys(copyTo[prop]).forEach(k => applyTo(k)); + else applyTo(modInfo.prop); + } + + static _doMod_scalarMultProp ({copyTo, copyFrom, modInfo, msgPtFailed, prop}) { + const applyTo = (k) => { + let out = Number(copyTo[prop][k]) * modInfo.scalar; + if (modInfo.floor) out = Math.floor(out); + const isString = typeof copyTo[prop][k] === "string"; + copyTo[prop][k] = isString ? `${out >= 0 ? "+" : ""}${out}` : out; + }; + + if (!copyTo[prop]) return; + if (modInfo.prop === "*") Object.keys(copyTo[prop]).forEach(k => applyTo(k)); + else applyTo(modInfo.prop); + } + + static _doMod_addSenses ({copyTo, copyFrom, modInfo, msgPtFailed}) { + this._doEnsureArray({obj: modInfo, prop: "senses"}); + copyTo.senses = copyTo.senses || []; + modInfo.senses.forEach(sense => { + let found = false; + for (let i = 0; i < copyTo.senses.length; ++i) { + const m = new RegExp(`${sense.type} (\\d+)`, "i").exec(copyTo.senses[i]); + if (m) { + found = true; + // if the creature already has a greater sense of this type, do nothing + if (Number(m[1]) < sense.range) { + copyTo.senses[i] = `${sense.type} ${sense.range} ft.`; + } + break; + } + } + + if (!found) copyTo.senses.push(`${sense.type} ${sense.range} ft.`); + }); + } + + static _doMod_addSaves ({copyTo, copyFrom, modInfo, msgPtFailed}) { + copyTo.save = copyTo.save || {}; + Object.entries(modInfo.saves).forEach(([save, mode]) => { + // mode: 1 = proficient; 2 = expert + const total = mode * Parser.crToPb(copyTo.cr) + Parser.getAbilityModNumber(copyTo[save]); + const asText = total >= 0 ? `+${total}` : total; + if (copyTo.save && copyTo.save[save]) { + // update only if ours is larger (prevent reduction in save) + if (Number(copyTo.save[save]) < total) copyTo.save[save] = asText; + } else copyTo.save[save] = asText; + }); + } + + static _doMod_addSkills ({copyTo, copyFrom, modInfo, msgPtFailed}) { + copyTo.skill = copyTo.skill || {}; + Object.entries(modInfo.skills).forEach(([skill, mode]) => { + // mode: 1 = proficient; 2 = expert + const total = mode * Parser.crToPb(copyTo.cr) + Parser.getAbilityModNumber(copyTo[Parser.skillToAbilityAbv(skill)]); + const asText = total >= 0 ? `+${total}` : total; + if (copyTo.skill && copyTo.skill[skill]) { + // update only if ours is larger (prevent reduction in skill score) + if (Number(copyTo.skill[skill]) < total) copyTo.skill[skill] = asText; + } else copyTo.skill[skill] = asText; + }); + } + + static _doMod_addAllSaves ({copyTo, copyFrom, modInfo, msgPtFailed}) { + return this._doMod_addSaves({ + copyTo, + copyFrom, + modInfo: { + mode: "addSaves", + saves: Object.keys(Parser.ATB_ABV_TO_FULL).mergeMap(it => ({[it]: modInfo.saves})), + }, + msgPtFailed, + }); + } + + static _doMod_addAllSkills ({copyTo, copyFrom, modInfo, msgPtFailed}) { + return this._doMod_addSkills({ + copyTo, + copyFrom, + modInfo: { + mode: "addSkills", + skills: Object.keys(Parser.SKILL_TO_ATB_ABV).mergeMap(it => ({[it]: modInfo.skills})), + }, + msgPtFailed, + }); + } + + static _doMod_addSpells ({copyTo, copyFrom, modInfo, msgPtFailed}) { + if (!copyTo.spellcasting) throw new Error(`${msgPtFailed} Creature did not have a spellcasting property!`); + + // TODO could accept a "position" or "name" parameter should spells need to be added to other spellcasting traits + const spellcasting = copyTo.spellcasting[0]; + + if (modInfo.spells) { + const spells = spellcasting.spells; + + Object.keys(modInfo.spells).forEach(k => { + if (!spells[k]) spells[k] = modInfo.spells[k]; + else { + // merge the objects + const spellCategoryNu = modInfo.spells[k]; + const spellCategoryOld = spells[k]; + Object.keys(spellCategoryNu).forEach(kk => { + if (!spellCategoryOld[kk]) spellCategoryOld[kk] = spellCategoryNu[kk]; + else { + if (typeof spellCategoryOld[kk] === "object") { + if (spellCategoryOld[kk] instanceof Array) spellCategoryOld[kk] = spellCategoryOld[kk].concat(spellCategoryNu[kk]).sort(SortUtil.ascSortLower); + else throw new Error(`${msgPtFailed} Object at key ${kk} not an array!`); + } else spellCategoryOld[kk] = spellCategoryNu[kk]; + } + }); + } + }); + } + + ["constant", "will", "ritual"].forEach(prop => { + if (!modInfo[prop]) return; + modInfo[prop].forEach(sp => (spellcasting[prop] = spellcasting[prop] || []).push(sp)); + }); + + ["recharge", "charges", "rest", "daily", "weekly", "monthly", "yearly"].forEach(prop => { + if (!modInfo[prop]) return; + + for (let i = 1; i <= 9; ++i) { + const e = `${i}e`; + + spellcasting[prop] = spellcasting[prop] || {}; + + if (modInfo[prop][i]) { + modInfo[prop][i].forEach(sp => (spellcasting[prop][i] = spellcasting[prop][i] || []).push(sp)); + } + + if (modInfo[prop][e]) { + modInfo[prop][e].forEach(sp => (spellcasting[prop][e] = spellcasting[prop][e] || []).push(sp)); + } + } + }); + } + + static _doMod_replaceSpells ({copyTo, copyFrom, modInfo, msgPtFailed}) { + if (!copyTo.spellcasting) throw new Error(`${msgPtFailed} Creature did not have a spellcasting property!`); + + // TODO could accept a "position" or "name" parameter should spells need to be added to other spellcasting traits + const spellcasting = copyTo.spellcasting[0]; + + const handleReplace = (curSpells, replaceMeta, k) => { + this._doEnsureArray({obj: replaceMeta, prop: "with"}); + + const ix = curSpells[k].indexOf(replaceMeta.replace); + if (~ix) { + curSpells[k].splice(ix, 1, ...replaceMeta.with); + curSpells[k].sort(SortUtil.ascSortLower); + } else throw new Error(`${msgPtFailed} Could not find spell "${replaceMeta.replace}" to replace`); + }; + + if (modInfo.spells) { + const trait0 = spellcasting.spells; + Object.keys(modInfo.spells).forEach(k => { // k is e.g. "4" + if (trait0[k]) { + const replaceMetas = modInfo.spells[k]; + const curSpells = trait0[k]; + replaceMetas.forEach(replaceMeta => handleReplace(curSpells, replaceMeta, "spells")); + } + }); + } + + // TODO should be extended to handle all non-slot-based spellcasters + if (modInfo.daily) { + for (let i = 1; i <= 9; ++i) { + const e = `${i}e`; + + if (modInfo.daily[i]) { + modInfo.daily[i].forEach(replaceMeta => handleReplace(spellcasting.daily, replaceMeta, i)); + } + + if (modInfo.daily[e]) { + modInfo.daily[e].forEach(replaceMeta => handleReplace(spellcasting.daily, replaceMeta, e)); + } + } + } + } + + static _doMod_removeSpells ({copyTo, copyFrom, modInfo, msgPtFailed}) { + if (!copyTo.spellcasting) throw new Error(`${msgPtFailed} Creature did not have a spellcasting property!`); + + // TODO could accept a "position" or "name" parameter should spells need to be added to other spellcasting traits + const spellcasting = copyTo.spellcasting[0]; + + if (modInfo.spells) { + const spells = spellcasting.spells; + + Object.keys(modInfo.spells).forEach(k => { + if (!spells[k]?.spells) return; + + spells[k].spells = spells[k].spells.filter(it => !modInfo.spells[k].includes(it)); + }); + } + + ["constant", "will", "ritual"].forEach(prop => { + if (!modInfo[prop]) return; + spellcasting[prop].filter(it => !modInfo[prop].includes(it)); + }); + + ["recharge", "charges", "rest", "daily", "weekly", "monthly", "yearly"].forEach(prop => { + if (!modInfo[prop]) return; + + for (let i = 1; i <= 9; ++i) { + const e = `${i}e`; + + spellcasting[prop] = spellcasting[prop] || {}; + + if (modInfo[prop][i]) { + spellcasting[prop][i] = spellcasting[prop][i].filter(it => !modInfo[prop][i].includes(it)); + } + + if (modInfo[prop][e]) { + spellcasting[prop][e] = spellcasting[prop][e].filter(it => !modInfo[prop][e].includes(it)); + } + } + }); + } + + static _doMod_scalarAddHit ({copyTo, copyFrom, modInfo, msgPtFailed, prop}) { + if (!copyTo[prop]) return; + copyTo[prop] = JSON.parse(JSON.stringify(copyTo[prop]).replace(/{@hit ([-+]?\d+)}/g, (m0, m1) => `{@hit ${Number(m1) + modInfo.scalar}}`)); + } + + static _doMod_scalarAddDc ({copyTo, copyFrom, modInfo, msgPtFailed, prop}) { + if (!copyTo[prop]) return; + copyTo[prop] = JSON.parse(JSON.stringify(copyTo[prop]).replace(/{@dc (\d+)(?:\|[^}]+)?}/g, (m0, m1) => `{@dc ${Number(m1) + modInfo.scalar}}`)); + } + + static _doMod_maxSize ({copyTo, copyFrom, modInfo, msgPtFailed}) { + const sizes = [...copyTo.size].sort(SortUtil.ascSortSize); + + const ixsCur = sizes.map(it => Parser.SIZE_ABVS.indexOf(it)); + const ixMax = Parser.SIZE_ABVS.indexOf(modInfo.max); + + if (!~ixMax || ixsCur.some(ix => !~ix)) throw new Error(`${msgPtFailed} Unhandled size!`); + + const ixsNxt = ixsCur.filter(ix => ix <= ixMax); + if (!ixsNxt.length) ixsNxt.push(ixMax); + + copyTo.size = ixsNxt.map(ix => Parser.SIZE_ABVS[ix]); + } + + static _doMod_scalarMultXp ({copyTo, copyFrom, modInfo, msgPtFailed}) { + const getOutput = (input) => { + let out = input * modInfo.scalar; + if (modInfo.floor) out = Math.floor(out); + return out; + }; + + if (copyTo.cr.xp) copyTo.cr.xp = getOutput(copyTo.cr.xp); + else { + const curXp = Parser.crToXpNumber(copyTo.cr); + if (!copyTo.cr.cr) copyTo.cr = {cr: copyTo.cr}; + copyTo.cr.xp = getOutput(curXp); + } + } + + static _doMod_setProp ({copyTo, copyFrom, modInfo, msgPtFailed, prop}) { + const propPath = modInfo.prop.split("."); + if (prop !== "*") propPath.unshift(prop); + MiscUtil.set(copyTo, ...propPath, MiscUtil.copyFast(modInfo.value)); + } + + static _doMod_handleProp ({copyTo, copyFrom, modInfos, msgPtFailed, prop = null}) { + modInfos.forEach(modInfo => { + if (typeof modInfo === "string") { + switch (modInfo) { + case "remove": return delete copyTo[prop]; + default: throw new Error(`${msgPtFailed} Unhandled mode: ${modInfo}`); + } + } else { + switch (modInfo.mode) { + case "appendStr": return this._doMod_appendStr({copyTo, copyFrom, modInfo, msgPtFailed, prop}); + case "replaceName": return this._doMod_replaceName({copyTo, copyFrom, modInfo, msgPtFailed, prop}); + case "replaceTxt": return this._doMod_replaceTxt({copyTo, copyFrom, modInfo, msgPtFailed, prop}); + case "prependArr": return this._doMod_prependArr({copyTo, copyFrom, modInfo, msgPtFailed, prop}); + case "appendArr": return this._doMod_appendArr({copyTo, copyFrom, modInfo, msgPtFailed, prop}); + case "replaceArr": return this._doMod_replaceArr({copyTo, copyFrom, modInfo, msgPtFailed, prop}); + case "replaceOrAppendArr": return this._doMod_replaceOrAppendArr({copyTo, copyFrom, modInfo, msgPtFailed, prop}); + case "appendIfNotExistsArr": return this._doMod_appendIfNotExistsArr({copyTo, copyFrom, modInfo, msgPtFailed, prop}); + case "insertArr": return this._doMod_insertArr({copyTo, copyFrom, modInfo, msgPtFailed, prop}); + case "removeArr": return this._doMod_removeArr({copyTo, copyFrom, modInfo, msgPtFailed, prop}); + case "calculateProp": return this._doMod_calculateProp({copyTo, copyFrom, modInfo, msgPtFailed, prop}); + case "scalarAddProp": return this._doMod_scalarAddProp({copyTo, copyFrom, modInfo, msgPtFailed, prop}); + case "scalarMultProp": return this._doMod_scalarMultProp({copyTo, copyFrom, modInfo, msgPtFailed, prop}); + case "setProp": return this._doMod_setProp({copyTo, copyFrom, modInfo, msgPtFailed, prop}); + // region Bestiary specific + case "addSenses": return this._doMod_addSenses({copyTo, copyFrom, modInfo, msgPtFailed}); + case "addSaves": return this._doMod_addSaves({copyTo, copyFrom, modInfo, msgPtFailed}); + case "addSkills": return this._doMod_addSkills({copyTo, copyFrom, modInfo, msgPtFailed}); + case "addAllSaves": return this._doMod_addAllSaves({copyTo, copyFrom, modInfo, msgPtFailed}); + case "addAllSkills": return this._doMod_addAllSkills({copyTo, copyFrom, modInfo, msgPtFailed}); + case "addSpells": return this._doMod_addSpells({copyTo, copyFrom, modInfo, msgPtFailed}); + case "replaceSpells": return this._doMod_replaceSpells({copyTo, copyFrom, modInfo, msgPtFailed}); + case "removeSpells": return this._doMod_removeSpells({copyTo, copyFrom, modInfo, msgPtFailed}); + case "maxSize": return this._doMod_maxSize({copyTo, copyFrom, modInfo, msgPtFailed}); + case "scalarMultXp": return this._doMod_scalarMultXp({copyTo, copyFrom, modInfo, msgPtFailed}); + case "scalarAddHit": return this._doMod_scalarAddHit({copyTo, copyFrom, modInfo, msgPtFailed, prop}); + case "scalarAddDc": return this._doMod_scalarAddDc({copyTo, copyFrom, modInfo, msgPtFailed, prop}); + // endregion + default: throw new Error(`${msgPtFailed} Unhandled mode: ${modInfo.mode}`); + } + } + }); + } + + /** + * @param copyTo + * @param copyFrom + * @param modInfos + * @param msgPtFailed + * @param {?array} props + * @param isExternalApplicationIdentityOnly + * @private + */ + static _doMod ({copyTo, copyFrom, modInfos, msgPtFailed, props = null, isExternalApplicationIdentityOnly}) { + if (isExternalApplicationIdentityOnly) return; + + if (props?.length) props.forEach(prop => this._doMod_handleProp({copyTo, copyFrom, modInfos, msgPtFailed, prop})); + // special case for "no property" modifications, i.e. underscore-key'd + else this._doMod_handleProp({copyTo, copyFrom, modInfos, msgPtFailed}); + } + + static getCopy (impl, copyFrom, copyTo, templateData, {isExternalApplicationKeepCopy = false, isExternalApplicationIdentityOnly = false} = {}) { + if (isExternalApplicationKeepCopy) copyTo.__copy = MiscUtil.copyFast(copyFrom); + + const msgPtFailed = `Failed to apply _copy to "${copyTo.name}" ("${copyTo.source}").`; + + const copyMeta = copyTo._copy || {}; + + if (copyMeta._mod) this._normaliseMods(copyMeta); + + // fetch and apply any external template -- append them to existing copy mods where available + let template = null; + if (copyMeta._trait) { + template = templateData.monsterTemplate.find(t => t.name.toLowerCase() === copyMeta._trait.name.toLowerCase() && t.source.toLowerCase() === copyMeta._trait.source.toLowerCase()); + if (!template) throw new Error(`${msgPtFailed} Could not find traits to apply with name "${copyMeta._trait.name}" and source "${copyMeta._trait.source}"`); + template = MiscUtil.copyFast(template); + + if (template.apply._mod) { + this._normaliseMods(template.apply); + + if (copyMeta._mod) { + Object.entries(template.apply._mod).forEach(([k, v]) => { + if (copyMeta._mod[k]) copyMeta._mod[k] = copyMeta._mod[k].concat(v); + else copyMeta._mod[k] = v; + }); + } else copyMeta._mod = template.apply._mod; + } + + delete copyMeta._trait; + } + + const copyToRootProps = new Set(Object.keys(copyTo)); + + // copy over required values + Object.keys(copyFrom).forEach(k => { + if (copyTo[k] === null) return delete copyTo[k]; + if (copyTo[k] == null) { + if (DataUtil.generic._MERGE_REQUIRES_PRESERVE_BASE[k] || impl?._MERGE_REQUIRES_PRESERVE[k]) { + if (copyTo._copy._preserve?.["*"] || copyTo._copy._preserve?.[k]) copyTo[k] = copyFrom[k]; + } else copyTo[k] = copyFrom[k]; + } + }); + + // apply any root racial properties after doing base copy + if (template && template.apply._root) { + Object.entries(template.apply._root) + .filter(([k, v]) => !copyToRootProps.has(k)) // avoid overwriting any real root properties + .forEach(([k, v]) => copyTo[k] = v); + } + + // apply mods + if (copyMeta._mod) { + // pre-convert any dynamic text + Object.entries(copyMeta._mod).forEach(([k, v]) => { + copyMeta._mod[k] = DataUtil.generic.variableResolver.resolve({obj: v, ent: copyTo}); + }); + + Object.entries(copyMeta._mod).forEach(([prop, modInfos]) => { + if (prop === "*") this._doMod({copyTo, copyFrom, modInfos, props: DataUtil.generic.COPY_ENTRY_PROPS, msgPtFailed, isExternalApplicationIdentityOnly}); + else if (prop === "_") this._doMod({copyTo, copyFrom, modInfos, msgPtFailed, isExternalApplicationIdentityOnly}); + else this._doMod({copyTo, copyFrom, modInfos, props: [prop], msgPtFailed, isExternalApplicationIdentityOnly}); + }); + } + + // add filter tag + copyTo._isCopy = true; + + // cleanup + delete copyTo._copy; + } + }, + + variableResolver: class { + static _getSize ({ent}) { return ent.size?.[0] || Parser.SZ_MEDIUM; } + + static _SIZE_TO_MULT = { + [Parser.SZ_LARGE]: 2, + [Parser.SZ_HUGE]: 3, + [Parser.SZ_GARGANTUAN]: 4, + }; + + static _getSizeMult (size) { return this._SIZE_TO_MULT[size] ?? 1; } + + static _getCleanMathExpression (str) { return str.replace(/[^-+/*0-9.,]+/g, ""); } + + static resolve ({obj, ent, msgPtFailed = null}) { + return JSON.parse( + JSON.stringify(obj) + .replace(/<\$(?[^$]+)\$>/g, (...m) => { + const [mode, detail] = m.last().variable.split("__"); + + switch (mode) { + case "name": return ent.name; + case "short_name": + case "title_short_name": { + return Renderer.monster.getShortName(ent, {isTitleCase: mode === "title_short_name"}); + } + + case "dc": + case "spell_dc": { + if (!Parser.ABIL_ABVS.includes(detail)) throw new Error(`${msgPtFailed ? `${msgPtFailed} ` : ""} Unknown ability score "${detail}"`); + return 8 + Parser.getAbilityModNumber(Number(ent[detail])) + Parser.crToPb(ent.cr); + } + + case "to_hit": { + if (!Parser.ABIL_ABVS.includes(detail)) throw new Error(`${msgPtFailed ? `${msgPtFailed} ` : ""} Unknown ability score "${detail}"`); + const total = Parser.crToPb(ent.cr) + Parser.getAbilityModNumber(Number(ent[detail])); + return total >= 0 ? `+${total}` : total; + } + + case "damage_mod": { + if (!Parser.ABIL_ABVS.includes(detail)) throw new Error(`${msgPtFailed ? `${msgPtFailed} ` : ""} Unknown ability score "${detail}"`); + const total = Parser.getAbilityModNumber(Number(ent[detail])); + return total === 0 ? "" : total > 0 ? ` + ${total}` : ` - ${Math.abs(total)}`; + } + + case "damage_avg": { + const replaced = detail + .replace(/\b(?str|dex|con|int|wis|cha)\b/gi, (...m) => Parser.getAbilityModNumber(Number(ent[m.last().abil]))) + .replace(/\bsize_mult\b/g, () => this._getSizeMult(this._getSize({ent}))); + + // eslint-disable-next-line no-eval + return Math.floor(eval(this._getCleanMathExpression(replaced))); + } + + case "size_mult": { + const mult = this._getSizeMult(this._getSize({ent})); + + if (!detail) return mult; + + // eslint-disable-next-line no-eval + return Math.floor(eval(`${mult} * ${this._getCleanMathExpression(detail)}`)); + } + + default: return m[0]; + } + }), + ); + } + }, + + getVersions (parent, {impl = null, isExternalApplicationIdentityOnly = false} = {}) { + if (!parent?._versions?.length) return []; + + return parent._versions + .map(ver => { + if (ver._template && ver._implementations?.length) return DataUtil.generic._getVersions_template({ver}); + return DataUtil.generic._getVersions_basic({ver}); + }) + .flat() + .map(ver => DataUtil.generic._getVersion({parentEntity: parent, version: ver, impl, isExternalApplicationIdentityOnly})); + }, + + _getVersions_template ({ver}) { + return ver._implementations + .map(impl => { + let cpyTemplate = MiscUtil.copyFast(ver._template); + const cpyImpl = MiscUtil.copyFast(impl); + + DataUtil.generic._getVersions_mutExpandCopy({ent: cpyTemplate}); + + if (cpyImpl._variables) { + cpyTemplate = MiscUtil.getWalker() + .walk( + cpyTemplate, + { + string: str => str.replace(/{{([^}]+)}}/g, (...m) => cpyImpl._variables[m[1]]), + }, + ); + delete cpyImpl._variables; + } + + Object.assign(cpyTemplate, cpyImpl); + + return cpyTemplate; + }); + }, + + _getVersions_basic ({ver}) { + const cpyVer = MiscUtil.copyFast(ver); + DataUtil.generic._getVersions_mutExpandCopy({ent: cpyVer}); + return cpyVer; + }, + + _getVersions_mutExpandCopy ({ent}) { + // Tweak the data structure to match what `_applyCopy` expects + ent._copy = { + _mod: ent._mod, + _preserve: ent._preserve || {"*": true}, + }; + delete ent._mod; + delete ent._preserve; + }, + + _getVersion ({parentEntity, version, impl = null, isExternalApplicationIdentityOnly}) { + const additionalData = { + _versionBase_isVersion: true, + _versionBase_name: parentEntity.name, + _versionBase_source: parentEntity.source, + _versionBase_hasToken: parentEntity.hasToken, + _versionBase_hasFluff: parentEntity.hasFluff, + _versionBase_hasFluffImages: parentEntity.hasFluffImages, + }; + const cpyParentEntity = MiscUtil.copyFast(parentEntity); + + delete cpyParentEntity._versions; + delete cpyParentEntity.hasToken; + delete cpyParentEntity.hasFluff; + delete cpyParentEntity.hasFluffImages; + + DataUtil.generic.copyApplier.getCopy( + impl, + cpyParentEntity, + version, + null, + {isExternalApplicationIdentityOnly}, + ); + Object.assign(version, additionalData); + return version; + }, + }, + + proxy: { + getVersions (prop, ent, {isExternalApplicationIdentityOnly = false} = {}) { + if (DataUtil[prop]?.getVersions) return DataUtil[prop]?.getVersions(ent, {isExternalApplicationIdentityOnly}); + return DataUtil.generic.getVersions(ent, {isExternalApplicationIdentityOnly}); + }, + + unpackUid (prop, uid, tag, opts) { + if (DataUtil[prop]?.unpackUid) return DataUtil[prop]?.unpackUid(uid, tag, opts); + return DataUtil.generic.unpackUid(uid, tag, opts); + }, + + getNormalizedUid (prop, uid, tag, opts) { + if (DataUtil[prop]?.getNormalizedUid) return DataUtil[prop].getNormalizedUid(uid, tag, opts); + return DataUtil.generic.getNormalizedUid(uid, tag, opts); + }, + + getUid (prop, ent, opts) { + if (DataUtil[prop]?.getUid) return DataUtil[prop].getUid(ent, opts); + return DataUtil.generic.getUid(ent, opts); + }, + }, + + monster: class extends _DataUtilPropConfigMultiSource { + static _MERGE_REQUIRES_PRESERVE = { + legendaryGroup: true, + environment: true, + soundClip: true, + altArt: true, + variant: true, + dragonCastingColor: true, + familiar: true, + }; + + static _PAGE = UrlUtil.PG_BESTIARY; + + static _DIR = "bestiary"; + static _PROP = "monster"; + + static async loadJSON () { + await DataUtil.monster.pPreloadMeta(); + return super.loadJSON(); + } + + static getVersions (mon, {isExternalApplicationIdentityOnly = false} = {}) { + const additionalVersionData = DataUtil.monster._getAdditionalVersionsData(mon); + if (additionalVersionData.length) { + mon = MiscUtil.copyFast(mon); + (mon._versions = mon._versions || []).push(...additionalVersionData); + } + return DataUtil.generic.getVersions(mon, {impl: DataUtil.monster, isExternalApplicationIdentityOnly}); + } + + static _getAdditionalVersionsData (mon) { + if (!mon.variant?.length) return []; + + return mon.variant + .filter(it => it._version) + .map(it => { + const toAdd = { + name: it._version.name || it.name, + source: it._version.source || it.source || mon.source, + variant: null, + }; + + if (it._version.addAs) { + const cpy = MiscUtil.copyFast(it); + delete cpy._version; + delete cpy.type; + delete cpy.source; + delete cpy.page; + + toAdd._mod = { + [it._version.addAs]: { + mode: "appendArr", + items: cpy, + }, + }; + + return toAdd; + } + + if (it._version.addHeadersAs) { + const cpy = MiscUtil.copyFast(it); + cpy.entries = cpy.entries.filter(it => it.name && it.entries); + cpy.entries.forEach(cpyEnt => { + delete cpyEnt.type; + delete cpyEnt.source; + }); + + toAdd._mod = { + [it._version.addHeadersAs]: { + mode: "appendArr", + items: cpy.entries, + }, + }; + + return toAdd; + } + }) + .filter(Boolean); + } + + static async pPreloadMeta () { + DataUtil.monster._pLoadMeta = DataUtil.monster._pLoadMeta || ((async () => { + const legendaryGroups = await DataUtil.legendaryGroup.pLoadAll(); + DataUtil.monster.populateMetaReference({legendaryGroup: legendaryGroups}); + })()); + await DataUtil.monster._pLoadMeta; + } + + static _pLoadMeta = null; + static metaGroupMap = {}; + static getMetaGroup (mon) { + if (!mon.legendaryGroup || !mon.legendaryGroup.source || !mon.legendaryGroup.name) return null; + return (DataUtil.monster.metaGroupMap[mon.legendaryGroup.source] || {})[mon.legendaryGroup.name]; + } + static populateMetaReference (data) { + (data.legendaryGroup || []).forEach(it => { + (DataUtil.monster.metaGroupMap[it.source] = + DataUtil.monster.metaGroupMap[it.source] || {})[it.name] = it; + }); + } + }, + + monsterFluff: class extends _DataUtilPropConfigMultiSource { + static _PAGE = UrlUtil.PG_BESTIARY; + static _DIR = "bestiary"; + static _PROP = "monsterFluff"; + }, + + monsterTemplate: class extends _DataUtilPropConfigSingleSource { + static _PAGE = "monsterTemplate"; + static _FILENAME = "bestiary/template.json"; + }, + + spell: class extends _DataUtilPropConfigMultiSource { + static _PAGE = UrlUtil.PG_SPELLS; + static _DIR = "spells"; + static _PROP = "spell"; + static _IS_MUT_ENTITIES = true; + + static _SPELL_SOURCE_LOOKUP = null; + + static PROPS_SPELL_SOURCE = [ + "classes", + "races", + "optionalfeatures", + "backgrounds", + "feats", + "charoptions", + "rewards", + ]; + + // region Utilities for external applications (i.e., the spell source generation script) to use + static setSpellSourceLookup (lookup, {isExternalApplication = false} = {}) { + if (!isExternalApplication) throw new Error("Should not be calling this!"); + this._SPELL_SOURCE_LOOKUP = MiscUtil.copyFast(lookup); + } + + static mutEntity (sp, {isExternalApplication = false} = {}) { + if (!isExternalApplication) throw new Error("Should not be calling this!"); + return this._mutEntity(sp); + } + + static unmutEntity (sp, {isExternalApplication = false} = {}) { + if (!isExternalApplication) throw new Error("Should not be calling this!"); + this.PROPS_SPELL_SOURCE.forEach(prop => delete sp[prop]); + delete sp._isMutEntity; + } + // endregion + + // region Special mutator for the homebrew builder + static mutEntityBrewBuilder (sp, sourcesLookup) { + const out = this._mutEntity(sp, {sourcesLookup}); + delete sp._isMutEntity; + return out; + } + // endregion + + static async _pInitPreData_ () { + this._SPELL_SOURCE_LOOKUP = await DataUtil.loadRawJSON(`${Renderer.get().baseUrl}data/generated/gendata-spell-source-lookup.json`); + } + + static _mutEntity (sp, {sourcesLookup = null} = {}) { + if (sp._isMutEntity) return sp; + + const spSources = (sourcesLookup ?? this._SPELL_SOURCE_LOOKUP)[sp.source.toLowerCase()]?.[sp.name.toLowerCase()]; + if (!spSources) return sp; + + this._mutSpell_class({sp, spSources, propSources: "class", propClasses: "fromClassList"}); + this._mutSpell_class({sp, spSources, propSources: "classVariant", propClasses: "fromClassListVariant"}); + this._mutSpell_subclass({sp, spSources}); + this._mutSpell_race({sp, spSources}); + this._mutSpell_optionalfeature({sp, spSources}); + this._mutSpell_background({sp, spSources}); + this._mutSpell_feat({sp, spSources}); + this._mutSpell_charoption({sp, spSources}); + this._mutSpell_reward({sp, spSources}); + + sp._isMutEntity = true; + + return sp; + } + + static _mutSpell_class ({sp, spSources, propSources, propClasses}) { + if (!spSources[propSources]) return; + + Object.entries(spSources[propSources]) + .forEach(([source, nameTo]) => { + const tgt = MiscUtil.getOrSet(sp, "classes", propClasses, []); + + Object.entries(nameTo) + .forEach(([name, val]) => { + if (tgt.some(it => it.name === nameTo && it.source === source)) return; + + const toAdd = {name, source}; + if (val === true) return tgt.push(toAdd); + + if (val.definedInSource) { + toAdd.definedInSource = val.definedInSource; + tgt.push(toAdd); + return; + } + + if (val.definedInSources) { + val.definedInSources + .forEach(definedInSource => { + const cpyToAdd = MiscUtil.copyFast(toAdd); + + if (definedInSource == null) { + return tgt.push(cpyToAdd); + } + + cpyToAdd.definedInSource = definedInSource; + tgt.push(cpyToAdd); + }); + + return; + } + + throw new Error("Unimplemented!"); + }); + }); + } + + static _mutSpell_subclass ({sp, spSources}) { + if (!spSources.subclass) return; + + Object.entries(spSources.subclass) + .forEach(([classSource, classNameTo]) => { + Object.entries(classNameTo) + .forEach(([className, sourceTo]) => { + Object.entries(sourceTo) + .forEach(([source, nameTo]) => { + const tgt = MiscUtil.getOrSet(sp, "classes", "fromSubclass", []); + + Object.entries(nameTo) + .forEach(([name, val]) => { + if (val === true) throw new Error("Unimplemented!"); + + if (tgt.some(it => it.class.name === className && it.class.source === classSource && it.subclass.name === name && it.subclass.source === source && ((it.subclass.subSubclass == null && val.subSubclasses == null) || val.subSubclasses.includes(it.subclass.subSubclass)))) return; + + const toAdd = { + class: { + name: className, + source: classSource, + }, + subclass: { + name: val.name, + shortName: name, + source, + }, + }; + + if (!val.subSubclasses?.length) return tgt.push(toAdd); + + val.subSubclasses + .forEach(subSubclass => { + const cpyToAdd = MiscUtil.copyFast(toAdd); + cpyToAdd.subclass.subSubclass = subSubclass; + tgt.push(cpyToAdd); + }); + }); + }); + }); + }); + } + + static _mutSpell_race ({sp, spSources}) { + this._mutSpell_generic({sp, spSources, propSources: "race", propSpell: "races"}); + } + + static _mutSpell_optionalfeature ({sp, spSources}) { + this._mutSpell_generic({sp, spSources, propSources: "optionalfeature", propSpell: "optionalfeatures"}); + } + + static _mutSpell_background ({sp, spSources}) { + this._mutSpell_generic({sp, spSources, propSources: "background", propSpell: "backgrounds"}); + } + + static _mutSpell_feat ({sp, spSources}) { + this._mutSpell_generic({sp, spSources, propSources: "feat", propSpell: "feats"}); + } + + static _mutSpell_charoption ({sp, spSources}) { + this._mutSpell_generic({sp, spSources, propSources: "charoption", propSpell: "charoptions"}); + } + + static _mutSpell_reward ({sp, spSources}) { + this._mutSpell_generic({sp, spSources, propSources: "reward", propSpell: "rewards"}); + } + + static _mutSpell_generic ({sp, spSources, propSources, propSpell}) { + if (!spSources[propSources]) return; + + Object.entries(spSources[propSources]) + .forEach(([source, nameTo]) => { + const tgt = MiscUtil.getOrSet(sp, propSpell, []); + + Object.entries(nameTo) + .forEach(([name, val]) => { + if (tgt.some(it => it.name === nameTo && it.source === source)) return; + + const toAdd = {name, source}; + if (val === true) return tgt.push(toAdd); + + Object.assign(toAdd, {...val}); + tgt.push(toAdd); + }); + }); + } + }, + + spellFluff: class extends _DataUtilPropConfigMultiSource { + static _PAGE = UrlUtil.PG_SPELLS; + static _DIR = "spells"; + static _PROP = "spellFluff"; + }, + + background: class extends _DataUtilPropConfigSingleSource { + static _PAGE = UrlUtil.PG_BACKGROUNDS; + static _FILENAME = "backgrounds.json"; + }, + + backgroundFluff: class extends _DataUtilPropConfigSingleSource { + static _PAGE = UrlUtil.PG_BACKGROUNDS; + static _FILENAME = "fluff-backgrounds.json"; + }, + + charoption: class extends _DataUtilPropConfigSingleSource { + static _PAGE = UrlUtil.PG_CHAR_CREATION_OPTIONS; + static _FILENAME = "charcreationoptions.json"; + }, + + charoptionFluff: class extends _DataUtilPropConfigSingleSource { + static _PAGE = UrlUtil.PG_CHAR_CREATION_OPTIONS; + static _FILENAME = "fluff-charcreationoptions.json"; + }, + + condition: class extends _DataUtilPropConfigSingleSource { + static _PAGE = UrlUtil.PG_CONDITIONS_DISEASES; + static _FILENAME = "conditionsdiseases.json"; + }, + + conditionFluff: class extends _DataUtilPropConfigSingleSource { + static _PAGE = UrlUtil.PG_CONDITIONS_DISEASES; + static _FILENAME = "fluff-conditionsdiseases.json"; + }, + + disease: class extends _DataUtilPropConfigSingleSource { + static _PAGE = UrlUtil.PG_CONDITIONS_DISEASES; + static _FILENAME = "conditionsdiseases.json"; + }, + + feat: class extends _DataUtilPropConfigSingleSource { + static _PAGE = UrlUtil.PG_FEATS; + static _FILENAME = "feats.json"; + }, + + featFluff: class extends _DataUtilPropConfigSingleSource { + static _PAGE = UrlUtil.PG_FEATS; + static _FILENAME = "fluff-feats.json"; + }, + + item: class extends _DataUtilPropConfigCustom { + static _MERGE_REQUIRES_PRESERVE = { + lootTables: true, + tier: true, + }; + static _PAGE = UrlUtil.PG_ITEMS; + + static async loadRawJSON () { + if (DataUtil.item._loadedRawJson) return DataUtil.item._loadedRawJson; + + DataUtil.item._pLoadingRawJson = (async () => { + const urlItems = `${Renderer.get().baseUrl}data/items.json`; + const urlItemsBase = `${Renderer.get().baseUrl}data/items-base.json`; + const urlVariants = `${Renderer.get().baseUrl}data/magicvariants.json`; + + const [dataItems, dataItemsBase, dataVariants] = await Promise.all([ + DataUtil.loadJSON(urlItems), + DataUtil.loadJSON(urlItemsBase), + DataUtil.loadJSON(urlVariants), + ]); + + DataUtil.item._loadedRawJson = { + item: MiscUtil.copyFast(dataItems.item), + itemGroup: MiscUtil.copyFast(dataItems.itemGroup), + magicvariant: MiscUtil.copyFast(dataVariants.magicvariant), + baseitem: MiscUtil.copyFast(dataItemsBase.baseitem), + }; + })(); + await DataUtil.item._pLoadingRawJson; + + return DataUtil.item._loadedRawJson; + } + + static async loadJSON () { + return {item: await Renderer.item.pBuildList()}; + } + + static async loadPrerelease () { + return {item: await Renderer.item.pGetItemsFromPrerelease()}; + } + + static async loadBrew () { + return {item: await Renderer.item.pGetItemsFromBrew()}; + } + }, + + itemGroup: class extends _DataUtilPropConfig { + static _MERGE_REQUIRES_PRESERVE = { + lootTables: true, + tier: true, + }; + static _PAGE = UrlUtil.PG_ITEMS; + + static async pMergeCopy (...args) { return DataUtil.item.pMergeCopy(...args); } + static async loadRawJSON (...args) { return DataUtil.item.loadRawJSON(...args); } + }, + + baseitem: class extends _DataUtilPropConfig { + static _PAGE = UrlUtil.PG_ITEMS; + + static async pMergeCopy (...args) { return DataUtil.item.pMergeCopy(...args); } + static async loadRawJSON (...args) { return DataUtil.item.loadRawJSON(...args); } + }, + + itemFluff: class extends _DataUtilPropConfigSingleSource { + static _PAGE = UrlUtil.PG_ITEMS; + static _FILENAME = "fluff-items.json"; + }, + + itemType: class extends _DataUtilPropConfig { + static _PAGE = "itemType"; + }, + + language: class extends _DataUtilPropConfigSingleSource { + static _PAGE = UrlUtil.PG_LANGUAGES; + static _FILENAME = "languages.json"; + + static async loadJSON () { + const rawData = await super.loadJSON(); + + // region Populate fonts, based on script + const scriptLookup = {}; + (rawData.languageScript || []).forEach(script => scriptLookup[script.name] = script); + + const out = {language: MiscUtil.copyFast(rawData.language)}; + out.language.forEach(lang => { + if (!lang.script || lang.fonts === false) return; + + const script = scriptLookup[lang.script]; + if (!script) return; + + lang._fonts = [...script.fonts]; + }); + // endregion + + return out; + } + }, + + languageFluff: class extends _DataUtilPropConfigSingleSource { + static _PAGE = UrlUtil.PG_LANGUAGES; + static _FILENAME = "fluff-languages.json"; + }, + + object: class extends _DataUtilPropConfigSingleSource { + static _PAGE = UrlUtil.PG_OBJECTS; + static _FILENAME = "objects.json"; + }, + + objectFluff: class extends _DataUtilPropConfigSingleSource { + static _PAGE = UrlUtil.PG_OBJECTS; + static _FILENAME = "fluff-objects.json"; + }, + + race: class extends _DataUtilPropConfigSingleSource { + static _PAGE = UrlUtil.PG_RACES; + static _FILENAME = "races.json"; + + static _loadCache = {}; + static _pIsLoadings = {}; + static async loadJSON ({isAddBaseRaces = false} = {}) { + if (!DataUtil.race._pIsLoadings[isAddBaseRaces]) { + DataUtil.race._pIsLoadings[isAddBaseRaces] = (async () => { + DataUtil.race._loadCache[isAddBaseRaces] = DataUtil.race.getPostProcessedSiteJson( + await this.loadRawJSON(), + {isAddBaseRaces}, + ); + })(); + } + await DataUtil.race._pIsLoadings[isAddBaseRaces]; + return DataUtil.race._loadCache[isAddBaseRaces]; + } + + static getPostProcessedSiteJson (rawRaceData, {isAddBaseRaces = false} = {}) { + rawRaceData = MiscUtil.copyFast(rawRaceData); + (rawRaceData.subrace || []).forEach(sr => { + const r = rawRaceData.race.find(it => it.name === sr.raceName && it.source === sr.raceSource); + if (!r) return JqueryUtil.doToast({content: `Failed to find race "${sr.raceName}" (${sr.raceSource})`, type: "danger"}); + const cpySr = MiscUtil.copyFast(sr); + delete cpySr.raceName; + delete cpySr.raceSource; + (r.subraces = r.subraces || []).push(sr); + }); + delete rawRaceData.subrace; + const raceData = Renderer.race.mergeSubraces(rawRaceData.race, {isAddBaseRaces}); + raceData.forEach(it => it.__prop = "race"); + return {race: raceData}; + } + + static async loadPrerelease ({isAddBaseRaces = true} = {}) { + return DataUtil.race._loadPrereleaseBrew({isAddBaseRaces, brewUtil: typeof PrereleaseUtil !== "undefined" ? PrereleaseUtil : null}); + } + + static async loadBrew ({isAddBaseRaces = true} = {}) { + return DataUtil.race._loadPrereleaseBrew({isAddBaseRaces, brewUtil: typeof BrewUtil2 !== "undefined" ? BrewUtil2 : null}); + } + + static async _loadPrereleaseBrew ({isAddBaseRaces = true, brewUtil} = {}) { + if (!brewUtil) return {}; + + const rawSite = await DataUtil.race.loadRawJSON(); + const brew = await brewUtil.pGetBrewProcessed(); + return DataUtil.race.getPostProcessedPrereleaseBrewJson(rawSite, brew, {isAddBaseRaces}); + } + + static getPostProcessedPrereleaseBrewJson (rawSite, brew, {isAddBaseRaces = false} = {}) { + rawSite = MiscUtil.copyFast(rawSite); + brew = MiscUtil.copyFast(brew); + + const rawSiteUsed = []; + (brew.subrace || []).forEach(sr => { + const rSite = rawSite.race.find(it => it.name === sr.raceName && it.source === sr.raceSource); + const rBrew = (brew.race || []).find(it => it.name === sr.raceName && it.source === sr.raceSource); + if (!rSite && !rBrew) return JqueryUtil.doToast({content: `Failed to find race "${sr.raceName}" (${sr.raceSource})`, type: "danger"}); + const rTgt = rSite || rBrew; + const cpySr = MiscUtil.copyFast(sr); + delete cpySr.raceName; + delete cpySr.raceSource; + (rTgt.subraces = rTgt.subraces || []).push(sr); + if (rSite && !rawSiteUsed.includes(rSite)) rawSiteUsed.push(rSite); + }); + delete brew.subrace; + + const raceDataBrew = Renderer.race.mergeSubraces(brew.race || [], {isAddBaseRaces}); + // Never add base races from site races when building brew race list + const raceDataSite = Renderer.race.mergeSubraces(rawSiteUsed, {isAddBaseRaces: false}); + + const out = [...raceDataBrew, ...raceDataSite]; + out.forEach(it => it.__prop = "race"); + return {race: out}; + } + }, + + raceFluff: class extends _DataUtilPropConfigSingleSource { + static _PAGE = UrlUtil.PG_RACES; + static _FILENAME = "fluff-races.json"; + + static _getApplyUncommonMonstrous (data) { + data = MiscUtil.copyFast(data); + data.raceFluff + .forEach(raceFluff => { + if (raceFluff.uncommon) { + raceFluff.entries = raceFluff.entries || []; + raceFluff.entries.push(MiscUtil.copyFast(data.raceFluffMeta.uncommon)); + delete raceFluff.uncommon; + } + + if (raceFluff.monstrous) { + raceFluff.entries = raceFluff.entries || []; + raceFluff.entries.push(MiscUtil.copyFast(data.raceFluffMeta.monstrous)); + delete raceFluff.monstrous; + } + }); + return data; + } + + static async loadJSON () { + const data = await super.loadJSON(); + return this._getApplyUncommonMonstrous(data); + } + + static async loadUnmergedJSON () { + const data = await super.loadUnmergedJSON(); + return this._getApplyUncommonMonstrous(data); + } + }, + + raceFeature: class extends _DataUtilPropConfig { + static _PAGE = "raceFeature"; + }, + + recipe: class extends _DataUtilPropConfigSingleSource { + static _PAGE = UrlUtil.PG_RECIPES; + static _FILENAME = "recipes.json"; + + static async loadJSON () { + const rawData = await super.loadJSON(); + return {recipe: await DataUtil.recipe.pGetPostProcessedRecipes(rawData.recipe)}; + } + + static async pGetPostProcessedRecipes (recipes) { + if (!recipes?.length) return; + + recipes = MiscUtil.copyFast(recipes); + + // Apply ingredient properties + recipes.forEach(r => Renderer.recipe.populateFullIngredients(r)); + + const out = []; + + // region Merge together main data and fluff, as we render the fluff in the main tab + for (const r of recipes) { + const fluff = await Renderer.utils.pGetFluff({ + entity: r, + fnGetFluffData: DataUtil.recipeFluff.loadJSON.bind(DataUtil.recipeFluff), + fluffProp: "recipeFluff", + }); + + if (!fluff) { + out.push(r); + continue; + } + + const cpyR = MiscUtil.copyFast(r); + cpyR.fluff = MiscUtil.copyFast(fluff); + delete cpyR.fluff.name; + delete cpyR.fluff.source; + out.push(cpyR); + } + // + + return out; + } + + static async loadPrerelease () { + return this._loadPrereleaseBrew({brewUtil: typeof PrereleaseUtil !== "undefined" ? PrereleaseUtil : null}); + } + + static async loadBrew () { + return this._loadPrereleaseBrew({brewUtil: typeof BrewUtil2 !== "undefined" ? BrewUtil2 : null}); + } + + static async _loadPrereleaseBrew ({brewUtil}) { + if (!brewUtil) return {}; + + const brew = await brewUtil.pGetBrewProcessed(); + if (!brew?.recipe?.length) return brew; + + return { + ...brew, + recipe: await DataUtil.recipe.pGetPostProcessedRecipes(brew.recipe), + }; + } + }, + + recipeFluff: class extends _DataUtilPropConfigSingleSource { + static _PAGE = UrlUtil.PG_RECIPES; + static _FILENAME = "fluff-recipes.json"; + }, + + vehicle: class extends _DataUtilPropConfigSingleSource { + static _PAGE = UrlUtil.PG_VEHICLES; + static _FILENAME = "vehicles.json"; + }, + + vehicleFluff: class extends _DataUtilPropConfigSingleSource { + static _PAGE = UrlUtil.PG_VEHICLES; + static _FILENAME = "fluff-vehicles.json"; + }, + + optionalfeature: class extends _DataUtilPropConfigSingleSource { + static _PAGE = UrlUtil.PG_OPT_FEATURES; + static _FILENAME = "optionalfeatures.json"; + }, + + class: class clazz extends _DataUtilPropConfigCustom { + static _PAGE = UrlUtil.PG_CLASSES; + + static _pLoadJson = null; + static _pLoadRawJson = null; + + static loadJSON () { + return DataUtil.class._pLoadJson = DataUtil.class._pLoadJson || (async () => { + return { + class: await DataLoader.pCacheAndGetAllSite("class"), + subclass: await DataLoader.pCacheAndGetAllSite("subclass"), + }; + })(); + } + + static loadRawJSON () { + return DataUtil.class._pLoadRawJson = DataUtil.class._pLoadRawJson || (async () => { + const index = await DataUtil.loadJSON(`${Renderer.get().baseUrl}data/class/index.json`); + const allData = await Promise.all(Object.values(index).map(it => DataUtil.loadJSON(`${Renderer.get().baseUrl}data/class/${it}`))); + + return { + class: MiscUtil.copyFast(allData.map(it => it.class || []).flat()), + subclass: MiscUtil.copyFast(allData.map(it => it.subclass || []).flat()), + classFeature: allData.map(it => it.classFeature || []).flat(), + subclassFeature: allData.map(it => it.subclassFeature || []).flat(), + }; + })(); + } + + static async loadPrerelease () { + return { + class: await DataLoader.pCacheAndGetAllPrerelease("class"), + subclass: await DataLoader.pCacheAndGetAllPrerelease("subclass"), + }; + } + + static async loadBrew () { + return { + class: await DataLoader.pCacheAndGetAllBrew("class"), + subclass: await DataLoader.pCacheAndGetAllBrew("subclass"), + }; + } + + static packUidSubclass (it) { + // ||| + const sourceDefault = Parser.getTagSource("subclass"); + return [ + it.name, + it.className, + (it.classSource || "").toLowerCase() === sourceDefault.toLowerCase() ? "" : it.classSource, + (it.source || "").toLowerCase() === sourceDefault.toLowerCase() ? "" : it.source, + ].join("|").replace(/\|+$/, ""); // Trim trailing pipes + } + + /** + * @param {string} uid + * @param [opts] + * @param [opts.isLower] If the returned values should be lowercase. + */ + static unpackUidClassFeature (uid, opts) { + opts = opts || {}; + if (opts.isLower) uid = uid.toLowerCase(); + let [name, className, classSource, level, source, displayText] = uid.split("|").map(it => it.trim()); + classSource = classSource || (opts.isLower ? Parser.SRC_PHB.toLowerCase() : Parser.SRC_PHB); + source = source || classSource; + level = Number(level); + return { + name, + className, + classSource, + level, + source, + displayText, + }; + } + + static isValidClassFeatureUid (uid) { + const {name, className, level} = DataUtil.class.unpackUidClassFeature(uid); + return !(!name || !className || isNaN(level)); + } + + static packUidClassFeature (f) { + // |||| + return [ + f.name, + f.className, + f.classSource === Parser.SRC_PHB ? "" : f.classSource, // assume the class has PHB source + f.level, + f.source === f.classSource ? "" : f.source, // assume the class feature has the class source + ].join("|").replace(/\|+$/, ""); // Trim trailing pipes + } + + /** + * @param uid + * @param [opts] + * @param [opts.isLower] If the returned values should be lowercase. + */ + static unpackUidSubclassFeature (uid, opts) { + opts = opts || {}; + if (opts.isLower) uid = uid.toLowerCase(); + let [name, className, classSource, subclassShortName, subclassSource, level, source, displayText] = uid.split("|").map(it => it.trim()); + classSource = classSource || (opts.isLower ? Parser.SRC_PHB.toLowerCase() : Parser.SRC_PHB); + subclassSource = subclassSource || (opts.isLower ? Parser.SRC_PHB.toLowerCase() : Parser.SRC_PHB); + source = source || subclassSource; + level = Number(level); + return { + name, + className, + classSource, + subclassShortName, + subclassSource, + level, + source, + displayText, + }; + } + + static isValidSubclassFeatureUid (uid) { + const {name, className, subclassShortName, level} = DataUtil.class.unpackUidSubclassFeature(uid); + return !(!name || !className || !subclassShortName || isNaN(level)); + } + + static packUidSubclassFeature (f) { + // |||||| + return [ + f.name, + f.className, + f.classSource === Parser.SRC_PHB ? "" : f.classSource, // assume the class has the PHB source + f.subclassShortName, + f.subclassSource === Parser.SRC_PHB ? "" : f.subclassSource, // assume the subclass has the PHB source + f.level, + f.source === f.subclassSource ? "" : f.source, // assume the feature has the same source as the subclass + ].join("|").replace(/\|+$/, ""); // Trim trailing pipes + } + + // region Subclass lookup + static _CACHE_SUBCLASS_LOOKUP_PROMISE = null; + static _CACHE_SUBCLASS_LOOKUP = null; + static async pGetSubclassLookup () { + DataUtil.class._CACHE_SUBCLASS_LOOKUP_PROMISE = DataUtil.class._CACHE_SUBCLASS_LOOKUP_PROMISE || (async () => { + const subclassLookup = {}; + Object.assign(subclassLookup, await DataUtil.loadJSON(`${Renderer.get().baseUrl}data/generated/gendata-subclass-lookup.json`)); + DataUtil.class._CACHE_SUBCLASS_LOOKUP = subclassLookup; + })(); + await DataUtil.class._CACHE_SUBCLASS_LOOKUP_PROMISE; + return DataUtil.class._CACHE_SUBCLASS_LOOKUP; + } + // endregion + }, + + subclass: class extends _DataUtilPropConfig { + static _PAGE = "subclass"; + }, + + deity: class extends _DataUtilPropConfigSingleSource { + static _PAGE = UrlUtil.PG_DEITIES; + static _FILENAME = "deities.json"; + + static doPostLoad (data) { + const PRINT_ORDER = [ + Parser.SRC_PHB, + Parser.SRC_DMG, + Parser.SRC_SCAG, + Parser.SRC_VGM, + Parser.SRC_MTF, + Parser.SRC_ERLW, + Parser.SRC_EGW, + Parser.SRC_TDCSR, + ]; + + const inSource = {}; + PRINT_ORDER.forEach(src => { + inSource[src] = {}; + data.deity.filter(it => it.source === src).forEach(it => inSource[src][it.reprintAlias || it.name] = it); // TODO need to handle similar names + }); + + const laterPrinting = [PRINT_ORDER.last()]; + [...PRINT_ORDER].reverse().slice(1).forEach(src => { + laterPrinting.forEach(laterSrc => { + Object.keys(inSource[src]).forEach(name => { + const newer = inSource[laterSrc][name]; + if (newer) { + const old = inSource[src][name]; + old.reprinted = true; + if (!newer._isEnhanced) { + newer.previousVersions = newer.previousVersions || []; + newer.previousVersions.push(old); + } + } + }); + }); + + laterPrinting.push(src); + }); + data.deity.forEach(g => g._isEnhanced = true); + + return data; + } + + static async loadJSON () { + const data = await super.loadJSON(); + DataUtil.deity.doPostLoad(data); + return data; + } + + static getUid (ent, opts) { + return this.packUidDeity(ent, opts); + } + + static getNormalizedUid (uid, tag) { + const {name, pantheon, source} = this.unpackUidDeity(uid, tag, {isLower: true}); + return [name, pantheon, source].join("|"); + } + + static unpackUidDeity (uid, opts) { + opts = opts || {}; + if (opts.isLower) uid = uid.toLowerCase(); + let [name, pantheon, source, displayText, ...others] = uid.split("|").map(it => it.trim()); + + pantheon = pantheon || "forgotten realms"; + if (opts.isLower) pantheon = pantheon.toLowerCase(); + + source = source || Parser.getTagSource("deity", source); + if (opts.isLower) source = source.toLowerCase(); + + return { + name, + pantheon, + source, + displayText, + others, + }; + } + + static packUidDeity (it) { + // || + const sourceDefault = Parser.getTagSource("deity"); + return [ + it.name, + (it.pantheon || "").toLowerCase() === "forgotten realms" ? "" : it.pantheon, + (it.source || "").toLowerCase() === sourceDefault.toLowerCase() ? "" : it.source, + ].join("|").replace(/\|+$/, ""); // Trim trailing pipes + } + }, + + table: class extends _DataUtilPropConfigCustom { + static async loadJSON () { + const datas = await Promise.all([ + `${Renderer.get().baseUrl}data/generated/gendata-tables.json`, + `${Renderer.get().baseUrl}data/tables.json`, + ].map(url => DataUtil.loadJSON(url))); + const combined = {}; + datas.forEach(data => { + Object.entries(data).forEach(([k, v]) => { + if (combined[k] && combined[k] instanceof Array && v instanceof Array) combined[k] = combined[k].concat(v); + else if (combined[k] == null) combined[k] = v; + else throw new Error(`Could not merge keys for key "${k}"`); + }); + }); + + return combined; + } + }, + + legendaryGroup: class extends _DataUtilPropConfigSingleSource { + static _PAGE = UrlUtil.PG_BESTIARY; + static _FILENAME = "bestiary/legendarygroups.json"; + + static async pLoadAll () { + return (await this.loadJSON()).legendaryGroup; + } + }, + + variantrule: class extends _DataUtilPropConfigSingleSource { + static _PAGE = UrlUtil.PG_VARIANTRULES; + static _FILENAME = "variantrules.json"; + + static async loadJSON () { + const rawData = await super.loadJSON(); + const rawDataGenerated = await DataUtil.loadJSON(`${Renderer.get().baseUrl}data/generated/gendata-variantrules.json`); + + return {variantrule: [...rawData.variantrule, ...rawDataGenerated.variantrule]}; + } + }, + + deck: class extends _DataUtilPropConfigCustom { + static _PAGE = UrlUtil.PG_DECKS; + + static _pLoadJson = null; + static _pLoadRawJson = null; + + static loadJSON () { + return DataUtil.deck._pLoadJson = DataUtil.deck._pLoadJson || (async () => { + return { + deck: await DataLoader.pCacheAndGetAllSite("deck"), + card: await DataLoader.pCacheAndGetAllSite("card"), + }; + })(); + } + + static loadRawJSON () { + return DataUtil.deck._pLoadRawJson = DataUtil.deck._pLoadRawJson || DataUtil.loadJSON(`${Renderer.get().baseUrl}data/decks.json`); + } + + static async loadPrerelease () { + return { + deck: await DataLoader.pCacheAndGetAllPrerelease("deck"), + card: await DataLoader.pCacheAndGetAllPrerelease("card"), + }; + } + + static async loadBrew () { + return { + deck: await DataLoader.pCacheAndGetAllBrew("deck"), + card: await DataLoader.pCacheAndGetAllBrew("card"), + }; + } + + /** + * @param uid + * @param [opts] + * @param [opts.isLower] If the returned values should be lowercase. + */ + static unpackUidCard (uid, opts) { + opts = opts || {}; + if (opts.isLower) uid = uid.toLowerCase(); + let [name, set, source, displayText] = uid.split("|").map(it => it.trim()); + set = set || "none"; + source = source || Parser.getTagSource("card", source)[opts.isLower ? "toLowerCase" : "toString"](); + return { + name, + set, + source, + displayText, + }; + } + }, + + reward: class extends _DataUtilPropConfigSingleSource { + static _PAGE = UrlUtil.PG_REWARDS; + static _FILENAME = "rewards.json"; + }, + + rewardFluff: class extends _DataUtilPropConfigSingleSource { + static _PAGE = UrlUtil.PG_REWARDS; + static _FILENAME = "fluff-rewards.json"; + }, + + trap: class extends _DataUtilPropConfigSingleSource { + static _PAGE = UrlUtil.PG_TRAPS_HAZARDS; + static _FILENAME = "trapshazards.json"; + }, + + trapFluff: class extends _DataUtilPropConfigSingleSource { + static _PAGE = UrlUtil.PG_TRAPS_HAZARDS; + static _FILENAME = "fluff-trapshazards.json"; + }, + + hazard: class extends _DataUtilPropConfigSingleSource { + static _PAGE = UrlUtil.PG_TRAPS_HAZARDS; + static _FILENAME = "trapshazards.json"; + }, + + hazardFluff: class extends _DataUtilPropConfigSingleSource { + static _PAGE = UrlUtil.PG_TRAPS_HAZARDS; + static _FILENAME = "fluff-trapshazards.json"; + }, + + quickreference: { + /** + * @param uid + * @param [opts] + * @param [opts.isLower] If the returned values should be lowercase. + */ + unpackUid (uid, opts) { + opts = opts || {}; + if (opts.isLower) uid = uid.toLowerCase(); + let [name, source, ixChapter, ixHeader, displayText] = uid.split("|").map(it => it.trim()); + source = source || (opts.isLower ? Parser.SRC_PHB.toLowerCase() : Parser.SRC_PHB); + ixChapter = Number(ixChapter || 0); + return { + name, + ixChapter, + ixHeader, + source, + displayText, + }; + }, + }, + + brew: new _DataUtilBrewHelper({defaultUrlRoot: VeCt.URL_ROOT_BREW}), + prerelease: new _DataUtilBrewHelper({defaultUrlRoot: VeCt.URL_ROOT_PRERELEASE}), +}; + +// ROLLING ============================================================================================================= +globalThis.RollerUtil = { + isCrypto () { + return typeof window !== "undefined" && typeof window.crypto !== "undefined"; + }, + + randomise (max, min = 1) { + if (min > max) return 0; + if (max === min) return max; + if (RollerUtil.isCrypto()) { + return RollerUtil._randomise(min, max + 1); + } else { + return RollerUtil.roll(max) + min; + } + }, + + rollOnArray (array) { + return array[RollerUtil.randomise(array.length) - 1]; + }, + + /** + * Cryptographically secure RNG + */ + _randomise: (min, max) => { + if (isNaN(min) || isNaN(max)) throw new Error(`Invalid min/max!`); + + const range = max - min; + const bytesNeeded = Math.ceil(Math.log2(range) / 8); + const randomBytes = new Uint8Array(bytesNeeded); + const maximumRange = (2 ** 8) ** bytesNeeded; + const extendedRange = Math.floor(maximumRange / range) * range; + let i; + let randomInteger; + while (true) { + window.crypto.getRandomValues(randomBytes); + randomInteger = 0; + for (i = 0; i < bytesNeeded; i++) { + randomInteger <<= 8; + randomInteger += randomBytes[i]; + } + if (randomInteger < extendedRange) { + randomInteger %= range; + return min + randomInteger; + } + } + }, + + /** + * Result in range: 0 to (max-1); inclusive + * e.g. roll(20) gives results ranging from 0 to 19 + * @param max range max (exclusive) + * @param fn funciton to call to generate random numbers + * @returns {number} rolled + */ + roll (max, fn = Math.random) { + return Math.floor(fn() * max); + }, + + getColRollType (colLabel) { + if (typeof colLabel !== "string") return false; + colLabel = Renderer.stripTags(colLabel); + + if (Renderer.dice.lang.getTree3(colLabel)) return RollerUtil.ROLL_COL_STANDARD; + + // Remove trailing variables, if they exist + colLabel = colLabel.replace(RollerUtil._REGEX_ROLLABLE_COL_LABEL, "$1"); + if (Renderer.dice.lang.getTree3(colLabel)) return RollerUtil.ROLL_COL_VARIABLE; + + return RollerUtil.ROLL_COL_NONE; + }, + + getFullRollCol (lbl) { + if (lbl.includes("@dice")) return lbl; + + if (Renderer.dice.lang.getTree3(lbl)) return `{@dice ${lbl}}`; + + // Try to split off any trailing variables, e.g. `d100 + Level` -> `d100`, `Level` + const m = RollerUtil._REGEX_ROLLABLE_COL_LABEL.exec(lbl); + if (!m) return lbl; + + return `{@dice ${m[1]}${m[2]}#$prompt_number:title=Enter a ${m[3].trim()}$#|${lbl}}`; + }, + + _DICE_REGEX_STR: "((([1-9]\\d*)?d([1-9]\\d*)(\\s*?[-+ร—x*รท/]\\s*?(\\d,\\d|\\d)+(\\.\\d+)?)?))+?", +}; +RollerUtil.DICE_REGEX = new RegExp(RollerUtil._DICE_REGEX_STR, "g"); +RollerUtil.REGEX_DAMAGE_DICE = /(?\d+)(? \((?:{@dice |{@damage ))(?[-+0-9d ]*)(?}\)(?:\s*\+\s*the spell's level)? [a-z]+( \([-a-zA-Z0-9 ]+\))?( or [a-z]+( \([-a-zA-Z0-9 ]+\))?)? damage)/gi; +RollerUtil.REGEX_DAMAGE_FLAT = /(?Hit: |{@h})(?[0-9]+)(? [a-z]+( \([-a-zA-Z0-9 ]+\))?( or [a-z]+( \([-a-zA-Z0-9 ]+\))?)? damage)/gi; +RollerUtil._REGEX_ROLLABLE_COL_LABEL = /^(.*?\d)(\s*[-+/*^ร—รท]\s*)([a-zA-Z0-9 ]+)$/; +RollerUtil.ROLL_COL_NONE = 0; +RollerUtil.ROLL_COL_STANDARD = 1; +RollerUtil.ROLL_COL_VARIABLE = 2; + +// STORAGE ============================================================================================================= +// Dependency: localforage +function StorageUtilBase () { + this._META_KEY = "_STORAGE_META_STORAGE"; + + this._fakeStorageBacking = {}; + this._fakeStorageBackingAsync = {}; + + this._getFakeStorageSync = function () { + return { + isSyncFake: true, + getItem: k => this._fakeStorageBacking[k], + removeItem: k => delete this._fakeStorageBacking[k], + setItem: (k, v) => this._fakeStorageBacking[k] = v, + }; + }; + + this._getFakeStorageAsync = function () { + return { + pIsAsyncFake: true, + setItem: async (k, v) => this._fakeStorageBackingAsync[k] = v, + getItem: async (k) => this._fakeStorageBackingAsync[k], + removeItem: async (k) => delete this._fakeStorageBackingAsync[k], + }; + }; + + this._getSyncStorage = function () { throw new Error(`Unimplemented!`); }; + this._getAsyncStorage = async function () { throw new Error(`Unimplemented!`); }; + + this.getPageKey = function (key, page) { return `${key}_${page || UrlUtil.getCurrentPage()}`; }; + + // region Synchronous + this.syncGet = function (key) { + const rawOut = this._getSyncStorage().getItem(key); + if (rawOut && rawOut !== "undefined" && rawOut !== "null") return JSON.parse(rawOut); + return null; + }; + + this.syncSet = function (key, value) { + this._getSyncStorage().setItem(key, JSON.stringify(value)); + this._syncTrackKey(key); + }; + + this.syncRemove = function (key) { + this._getSyncStorage().removeItem(key); + this._syncTrackKey(key, true); + }; + + this.syncGetForPage = function (key) { return this.syncGet(`${key}_${UrlUtil.getCurrentPage()}`); }; + this.syncSetForPage = function (key, value) { this.syncSet(`${key}_${UrlUtil.getCurrentPage()}`, value); }; + + this.isSyncFake = function () { + return !!this._getSyncStorage().isSyncFake; + }; + + this._syncTrackKey = function (key, isRemove) { + const meta = this.syncGet(this._META_KEY) || {}; + if (isRemove) delete meta[key]; + else meta[key] = 1; + this._getSyncStorage().setItem(this._META_KEY, JSON.stringify(meta)); + }; + + this.syncGetDump = function () { + const out = {}; + this._syncGetPresentKeys().forEach(key => out[key] = this.syncGet(key)); + return out; + }; + + this._syncGetPresentKeys = function () { + const meta = this.syncGet(this._META_KEY) || {}; + return Object.entries(meta).filter(([, isPresent]) => isPresent).map(([key]) => key); + }; + + this.syncSetFromDump = function (dump) { + const keysToRemove = new Set(this._syncGetPresentKeys()); + Object.entries(dump).map(([k, v]) => { + keysToRemove.delete(k); + return this.syncSet(k, v); + }); + [...keysToRemove].map(k => this.syncRemove(k)); + }; + // endregion + + // region Asynchronous + this.pIsAsyncFake = async function () { + const storage = await this._getAsyncStorage(); + return !!storage.pIsAsyncFake; + }; + + this.pSet = async function (key, value) { + this._pTrackKey(key).then(null); + const storage = await this._getAsyncStorage(); + return storage.setItem(key, value); + }; + + this.pGet = async function (key) { + const storage = await this._getAsyncStorage(); + return storage.getItem(key); + }; + + this.pRemove = async function (key) { + this._pTrackKey(key, true).then(null); + const storage = await this._getAsyncStorage(); + return storage.removeItem(key); + }; + + this.pGetForPage = async function (key, {page = null} = {}) { return this.pGet(this.getPageKey(key, page)); }; + this.pSetForPage = async function (key, value, {page = null} = {}) { return this.pSet(this.getPageKey(key, page), value); }; + this.pRemoveForPage = async function (key, {page = null} = {}) { return this.pRemove(this.getPageKey(key, page)); }; + + this._pTrackKey = async function (key, isRemove) { + const storage = await this._getAsyncStorage(); + const meta = (await this.pGet(this._META_KEY)) || {}; + if (isRemove) delete meta[key]; + else meta[key] = 1; + return storage.setItem(this._META_KEY, meta); + }; + + this.pGetDump = async function () { + const out = {}; + await Promise.all( + (await this._pGetPresentKeys()).map(async (key) => out[key] = await this.pGet(key)), + ); + return out; + }; + + this._pGetPresentKeys = async function () { + const meta = (await this.pGet(this._META_KEY)) || {}; + return Object.entries(meta).filter(([, isPresent]) => isPresent).map(([key]) => key); + }; + + this.pSetFromDump = async function (dump) { + const keysToRemove = new Set(await this._pGetPresentKeys()); + await Promise.all( + Object.entries(dump).map(([k, v]) => { + keysToRemove.delete(k); + return this.pSet(k, v); + }), + ); + await Promise.all( + [...keysToRemove].map(k => this.pRemove(k)), + ); + }; + // endregion +} + +function StorageUtilMemory () { + StorageUtilBase.call(this); + + this._fakeStorage = null; + this._fakeStorageAsync = null; + + this._getSyncStorage = function () { + this._fakeStorage = this._fakeStorage || this._getFakeStorageSync(); + return this._fakeStorage; + }; + + this._getAsyncStorage = async function () { + this._fakeStorageAsync = this._fakeStorageAsync || this._getFakeStorageAsync(); + return this._fakeStorageAsync; + }; +} + +globalThis.StorageUtilMemory = StorageUtilMemory; + +function StorageUtilBacked () { + StorageUtilBase.call(this); + + this._isInit = false; + this._isInitAsync = false; + this._fakeStorage = null; + this._fakeStorageAsync = null; + + this._initSyncStorage = function () { + if (this._isInit) return; + + if (typeof window === "undefined") { + this._fakeStorage = this._getFakeStorageSync(); + this._isInit = true; + return; + } + + try { + window.localStorage.setItem("_test_storage", true); + } catch (e) { + // if the user has disabled cookies, build a fake version + this._fakeStorage = this._getFakeStorageSync(); + } + + this._isInit = true; + }; + + this._getSyncStorage = function () { + this._initSyncStorage(); + if (this._fakeStorage) return this._fakeStorage; + return window.localStorage; + }; + + this._initAsyncStorage = async function () { + if (this._isInitAsync) return; + + if (typeof window === "undefined") { + this._fakeStorageAsync = this._getFakeStorageAsync(); + this._isInitAsync = true; + return; + } + + try { + // check if IndexedDB is available (i.e. not in Firefox private browsing) + await new Promise((resolve, reject) => { + const request = window.indexedDB.open("_test_db", 1); + request.onerror = reject; + request.onsuccess = resolve; + }); + await localforage.setItem("_storage_check", true); + } catch (e) { + this._fakeStorageAsync = this._getFakeStorageAsync(); + } + + this._isInitAsync = true; + }; + + this._getAsyncStorage = async function () { + await this._initAsyncStorage(); + if (this._fakeStorageAsync) return this._fakeStorageAsync; + else return localforage; + }; +} + +globalThis.StorageUtil = new StorageUtilBacked(); + +// TODO transition cookie-like storage items over to this +globalThis.SessionStorageUtil = { + _fakeStorage: {}, + __storage: null, + getStorage: () => { + try { + return window.sessionStorage; + } catch (e) { + // if the user has disabled cookies, build a fake version + if (SessionStorageUtil.__storage) return SessionStorageUtil.__storage; + else { + return SessionStorageUtil.__storage = { + isFake: true, + getItem: (k) => { + return SessionStorageUtil._fakeStorage[k]; + }, + removeItem: (k) => { + delete SessionStorageUtil._fakeStorage[k]; + }, + setItem: (k, v) => { + SessionStorageUtil._fakeStorage[k] = v; + }, + }; + } + } + }, + + isFake () { + return SessionStorageUtil.getStorage().isSyncFake; + }, + + setForPage: (key, value) => { + SessionStorageUtil.set(`${key}_${UrlUtil.getCurrentPage()}`, value); + }, + + set (key, value) { + SessionStorageUtil.getStorage().setItem(key, JSON.stringify(value)); + }, + + getForPage: (key) => { + return SessionStorageUtil.get(`${key}_${UrlUtil.getCurrentPage()}`); + }, + + get (key) { + const rawOut = SessionStorageUtil.getStorage().getItem(key); + if (rawOut && rawOut !== "undefined" && rawOut !== "null") return JSON.parse(rawOut); + return null; + }, + + removeForPage: (key) => { + SessionStorageUtil.remove(`${key}_${UrlUtil.getCurrentPage()}`); + }, + + remove (key) { + SessionStorageUtil.getStorage().removeItem(key); + }, +}; + +// ID GENERATION ======================================================================================================= +globalThis.CryptUtil = { + // region md5 internals + // stolen from http://www.myersdaily.org/joseph/javascript/md5.js + _md5cycle: (x, k) => { + let a = x[0]; + let b = x[1]; + let c = x[2]; + let d = x[3]; + + a = CryptUtil._ff(a, b, c, d, k[0], 7, -680876936); + d = CryptUtil._ff(d, a, b, c, k[1], 12, -389564586); + c = CryptUtil._ff(c, d, a, b, k[2], 17, 606105819); + b = CryptUtil._ff(b, c, d, a, k[3], 22, -1044525330); + a = CryptUtil._ff(a, b, c, d, k[4], 7, -176418897); + d = CryptUtil._ff(d, a, b, c, k[5], 12, 1200080426); + c = CryptUtil._ff(c, d, a, b, k[6], 17, -1473231341); + b = CryptUtil._ff(b, c, d, a, k[7], 22, -45705983); + a = CryptUtil._ff(a, b, c, d, k[8], 7, 1770035416); + d = CryptUtil._ff(d, a, b, c, k[9], 12, -1958414417); + c = CryptUtil._ff(c, d, a, b, k[10], 17, -42063); + b = CryptUtil._ff(b, c, d, a, k[11], 22, -1990404162); + a = CryptUtil._ff(a, b, c, d, k[12], 7, 1804603682); + d = CryptUtil._ff(d, a, b, c, k[13], 12, -40341101); + c = CryptUtil._ff(c, d, a, b, k[14], 17, -1502002290); + b = CryptUtil._ff(b, c, d, a, k[15], 22, 1236535329); + + a = CryptUtil._gg(a, b, c, d, k[1], 5, -165796510); + d = CryptUtil._gg(d, a, b, c, k[6], 9, -1069501632); + c = CryptUtil._gg(c, d, a, b, k[11], 14, 643717713); + b = CryptUtil._gg(b, c, d, a, k[0], 20, -373897302); + a = CryptUtil._gg(a, b, c, d, k[5], 5, -701558691); + d = CryptUtil._gg(d, a, b, c, k[10], 9, 38016083); + c = CryptUtil._gg(c, d, a, b, k[15], 14, -660478335); + b = CryptUtil._gg(b, c, d, a, k[4], 20, -405537848); + a = CryptUtil._gg(a, b, c, d, k[9], 5, 568446438); + d = CryptUtil._gg(d, a, b, c, k[14], 9, -1019803690); + c = CryptUtil._gg(c, d, a, b, k[3], 14, -187363961); + b = CryptUtil._gg(b, c, d, a, k[8], 20, 1163531501); + a = CryptUtil._gg(a, b, c, d, k[13], 5, -1444681467); + d = CryptUtil._gg(d, a, b, c, k[2], 9, -51403784); + c = CryptUtil._gg(c, d, a, b, k[7], 14, 1735328473); + b = CryptUtil._gg(b, c, d, a, k[12], 20, -1926607734); + + a = CryptUtil._hh(a, b, c, d, k[5], 4, -378558); + d = CryptUtil._hh(d, a, b, c, k[8], 11, -2022574463); + c = CryptUtil._hh(c, d, a, b, k[11], 16, 1839030562); + b = CryptUtil._hh(b, c, d, a, k[14], 23, -35309556); + a = CryptUtil._hh(a, b, c, d, k[1], 4, -1530992060); + d = CryptUtil._hh(d, a, b, c, k[4], 11, 1272893353); + c = CryptUtil._hh(c, d, a, b, k[7], 16, -155497632); + b = CryptUtil._hh(b, c, d, a, k[10], 23, -1094730640); + a = CryptUtil._hh(a, b, c, d, k[13], 4, 681279174); + d = CryptUtil._hh(d, a, b, c, k[0], 11, -358537222); + c = CryptUtil._hh(c, d, a, b, k[3], 16, -722521979); + b = CryptUtil._hh(b, c, d, a, k[6], 23, 76029189); + a = CryptUtil._hh(a, b, c, d, k[9], 4, -640364487); + d = CryptUtil._hh(d, a, b, c, k[12], 11, -421815835); + c = CryptUtil._hh(c, d, a, b, k[15], 16, 530742520); + b = CryptUtil._hh(b, c, d, a, k[2], 23, -995338651); + + a = CryptUtil._ii(a, b, c, d, k[0], 6, -198630844); + d = CryptUtil._ii(d, a, b, c, k[7], 10, 1126891415); + c = CryptUtil._ii(c, d, a, b, k[14], 15, -1416354905); + b = CryptUtil._ii(b, c, d, a, k[5], 21, -57434055); + a = CryptUtil._ii(a, b, c, d, k[12], 6, 1700485571); + d = CryptUtil._ii(d, a, b, c, k[3], 10, -1894986606); + c = CryptUtil._ii(c, d, a, b, k[10], 15, -1051523); + b = CryptUtil._ii(b, c, d, a, k[1], 21, -2054922799); + a = CryptUtil._ii(a, b, c, d, k[8], 6, 1873313359); + d = CryptUtil._ii(d, a, b, c, k[15], 10, -30611744); + c = CryptUtil._ii(c, d, a, b, k[6], 15, -1560198380); + b = CryptUtil._ii(b, c, d, a, k[13], 21, 1309151649); + a = CryptUtil._ii(a, b, c, d, k[4], 6, -145523070); + d = CryptUtil._ii(d, a, b, c, k[11], 10, -1120210379); + c = CryptUtil._ii(c, d, a, b, k[2], 15, 718787259); + b = CryptUtil._ii(b, c, d, a, k[9], 21, -343485551); + + x[0] = CryptUtil._add32(a, x[0]); + x[1] = CryptUtil._add32(b, x[1]); + x[2] = CryptUtil._add32(c, x[2]); + x[3] = CryptUtil._add32(d, x[3]); + }, + + _cmn: (q, a, b, x, s, t) => { + a = CryptUtil._add32(CryptUtil._add32(a, q), CryptUtil._add32(x, t)); + return CryptUtil._add32((a << s) | (a >>> (32 - s)), b); + }, + + _ff: (a, b, c, d, x, s, t) => { + return CryptUtil._cmn((b & c) | ((~b) & d), a, b, x, s, t); + }, + + _gg: (a, b, c, d, x, s, t) => { + return CryptUtil._cmn((b & d) | (c & (~d)), a, b, x, s, t); + }, + + _hh: (a, b, c, d, x, s, t) => { + return CryptUtil._cmn(b ^ c ^ d, a, b, x, s, t); + }, + + _ii: (a, b, c, d, x, s, t) => { + return CryptUtil._cmn(c ^ (b | (~d)), a, b, x, s, t); + }, + + _md51: (s) => { + let n = s.length; + let state = [1732584193, -271733879, -1732584194, 271733878]; + let i; + for (i = 64; i <= s.length; i += 64) { + CryptUtil._md5cycle(state, CryptUtil._md5blk(s.substring(i - 64, i))); + } + s = s.substring(i - 64); + let tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + for (i = 0; i < s.length; i++) tail[i >> 2] |= s.charCodeAt(i) << ((i % 4) << 3); + tail[i >> 2] |= 0x80 << ((i % 4) << 3); + if (i > 55) { + CryptUtil._md5cycle(state, tail); + for (i = 0; i < 16; i++) tail[i] = 0; + } + tail[14] = n * 8; + CryptUtil._md5cycle(state, tail); + return state; + }, + + _md5blk: (s) => { + let md5blks = []; + for (let i = 0; i < 64; i += 4) { + md5blks[i >> 2] = s.charCodeAt(i) + (s.charCodeAt(i + 1) << 8) + (s.charCodeAt(i + 2) << 16) + (s.charCodeAt(i + 3) << 24); + } + return md5blks; + }, + + _hex_chr: "0123456789abcdef".split(""), + + _rhex: (n) => { + let s = ""; + for (let j = 0; j < 4; j++) { + s += CryptUtil._hex_chr[(n >> (j * 8 + 4)) & 0x0F] + CryptUtil._hex_chr[(n >> (j * 8)) & 0x0F]; + } + return s; + }, + + _add32: (a, b) => { + return (a + b) & 0xFFFFFFFF; + }, + // endregion + + hex: (x) => { + for (let i = 0; i < x.length; i++) { + x[i] = CryptUtil._rhex(x[i]); + } + return x.join(""); + }, + + hex2Dec (hex) { + return parseInt(`0x${hex}`); + }, + + md5: (s) => { + return CryptUtil.hex(CryptUtil._md51(s)); + }, + + /** + * Based on Java's implementation. + * @param obj An object to hash. + * @return {*} An integer hashcode for the object. + */ + hashCode (obj) { + if (typeof obj === "string") { + if (!obj) return 0; + let h = 0; + for (let i = 0; i < obj.length; ++i) h = 31 * h + obj.charCodeAt(i); + return h; + } else if (typeof obj === "number") return obj; + else throw new Error(`No hashCode implementation for ${obj}`); + }, + + uid () { // https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript + if (RollerUtil.isCrypto()) { + return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c => (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)); + } else { + let d = Date.now(); + if (typeof performance !== "undefined" && typeof performance.now === "function") { + d += performance.now(); + } + return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) { + const r = (d + Math.random() * 16) % 16 | 0; + d = Math.floor(d / 16); + return (c === "x" ? r : (r & 0x3 | 0x8)).toString(16); + }); + } + }, +}; + +// COLLECTIONS ========================================================================================================= +globalThis.CollectionUtil = { + ObjectSet: class ObjectSet { + constructor () { + this.map = new Map(); + this[Symbol.iterator] = this.values; + } + // Each inserted element has to implement _toIdString() method that returns a string ID. + // Two objects are considered equal if their string IDs are equal. + add (item) { + this.map.set(item._toIdString(), item); + } + + values () { + return this.map.values(); + } + }, + + setEq (a, b) { + if (a.size !== b.size) return false; + for (const it of a) if (!b.has(it)) return false; + return true; + }, + + setDiff (set1, set2) { + return new Set([...set1].filter(it => !set2.has(it))); + }, + + objectDiff (obj1, obj2) { + const out = {}; + + [...new Set([...Object.keys(obj1), ...Object.keys(obj2)])] + .forEach(k => { + const diff = CollectionUtil._objectDiff_recurse(obj1[k], obj2[k]); + if (diff !== undefined) out[k] = diff; + }); + + return out; + }, + + _objectDiff_recurse (a, b) { + if (CollectionUtil.deepEquals(a, b)) return undefined; + + if (a && b && typeof a === "object" && typeof b === "object") { + return CollectionUtil.objectDiff(a, b); + } + + return b; + }, + + objectIntersect (obj1, obj2) { + const out = {}; + + [...new Set([...Object.keys(obj1), ...Object.keys(obj2)])] + .forEach(k => { + const diff = CollectionUtil._objectIntersect_recurse(obj1[k], obj2[k]); + if (diff !== undefined) out[k] = diff; + }); + + return out; + }, + + _objectIntersect_recurse (a, b) { + if (CollectionUtil.deepEquals(a, b)) return a; + + if (a && b && typeof a === "object" && typeof b === "object") { + return CollectionUtil.objectIntersect(a, b); + } + + return undefined; + }, + + deepEquals (a, b) { + if (Object.is(a, b)) return true; + if (a && b && typeof a === "object" && typeof b === "object") { + if (CollectionUtil._eq_isPlainObject(a) && CollectionUtil._eq_isPlainObject(b)) return CollectionUtil._eq_areObjectsEqual(a, b); + const isArrayA = Array.isArray(a); + const isArrayB = Array.isArray(b); + if (isArrayA || isArrayB) return isArrayA === isArrayB && CollectionUtil._eq_areArraysEqual(a, b); + const isSetA = a instanceof Set; + const isSetB = b instanceof Set; + if (isSetA || isSetB) return isSetA === isSetB && CollectionUtil.setEq(a, b); + return CollectionUtil._eq_areObjectsEqual(a, b); + } + return false; + }, + + _eq_isPlainObject: (value) => value.constructor === Object || value.constructor == null, + _eq_areObjectsEqual (a, b) { + const keysA = Object.keys(a); + const {length} = keysA; + if (Object.keys(b).length !== length) return false; + for (let i = 0; i < length; i++) { + if (!b.hasOwnProperty(keysA[i])) return false; + if (!CollectionUtil.deepEquals(a[keysA[i]], b[keysA[i]])) return false; + } + return true; + }, + _eq_areArraysEqual (a, b) { + const {length} = a; + if (b.length !== length) return false; + for (let i = 0; i < length; i++) if (!CollectionUtil.deepEquals(a[i], b[i])) return false; + return true; + }, + + // region Find first + dfs (obj, opts) { + const {prop = null, fnMatch = null} = opts; + if (!prop && !fnMatch) throw new Error(`One of "prop" or "fnMatch" must be specified!`); + + if (obj instanceof Array) { + for (const child of obj) { + const n = CollectionUtil.dfs(child, opts); + if (n) return n; + } + return; + } + + if (obj instanceof Object) { + if (prop && obj[prop]) return obj[prop]; + if (fnMatch && fnMatch(obj)) return obj; + + for (const child of Object.values(obj)) { + const n = CollectionUtil.dfs(child, opts); + if (n) return n; + } + } + }, + + bfs (obj, opts) { + const {prop = null, fnMatch = null} = opts; + if (!prop && !fnMatch) throw new Error(`One of "prop" or "fnMatch" must be specified!`); + + if (obj instanceof Array) { + for (const child of obj) { + if (!(child instanceof Array) && child instanceof Object) { + if (prop && child[prop]) return child[prop]; + if (fnMatch && fnMatch(child)) return child; + } + } + + for (const child of obj) { + const n = CollectionUtil.bfs(child, opts); + if (n) return n; + } + + return; + } + + if (obj instanceof Object) { + if (prop && obj[prop]) return obj[prop]; + if (fnMatch && fnMatch(obj)) return obj; + + return CollectionUtil.bfs(Object.values(obj)); + } + }, + // endregion +}; + +Array.prototype.last || Object.defineProperty(Array.prototype, "last", { + enumerable: false, + writable: true, + value: function (arg) { + if (arg !== undefined) this[this.length - 1] = arg; + else return this[this.length - 1]; + }, +}); + +Array.prototype.filterIndex || Object.defineProperty(Array.prototype, "filterIndex", { + enumerable: false, + writable: true, + value: function (fnCheck) { + const out = []; + this.forEach((it, i) => { + if (fnCheck(it)) out.push(i); + }); + return out; + }, +}); + +Array.prototype.equals || Object.defineProperty(Array.prototype, "equals", { + enumerable: false, + writable: true, + value: function (array2) { + const array1 = this; + if (!array1 && !array2) return true; + else if ((!array1 && array2) || (array1 && !array2)) return false; + + let temp = []; + if ((!array1[0]) || (!array2[0])) return false; + if (array1.length !== array2.length) return false; + let key; + // Put all the elements from array1 into a "tagged" array + for (let i = 0; i < array1.length; i++) { + key = `${(typeof array1[i])}~${array1[i]}`; // Use "typeof" so a number 1 isn't equal to a string "1". + if (temp[key]) temp[key]++; + else temp[key] = 1; + } + // Go through array2 - if same tag missing in "tagged" array, not equal + for (let i = 0; i < array2.length; i++) { + key = `${(typeof array2[i])}~${array2[i]}`; + if (temp[key]) { + if (temp[key] === 0) return false; + else temp[key]--; + } else return false; + } + return true; + }, +}); + +// Alternate name due to clash with Foundry VTT +Array.prototype.segregate || Object.defineProperty(Array.prototype, "segregate", { + enumerable: false, + writable: true, + value: function (fnIsValid) { + return this.reduce(([pass, fail], elem) => fnIsValid(elem) ? [[...pass, elem], fail] : [pass, [...fail, elem]], [[], []]); + }, +}); + +Array.prototype.partition || Object.defineProperty(Array.prototype, "partition", { + enumerable: false, + writable: true, + value: Array.prototype.segregate, +}); + +Array.prototype.getNext || Object.defineProperty(Array.prototype, "getNext", { + enumerable: false, + writable: true, + value: function (curVal) { + let ix = this.indexOf(curVal); + if (!~ix) throw new Error("Value was not in array!"); + if (++ix >= this.length) ix = 0; + return this[ix]; + }, +}); + +// See: https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle +Array.prototype.shuffle || Object.defineProperty(Array.prototype, "shuffle", { + enumerable: false, + writable: true, + value: function () { + const len = this.length; + const ixLast = len - 1; + for (let i = 0; i < len; ++i) { + const j = i + Math.floor(Math.random() * (ixLast - i + 1)); + [this[i], this[j]] = [this[j], this[i]]; + } + return this; + }, +}); + +/** Map each array item to a k:v pair, then flatten them into one object. */ +Array.prototype.mergeMap || Object.defineProperty(Array.prototype, "mergeMap", { + enumerable: false, + writable: true, + value: function (fnMap) { + return this.map((...args) => fnMap(...args)).filter(it => it != null).reduce((a, b) => Object.assign(a, b), {}); + }, +}); + +Array.prototype.first || Object.defineProperty(Array.prototype, "first", { + enumerable: false, + writable: true, + value: function (fnMapFind) { + for (let i = 0, len = this.length; i < len; ++i) { + const result = fnMapFind(this[i], i, this); + if (result) return result; + } + }, +}); + +Array.prototype.pMap || Object.defineProperty(Array.prototype, "pMap", { + enumerable: false, + writable: true, + value: async function (fnMap) { + return Promise.all(this.map((it, i) => fnMap(it, i, this))); + }, +}); + +/** Map each item via an async function, awaiting for each to complete before starting the next. */ +Array.prototype.pSerialAwaitMap || Object.defineProperty(Array.prototype, "pSerialAwaitMap", { + enumerable: false, + writable: true, + value: async function (fnMap) { + const out = []; + for (let i = 0, len = this.length; i < len; ++i) out.push(await fnMap(this[i], i, this)); + return out; + }, +}); + +Array.prototype.pSerialAwaitFilter || Object.defineProperty(Array.prototype, "pSerialAwaitFilter", { + enumerable: false, + writable: true, + value: async function (fnFilter) { + const out = []; + for (let i = 0, len = this.length; i < len; ++i) { + if (await fnFilter(this[i], i, this)) out.push(this[i]); + } + return out; + }, +}); + +Array.prototype.pSerialAwaitFind || Object.defineProperty(Array.prototype, "pSerialAwaitFind", { + enumerable: false, + writable: true, + value: async function (fnFind) { + for (let i = 0, len = this.length; i < len; ++i) if (await fnFind(this[i], i, this)) return this[i]; + }, +}); + +Array.prototype.pSerialAwaitSome || Object.defineProperty(Array.prototype, "pSerialAwaitSome", { + enumerable: false, + writable: true, + value: async function (fnSome) { + for (let i = 0, len = this.length; i < len; ++i) if (await fnSome(this[i], i, this)) return true; + return false; + }, +}); + +Array.prototype.pSerialAwaitFirst || Object.defineProperty(Array.prototype, "pSerialAwaitFirst", { + enumerable: false, + writable: true, + value: async function (fnMapFind) { + for (let i = 0, len = this.length; i < len; ++i) { + const result = await fnMapFind(this[i], i, this); + if (result) return result; + } + }, +}); + +Array.prototype.pSerialAwaitReduce || Object.defineProperty(Array.prototype, "pSerialAwaitReduce", { + enumerable: false, + writable: true, + value: async function (fnReduce, initialValue) { + let accumulator = initialValue === undefined ? this[0] : initialValue; + for (let i = (initialValue === undefined ? 1 : 0), len = this.length; i < len; ++i) { + accumulator = await fnReduce(accumulator, this[i], i, this); + } + return accumulator; + }, +}); + +Array.prototype.unique || Object.defineProperty(Array.prototype, "unique", { + enumerable: false, + writable: true, + value: function (fnGetProp) { + const seen = new Set(); + return this.filter((...args) => { + const val = fnGetProp ? fnGetProp(...args) : args[0]; + if (seen.has(val)) return false; + seen.add(val); + return true; + }); + }, +}); + +Array.prototype.zip || Object.defineProperty(Array.prototype, "zip", { + enumerable: false, + writable: true, + value: function (otherArray) { + const out = []; + const len = Math.max(this.length, otherArray.length); + for (let i = 0; i < len; ++i) { + out.push([this[i], otherArray[i]]); + } + return out; + }, +}); + +Array.prototype.nextWrap || Object.defineProperty(Array.prototype, "nextWrap", { + enumerable: false, + writable: true, + value: function (item) { + const ix = this.indexOf(item); + if (~ix) { + if (ix + 1 < this.length) return this[ix + 1]; + else return this[0]; + } else return this.last(); + }, +}); + +Array.prototype.prevWrap || Object.defineProperty(Array.prototype, "prevWrap", { + enumerable: false, + writable: true, + value: function (item) { + const ix = this.indexOf(item); + if (~ix) { + if (ix - 1 >= 0) return this[ix - 1]; + else return this.last(); + } else return this[0]; + }, +}); + +Array.prototype.findLast || Object.defineProperty(Array.prototype, "findLast", { + enumerable: false, + writable: true, + value: function (fn) { + for (let i = this.length - 1; i >= 0; --i) if (fn(this[i])) return this[i]; + }, +}); + +Array.prototype.findLastIndex || Object.defineProperty(Array.prototype, "findLastIndex", { + enumerable: false, + writable: true, + value: function (fn) { + for (let i = this.length - 1; i >= 0; --i) if (fn(this[i])) return i; + return -1; + }, +}); + +Array.prototype.sum || Object.defineProperty(Array.prototype, "sum", { + enumerable: false, + writable: true, + value: function () { + let tmp = 0; + const len = this.length; + for (let i = 0; i < len; ++i) tmp += this[i]; + return tmp; + }, +}); + +Array.prototype.mean || Object.defineProperty(Array.prototype, "mean", { + enumerable: false, + writable: true, + value: function () { + return this.sum() / this.length; + }, +}); + +Array.prototype.meanAbsoluteDeviation || Object.defineProperty(Array.prototype, "meanAbsoluteDeviation", { + enumerable: false, + writable: true, + value: function () { + const mean = this.mean(); + return (this.map(num => Math.abs(num - mean)) || []).mean(); + }, +}); + +Map.prototype.getOrSet || Object.defineProperty(Map.prototype, "getOrSet", { + enumerable: false, + writable: true, + value: function (k, orV) { + if (this.has(k)) return this.get(k); + this.set(k, orV); + return orV; + }, +}); + +// OVERLAY VIEW ======================================================================================================== +/** + * Relies on: + * - page implementing HashUtil's `loadSubHash` with handling to show/hide the book view based on hashKey changes + * - page running no-argument `loadSubHash` when `hashchange` occurs + * + * @param opts Options object. + * @param opts.hashKey to use in the URL so that forward/back can open/close the view + * @param opts.$btnOpen jQuery-selected button to bind click open/close + * @param [opts.$eleNoneVisible] "error" message to display if user has not selected any viewable content + * @param opts.pageTitle Title. + * @param opts.state State to modify when opening/closing. + * @param opts.stateKey Key in state to set true/false when opening/closing. + * @param [opts.hasPrintColumns] True if the overlay should contain a dropdown for adjusting print columns. + * @param [opts.isHideContentOnNoneShown] + * @param [opts.isHideButtonCloseNone] + * @constructor + * + * @abstract + */ +class BookModeViewBase { + static _BOOK_VIEW_COLUMNS_K = "bookViewColumns"; + + _hashKey; + _stateKey; + _pageTitle; + _isColumns = true; + _hasPrintColumns = false; + + constructor (opts) { + opts = opts || {}; + const {$btnOpen, state} = opts; + + if (this._hashKey && this._stateKey) throw new Error(`Only one of "hashKey" and "stateKey" may be specified!`); + + this._state = state; + this._$btnOpen = $btnOpen; + + this._isActive = false; + this._$wrpBook = null; + + this._$btnOpen.off("click").on("click", () => this.setStateOpen()); + } + + /* -------------------------------------------- */ + + setStateOpen () { + if (this._stateKey) return this._state[this._stateKey] = true; + Hist.cleanSetHash(`${window.location.hash}${HASH_PART_SEP}${this._hashKey}${HASH_SUB_KV_SEP}true`); + } + + setStateClosed () { + if (this._stateKey) return this._state[this._stateKey] = false; + Hist.cleanSetHash(window.location.hash.replace(`${this._hashKey}${HASH_SUB_KV_SEP}true`, "")); + } + + /* -------------------------------------------- */ + + _$getWindowHeaderLhs () { + return $(`
            `); + } + + _$getBtnWindowClose () { + return $(``) + .click(() => this.setStateClosed()); + } + + /* -------------------------------------------- */ + + async _$pGetWrpControls ({$wrpContent}) { + const $wrp = $(`
            `); + + if (!this._hasPrintColumns) return $wrp; + + $wrp.addClass("px-2 mt-2 bb-1p pb-1"); + + const onChangeColumnCount = (cols) => { + $wrpContent.toggleClass(`bkmv__wrp--columns-1`, cols === 1); + $wrpContent.toggleClass(`bkmv__wrp--columns-2`, cols === 2); + }; + + const lastColumns = StorageUtil.syncGetForPage(BookModeViewBase._BOOK_VIEW_COLUMNS_K); + + const $selColumns = $(``) + .change(() => { + const val = Number($selColumns.val()); + if (val === 0) onChangeColumnCount(2); + else onChangeColumnCount(1); + + StorageUtil.syncSetForPage(BookModeViewBase._BOOK_VIEW_COLUMNS_K, val); + }); + if (lastColumns != null) $selColumns.val(lastColumns); + $selColumns.change(); + + const $wrpPrint = $$`
            +
            Print columns:
            ${$selColumns}
            +
            `.appendTo($wrp); + + return {$wrp, $wrpPrint}; + } + + /* -------------------------------------------- */ + + _$getEleNoneVisible () { return null; } + + _$getBtnNoneVisibleClose () { + return $(``) + .click(() => this.setStateClosed()); + } + + /** @abstract */ + async _pGetRenderContentMeta ({$wrpContent, $wrpContentOuter}) { + return {cntSelectedEnts: 0, isAnyEntityRendered: false}; + } + + /* -------------------------------------------- */ + + async pOpen () { + if (this._isActive) return; + this._isActive = true; + + document.title = `${this._pageTitle} - 5etools`; + document.body.style.overflow = "hidden"; + document.body.classList.add("bkmv-active"); + + const {$wrpContentOuter, $wrpContent} = await this._pGetContentElementMetas(); + + this._$wrpBook = $$`` + .appendTo(document.body); + } + + async _pGetContentElementMetas () { + const $wrpContent = $(``); + + const $wrpContentOuter = $$``; + + const out = { + $wrpContentOuter, + $wrpContent, + }; + + const {cntSelectedEnts, isAnyEntityRendered} = await this._pGetRenderContentMeta({$wrpContent, $wrpContentOuter}); + + if (isAnyEntityRendered) $wrpContentOuter.append($wrpContent); + + if (cntSelectedEnts) return out; + + $wrpContentOuter.append(this._$getEleNoneVisible()); + + return out; + } + + teardown () { + if (!this._isActive) return; + + document.body.style.overflow = ""; + document.body.classList.remove("bkmv-active"); + + this._$wrpBook.remove(); + this._isActive = false; + } + + async pHandleSub (sub) { + if (this._stateKey) return sub; // Assume anything with state will handle this itself. + + const bookViewHash = sub.find(it => it.startsWith(this._hashKey)); + if (!bookViewHash) { + this.teardown(); + return sub; + } + + if (UrlUtil.unpackSubHash(bookViewHash)[this._hashKey][0] === "true") await this.pOpen(); + return sub.filter(it => !it.startsWith(this._hashKey)); + } +} + +// CONTENT EXCLUSION =================================================================================================== +globalThis.ExcludeUtil = { + isInitialised: false, + _excludes: null, + _cache_excludesLookup: null, + _lock: null, + + async pInitialise ({lockToken = null} = {}) { + try { + await ExcludeUtil._lock.pLock({token: lockToken}); + await ExcludeUtil._pInitialise(); + } finally { + ExcludeUtil._lock.unlock(); + } + }, + + async _pInitialise () { + if (ExcludeUtil.isInitialised) return; + + ExcludeUtil.pSave = MiscUtil.throttle(ExcludeUtil._pSave, 50); + try { + ExcludeUtil._excludes = await StorageUtil.pGet(VeCt.STORAGE_EXCLUDES) || []; + ExcludeUtil._excludes = ExcludeUtil._excludes.filter(it => it.hash); // remove legacy rows + } catch (e) { + JqueryUtil.doToast({ + content: "Error when loading content blocklist! Purged blocklist data. (See the log for more information.)", + type: "danger", + }); + try { + await StorageUtil.pRemove(VeCt.STORAGE_EXCLUDES); + } catch (e) { + setTimeout(() => { throw e; }); + } + ExcludeUtil._excludes = null; + window.location.hash = ""; + setTimeout(() => { throw e; }); + } + ExcludeUtil.isInitialised = true; + }, + + getList () { + return MiscUtil.copyFast(ExcludeUtil._excludes || []); + }, + + async pSetList (toSet) { + ExcludeUtil._excludes = toSet; + ExcludeUtil._cache_excludesLookup = null; + await ExcludeUtil.pSave(); + }, + + async pExtendList (toAdd) { + try { + const lockToken = await ExcludeUtil._lock.pLock(); + await ExcludeUtil._pExtendList({toAdd, lockToken}); + } finally { + ExcludeUtil._lock.unlock(); + } + }, + + async _pExtendList ({toAdd, lockToken}) { + await ExcludeUtil.pInitialise({lockToken}); + this._doBuildCache(); + + const out = MiscUtil.copyFast(ExcludeUtil._excludes || []); + MiscUtil.copyFast(toAdd || []) + .filter(({hash, category, source}) => { + if (!hash || !category || !source) return false; + const cacheUid = ExcludeUtil._getCacheUids(hash, category, source, true); + return !ExcludeUtil._cache_excludesLookup[cacheUid]; + }) + .forEach(it => out.push(it)); + + await ExcludeUtil.pSetList(out); + }, + + _doBuildCache () { + if (ExcludeUtil._cache_excludesLookup) return; + if (!ExcludeUtil._excludes) return; + + ExcludeUtil._cache_excludesLookup = {}; + ExcludeUtil._excludes.forEach(({source, category, hash}) => { + const cacheUid = ExcludeUtil._getCacheUids(hash, category, source, true); + ExcludeUtil._cache_excludesLookup[cacheUid] = true; + }); + }, + + _getCacheUids (hash, category, source, isExact) { + hash = (hash || "").toLowerCase(); + category = (category || "").toLowerCase(); + source = (source?.source || source || "").toLowerCase(); + + const exact = `${hash}__${category}__${source}`; + if (isExact) return [exact]; + + return [ + `${hash}__${category}__${source}`, + `*__${category}__${source}`, + `${hash}__*__${source}`, + `${hash}__${category}__*`, + `*__*__${source}`, + `*__${category}__*`, + `${hash}__*__*`, + `*__*__*`, + ]; + }, + + _excludeCount: 0, + /** + * @param hash + * @param category + * @param source + * @param [opts] + * @param [opts.isNoCount] + */ + isExcluded (hash, category, source, opts) { + if (!ExcludeUtil._excludes || !ExcludeUtil._excludes.length) return false; + if (!source) throw new Error(`Entity had no source!`); + opts = opts || {}; + + this._doBuildCache(); + + hash = (hash || "").toLowerCase(); + category = (category || "").toLowerCase(); + source = (source.source || source || "").toLowerCase(); + + const isExcluded = ExcludeUtil._isExcluded(hash, category, source); + if (!isExcluded) return isExcluded; + + if (!opts.isNoCount) ++ExcludeUtil._excludeCount; + + return isExcluded; + }, + + _isExcluded (hash, category, source) { + for (const cacheUid of ExcludeUtil._getCacheUids(hash, category, source)) { + if (ExcludeUtil._cache_excludesLookup[cacheUid]) return true; + } + return false; + }, + + isAllContentExcluded (list) { return (!list.length && ExcludeUtil._excludeCount) || (list.length > 0 && list.length === ExcludeUtil._excludeCount); }, + getAllContentBlocklistedHtml () { return `
            (All content blocklisted)
            `; }, + + async _pSave () { + return StorageUtil.pSet(VeCt.STORAGE_EXCLUDES, ExcludeUtil._excludes); + }, + + // The throttled version, available post-initialisation + async pSave () { /* no-op */ }, +}; + +// EXTENSIONS ========================================================================================================== +globalThis.ExtensionUtil = { + ACTIVE: false, + + _doSend (type, data) { + const detail = MiscUtil.copy({type, data}); // Note that this needs to include `JSON.parse` to function + window.dispatchEvent(new CustomEvent("rivet.send", {detail})); + }, + + async pDoSendStats (evt, ele) { + const {page, source, hash, extensionData} = ExtensionUtil._getElementData({ele}); + + if (page && source && hash) { + let toSend = ExtensionUtil._getEmbeddedFromCache(page, source, hash) + || await DataLoader.pCacheAndGet(page, source, hash); + + if (extensionData) { + switch (page) { + case UrlUtil.PG_BESTIARY: { + if (extensionData._scaledCr) toSend = await ScaleCreature.scale(toSend, extensionData._scaledCr); + else if (extensionData._scaledSpellSummonLevel) toSend = await ScaleSpellSummonedCreature.scale(toSend, extensionData._scaledSpellSummonLevel); + else if (extensionData._scaledClassSummonLevel) toSend = await ScaleClassSummonedCreature.scale(toSend, extensionData._scaledClassSummonLevel); + } + } + } + + ExtensionUtil._doSend("entity", {page, entity: toSend, isTemp: !!evt.shiftKey}); + } + }, + + async doDragStart (evt, ele) { + const {page, source, hash} = ExtensionUtil._getElementData({ele}); + const meta = { + type: VeCt.DRAG_TYPE_IMPORT, + page, + source, + hash, + }; + evt.dataTransfer.setData("application/json", JSON.stringify(meta)); + }, + + _getElementData ({ele}) { + const $parent = $(ele).closest(`[data-page]`); + const page = $parent.attr("data-page"); + const source = $parent.attr("data-source"); + const hash = $parent.attr("data-hash"); + const rawExtensionData = $parent.attr("data-extension"); + const extensionData = rawExtensionData ? JSON.parse(rawExtensionData) : null; + + return {page, source, hash, extensionData}; + }, + + pDoSendStatsPreloaded ({page, entity, isTemp, options}) { + ExtensionUtil._doSend("entity", {page, entity, isTemp, options}); + }, + + pDoSendCurrency ({currency}) { + ExtensionUtil._doSend("currency", {currency}); + }, + + doSendRoll (data) { ExtensionUtil._doSend("roll", data); }, + + pDoSend ({type, data}) { ExtensionUtil._doSend(type, data); }, + + /* -------------------------------------------- */ + + _CACHE_EMBEDDED_STATS: {}, + + addEmbeddedToCache (page, source, hash, ent) { + MiscUtil.set(ExtensionUtil._CACHE_EMBEDDED_STATS, page.toLowerCase(), source.toLowerCase(), hash.toLowerCase(), MiscUtil.copyFast(ent)); + }, + + _getEmbeddedFromCache (page, source, hash) { + return MiscUtil.get(ExtensionUtil._CACHE_EMBEDDED_STATS, page.toLowerCase(), source.toLowerCase(), hash.toLowerCase()); + }, + + /* -------------------------------------------- */ +}; +if (typeof window !== "undefined") window.addEventListener("rivet.active", () => ExtensionUtil.ACTIVE = true); + +// TOKENS ============================================================================================================== +globalThis.TokenUtil = { + handleStatblockScroll (event, ele) { + $(`#token_image`) + .toggle(ele.scrollTop < 32) + .css({ + opacity: (32 - ele.scrollTop) / 32, + top: -ele.scrollTop, + }); + }, +}; + +// LOCKS =============================================================================================================== +/** + * @param {string} name + * @param {boolean} isDbg + * @constructor + */ +globalThis.VeLock = function ({name = null, isDbg = false} = {}) { + this._name = name; + this._isDbg = isDbg; + this._lockMeta = null; + + this._getCaller = () => { + return (new Error()).stack.split("\n")[3].trim(); + }; + + this.pLock = async ({token = null} = {}) => { + if (token != null && this._lockMeta?.token === token) { + ++this._lockMeta.depth; + // eslint-disable-next-line no-console + if (this._isDbg) console.warn(`Lock "${this._name || "(unnamed)"}" add (now ${this._lockMeta.depth}) at ${this._getCaller()}`); + return token; + } + + while (this._lockMeta) await this._lockMeta.lock; + + // eslint-disable-next-line no-console + if (this._isDbg) console.warn(`Lock "${this._name || "(unnamed)"}" acquired at ${this._getCaller()}`); + + let unlock = null; + const lock = new Promise(resolve => unlock = resolve); + this._lockMeta = { + lock, + unlock, + token: CryptUtil.uid(), + depth: 0, + }; + + return this._lockMeta.token; + }; + + this.unlock = () => { + if (!this._lockMeta) return; + + if (this._lockMeta.depth > 0) { + // eslint-disable-next-line no-console + if (this._isDbg) console.warn(`Lock "${this._name || "(unnamed)"}" sub (now ${this._lockMeta.depth - 1}) at ${this._getCaller()}`); + return --this._lockMeta.depth; + } + + // eslint-disable-next-line no-console + if (this._isDbg) console.warn(`Lock "${this._name || "(unnamed)"}" released at ${this._getCaller()}`); + + const lockMeta = this._lockMeta; + this._lockMeta = null; + lockMeta.unlock(); + }; +}; +ExcludeUtil._lock = new VeLock(); + +// DATETIME ============================================================================================================ +globalThis.DatetimeUtil = { + getDateStr ({date, isShort = false, isPad = false} = {}) { + const month = DatetimeUtil._MONTHS[date.getMonth()]; + return `${isShort ? month.substring(0, 3) : month} ${isPad && date.getDate() < 10 ? "\u00A0" : ""}${Parser.getOrdinalForm(date.getDate())}, ${date.getFullYear()}`; + }, + + getDatetimeStr ({date, isPlainText = false} = {}) { + date = date ?? new Date(); + const monthName = DatetimeUtil._MONTHS[date.getMonth()]; + return `${date.getDate()} ${!isPlainText ? `` : ""}${monthName.substring(0, 3)}.${!isPlainText ? `` : ""} ${date.getFullYear()}, ${DatetimeUtil._getPad2(date.getHours())}:${DatetimeUtil._getPad2(date.getMinutes())}:${DatetimeUtil._getPad2(date.getSeconds())}`; + }, + + _getPad2 (num) { return `${num}`.padStart(2, "0"); }, + + getIntervalStr (millis) { + if (millis < 0 || isNaN(millis)) return "(Unknown interval)"; + + const s = number => (number !== 1) ? "s" : ""; + + const stack = []; + + let numSecs = Math.floor(millis / 1000); + + const numYears = Math.floor(numSecs / DatetimeUtil._SECS_PER_YEAR); + if (numYears) { + stack.push(`${numYears} year${s(numYears)}`); + numSecs = numSecs - (numYears * DatetimeUtil._SECS_PER_YEAR); + } + + const numDays = Math.floor(numSecs / DatetimeUtil._SECS_PER_DAY); + if (numDays) { + stack.push(`${numDays} day${s(numDays)}`); + numSecs = numSecs - (numDays * DatetimeUtil._SECS_PER_DAY); + } + + const numHours = Math.floor(numSecs / DatetimeUtil._SECS_PER_HOUR); + if (numHours) { + stack.push(`${numHours} hour${s(numHours)}`); + numSecs = numSecs - (numHours * DatetimeUtil._SECS_PER_HOUR); + } + + const numMinutes = Math.floor(numSecs / DatetimeUtil._SECS_PER_MINUTE); + if (numMinutes) { + stack.push(`${numMinutes} minute${s(numMinutes)}`); + numSecs = numSecs - (numMinutes * DatetimeUtil._SECS_PER_MINUTE); + } + + if (numSecs) stack.push(`${numSecs} second${s(numSecs)}`); + else if (!stack.length) stack.push("less than a second"); // avoid adding this if there's already info + + return stack.join(", "); + }, +}; +DatetimeUtil._MONTHS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; +DatetimeUtil._SECS_PER_YEAR = 31536000; +DatetimeUtil._SECS_PER_DAY = 86400; +DatetimeUtil._SECS_PER_HOUR = 3600; +DatetimeUtil._SECS_PER_MINUTE = 60; + +globalThis.EditorUtil = { + getTheme () { + const {isNight} = styleSwitcher.getSummary(); + return isNight ? "ace/theme/tomorrow_night" : "ace/theme/textmate"; + }, + + initEditor (id, additionalOpts = null) { + additionalOpts = additionalOpts || {}; + + const editor = ace.edit(id); + editor.setOptions({ + theme: EditorUtil.getTheme(), + wrap: true, + showPrintMargin: false, + tabSize: 2, + useWorker: false, + ...additionalOpts, + }); + + styleSwitcher.addFnOnChange(() => editor.setOptions({theme: EditorUtil.getTheme()})); + + return editor; + }, +}; + +// MISC WEBPAGE ONLOADS ================================================================================================ +if (!IS_VTT && typeof window !== "undefined") { + window.addEventListener("load", () => { + const docRoot = document.querySelector(":root"); + + // TODO(iOS) + if (CSS?.supports("top: constant(safe-area-inset-top)")) { + docRoot.style.setProperty("--safe-area-inset-top", "constant(safe-area-inset-top, 0)"); + docRoot.style.setProperty("--safe-area-inset-right", "constant(safe-area-inset-right, 0)"); + docRoot.style.setProperty("--safe-area-inset-bottom", "constant(safe-area-inset-bottom, 0)"); + docRoot.style.setProperty("--safe-area-inset-left", "constant(safe-area-inset-left, 0)"); + } else if (CSS?.supports("top: env(safe-area-inset-top)")) { + docRoot.style.setProperty("--safe-area-inset-top", "env(safe-area-inset-top, 0)"); + docRoot.style.setProperty("--safe-area-inset-right", "env(safe-area-inset-right, 0)"); + docRoot.style.setProperty("--safe-area-inset-bottom", "env(safe-area-inset-bottom, 0)"); + docRoot.style.setProperty("--safe-area-inset-left", "env(safe-area-inset-left, 0)"); + } + }); + + window.addEventListener("load", () => { + document.body.addEventListener("click", (evt) => { + const eleDice = evt.target.hasAttribute("data-packed-dice") + ? evt.target + // Tolerate e.g. Bestiary wrapped proficiency dice rollers + : evt.target.parentElement?.hasAttribute("data-packed-dice") + ? evt.target.parentElement + : null; + + if (!eleDice) return; + + evt.preventDefault(); + evt.stopImmediatePropagation(); + Renderer.dice.pRollerClickUseData(evt, eleDice).then(null); + }); + Renderer.events.bindGeneric(); + }); + + if (location.origin === VeCt.LOC_ORIGIN_CANCER) { + const ivsCancer = []; + + window.addEventListener("load", () => { + let isPadded = false; + let anyFound = false; + [ + "div-gpt-ad-5etools35927", // main banner + "div-gpt-ad-5etools35930", // side banner + "div-gpt-ad-5etools35928", // sidebar top + "div-gpt-ad-5etools35929", // sidebar bottom + "div-gpt-ad-5etools36159", // bottom floater + "div-gpt-ad-5etools36834", // mobile middle + ].forEach(id => { + const iv = setInterval(() => { + const $wrp = $(`#${id}`); + if (!$wrp.length) return; + if (!$wrp.children().length) return; + if ($wrp.children()[0].tagName === "SCRIPT") return; + const $tgt = $wrp.closest(".cancer__anchor").find(".cancer__disp-cancer"); + if ($tgt.length) { + anyFound = true; + $tgt.css({display: "flex"}).text("Advertisements"); + clearInterval(iv); + } + }, 250); + + ivsCancer.push(iv); + }); + + const ivPad = setInterval(() => { + if (!anyFound) return; + if (isPadded) return; + isPadded = true; + // Pad the bottom of the page so the adhesive unit doesn't overlap the content + $(`.view-col-group--cancer`).append(`
            `); + }, 300); + ivsCancer.push(ivPad); + }); + + // Hack to lock the ad space at original size--prevents the screen from shifting around once loaded + setTimeout(() => { + const $wrp = $(`.cancer__wrp-leaderboard-inner`); + const h = $wrp.outerHeight(); + $wrp.css({height: h}); + ivsCancer.forEach(iv => clearInterval(iv)); + }, 5000); + } else { + window.addEventListener("load", () => $(`.cancer__anchor`).remove()); + } + + // window.addEventListener("load", () => { + // $(`.cancer__sidebar-rhs-inner--top`).append(`
            `) + // $(`.cancer__sidebar-rhs-inner--bottom`).append(`
            `) + // }); + + // TODO(img) remove this in future + window.addEventListener("load", () => { + if (window.location?.host !== "5etools-mirror-1.github.io") return; + + JqueryUtil.doToast({ + type: "warning", + isAutoHide: false, + content: $(`
            This mirror is no longer being updated/maintained, and will be shut down on March 1st 2024.
            Please use 5etools-mirror-2.github.io instead, and migrate your data.
            `), + }); + }); +} \ No newline at end of file diff --git a/js/navigation.js b/js/navigation.js index 3f966f4..cc2e314 100644 --- a/js/navigation.js +++ b/js/navigation.js @@ -108,6 +108,7 @@ class NavBar { this._addElement_divider(NavBar._CAT_UTILITIES); this._addElement_li(NavBar._CAT_UTILITIES, "renderdemo.html", "Renderer Demo"); this._addElement_li(NavBar._CAT_UTILITIES, "makecards.html", "RPG Cards JSON Builder"); + this._addElement_li(NavBar._CAT_UTILITIES, "charbuilder.html", "Character Builder"); this._addElement_li(NavBar._CAT_UTILITIES, "converter.html", "Text Converter"); this._addElement_divider(NavBar._CAT_UTILITIES); this._addElement_li(NavBar._CAT_UTILITIES, "plutonium.html", "Plutonium (Foundry Module) Features"); diff --git a/js/utils.js b/js/utils.js index 9f9eca5..32af1f7 100644 --- a/js/utils.js +++ b/js/utils.js @@ -2864,6 +2864,7 @@ UrlUtil.PG_DM_SCREEN = "dmscreen.html"; UrlUtil.PG_CR_CALCULATOR = "crcalculator.html"; UrlUtil.PG_ENCOUNTERGEN = "encountergen.html"; UrlUtil.PG_LOOTGEN = "lootgen.html"; +UrlUtil.PG_CHAR_BUILDER = "charbuilder.html"; UrlUtil.PG_TEXT_CONVERTER = "converter.html"; UrlUtil.PG_CHANGELOG = "changelog.html"; UrlUtil.PG_CHAR_CREATION_OPTIONS = "charcreationoptions.html";