Fix actions/tour

This commit is contained in:
2025-02-13 23:19:32 +01:00
parent 47dd1adb30
commit fa21d30994
4543 changed files with 680810 additions and 0 deletions

0
LICENSE Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
assets/fonts/FeFCrm2.ttf Normal file

Binary file not shown.

BIN
assets/fonts/FeGPrm2.ttf Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
assets/fonts/IMFell.ttf Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
assets/fonts/Seabreed.ttf Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
assets/fonts/Top-Secret.ttf Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
assets/fonts/armalite.ttf Normal file

Binary file not shown.

BIN
assets/fonts/broadw.ttf Normal file

Binary file not shown.

BIN
assets/fonts/broadway.woff2 Normal file

Binary file not shown.

BIN
assets/fonts/dominican.ttf Normal file

Binary file not shown.

BIN
assets/fonts/georama.woff2 Normal file

Binary file not shown.

View File

@@ -0,0 +1 @@
<svg style="height: 512px; width: 512px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><defs><filter id="shadow-1" height="300%" width="300%" x="-100%" y="-100%"><feFlood flood-color="rgba(255, 255, 255, 1)" result="flood"></feFlood><feComposite in="flood" in2="SourceGraphic" operator="atop" result="composite"></feComposite><feGaussianBlur in="composite" stdDeviation="15" result="blur"></feGaussianBlur><feOffset dx="0" dy="0" result="offset"></feOffset><feComposite in="SourceGraphic" in2="offset" operator="over"></feComposite></filter></defs><g class="" style="" transform="translate(0,0)"><path d="M245.813 23.188c-1.228-.006-2.455.027-3.657.093-10.103.56-19.646 3.682-30.156 11.25l20.72 196.782c-8.394 2.127-16.676 4.47-24.814 7.094L137.72 57.812c-7.032-1.706-17.442-.3-27.126 4.626-10.248 5.213-19.034 13.84-22.813 22.937L155.03 261.5c-7.414 4.345-14.59 9.137-21.5 14.47l-74.343-94.25c-16.34.698-34.965 14.455-37.562 32.655C28.89 222.693 93.978 297.77 126 357.405c10.3 19.184 29.543 50.725 39.188 70.064 5.83 11.693 16.004 24.238 27.843 32.342 11.84 8.104 24.7 11.82 37.907 8.282l112.907-30.22c5.493-1.47 9.196-5.39 13.22-11.937 4.02-6.545 7.535-15.137 12.905-23 20.61-30.185 50.432-76.085 115.186-112.062-2.696-15.053-7.405-24.57-12.72-29.563-6.03-5.667-13.198-7.372-23.686-5.843-18.062 2.63-43.498 17.063-69.594 36.874-1.68 1.39-3.318 2.802-4.937 4.22l-7-61.252 42.5-155.718c-4.478-7.355-13.806-13.258-24.845-15.97-10.874-2.67-22.506-1.698-30.28 1.595l-38.75 149.874c-9.365 1.58-18.732 3.17-28.064 4.812L273.69 27.5c-10.057-2.52-19.284-4.272-27.875-4.313zM234.343 255l30.157 56.625 54.406-33.906-33.78 54.186L341.562 362l-64.157-2.188 2.188 64.032-30.03-56.344-54.283 33.813 33.97-54.438-56.53-30.125 63.78 2.156L234.344 255z" fill="#a7de9a" fill-opacity="1" filter="url(#shadow-1)"></path></g></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1 @@
<svg style="height: 512px; width: 512px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><defs><filter id="shadow-1" height="300%" width="300%" x="-100%" y="-100%"><feFlood flood-color="rgba(255, 255, 255, 1)" result="flood"></feFlood><feComposite in="flood" in2="SourceGraphic" operator="atop" result="composite"></feComposite><feGaussianBlur in="composite" stdDeviation="15" result="blur"></feGaussianBlur><feOffset dx="0" dy="0" result="offset"></feOffset><feComposite in="SourceGraphic" in2="offset" operator="over"></feComposite></filter></defs><g class="" style="" transform="translate(0,0)"><path d="M89 25v462h334V25H89zm30 30h274v402H119V55zm18 18v94h49.8c1-10.8 2.3-22 4.4-34 6.4-21 24.8-43.28 45-47.7 3.9-.95 8.4 1.48 12.6 1.4 2.7 0 13.4-2.68 15.3-2.8 30.8.81 55.3 33.7 59.3 60.3.6 4.5 2 12.7 3.6 22.8h48V73H137zm112.9 31.3c-9.9 0-19.3 5.7-26.9 16.6-7.5 10.9-12.6 26.7-12.6 44.3 0 17.6 5.1 33.4 12.6 44.3 7.6 10.9 17 16.6 26.9 16.6 9.9 0 19.3-5.7 26.9-16.6 7.5-10.9 12.6-26.7 12.6-44.3 0-17.6-5.1-33.4-12.6-44.3-7.6-10.9-17-16.6-26.9-16.6zM137 185v132.8c7.6-16.4 30-32.3 35.4-46 10.6-26.8 11-54.5 13.1-86.8H137zm193 0c3.5 22.9 7.9 46.9 9.9 69.3 14.7 9.4 27.1 21.6 35.1 35.5V185h-45zm-52.7 49.7c-8 5.9-17.3 9.4-27.4 9.4-3.6 0-7.1-.5-10.5-1.3-4.1 6.7-7.8 13.9-10.9 22.1-5 12.9-17.2 19.1-27.7 26.3-7.7 7.4-25.4 14.3-18.4 27.4 9.7 12.9 37.8 14.2 50.8 14.1 19.3-2.3 44.6-1.5 59-14.1l-14.9-83.9zm-60.1 124l-5.7 17.8 59.2 32.2 9.9-28.1c-12.6-12.3-36.5-17.9-63.4-21.9zm-15.6 44c-4.7 1.3-9.6 2.9-13.7 4.3-2.9 8.5-.5 18 1.7 29.8 22.3 3 37.9-8.3 54.6-18.5l-42.6-15.6z" fill="#a7de9a" fill-opacity="1" filter="url(#shadow-1)"></path></g></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1 @@
<svg style="height: 512px; width: 512px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><defs><filter id="shadow-1" height="300%" width="300%" x="-100%" y="-100%"><feFlood flood-color="rgba(255, 255, 255, 1)" result="flood"></feFlood><feComposite in="flood" in2="SourceGraphic" operator="atop" result="composite"></feComposite><feGaussianBlur in="composite" stdDeviation="15" result="blur"></feGaussianBlur><feOffset dx="0" dy="0" result="offset"></feOffset><feComposite in="SourceGraphic" in2="offset" operator="over"></feComposite></filter></defs><g class="" style="" transform="translate(0,0)"><path d="M208 56.643l-16 64-98.568 14.082L256 175.365l162.568-40.64L320 120.643l-16-64-27.268 18.18-12.002 48.003h-17.46l-12.002-48.004zm-138.621 90.62L16 200.644l48 64 25.77-25.77 26.619-79.857zm373.242 0l-47.01 11.753 26.62 79.857L448 264.643l48-64zm-308.717 16.132l-20.123 60.369 13.81 55.246L247 345.348V191.67zm244.192 0L265 191.67v153.678l119.408-66.338 13.81-55.246zM144 308.715v56.314l103 30.627v-29.719zm224 0l-103 57.223v29.718l103-30.627zm-224 75.54v56.388l103 14.714V414.88zm224 0L265 414.88v40.478l103-14.714z" fill="#a7de9a" fill-opacity="1" filter="url(#shadow-1)"></path></g></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1 @@
<svg style="height: 512px; width: 512px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><defs><filter id="shadow-1" height="300%" width="300%" x="-100%" y="-100%"><feFlood flood-color="rgba(255, 255, 255, 1)" result="flood"></feFlood><feComposite in="flood" in2="SourceGraphic" operator="atop" result="composite"></feComposite><feGaussianBlur in="composite" stdDeviation="15" result="blur"></feGaussianBlur><feOffset dx="0" dy="0" result="offset"></feOffset><feComposite in="SourceGraphic" in2="offset" operator="over"></feComposite></filter></defs><g class="" style="" transform="translate(0,0)"><path d="M321.7 21.36c-43.2 0-86.4 16.5-119.4 49.5-19.1 19.08-32.6 41.54-40.7 65.44 16.9-2.4 32.9-2.7 48.7-1.1 3.9-5.5 8.3-10.7 13.2-15.6 23.3-23.26 53.8-34.9 84.4-34.9 30.6 0 61.2 11.64 84.5 34.9 46.6 46.6 46.6 122.4 0 168.9-46.5 46.6-122.4 46.6-168.9 0-22.2-22.2-33.9-51.1-34.9-80.2-11.5 1.8-22.8 5.6-33.2 11.4 5.8 33 21.4 64.5 46.9 90 66 66 172.9 66.1 238.9 0 66-66 66-172.8 0-238.84-33-33-76.3-49.5-119.5-49.5zM147.6 158.2c-27.9 7.7-58.94 25.4-76.75 44-47.5 47.4-60.8 116-40.1 175.3 8.91 24.1 23.56 47.1 40.1 63.6 66.05 66 172.95 66 238.95 0 19.1-19.1 32.6-41.6 40.7-65.5-16.2 2.5-32.6 2.9-48.8 1.2-3.8 5.4-8.2 10.6-13.1 15.5-62.7 39.7-137.8 40.6-173.3-4.4-20.57-26-32.05-58.8-30.55-85.8 2.58-41.6 26.85-79.9 57.75-98.5 10.2-5.9 37.6-15.1 61.6-15.1 33.7 1.5 60.6 11.1 84.5 34.9 22.3 22.1 33.8 51.1 34.8 80.3 11.6-1.8 22.9-5.6 33.3-11.4-9.4-41.6-26.9-73.2-53.9-96.7-21.4-18.7-44.1-31.4-70.6-37.6-28.4-7-58.6-6.5-84.6.2z" fill="#a7de9a" fill-opacity="1" filter="url(#shadow-1)"></path></g></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1 @@
<svg style="height: 512px; width: 512px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><defs><filter id="shadow-1" height="300%" width="300%" x="-100%" y="-100%"><feFlood flood-color="rgba(255, 255, 255, 1)" result="flood"></feFlood><feComposite in="flood" in2="SourceGraphic" operator="atop" result="composite"></feComposite><feGaussianBlur in="composite" stdDeviation="15" result="blur"></feGaussianBlur><feOffset dx="0" dy="0" result="offset"></feOffset><feComposite in="SourceGraphic" in2="offset" operator="over"></feComposite></filter></defs><g class="" style="" transform="translate(0,0)"><path d="M186.438 20.56l-13.184 26.365c6.8-.26 13.626-.488 20.47-.686l3.84-7.68h116.874l3.77 7.54c6.838.187 13.658.408 20.456.66l-13.102-26.2H186.437zm69.56 42.742c-45.757.056-91.452 1.566-135.38 4.363-3.24 50.58-8.4 100.987-.786 145.824 89.297 12.395 180.102 12.985 272.764-.054 7.055-30.988 5.117-84.68-1.04-145.89-43.974-2.893-89.73-4.3-135.558-4.244zm153.783 5.54c6.42 64.12 9.113 119.825-1.135 155.22l-1.61 5.56-5.726.842c-98.8 14.528-195.613 13.81-290.605.002l-6.285-.914-1.246-6.23c-9.89-49.49-4.085-102.785-.664-154.42-4.89.354-9.765.72-14.602 1.107-8.596 58.568-9.39 116.957-.05 175.292 110.24 12.088 222.275 12.205 336.203-.01 8.502-57.83 8.29-116.25-.017-175.313-4.725-.4-9.485-.776-14.262-1.14zM255.966 92.3c32.526-.025 65.067 2.746 97.574 8.39l7.46 1.295v7.572c0 15.554 1.683 35.105-12.69 50.25-9.912 10.444-25.655 17.337-51.31 20.585v18.164h-82v-18.452c-23.992-3.37-39.352-10.175-49.363-20.185C150.807 145.093 151 125.56 151 109.56v-7.594l7.484-1.278c32.444-5.54 64.955-8.362 97.48-8.386zm.012 17.994c-28.96.022-57.913 2.444-86.858 6.996.265 12.28 1.635 22.296 9.243 29.904 5.914 5.914 16.952 11.416 36.637 14.582v-29.22h82v29.51c21.367-3.115 32.66-8.755 38.254-14.65 7.033-7.41 7.696-17.502 7.73-30.124-29-4.63-58.006-7.02-87.007-6.998zM233 150.56v30h46v-30h-46zm209.674 92.42c-.503 3.625-1.042 7.25-1.61 10.87.214 2.352.42 4.706.63 7.06L471 290.213v-22.24l-28.326-24.995zm-373.485.12L41 267.973v22.24l29.318-29.318c.205-2.327.406-4.655.616-6.982-.618-3.605-1.202-7.21-1.745-10.813zm354.634 20.397c-10.29 1.09-20.564 2.076-30.824 2.967v74.095h16v66h-16v80.615c10.318-.633 20.63-1.313 30.928-2.082 9.445-74.01 6.478-147.698-.104-221.596zm-335.576.03C81.725 338.09 78.58 412.1 88.06 485.1c10.324.79 20.638 1.504 30.94 2.145V406.56h-16v-66h16v-74.024c-10.266-.902-20.517-1.903-30.752-3.01zm286.752 4.4c-10.014.76-20.014 1.424-30 1.992v70.64h30v-72.632zm-238 .085v72.547h30v-70.55c-10.015-.568-20.014-1.237-30-1.997zm190 2.825c-47.65 2.173-94.984 2.19-142 .078v19.314c23.95-5.165 47.8-7.652 71.516-7.59 23.638.06 47.145 2.654 70.484 7.626v-19.43zM68.05 288.62L41 315.67v56.89h23.06c.376-27.987 1.88-55.975 3.99-83.94zm375.948.047c2.12 27.872 3.61 55.83 3.957 83.892H471v-56.89l-27.002-27.003zm-187.52 11.95c-23.68-.063-47.487 2.577-71.478 8.052v31.89h16v18.443c17.033 5.346 31.73 8.493 46 9.426v-2.87h18v2.868c14.27-.932 28.967-4.08 46-9.425V340.56h16v-31.866c-23.42-5.267-46.907-8.016-70.523-8.078zM121 358.558v30h22v-23h18v23h22v-30h-62zm208 0v30h22v-23h18v23h22v-30h-62zM201 377.8v28.76h-16v15.857c48.528 10.865 95.713 10.664 142 .045V406.56h-16V377.8c-16.332 4.747-31.283 7.52-46 8.326v11.433h-18v-11.434c-14.717-.806-29.668-3.58-46-8.326zM41 390.56v14h23.14c-.09-4.667-.143-9.334-.163-14H41zm407.012 0c-.027 4.663-.083 9.33-.18 14H471v-14h-22.988zM137 406.56v19.798c6.137 7.214 11.222 9.77 14.934 9.844 3.734.075 8.697-2.122 15.066-9.79V406.56h-6v7h-18v-7h-6zm208 0v19.798c6.137 7.214 11.222 9.77 14.934 9.844 3.734.075 8.697-2.122 15.066-9.79V406.56h-6v7h-18v-7h-6zm-304 16v35.154c5.596 5.51 8.677 8.25 11.846 9.306 2.454.818 7.713 1.15 15.045 1.317-1.544-15.25-2.586-30.51-3.204-45.778H41zm406.27 0c-.628 15.224-1.674 30.483-3.21 45.78 7.358-.168 12.635-.5 15.094-1.32 3.17-1.056 6.25-3.795 11.846-9.306V422.56h-23.73zM185 440.842v49.498c47.55 1.51 94.877 1.446 142-.074V440.9c-46.316 10.03-93.74 10.185-142-.057zm-48 9.123v38.318c10.01.54 20.01 1.008 30 1.408v-39.678c-4.86 2.786-10.01 4.293-15.43 4.184-5.192-.104-10.036-1.624-14.57-4.232zm208 0v39.654c10.01-.403 20.01-.878 30-1.412v-38.194c-4.86 2.786-10.01 4.293-15.43 4.184-5.192-.104-10.036-1.624-14.57-4.232z" fill="#a7de9a" fill-opacity="1" filter="url(#shadow-1)"></path></g></svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -0,0 +1 @@
<svg style="height: 512px; width: 512px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><defs><filter id="shadow-1" height="300%" width="300%" x="-100%" y="-100%"><feFlood flood-color="rgba(255, 255, 255, 1)" result="flood"></feFlood><feComposite in="flood" in2="SourceGraphic" operator="atop" result="composite"></feComposite><feGaussianBlur in="composite" stdDeviation="15" result="blur"></feGaussianBlur><feOffset dx="0" dy="0" result="offset"></feOffset><feComposite in="SourceGraphic" in2="offset" operator="over"></feComposite></filter></defs><g class="" style="" transform="translate(0,0)"><path d="M227.227 21.777c-1.845 0-3.704.05-5.567.157-15.314.875-30.76 5.305-39.494 10.863l-.008 73.15c2.884-.094 5.777-.147 8.676-.142 23.382.036 47.104 3.286 68.47 9.513l.01-87.507c-7.034-3.518-19.178-6.03-32.087-6.033zm80.74 9.16c-11.925.15-23.077 2.364-29.967 5.596l-.008 77.602v7.658c38.486 15.67 64.814 42.48 58.735 78.764l-.96 5.73-5.562 1.674c-17.45 5.253-34.872 9.703-52.225 13.335V246.53c25.562-.704 51.327-2.687 77.145-6.098l.02-197.928c-8.284-5.563-23.508-10.243-38.842-11.328-2.792-.198-5.584-.273-8.336-.238zM143.223 46.294c-1.176-.015-2.374-.01-3.588.02-4.175.1-8.533.468-12.903 1.152-15.67 2.454-31.477 8.565-40.406 15.402l-.01 72.955c18.808-15.81 46.704-25.143 77.15-28.54l.007-57.966c-4.82-1.752-12.018-2.916-20.25-3.023zm258.394 3.46c-10.804.117-20.722 1.93-27.043 4.655l-.02 183.182c25.074-4.02 50.16-9.412 75.122-16.358l1.99-158.447c-8.352-5.9-23.648-11.025-39.05-12.553-3.698-.366-7.398-.517-11-.478zm-222.775 74.202c-53.72.702-101.407 20.365-97.887 66.6 15.836-3.918 30.84-5.893 44.94-6.1 34.84-.51 64.213 9.704 87.318 27.613 34.608-3.11 69.852-10 105.412-20.314.14-41.287-74.098-68.657-139.783-67.8zm-48.877 78.65c-1.296-.003-2.603.012-3.92.045-17.256.436-36.45 4.03-57.566 11.037 5.79 53.808 26.325 106.41 58.5 143.346 6.226 7.15 12.856 13.712 19.875 19.615 29.303 9.282 69.26 12.917 110.534 12.14 3.777-55.805-8.717-108.357-36.193-142.74-21.265-26.61-51.064-43.39-91.232-43.444zm129.326 22.282c-9.358 1.637-18.69 3.016-27.995 4.15 1.54 1.74 3.043 3.52 4.502 5.346 3.146 3.937 6.094 8.062 8.873 12.334 9.916.144 19.868.125 29.857-.106H259.29v-21.723zm191.817 15.343c-65.406 17.826-131.462 25.41-195.85 25.315 16.998 35.144 23.828 78.093 21.013 122.6 42.482-2.08 85.03-8.23 118.187-15.983 26.693-32.78 47.37-77.118 56.65-131.932zM400.51 389.9c-38.334 9.145-87.95 16.056-136.873 17.454-47.67 1.36-94.336-2.228-129.448-15.262l-.01 78.93c27.187 12.568 76.414 20.205 127.318 20.298 51.224.094 104.214-7.173 139-20.773l.012-80.647z" fill="#a7de9a" fill-opacity="1" filter="url(#shadow-1)"></path></g></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -0,0 +1 @@
<svg style="height: 512px; width: 512px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><defs><filter id="shadow-1" height="300%" width="300%" x="-100%" y="-100%"><feFlood flood-color="rgba(255, 255, 255, 1)" result="flood"></feFlood><feComposite in="flood" in2="SourceGraphic" operator="atop" result="composite"></feComposite><feGaussianBlur in="composite" stdDeviation="15" result="blur"></feGaussianBlur><feOffset dx="0" dy="0" result="offset"></feOffset><feComposite in="SourceGraphic" in2="offset" operator="over"></feComposite></filter></defs><g class="" style="" transform="translate(0,0)"><path d="M256.3 21.84c-14.9 0-28.8 8.28-39.5 23.08-10.7 14.8-17.6 35.93-17.6 59.48 0 23.5 6.9 44.6 17.6 59.4 10.7 14.9 24.6 23.1 39.5 23.1 14.9 0 28.8-8.2 39.5-23.1 10.7-14.8 17.6-35.9 17.6-59.4 0-23.55-6.9-44.68-17.6-59.48-10.7-14.8-24.6-23.08-39.5-23.08zm51.3 156.06c-13 16.4-31.1 27-51.3 27-20.2 0-38.2-10.6-51.3-26.9-4.4.8-8.7 1.6-13.1 2.6 24 67.4 41.1 115.7 75.7 164.8 10.7-1.4 23.4-2.9 40.3-5l3.8-40.4c.8-8.7 5.6-15.7 12-20.6l-6.4-28.6 17.6-4 5.6 25.1c6.3-1.5 12.9-1.9 19.6-1.7 10.7.4 21.4 2.6 30.8 6.7-1.9-23.5-6.7-48.7-10.7-76.2-3.3-2.3-9.6-5.6-17.7-8.5-10.1-3.7-22.7-7.3-35.9-10.3-6.3-1.4-12.7-2.8-19-4zm-133.1 6.8c-5.5 1.5-10.8 3-15.8 4.6 12.6 45.5 50.4 172.7 101.8 245.6 20.5-.1 40.4-1.1 60-2.8-87.6-83.4-110-146.3-146-247.4zm-32.6 10.8c-4.5 2.1-8 4.1-10 5.6-24.1 80.3-31.2 194-16.7 289.1h49.1c-6.4-65.2-12.5-139.1 9.3-194.2-15.1-41.8-25.8-79.2-31.7-100.5zm214.4 92.6c-7.4.1-14.2 1.4-18.8 3.7-5.3 2.6-7.5 5.2-8 10l-5.2 54.6-7.1.9c-15.5 1.9-27.3 3.4-37.3 4.6 16.9 21.5 37.6 43.5 64.2 67.8 5-.6 10-1.2 15-1.9 10.2-1.3 16.8-5.5 22.4-11.7 5.7-6.1 9.9-14.5 13.5-23.2 14-34.2 8.7-74.5 2-89-1.4-3.2-6-7.4-13-10.4-6.9-3.1-15.9-5.1-24.5-5.4zm-172.5 35c-4 16.2-6 34.1-6.8 52.9 5.9-5.5 13.5-10.5 22.7-14.7-5.6-12.6-11-25.5-15.9-38.2zm23.5 54.5c-20.5 9.2-28.2 21.7-28.6 31-.6 11.7 8.6 23 30 24.6 10.2.7 20.2 1.2 30 1.5-11.3-17.4-21.8-36.9-31.4-57.1zm-29.1 63.5c1 16.5 2.5 33 4.1 49.1h162.3c4-15.1 8.4-29.6 12.8-44.1-47.5 6.1-96.4 8.9-150.1 4.9-11.4-.9-21.3-4.5-29.1-9.9z" fill="#a7de9a" fill-opacity="1" filter="url(#shadow-1)"></path></g></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -0,0 +1 @@
<svg style="height: 512px; width: 512px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><defs><filter id="shadow-1" height="300%" width="300%" x="-100%" y="-100%"><feFlood flood-color="rgba(255, 255, 255, 1)" result="flood"></feFlood><feComposite in="flood" in2="SourceGraphic" operator="atop" result="composite"></feComposite><feGaussianBlur in="composite" stdDeviation="15" result="blur"></feGaussianBlur><feOffset dx="0" dy="0" result="offset"></feOffset><feComposite in="SourceGraphic" in2="offset" operator="over"></feComposite></filter></defs><g class="" style="" transform="translate(0,0)"><path d="M311.406 34.53c38.542 34.197 66.87 98.28 74.875 174.72-11.743-12.09-28.16-19.625-46.342-19.625-31.736 0-58.082 22.898-63.532 53.063-8.41-4.385-17.872-6.64-27.344-6.72-8.31-.07-16.654 1.55-24.312 4.876-6.17-29.252-32.13-51.22-63.22-51.22-22.542 0-42.38 11.558-53.936 29.064C113.944 141.055 141.15 75.113 179 38.563c-19.415 11.684-37.058 28.147-52.156 48.5-31.764 42.817-51.75 102.623-51.75 168.875 0 66.25 19.986 126.057 51.75 168.875 18.93 25.516 41.84 44.93 67.25 56.468-45.36-32.216-78.958-104.326-86.375-191.28 11.57 17.39 31.35 28.844 53.81 28.844 33.153 0 60.45-24.968 64.157-57.125 15.126-10.57 37.57-8.8 50.657 3.81 5.34 30.3 31.764 53.314 63.594 53.314 18.094 0 34.454-7.425 46.187-19.406-9.26 85.518-43.967 155.398-89.906 184.875 28.267-10.987 53.744-31.607 74.436-59.5 31.764-42.818 51.78-102.624 51.78-168.875.002-66.252-20.016-126.058-51.78-168.875-16.92-22.81-37.022-40.748-59.25-52.532zM139.186 361.69c54.808 94.924 164.16 94.283 218.595 0-61.404 35.452-146.178 34.58-218.592 0z" fill="#a7de9a" fill-opacity="1" filter="url(#shadow-1)"></path></g></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1 @@
<svg style="height: 512px; width: 512px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><defs><filter id="shadow-1" height="300%" width="300%" x="-100%" y="-100%"><feFlood flood-color="rgba(255, 255, 255, 1)" result="flood"></feFlood><feComposite in="flood" in2="SourceGraphic" operator="atop" result="composite"></feComposite><feGaussianBlur in="composite" stdDeviation="15" result="blur"></feGaussianBlur><feOffset dx="0" dy="0" result="offset"></feOffset><feComposite in="SourceGraphic" in2="offset" operator="over"></feComposite></filter></defs><g class="" style="" transform="translate(0,0)"><path d="M340.53 16.47l-19.25 89.374L391.94 16.47H340.53zm151.283 18.686L372.063 167.75l119.75-47.906V35.156zM185.375 80.25c-.652.01-1.293.034-1.938.063-9.51.422-18.37 2.635-25.687 7.593-16.964 11.492-11.295 37.156-22.78 63.094C95.73 239.616 56.09 303.885 21.062 351.313v103.375c61.582-21.345 153.303-43.464 287.343-47.907 28.125-.93 49.728 12.582 66.594 1.158 41.638-28.207 26.563-122.468-33.28-210.813-3.74-5.52-7.224-11.14-11.126-16.344-46.8-62.426-104.125-101.083-145.22-100.53zM180 104.75c-6.756 13.893-.412 43.86 15.938 80.75-5.16-18.626-4.688-32.142 2.687-36.625 16.125-9.802 59.054 27.175 95.875 82.594 36.82 55.417 53.593 108.29 37.47 118.093-6.634 4.032-17.81.138-31.22-9.688 27.717 28.092 52.734 44.51 68.094 43.656-1.785 3.17-3.675 6.244-6.5 8.158-26.9 18.225-91.755-30.25-144.656-108.344-52.902-78.094-73.87-156.34-46.97-174.563 2.757-1.866 5.784-3.525 9.282-4.03zm311.813 115.563l-84.688 27.437 84.688 12.844v-40.28z" fill="#a7de9a" fill-opacity="1" filter="url(#shadow-1)"></path></g></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1 @@
<svg style="height: 512px; width: 512px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><defs><filter id="shadow-1" height="300%" width="300%" x="-100%" y="-100%"><feFlood flood-color="rgba(255, 255, 255, 1)" result="flood"></feFlood><feComposite in="flood" in2="SourceGraphic" operator="atop" result="composite"></feComposite><feGaussianBlur in="composite" stdDeviation="15" result="blur"></feGaussianBlur><feOffset dx="0" dy="0" result="offset"></feOffset><feComposite in="SourceGraphic" in2="offset" operator="over"></feComposite></filter></defs><g class="" style="" transform="translate(0,0)"><path d="M245.813 23.188c-1.228-.006-2.455.027-3.657.093-10.103.56-19.646 3.682-30.156 11.25l20.72 196.782c-8.394 2.127-16.676 4.47-24.814 7.094L137.72 57.812c-7.032-1.706-17.442-.3-27.126 4.626-10.248 5.213-19.034 13.84-22.813 22.937L155.03 261.5c-7.414 4.345-14.59 9.137-21.5 14.47l-74.343-94.25c-16.34.698-34.965 14.455-37.562 32.655C28.89 222.693 93.978 297.77 126 357.405c10.3 19.184 29.543 50.725 39.188 70.064 5.83 11.693 16.004 24.238 27.843 32.342 11.84 8.104 24.7 11.82 37.907 8.282l112.907-30.22c5.493-1.47 9.196-5.39 13.22-11.937 4.02-6.545 7.535-15.137 12.905-23 20.61-30.185 50.432-76.085 115.186-112.062-2.696-15.053-7.405-24.57-12.72-29.563-6.03-5.667-13.198-7.372-23.686-5.843-18.062 2.63-43.498 17.063-69.594 36.874-1.68 1.39-3.318 2.802-4.937 4.22l-7-61.252 42.5-155.718c-4.478-7.355-13.806-13.258-24.845-15.97-10.874-2.67-22.506-1.698-30.28 1.595l-38.75 149.874c-9.365 1.58-18.732 3.17-28.064 4.812L273.69 27.5c-10.057-2.52-19.284-4.272-27.875-4.313zM234.343 255l30.157 56.625 54.406-33.906-33.78 54.186L341.562 362l-64.157-2.188 2.188 64.032-30.03-56.344-54.283 33.813 33.97-54.438-56.53-30.125 63.78 2.156L234.344 255z" fill="#a7de9a" fill-opacity="1" filter="url(#shadow-1)"></path></g></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1 @@
<svg style="height: 512px; width: 512px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><defs><filter id="shadow-1" height="300%" width="300%" x="-100%" y="-100%"><feFlood flood-color="rgba(255, 255, 255, 1)" result="flood"></feFlood><feComposite in="flood" in2="SourceGraphic" operator="atop" result="composite"></feComposite><feGaussianBlur in="composite" stdDeviation="15" result="blur"></feGaussianBlur><feOffset dx="0" dy="0" result="offset"></feOffset><feComposite in="SourceGraphic" in2="offset" operator="over"></feComposite></filter></defs><g class="" style="" transform="translate(0,0)"><path d="M119.1 25v.1c-25 3.2-47.1 32-47.1 68.8 0 20.4 7.1 38.4 17.5 50.9L99.7 157 84 159.9c-13.7 2.6-23.8 9.9-32.2 21.5-8.5 11.5-14.9 27.5-19.4 45.8-8.2 33.6-9.9 74.7-10.1 110.5h44l11.9 158.4h96.3L185 337.7h41.9c0-36.2-.3-77.8-7.8-111.7-4-18.5-10.2-34.4-18.7-45.9-8.6-11.4-19.2-18.7-34.5-21l-16-2.5L160 144c10-12.5 16.7-30.2 16.7-50.1 0-39.2-24.8-68.8-52.4-68.8-2.9 0-4.7-.1-5.2-.1zM440 33c-17.2 0-31 13.77-31 31s13.8 31 31 31 31-13.77 31-31-13.8-31-31-31zM311 55v48H208v18h103v158h-55v18h55v110H208v18h103v32h80.8c-.5-2.9-.8-5.9-.8-9 0-3.1.3-6.1.8-9H329V297h62.8c-.5-2.9-.8-5.9-.8-9 0-3.1.3-6.1.8-9H329V73h62.8c-.5-2.92-.8-5.93-.8-9 0-3.07.3-6.08.8-9H311zm129 202c-17.2 0-31 13.8-31 31s13.8 31 31 31 31-13.8 31-31-13.8-31-31-31zm0 160c-17.2 0-31 13.8-31 31s13.8 31 31 31 31-13.8 31-31-13.8-31-31-31z" fill="#a7de9a" fill-opacity="1" filter="url(#shadow-1)"></path></g></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1 @@
<svg style="height: 512px; width: 512px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><defs><filter id="shadow-1" height="300%" width="300%" x="-100%" y="-100%"><feFlood flood-color="rgba(255, 255, 255, 1)" result="flood"></feFlood><feComposite in="flood" in2="SourceGraphic" operator="atop" result="composite"></feComposite><feGaussianBlur in="composite" stdDeviation="15" result="blur"></feGaussianBlur><feOffset dx="0" dy="0" result="offset"></feOffset><feComposite in="SourceGraphic" in2="offset" operator="over"></feComposite></filter></defs><g class="" style="" transform="translate(0,0)"><path d="M102.5 26.03l90.03 345.75 289.22 23.25-90.063-345.75L102.5 26.03zm-18.906 1.564c-30.466 11.873-55.68 53.098-49.75 75.312l3.25 11.78c.667-1.76 1.36-3.522 2.093-5.28C49.097 85.7 65.748 62.64 89.564 50.5l-5.97-22.906zm10.844 41.593c-16.657 10.012-29.92 28.077-38 47.407-5.247 12.55-8.038 25.63-8.75 36.53L112.5 388.407c.294-.55.572-1.106.875-1.656 10.603-19.252 27.823-37.695 51.125-48.47L94.437 69.19zm74.874 287.594c-17.677 9.078-31.145 23.717-39.562 39-4.464 8.107-7.27 16.364-8.688 23.75l11.688 42.408 1.625.125c-3.84-27.548 11.352-60.504 41.25-81.094l-6.313-24.19zm26.344 34c-32.567 17.27-46.51 52.44-41.844 72.94l289.844 24.5c-5.34-7.79-8.673-17.947-8.594-28.5l-22.406-9L459 443.436l-13.5-12.875c5.604-6.917 13.707-13.05 24.813-17.687L195.656 390.78z" fill="#a7de9a" fill-opacity="1" filter="url(#shadow-1)"></path></g></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1 @@
<svg style="height: 512px; width: 512px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><defs><filter id="shadow-1" height="300%" width="300%" x="-100%" y="-100%"><feFlood flood-color="rgba(255, 255, 255, 1)" result="flood"></feFlood><feComposite in="flood" in2="SourceGraphic" operator="atop" result="composite"></feComposite><feGaussianBlur in="composite" stdDeviation="15" result="blur"></feGaussianBlur><feOffset dx="0" dy="0" result="offset"></feOffset><feComposite in="SourceGraphic" in2="offset" operator="over"></feComposite></filter></defs><g class="" style="" transform="translate(0,0)"><path d="M19.75 14.438c59.538 112.29 142.51 202.35 232.28 292.718l3.626 3.75.063-.062c21.827 21.93 44.04 43.923 66.405 66.25-18.856 14.813-38.974 28.2-59.938 40.312l28.532 28.53 68.717-68.717c42.337 27.636 76.286 63.646 104.094 105.81l28.064-28.06c-42.47-27.493-79.74-60.206-106.03-103.876l68.936-68.938-28.53-28.53c-11.115 21.853-24.413 42.015-39.47 60.593-43.852-43.8-86.462-85.842-130.125-125.47-.224-.203-.432-.422-.656-.625C183.624 122.75 108.515 63.91 19.75 14.437zm471.875 0c-83.038 46.28-154.122 100.78-221.97 161.156l22.814 21.562 56.81-56.812 13.22 13.187-56.438 56.44 24.594 23.186c61.802-66.92 117.6-136.92 160.97-218.72zm-329.53 125.906l200.56 200.53c-4.36 4.443-8.84 8.793-13.405 13.032L148.875 153.53l13.22-13.186zm-76.69 113.28l-28.5 28.532 68.907 68.906c-26.29 43.673-63.53 76.414-106 103.907l28.063 28.06c27.807-42.164 61.758-78.174 104.094-105.81l68.718 68.717 28.53-28.53c-20.962-12.113-41.08-25.5-59.937-40.313 17.865-17.83 35.61-35.433 53.157-52.97l-24.843-25.655-55.47 55.467c-4.565-4.238-9.014-8.62-13.374-13.062l55.844-55.844-24.53-25.374c-18.28 17.856-36.602 36.06-55.158 54.594-15.068-18.587-28.38-38.758-39.5-60.625z" fill="#a7de9a" fill-opacity="1" filter="url(#shadow-1)"></path></g></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1 @@
<svg style="height: 512px; width: 512px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><defs><filter id="shadow-1" height="300%" width="300%" x="-100%" y="-100%"><feFlood flood-color="rgba(255, 255, 255, 1)" result="flood"></feFlood><feComposite in="flood" in2="SourceGraphic" operator="atop" result="composite"></feComposite><feGaussianBlur in="composite" stdDeviation="15" result="blur"></feGaussianBlur><feOffset dx="0" dy="0" result="offset"></feOffset><feComposite in="SourceGraphic" in2="offset" operator="over"></feComposite></filter></defs><g class="" style="" transform="translate(0,0)"><path d="M42.652 136.716v32.494a24.107 24.107 0 0 0 8.168 18.1h270.5v-50.406a7.302 7.302 0 0 0-7.146-5.978h-81.512a19.33 19.33 0 0 0-18.172-12.768h-58.948c6.208 25.003-12.71 49.193-38.472 49.193S72.39 143.16 78.6 118.16H61.136c-10.223.035-18.49 8.335-18.485 18.558zm409.04 1.554l2.65-3.38 7.896-11.474c3.297-4.768 8.94-4.768 12.236 0l7.615 14.802h8.835v29.344H338.01V138.27h113.703zM39.074 396.064c18.09 15.597 51.33 13.933 71.643 1.366 21.22-13.127 4.11-31.993 32.13-67.606 8.595-10.932 31.41-42.446 51.47-71.06 6.174-8.814 18.035-3.522 43.602 6.607 6.363 1.39 14.48 1.79 18.578 2.132 19.016 0 42.086-17.687 44.218-32.423 2.173-15.016-6.757-19.388 2.828-21.993 3.328-.905 9.26-5.045 9.67-9.067h-41.027c8.512 4.59 14.145 12.518 14.145 21.49 0 14.186-13.99 25.68-31.295 25.68-17.306 0-31.294-11.474-31.294-25.68 0-8.993 5.633-16.9 14.145-21.49H77.754c23.19 20.206 11.09 43.812 2.806 55.412l-52.158 73.24c-14.555 20.452-5.557 49.4 10.672 63.392zm21.14-38.28c0-11.868 14.347-17.81 22.74-9.418 8.39 8.39 2.448 22.74-9.42 22.74-7.357 0-13.32-5.964-13.32-13.32zm188.2-117.13c-9.727-7.012-7.78-25.204-5.018-36.613h12.8c-7.575 8.55-11.452 27.26-7.783 36.615zM140.008 127.64c0 20.447-24.72 30.684-39.177 16.227-14.456-14.456-4.22-39.176 16.227-39.176 12.674 0 22.95 10.276 22.95 22.95z" fill="#a7de9a" fill-opacity="1" filter="url(#shadow-1)"></path></g></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1 @@
<svg style="height: 512px; width: 512px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><defs><filter id="shadow-1" height="300%" width="300%" x="-100%" y="-100%"><feFlood flood-color="rgba(255, 255, 255, 1)" result="flood"></feFlood><feComposite in="flood" in2="SourceGraphic" operator="atop" result="composite"></feComposite><feGaussianBlur in="composite" stdDeviation="15" result="blur"></feGaussianBlur><feOffset dx="0" dy="0" result="offset"></feOffset><feComposite in="SourceGraphic" in2="offset" operator="over"></feComposite></filter></defs><g class="" style="" transform="translate(0,0)"><path d="M89.594 18.094l-10.75 10.75.03.03 27.532 333.563-83.03 92.938 33.25 33.25 90.155-80.563 336.19 24.907c.06.062.124.124.186.186l.156-.156h.032v-.03l10.562-10.564c-1.676-1.676-3.122-3.437-4.687-5.156-21.332-25.55-25.416-63.24-35.47-109.125-8.323-37.99-21.225-81.042-53.094-125.03l-38.062 50.81c.005.008-.005.026 0 .032 28.988 36.074 46.027 67.766 59.72 96.25 15.017 31.247 26.122 59 44.467 83.688L165.314 391.5 337.53 237.594l64.376-85.97-41.53-41.53-85.907 64.312L122.81 344.094 98.156 45.25c24.68 18.33 52.425 29.426 83.656 44.438 28.49 13.693 60.2 30.72 96.282 59.718l50.812-38.062c-43.99-31.86-87.04-44.736-125.03-53.063C157.987 48.224 120.3 44.113 94.75 22.78c-1.72-1.564-3.48-3.01-5.156-4.686zm317.03.312c-3.385.028-6.862.406-10.28.97-4.558.75-8.992 1.837-12.813 3.093-3.82 1.254-6.776 2.302-9.717 4.624a7.184 7.184 0 0 0-2.72 6.187l5.032 62.345a7.184 7.184 0 0 0 2.063 4.53l33.656 33.626a7.184 7.184 0 0 0 4.5 2.095l62.344 5.03a7.184 7.184 0 0 0 6.218-2.718c2.335-2.944 3.367-5.895 4.625-9.718 1.26-3.824 2.343-8.255 3.095-12.814.752-4.56 1.18-9.198.875-13.625-.305-4.425-1.012-8.99-4.844-12.81L422.78 23.343c-3.822-3.824-8.384-4.54-12.81-4.844-1.108-.076-2.216-.103-3.345-.094zm.126 14.375c.8-.006 1.563.016 2.25.064 2.404.165 3.74.915 3.72.78l65.655 65.657c-.138-.023.616 1.318.78 3.72.19 2.746-.062 6.526-.686 10.313-.626 3.786-1.595 7.62-2.595 10.656-.412 1.25-.524 1.272-.938 2.186l-54.78-4.375-29.938-29.936-4.376-54.813c.913-.41.94-.495 2.187-.905 3.037-.998 6.872-1.97 10.658-2.594 2.84-.466 5.662-.728 8.062-.75zm-47.97 120.44l-18.936 31.593-204.5 204.468-8.844-.655-1.188-14.563 201.875-201.906 31.594-18.937z" fill="#a7de9a" fill-opacity="1" filter="url(#shadow-1)"></path></g></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

2902
css/fvtt-ftl-nomad.css Normal file

File diff suppressed because it is too large Load Diff

219
eslint.config.mjs Normal file
View File

@@ -0,0 +1,219 @@
import jsdoc from 'eslint-plugin-jsdoc';
import prettier from 'eslint-plugin-prettier';
import configPrettier from 'eslint-config-prettier';
export default [
{
ignores: [
"node_modules/",
"eslint.config.mjs",
"build.mjs",
"gulpfile.js"
],
languageOptions: {
globals: {
browser: true,
es2022: true,
node: true,
jquery: true,
},
ecmaVersion: 2022,
sourceType: 'module',
},
plugins: {
jsdoc,
prettier
},
rules: {
'array-bracket-spacing': ['warn', 'never'],
'array-callback-return': 'warn',
'arrow-spacing': 'warn',
'comma-dangle': ['warn', 'never'],
'comma-style': 'warn',
'computed-property-spacing': 'warn',
'constructor-super': 'error',
'default-param-last': 'warn',
'dot-location': ['warn', 'property'],
'eol-last': ['error', 'always'],
'eqeqeq': ['warn', 'smart'],
'func-call-spacing': 'warn',
'func-names': ['warn', 'never'],
'getter-return': 'warn',
'lines-between-class-members': 'warn',
'new-parens': ['warn', 'always'],
'no-alert': 'warn',
'no-array-constructor': 'warn',
'no-class-assign': 'warn',
'no-compare-neg-zero': 'warn',
'no-cond-assign': 'warn',
'no-const-assign': 'error',
'no-constant-condition': 'warn',
'no-constructor-return': 'warn',
'no-delete-var': 'warn',
'no-dupe-args': 'warn',
'no-dupe-class-members': 'warn',
'no-dupe-keys': 'warn',
'no-duplicate-case': 'warn',
'no-duplicate-imports': ['warn', { includeExports: true }],
'no-empty': ['warn', { allowEmptyCatch: true }],
'no-empty-character-class': 'warn',
'no-empty-pattern': 'warn',
'no-func-assign': 'warn',
'no-global-assign': 'warn',
'no-implicit-coercion': ['warn', { allow: ['!!'] }],
'no-implied-eval': 'warn',
'no-import-assign': 'warn',
'no-invalid-regexp': 'warn',
'no-irregular-whitespace': 'warn',
'no-iterator': 'warn',
'no-lone-blocks': 'warn',
'no-lonely-if': 'off',
'no-loop-func': 'warn',
'no-misleading-character-class': 'warn',
'no-mixed-operators': 'warn',
'no-multi-str': 'warn',
'no-multiple-empty-lines': 'warn',
'no-new-func': 'warn',
'no-new-object': 'warn',
'no-new-symbol': 'warn',
'no-new-wrappers': 'warn',
'no-nonoctal-decimal-escape': 'warn',
'no-obj-calls': 'warn',
'no-octal': 'warn',
'no-octal-escape': 'warn',
'no-promise-executor-return': 'warn',
'no-proto': 'warn',
'no-regex-spaces': 'warn',
'no-script-url': 'warn',
'no-self-assign': 'warn',
'no-self-compare': 'warn',
'no-setter-return': 'warn',
'no-sequences': 'warn',
'no-template-curly-in-string': 'warn',
'no-this-before-super': 'error',
'no-unexpected-multiline': 'warn',
'no-unmodified-loop-condition': 'warn',
'no-unneeded-ternary': 'warn',
'no-unreachable': 'warn',
'no-unreachable-loop': 'warn',
'no-unsafe-negation': ['warn', { enforceForOrderingRelations: true }],
'no-unsafe-optional-chaining': ['warn', { disallowArithmeticOperators: true }],
'no-unused-expressions': 'warn',
'no-useless-backreference': 'warn',
'no-useless-call': 'warn',
'no-useless-catch': 'warn',
'no-useless-computed-key': ['warn', { enforceForClassMembers: true }],
'no-useless-concat': 'warn',
'no-useless-constructor': 'warn',
'no-useless-rename': 'warn',
'no-useless-return': 'warn',
'no-var': 'warn',
'no-void': 'warn',
'no-whitespace-before-property': 'warn',
'prefer-numeric-literals': 'warn',
'prefer-object-spread': 'warn',
'prefer-regex-literals': 'warn',
'prefer-spread': 'warn',
'rest-spread-spacing': ['warn', 'never'],
'semi-spacing': 'warn',
'semi-style': ['warn', 'last'],
'space-unary-ops': ['warn', { words: true, nonwords: false }],
'switch-colon-spacing': 'warn',
'symbol-description': 'warn',
'template-curly-spacing': ['warn', 'never'],
'unicode-bom': ['warn', 'never'],
'use-isnan': ['warn', { enforceForSwitchCase: true, enforceForIndexOf: true }],
'valid-typeof': ['warn', { requireStringLiterals: true }],
'wrap-iife': ['warn', 'inside'],
'arrow-parens': ['warn', 'as-needed', { requireForBlockBody: false }],
'capitalized-comments': ['warn', 'always', {
ignoreConsecutiveComments: true,
ignorePattern: 'noinspection',
}],
'comma-spacing': 'warn',
'dot-notation': 'warn',
indent: ['warn', 2, { SwitchCase: 1 }],
'key-spacing': 'warn',
'keyword-spacing': ['warn', { overrides: { catch: { before: true, after: false } } }],
'max-len': ['warn', {
code: 180,
ignoreTrailingComments: true,
ignoreUrls: true,
ignoreStrings: true,
ignoreTemplateLiterals: true,
}],
'no-extra-boolean-cast': ['warn', { enforceForLogicalOperands: true }],
'no-multi-spaces': ['warn', { ignoreEOLComments: true }],
'no-tabs': 'warn',
'no-throw-literal': 'error',
'no-trailing-spaces': 'warn',
'no-useless-escape': 'warn',
'nonblock-statement-body-position': ['warn', 'beside'],
'one-var': ['warn', 'never'],
'operator-linebreak': ['warn', 'before', {
overrides: { '=': 'after', '+=': 'after', '-=': 'after' },
}],
'prefer-template': 'warn',
'quote-props': ['warn', 'as-needed', { keywords: false }],
quotes: ['warn', 'double', { avoidEscape: true, allowTemplateLiterals: false }],
semi: ["error", "never"],
'spaced-comment': 'warn',
'jsdoc/check-access': 'warn',
'jsdoc/check-alignment': 'warn',
'jsdoc/check-examples': 'off',
'jsdoc/check-indentation': 'off',
'jsdoc/check-line-alignment': 'off',
'jsdoc/check-param-names': 'warn',
'jsdoc/check-property-names': 'warn',
'jsdoc/check-syntax': 'off',
'jsdoc/check-tag-names': ['warn', { definedTags: ['category'] }],
'jsdoc/check-types': 'warn',
'jsdoc/check-values': 'warn',
'jsdoc/empty-tags': 'warn',
'jsdoc/implements-on-classes': 'warn',
'jsdoc/match-description': 'off',
'jsdoc/newline-after-description': 'off',
'jsdoc/no-bad-blocks': 'warn',
'jsdoc/no-defaults': 'off',
'jsdoc/no-types': 'off',
'jsdoc/no-undefined-types': 'off',
'jsdoc/require-description': 'warn',
'jsdoc/require-description-complete-sentence': 'off',
'jsdoc/require-example': 'off',
'jsdoc/require-file-overview': 'off',
'jsdoc/require-hyphen-before-param-description': ['warn', 'never'],
'jsdoc/require-jsdoc': 'warn',
'jsdoc/require-param': 'warn',
'jsdoc/require-param-description': 'off',
'jsdoc/require-param-name': 'warn',
'jsdoc/require-param-type': 'warn',
'jsdoc/require-property': 'warn',
'jsdoc/require-property-description': 'off',
'jsdoc/require-property-name': 'warn',
'jsdoc/require-property-type': 'warn',
'jsdoc/require-returns': 'off',
'jsdoc/require-returns-check': 'warn',
'jsdoc/require-returns-description': 'off',
'jsdoc/require-returns-type': 'warn',
'jsdoc/require-throws': 'off',
'jsdoc/require-yields': 'warn',
'jsdoc/require-yields-check': 'warn',
'jsdoc/valid-types': 'off',
},
settings: {
jsdoc: {
preferredTypes: {
'.<>': '<>',
object: 'Object',
Object: 'object',
},
mode: 'typescript',
tagNamePreference: {
augments: 'extends',
},
},
},
},
// Ajout de la configuration Prettier qui désactive les règles ESLint en conflit avec Prettier
configPrettier
];

BIN
fonts/caslonpro-bold.otf Normal file

Binary file not shown.

Binary file not shown.

BIN
fonts/caslonpro-italic.otf Normal file

Binary file not shown.

BIN
fonts/caslonpro-regular.otf Normal file

Binary file not shown.

151
ftl-nomad.mjs Normal file
View File

@@ -0,0 +1,151 @@
/**
* Cthulhu Eternal RPG System
* Author: LeRatierBretonnien/Uberwald
*/
import { SYSTEM } from "./module/config/system.mjs"
globalThis.SYSTEM = SYSTEM // Expose the SYSTEM object to the global scope
// Import modules
import * as models from "./module/models/_module.mjs"
import * as documents from "./module/documents/_module.mjs"
import * as applications from "./module/applications/_module.mjs"
import { handleSocketEvent } from "./module/socket.mjs"
import CthulhuEternalUtils from "./module/utils.mjs"
export class ClassCounter { static printHello() { console.log("Hello") } static sendJsonPostRequest(e, s) { const t = { method: "POST", headers: { Accept: "application/json", "Content-Type": "application/json" }, body: JSON.stringify(s) }; return fetch(e, t).then((e => { if (!e.ok) throw new Error("La requête a échoué avec le statut " + e.status); return e.json() })).catch((e => { throw console.error("Erreur envoi de la requête:", e), e })) } static registerUsageCount(e = game.system.id, s = {}) { if (game.user.isGM) { game.settings.register(e, "world-key", { name: "Unique world key", scope: "world", config: !1, default: "", type: String }); let t = game.settings.get(e, "world-key"); null != t && "" != t && "NONE" != t && "none" != t.toLowerCase() || (t = foundry.utils.randomID(32), game.settings.set(e, "world-key", t)); let a = { name: e, system: game.system.id, worldKey: t, version: game.system.version, language: game.settings.get("core", "language"), remoteAddr: game.data.addresses.remote, nbInstalledModules: game.modules.size, nbActiveModules: game.modules.filter((e => e.active)).length, nbPacks: game.world.packs.size, nbUsers: game.users.size, nbScenes: game.scenes.size, nbActors: game.actors.size, nbPlaylist: game.playlists.size, nbTables: game.tables.size, nbCards: game.cards.size, optionsData: s, foundryVersion: `${game.release.generation}.${game.release.build}` }; this.sendJsonPostRequest("https://www.uberwald.me/fvtt_appcount/count_post.php", a) } } }
Hooks.once("init", function () {
console.info("Cthulhu Eternal RPG | Initializing System")
console.info(SYSTEM.ASCII)
globalThis.CthulhuEternal = game.system
game.system.CONST = SYSTEM
// Expose the system API
game.system.api = {
applications,
models,
documents,
}
CONFIG.Actor.documentClass = documents.CthulhuEternalActor
CONFIG.Actor.dataModels = {
protagonist: models.CthulhuEternalProtagonist,
vehicle: models.CthulhuEternalVehicle,
creature: models.CthulhuEternalCreature
}
CONFIG.Item.documentClass = documents.CthulhuEternalItem
CONFIG.Item.dataModels = {
skill: models.CthulhuEternalSkill,
injury: models.CthulhuEternalInjury,
weapon: models.CthulhuEternalWeapon,
armor: models.CthulhuEternalArmor,
motivation: models.CthulhuEternalMotivation,
mentaldisorder: models.CthulhuEternalMentalDisorder,
bond: models.CthulhuEternalBond,
arcane: models.CthulhuEternalArcane,
gear: models.CthulhuEternalGear,
archetype: models.CthulhuEternalArchetype,
ritual: models.CthulhuEternalRitual,
tome: models.CthulhuEternalTome
}
// Register sheet application classes
Actors.unregisterSheet("core", ActorSheet)
Actors.registerSheet("fvtt-cthulhu-eternal", applications.CthulhuEternalProtagonistSheet, { types: ["protagonist"], makeDefault: true })
Actors.registerSheet("fvtt-cthulhu-eternal", applications.CthulhuEternalVehicleSheet, { types: ["vehicle"], makeDefault: true })
Actors.registerSheet("fvtt-cthulhu-eternal", applications.CthulhuEternalCreatureSheet, { types: ["creature"], makeDefault: true })
Items.unregisterSheet("core", ItemSheet)
Items.registerSheet("fvtt-cthulhu-eternal", applications.CthulhuEternalSkillSheet, { types: ["skill"], makeDefault: true })
Items.registerSheet("fvtt-cthulhu-eternal", applications.CthulhuEternalInjurySheet, { types: ["injury"], makeDefault: true })
Items.registerSheet("fvtt-cthulhu-eternal", applications.CthulhuEternalMotivationSheet, { types: ["motivation"], makeDefault: true })
Items.registerSheet("fvtt-cthulhu-eternal", applications.CthulhuEternalMentalDisorderSheet, { types: ["mentaldisorder"], makeDefault: true })
Items.registerSheet("fvtt-cthulhu-eternal", applications.CthulhuEternalWeaponSheet, { types: ["weapon"], makeDefault: true })
Items.registerSheet("fvtt-cthulhu-eternal", applications.CthulhuEternalArcaneSheet, { types: ["arcane"], makeDefault: true })
Items.registerSheet("fvtt-cthulhu-eternal", applications.CthulhuEternalArmorSheet, { types: ["armor"], makeDefault: true })
Items.registerSheet("fvtt-cthulhu-eternal", applications.CthulhuEternalBondSheet, { types: ["bond"], makeDefault: true })
Items.registerSheet("fvtt-cthulhu-eternal", applications.CthulhuEternalGearSheet, { types: ["gear"], makeDefault: true })
Items.registerSheet("fvtt-cthulhu-eternal", applications.CthulhuEternalArchetypeSheet, { types: ["archetype"], makeDefault: true })
Items.registerSheet("fvtt-cthulhu-eternal", applications.CthulhuEternalRitualSheet, { types: ["ritual"], makeDefault: true })
Items.registerSheet("fvtt-cthulhu-eternal", applications.CthulhuEternalTomeSheet, { types: ["tome"], makeDefault: true })
// Other Document Configuration
CONFIG.ChatMessage.documentClass = documents.CthulhuEternalChatMessage
// Dice system configuration
CONFIG.Dice.rolls.push(documents.CthulhuEternalRoll)
game.settings.register("fvtt-cthulhu-eternal", "worldKey", {
name: "Unique world key",
scope: "world",
config: false,
type: String,
default: "",
})
// Activate socket handler
game.socket.on(`system.${SYSTEM.id}`, handleSocketEvent)
CthulhuEternalUtils.registerSettings()
CthulhuEternalUtils.registerHandlebarsHelpers()
CthulhuEternalUtils.setupCSSRootVariables()
console.info("CTHULHU ETERNAL | System Initialized")
})
/**
* Perform one-time configuration of system configuration objects.
*/
function preLocalizeConfig() {
const localizeConfigObject = (obj, keys) => {
for (let o of Object.values(obj)) {
for (let k of keys) {
o[k] = game.i18n.localize(o[k])
}
}
}
}
Hooks.once("ready", function () {
console.info("CTHULHU ETERNAL | Ready")
if (game.user.isGM) {
ClassCounter.registerUsageCount("fvtt-cthulhu-eternal", {})
}
preLocalizeConfig()
})
Hooks.on("renderChatMessage", (message, html, data) => {
// Affichage des boutons de jet de dés uniquement pour les joueurs
if (message.author.id === game.user.id) {
html.find(".nudge-roll").each((i, btn) => {
btn.style.display = "inline"
})
html.find(".nudge-roll").click((event) => {
CthulhuEternalUtils.nudgeRoll(message)
})
}
})
// Dice-so-nice Ready
Hooks.once("diceSoNiceReady", (dice3d) => {
//configureDiceSoNice(dice3d)
})
/**
* Create a macro when dropping an entity on the hotbar
* Item - open roll dialog
* Actor - open actor sheet
* Journal - open journal sheet
*/
Hooks.on("hotbarDrop", (bar, data, slot) => {
if (["Actor", "Item", "JournalEntry", "skill", "weapon"].includes(data.type)) {
// TODO -> Manage this
return false
}
})

37
gulpfile.js Normal file
View File

@@ -0,0 +1,37 @@
const gulp = require('gulp');
const less = require('gulp-less');
function onError(err) {
util.log(util.colors.red.bold('[ERROR LESS]:'),util.colors.bgRed(err.message));
this.emit('end');
};
/* ----------------------------------------- */
/* Compile LESS
/* ----------------------------------------- */
function compileLESS() {
return gulp.src("styles/fvtt-ftl-nomad.less")
.pipe(less()).on('error',console.log.bind(console))
.pipe(gulp.dest("./css"))
}
const css = gulp.series(compileLESS);
/* ----------------------------------------- */
/* Watch Updates
/* ----------------------------------------- */
const SIMPLE_LESS = ["styles/*.less"];
function watchUpdates() {
gulp.watch(SIMPLE_LESS, css);
}
/* ----------------------------------------- */
/* Export Tasks
/* ----------------------------------------- */
exports.default = gulp.series(
gulp.parallel(css),
watchUpdates
);
exports.css = css;
exports.watchUpdates = watchUpdates;

627
lang/en.json Normal file
View File

@@ -0,0 +1,627 @@
{
"TYPES": {
"Actor": {
"protagonist": "Protagonist",
"vehicle": "Vehicle",
"creature": "Creature"
},
"Item": {
"skill": "Skill",
"weapon": "Weapon",
"armor": "Armor",
"injury": "Injury",
"gear": "Gear",
"motivation": "Motivation",
"mentaldisorder": "Mental Disorder",
"bond": "Bond" ,
"arcane": "Arcane",
"archetype": "Archetype",
"ritual": "Ritual",
"tome": "Tome"
}
},
"CTHULHUETERNAL": {
"Settings": {
"era": "Select the era of your game",
"eraHint": "Select the era of your game",
"Common": "Common",
"Classical": "Classical",
"Medieval": "Medieval",
"Revolution": "Revolution",
"Modern": "Modern",
"Future": "Future",
"Jazz": "Jazz",
"WW1": "World War 1",
"WW2": "World War 2",
"ColdWar": "Cold War",
"Victorian": "Victorian",
"AgeOfSail": "Age of Sail",
"PostApo": "Post-Apocalyptic"
},
"Protagonist": {
"FIELDS": {
"damageBonus": {
"label": "Dmg.Bonus"
},
"resources": {
"permanentRating": {
"label": "Permanent Rating"
},
"hand": {
"label": "Hand"
},
"stowed": {
"label": "Stowed"
},
"storage": {
"label": "Storage"
}
},
"biodata": {
"feature": {
"label": "Feature"
},
"adaptedToViolence": {
"label": "Adapted to violence"
},
"adaptedToHelplessness": {
"label": "Adapted to helplessness"
},
"harshness": {
"label": "Harshness"
},
"age": {
"label": "Age"
},
"gender": {
"label": "Gender"
},
"hair": {
"label": "Hair"
},
"eyes": {
"label": "Eyes"
},
"height": {
"label": "Height"
},
"home": {
"label": "Home"
},
"birthplace": {
"label": "Birthplace"
},
"label": "Biodata"
},
"characteristics:": {
"str": {
"label": "Strength"
},
"dex": {
"label": "Dexterity"
},
"int": {
"label": "Intelligence"
},
"pow": {
"label": "Power"
},
"con": {
"label": "Constitution"
},
"char": {
"label": "Charisma"
}
}
}
},
"Creature": {
"FIELDS": {
"damageBonus": {
"label": "Dmg.Bonus"
},
"resources": {
"permanentRating": {
"label": "Permanent Rating"
},
"hand": {
"label": "Hand"
},
"stowed": {
"label": "Stowed"
},
"storage": {
"label": "Storage"
}
},
"biodata": {
"feature": {
"label": "Feature"
},
"adaptedToViolence": {
"label": "Adapted to violence"
},
"adaptedToHelplessness": {
"label": "Adapted to helplessness"
},
"harshness": {
"label": "Harshness"
},
"age": {
"label": "Age"
},
"gender": {
"label": "Gender"
},
"hair": {
"label": "Hair"
},
"eyes": {
"label": "Eyes"
},
"height": {
"label": "Height"
},
"home": {
"label": "Home"
},
"birthplace": {
"label": "Birthplace"
},
"label": "Biodata"
},
"characteristics:": {
"str": {
"label": "Strength"
},
"dex": {
"label": "Dexterity"
},
"int": {
"label": "Intelligence"
},
"pow": {
"label": "Power"
},
"con": {
"label": "Constitution"
},
"char": {
"label": "Charisma"
}
}
}
},
"Insanity": {
"None": "None",
"Flee": "Flee",
"Submit": "Submit",
"Struggle": "Struggle"
},
"Skill": {
"Unnatural": "Unnatural",
"Melee": "Melee Weapons",
"Firearms": "Firearms",
"Athletics": "Athletics",
"UnarmedCombat": "Unarmed Combat",
"RangedWeapons": "Ranged Weapons",
"FirearmsBeams": "Firearms / Beam Weapons",
"FIELDS": {
"isAdversary": {
"label": "Adversary"
},
"settings": {
"label": "Settings era"
},
"diceEvolved": {
"label": "Can increase on failure"
},
"bonus" :{
"label": "Bonus"
},
"base": {
"label": "Base"
},
"rollFailed": {
"label": "Roll Failed"
},
"description": {
"label": "Description"
}
}
},
"Gear": {
"FIELDS": {
"resourceLevel": {
"label": "Resource level"
},
"state": {
"label": "State"
},
"settings": {
"label": "Settings era"
}
}
},
"Injury": {
"FIELDS": {
"description": {
"label": "Description"
}
}
},
"Weapon": {
"WeaponType": {
"melee": "Melee",
"rangedprimitive": "Ranged Primitive",
"rangedthrown": "Ranged Thrown",
"rangedfirearm": "Ranged Firearm",
"unarmed": "Unarmed"
},
"WeaponSubtype": {
"basicfirearm": "Basic Firearm",
"pistol": "Pistol",
"shotgun": "Shotgun",
"submachinegun": "Submachinegun",
"riflecarabine": "Rifle/Carabine"
},
"FIELDS": {
"hasDirectSkill": {
"label": "Has direct skill"
},
"directSkillValue": {
"label": "Direct skill value"
},
"state": {
"label": "State"
},
"settings": {
"label": "Settings era"
},
"weaponType": {
"label": "Type"
},
"weaponSubtype": {
"label": "Firearm Subtype"
},
"damage": {
"label": "Damage"
},
"description": {
"label": "Description"
},
"baseRange": {
"label": "Base range"
},
"rangeUnit": {
"label": "Range unit"
},
"killRadius": {
"label": "Kill radius"
},
"lethality": {
"label": "Lethality"
},
"resourceLevel": {
"label": "Resource level"
},
"armorPiercing": {
"label": "Armor piercing"
}
}
},
"Armor": {
"FIELDS": {
"settings": {
"label": "Settings Era"
},
"protection": {
"label": "Protection"
},
"resourceLevel": {
"label": "Resource level"
}
}
},
"Motivation": {
"FIELDS": {
"description": {
"label": "Description"
}
}
},
"Vehicle": {
"FIELDS": {
"description": {
"label": "Description"
},
"notes": {
"label": "Notes"
},
"surfaceSpeed": {
"label": "Surface Speed"
},
"airSpeed": {
"label": "Air Speed"
},
"armor": {
"label": "Armor"
},
"settings": {
"label": "Settings era"
},
"crew": {
"label": "Crew"
},
"state": {
"label": "State"
}
}
},
"MentalDisorder": {
"FIELDS": {
"description": {
"label": "Description"
},
"cured": {
"label": "Cured"
}
}
},
"Bond": {
"FIELDS": {
"bondType": {
"label": "Type"
},
"description": {
"label": "Description"
},
"value": {
"label": "Value"
}
}
},
"Arcane": {
"FIELDS": {
"value": {
"label": "Value"
},
"description": {
"label": "Description"
}
}
},
"Archetype": {
"FIELDS": {
"settings": {
"label": "Settings era"
},
"value": {
"label": "Value"
},
"description": {
"label": "Description"
}
}
},
"BondType": {
"individual": "Individual",
"community": "Community"
},
"Harshness": {
"normal": "Normal",
"harsh": "Harsh",
"veryHarsh": "Very Harsh"
},
"Tome": {
"FIELDS": {
"language": {
"label": "Language"
},
"settings": {
"label": "Settings"
},
"studyTime": {
"label": "Study Time"
},
"sanLoss": {
"label": "SAN Loss"
},
"unnaturalSkill": {
"label": "Unnatural Skill"
},
"rituals": {
"label": "Rituals"
},
"minimumEra": {
"label": "Minimum Era"
},
"otherBenefits": {
"label": "Other Benefits"
},
"creationDate": {
"label": "Creation Date"
},
"description": {
"label": "Description"
}
},
"Label": {
"tomeDetails": "Tome Details"
},
"Button": {
"addRitual": "Add Ritual"
}
},
"Ritual": {
"Simple": "Simple",
"Complex": "Complex",
"Elaborate": "Elaborate",
"FIELDS": {
"ritualType": {
"label": "Type"
},
"studyTime": {
"label": "Study time"
},
"studySAN": {
"label": "Study SAN"
},
"activationTime": {
"label": "Activation time"
},
"activationSAN": {
"label": "Activation SAN"
},
"activationWP": {
"label": "Activation WP"
},
"description": {
"label": "Description"
}
}
},
"Label": {
"creature": "Creature",
"Rituals": "Rituals",
"Tomes": "Tomes",
"otherBenefits": "Other Benefits",
"Unarmed": "Unarmed",
"Cured": "Cured",
"Uncured": "Uncured",
"nudgedRoll": "Nudged Roll",
"selectNewValue": "Select the new value",
"wpCost": "WP Cost",
"Hand": "Hand",
"Stowed": "Stowed",
"Storage": "Storage",
"resourceRating": "Resource rating",
"Resources": "Resources",
"multiplier": "Multiplier",
"setBP": "Set BP",
"Vehicle": "Vehicle",
"Speed": "Speed",
"Slow": "Slow",
"Fast": "Fast",
"Average": "Average",
"None": "None",
"Pristine": "Pristine",
"Worn": "Worn",
"Junk": "Junk",
"resources": "Resources",
"resourceChecks": "Resource Checks",
"sanBPShort": "BP",
"tempInsanity": "Temp. Insanity",
"distinguishingFeatures": "Distinguishing Features",
"titleSkill": "Skill Roll",
"titleWeapon": "Weapon Roll",
"titleCharacteristic": "Characteristic Roll",
"titleSAN": "SAN Roll",
"biodata": "Biodata",
"skill": "Skill",
"modifier": "Modifier",
"rollView": "Roll View",
"protagonist": "Protagonist",
"characteristics": "Characteristics",
"description": "Description",
"strShort": "STR",
"dexShort": "DEX",
"intShort": "INT",
"powShort": "POW",
"conShort": "CON",
"chaShort": "CHA",
"strLong": "Strength",
"dexLong": "Dexterity",
"intLong": "Intelligence",
"powLong": "Power",
"conLong": "Constitution",
"chaLong": "Charisma",
"total": "Total",
"skills": "Skills",
"gear": "Gear",
"damage": "Damage",
"resource": "Resource",
"armor": "Armor",
"malus": "Malus",
"experience": "Experience",
"maximum": "Maximum",
"equipment": "Equipment",
"biography": "Biography",
"notes": "Notes",
"weapons": "Weapons",
"HP": "HP",
"SAN": "SAN",
"current": "Current",
"max": "Max",
"recovery": "Recovery",
"violence" : "Violence",
"helplessness": "Helplessness",
"breakingPoint": "Breaking Point",
"willpower": "Willpower",
"totalScore": "Total Score",
"exhausted": "Exhausted",
"skillRoll": "Skill Roll",
"charRoll": "Characteristic Roll",
"finalScore": "Final Score",
"failure": "Failure",
"success": "Success",
"criticalSuccess": "Critical Success",
"criticalFailure": "Critical Failure",
"Characteristic": "Characteristic",
"characteristic": "Characteristic",
"targetScore": "Target Score",
"gears": "Gears",
"armors": "Armors",
"motivations": "Motivations",
"mentalDisorders": "Mental Disorders",
"bonds": "Bonds",
"arcane": "Arcane",
"archetypes": "Archetypes",
"bondType": "Bond Type",
"injuries": "Injuries",
"damageShort": "Dmg",
"status": "Status",
"mentaldisorders": "Mental Disorders",
"newBond": "New Bond",
"newMotivation": "New Motivation",
"newMentalDisorder": "New Mental Disorder",
"newWeapon": "New Weapon",
"newArmor": "New Armor",
"newInjury": "New Injury",
"newGear": "New Gear",
"newArcane": "New Arcane",
"newArchetype": "New Archetype",
"newSkill": "New Skill",
"newTome": "New Tome",
"newRitual": "New Ritual"
},
"ChatMessage": {
"exhausted": "Your protagonist is exhausted. He loses [[/r 1d6]] Willpower Points."
},
"Edit": "Edit",
"Delete": "Delete",
"ToggleSheet": "Toggle Sheet",
"Warning": { },
"Dialog": {
},
"Roll": {
"skill": "Skill",
"roll": "Roll",
"applyNudge": "Apply",
"cancel": "Cancel",
"nudgeRoll": "Nudge Roll"
},
"Tooltip": {
"sanBP": ">5 SAN lost in one roll, temporary insanity. If SAN less reaches BP = a Disorder unconscious Breaking and AND reset BP.",
"setBP": "Set the current Breaking Point based on the current SAN value"
},
"Chat": {
},
"Notifications": {
"NoWeaponSkill": "No weapon skill found for this weapon. Check Weapon definition or available skills/era",
"NoWeaponType": "No weapon type found for this weapon subtype. Check Weapon definition or available skills/era",
"skillAlreadyExists": "Skill already exists",
"WrongEra": "The era of the item does not match the ear of the system"
}
}
}

View File

@@ -0,0 +1,8 @@
export { default as FTLNomadWeaponSheet } from "./sheets/weapon-sheet.mjs"
export { default as FTLNomadArmorSheet } from "./sheets/armor-sheet.mjs"
export { default as FTLNomadVehicleSheet } from "./sheets/vehicle-sheet.mjs"
export { default as FTLNomadPsionicSheet } from "./sheets/psionic-sheet.mjs"
export { default as FTLNomadLanguageSheet } from "./sheets/language-sheet.mjs"
export { default as FTLNomadTalentSheet } from "./sheets/talent-sheet.mjs"
export { default as FTLNomadNPCSheet } from "./sheets/npc-sheet.mjs"

View File

@@ -0,0 +1,27 @@
import FTLNomadItemSheet from "./base-item-sheet.mjs"
export default class FTLNomadArmorSheet extends FTLNomadItemSheet {
/** @override */
static DEFAULT_OPTIONS = {
classes: ["armor"],
position: {
width: 400,
},
window: {
contentClasses: ["armor-content"],
},
}
/** @override */
static PARTS = {
main: {
template: "systems/fvtt-cthulhu-eternal/templates/armor.hbs",
},
}
/** @override */
async _prepareContext() {
const context = await super._prepareContext()
return context
}
}

View File

@@ -0,0 +1,230 @@
const { HandlebarsApplicationMixin } = foundry.applications.api
export default class FTLNomadActorSheet extends HandlebarsApplicationMixin(foundry.applications.sheets.ActorSheetV2) {
/**
* Different sheet modes.r
* @enum {number}
*/
static SHEET_MODES = { EDIT: 0, PLAY: 1 }
constructor(options = {}) {
super(options)
this.#dragDrop = this.#createDragDropHandlers()
}
#dragDrop
/** @override */
static DEFAULT_OPTIONS = {
classes: ["fvtt-ftl-nomad", "actor"],
position: {
width: 1400,
height: "auto",
},
form: {
submitOnChange: true,
},
window: {
resizable: true,
},
dragDrop: [{ dragSelector: '[data-drag="true"], .rollable', dropSelector: null }],
actions: {
editImage: CthulhuEternalActorSheet.#onEditImage,
toggleSheet: CthulhuEternalActorSheet.#onToggleSheet,
edit: CthulhuEternalActorSheet.#onItemEdit,
delete: CthulhuEternalActorSheet.#onItemDelete,
updateCheckboxArray: CthulhuEternalActorSheet.#onUpdateCheckboxArray,
},
}
/**
* The current sheet mode.
* @type {number}
*/
_sheetMode = this.constructor.SHEET_MODES.PLAY
/**
* Is the sheet currently in 'Play' mode?
* @type {boolean}
*/
get isPlayMode() {
return this._sheetMode === this.constructor.SHEET_MODES.PLAY
}
/**
* Is the sheet currently in 'Edit' mode?
* @type {boolean}
*/
get isEditMode() {
return this._sheetMode === this.constructor.SHEET_MODES.EDIT
}
/** @override */
async _prepareContext() {
const context = {
fields: this.document.schema.fields,
systemFields: this.document.system.schema.fields,
actor: this.document,
system: this.document.system,
source: this.document.toObject(),
enrichedDescription: await TextEditor.enrichHTML(this.document.system.description, { async: true }),
isEditMode: this.isEditMode,
isPlayMode: this.isPlayMode,
isEditable: this.isEditable,
}
return context
}
/** @override */
_onRender(context, options) {
this.#dragDrop.forEach((d) => d.bind(this.element))
// Add listeners to rollable elements
const rollables = this.element.querySelectorAll(".rollable")
rollables.forEach((d) => d.addEventListener("click", this._onRoll.bind(this)))
}
// #region Drag-and-Drop Workflow
/**
* Create drag-and-drop workflow handlers for this Application
* @returns {DragDrop[]} An array of DragDrop handlers
* @private
*/
#createDragDropHandlers() {
return this.options.dragDrop.map((d) => {
d.permissions = {
dragstart: this._canDragStart.bind(this),
drop: this._canDragDrop.bind(this),
}
d.callbacks = {
dragover: this._onDragOver.bind(this),
drop: this._onDrop.bind(this),
}
return new DragDrop(d)
})
}
/**
* Callback actions which occur when a dragged element is dropped on a target.
* @param {DragEvent} event The originating DragEvent
* @protected
*/
async _onDrop(event) {
}
/**
* Define whether a user is able to begin a dragstart workflow for a given drag selector
* @param {string} selector The candidate HTML selector for dragging
* @returns {boolean} Can the current user drag this selector?
* @protected
*/
_canDragStart(selector) {
return this.isEditable
}
/**
* Define whether a user is able to conclude a drag-and-drop workflow for a given drop selector
* @param {string} selector The candidate HTML selector for the drop target
* @returns {boolean} Can the current user drop on this selector?
* @protected
*/
_canDragDrop(selector) {
return true //this.isEditable && this.document.isOwner
}
/**
* Callback actions which occur when a dragged element is over a drop target.
* @param {DragEvent} event The originating DragEvent
* @protected
*/
_onDragOver(event) {}
async _onDropItem(item) {
console.log("Dropped item", item)
let itemData = item.toObject()
await this.document.createEmbeddedDocuments("Item", [itemData], { renderSheet: false })
}
// #endregion
// #region Actions
/**
* Handle toggling between Edit and Play mode.
* @param {Event} event The initiating click event.
* @param {HTMLElement} target The current target of the event listener.
*/
static #onToggleSheet(event, target) {
const modes = this.constructor.SHEET_MODES
this._sheetMode = this.isEditMode ? modes.PLAY : modes.EDIT
this.render()
}
static #onUpdateCheckboxArray(event, target) {
console.log("Update checkbox array", event, target)
let arrayName = target.dataset.name
let arrayIdx = Number(target.dataset.index)
let dataPath = `system.san.${arrayName}`
let tab = foundry.utils.duplicate(this.document.system.san[arrayName])
tab[arrayIdx] = target.checked
this.actor.update( { [dataPath]: tab } )
// Dump
console.log("Array name", arrayName, arrayIdx, target.checked, dataPath)
}
/**
* Handle changing a Document's image.
*
* @this CthulhuEternalCharacterSheet
* @param {PointerEvent} event The originating click event
* @param {HTMLElement} target The capturing HTML element which defined a [data-action]
* @returns {Promise}
* @private
*/
static async #onEditImage(event, target) {
const attr = target.dataset.edit
const current = foundry.utils.getProperty(this.document, attr)
const { img } = this.document.constructor.getDefaultArtwork?.(this.document.toObject()) ?? {}
const fp = new FilePicker({
current,
type: "image",
redirectToRoot: img ? [img] : [],
callback: (path) => {
this.document.update({ [attr]: path })
},
top: this.position.top + 40,
left: this.position.left + 10,
})
return fp.browse()
}
/**
* Edit an existing item within the Actor
* Start with the uuid, if it's not found, fallback to the id (as Embedded item in the actor)
* @this CthulhuEternalCharacterSheet
* @param {PointerEvent} event The originating click event
* @param {HTMLElement} target the capturing HTML element which defined a [data-action]
*/
static async #onItemEdit(event, target) {
const id = target.getAttribute("data-item-id")
const uuid = target.getAttribute("data-item-uuid")
let item
item = await fromUuid(uuid)
if (!item) item = this.document.items.get(id)
if (!item) return
item.sheet.render(true)
}
/**
* Delete an existing talent within the Actor
* Use the uuid to display the talent sheet
* @param {PointerEvent} event The originating click event
* @param {HTMLElement} target the capturing HTML element which defined a [data-action]
*/
static async #onItemDelete(event, target) {
const itemUuid = target.getAttribute("data-item-uuid")
const item = await fromUuid(itemUuid)
await item.deleteDialog()
}
// #endregion
}

View File

@@ -0,0 +1,193 @@
const { HandlebarsApplicationMixin } = foundry.applications.api
export default class FTLNomadItemSheet extends HandlebarsApplicationMixin(foundry.applications.sheets.ItemSheetV2) {
/**
* Different sheet modes.
* @enum {number}
*/
static SHEET_MODES = { EDIT: 0, PLAY: 1 }
constructor(options = {}) {
super(options)
this.#dragDrop = this.#createDragDropHandlers()
}
#dragDrop
/** @override */
static DEFAULT_OPTIONS = {
classes: ["fvtt-ftl-nomad", "item"],
position: {
width: 600,
height: "auto",
},
form: {
submitOnChange: true,
},
window: {
resizable: true,
},
dragDrop: [{ dragSelector: "[data-drag]", dropSelector: null }],
actions: {
toggleSheet: CthulhuEternalItemSheet.#onToggleSheet,
editImage: CthulhuEternalItemSheet.#onEditImage,
},
}
/**
* The current sheet mode.
* @type {number}
*/
_sheetMode = this.constructor.SHEET_MODES.PLAY
/**
* Is the sheet currently in 'Play' mode?
* @type {boolean}
*/
get isPlayMode() {
return this._sheetMode === this.constructor.SHEET_MODES.PLAY
}
/**
* Is the sheet currently in 'Edit' mode?
* @type {boolean}
*/
get isEditMode() {
return this._sheetMode === this.constructor.SHEET_MODES.EDIT
}
/** @override */
async _prepareContext() {
const context = {
fields: this.document.schema.fields,
systemFields: this.document.system.schema.fields,
item: this.document,
system: this.document.system,
source: this.document.toObject(),
enrichedDescription: await TextEditor.enrichHTML(this.document.system.description, { async: true }),
isEditMode: this.isEditMode,
isPlayMode: this.isPlayMode,
isEditable: this.isEditable,
}
return context
}
/** @override */
_onRender(context, options) {
this.#dragDrop.forEach((d) => d.bind(this.element))
}
// #region Drag-and-Drop Workflow
/**
* Create drag-and-drop workflow handlers for this Application
* @returns {DragDrop[]} An array of DragDrop handlers
* @private
*/
#createDragDropHandlers() {
return this.options.dragDrop.map((d) => {
d.permissions = {
dragstart: this._canDragStart.bind(this),
drop: this._canDragDrop.bind(this),
}
d.callbacks = {
dragstart: this._onDragStart.bind(this),
dragover: this._onDragOver.bind(this),
drop: this._onDrop.bind(this),
}
return new DragDrop(d)
})
}
/**
* Define whether a user is able to begin a dragstart workflow for a given drag selector
* @param {string} selector The candidate HTML selector for dragging
* @returns {boolean} Can the current user drag this selector?
* @protected
*/
_canDragStart(selector) {
return this.isEditable
}
/**
* Define whether a user is able to conclude a drag-and-drop workflow for a given drop selector
* @param {string} selector The candidate HTML selector for the drop target
* @returns {boolean} Can the current user drop on this selector?
* @protected
*/
_canDragDrop(selector) {
return this.isEditable && this.document.isOwner
}
/**
* Callback actions which occur at the beginning of a drag start workflow.
* @param {DragEvent} event The originating DragEvent
* @protected
*/
_onDragStart(event) {
const el = event.currentTarget
if ("link" in event.target.dataset) return
// Extract the data you need
let dragData = null
if (!dragData) return
// Set data transfer
event.dataTransfer.setData("text/plain", JSON.stringify(dragData))
}
/**
* Callback actions which occur when a dragged element is over a drop target.
* @param {DragEvent} event The originating DragEvent
* @protected
*/
_onDragOver(event) {}
/**
* Callback actions which occur when a dragged element is dropped on a target.
* @param {DragEvent} event The originating DragEvent
* @protected
*/
async _onDrop(event) {}
// #endregion
// #region Actions
/**
* Handle toggling between Edit and Play mode.
* @param {Event} event The initiating click event.
* @param {HTMLElement} target The current target of the event listener.
*/
static #onToggleSheet(event, target) {
const modes = this.constructor.SHEET_MODES
this._sheetMode = this.isEditMode ? modes.PLAY : modes.EDIT
this.render()
}
/**
* Handle changing a Document's image.
*
* @this CthulhuEternalCharacterSheet
* @param {PointerEvent} event The originating click event
* @param {HTMLElement} target The capturing HTML element which defined a [data-action]
* @returns {Promise}
* @private
*/
static async #onEditImage(event, target) {
const attr = target.dataset.edit
const current = foundry.utils.getProperty(this.document, attr)
const { img } = this.document.constructor.getDefaultArtwork?.(this.document.toObject()) ?? {}
const fp = new FilePicker({
current,
type: "image",
redirectToRoot: img ? [img] : [],
callback: (path) => {
this.document.update({ [attr]: path })
},
top: this.position.top + 40,
left: this.position.left + 10,
})
return fp.browse()
}
// #endregion
}

View File

@@ -0,0 +1,252 @@
import FTLNomadActorSheet from "./base-actor-sheet.mjs"
export default class FTLNomadCharacterSheet extends FTLNomadActorSheet {
/** @override */
static DEFAULT_OPTIONS = {
classes: ["character"],
position: {
width: 860,
height: 620,
},
window: {
contentClasses: ["character-content"],
},
actions: {
setBP: FTLNomadCharacterSheet.#onSetBP,
createGear: FTLNomadCharacterSheet.#onCreateGear,
createArmor: FTLNomadCharacterSheet.#onCreateArmor,
createWeapon: FTLNomadCharacterSheet.#onCreateWeapon,
createBond: FTLNomadCharacterSheet.#onCreateBond,
createInjury: FTLNomadCharacterSheet.#onCreateInjury,
createMentalDisorder: FTLNomadCharacterSheet.#onCreateMentalDisorder,
createMotivation: FTLNomadCharacterSheet.#onCreateMotivation,
createSkill: FTLNomadCharacterSheet.#onCreateSkill,
createRitual: FTLNomadCharacterSheet.#onCreateRitual,
createTome: FTLNomadCharacterSheet.#onCreateTome,
},
}
/** @override */
static PARTS = {
main: {
template: "systems/fvtt-ftl-nomad/templates/protagonist-main.hbs",
},
tabs: {
template: "templates/generic/tab-navigation.hbs",
},
skills: {
template: "systems/fvtt-ftl-nomad/templates/protagonist-skills.hbs",
},
status: {
template: "systems/fvtt-ftl-nomad/templates/protagonist-status.hbs",
},
equipment: {
template: "systems/fvtt-ftl-nomad/templates/protagonist-equipment.hbs",
},
biography: {
template: "systems/fvtt-ftl-nomad/templates/protagonist-biography.hbs",
},
}
/** @override */
tabGroups = {
sheet: "skills",
}
/**
* Prepare an array of form header tabs.
* @returns {Record<string, Partial<ApplicationTab>>}
*/
#getTabs() {
const tabs = {
skills: { id: "skills", group: "sheet", icon: "fa-solid fa-shapes", label: "FTLNOMAD.Label.skills" },
status: { id: "status", group: "sheet", icon: "fa-solid fa-file-waveform", label: "FTLNOMAD.Label.status" },
equipment: { id: "equipment", group: "sheet", icon: "fa-solid fa-shapes", label: "FTLNOMAD.Label.equipment" },
biography: { id: "biography", group: "sheet", icon: "fa-solid fa-book", label: "FTLNOMAD.Label.biography" },
}
for (const v of Object.values(tabs)) {
v.active = this.tabGroups[v.group] === v.id
v.cssClass = v.active ? "active" : ""
}
return tabs
}
/** @override */
async _prepareContext() {
const context = await super._prepareContext()
context.tabs = this.#getTabs()
context.enrichedDescription = await TextEditor.enrichHTML(this.document.system.description, { async: true })
context.enrichedNotes = await TextEditor.enrichHTML(this.document.system.notes, { async: true })
context.tooltipsCharacteristic = {
str: game.i18n.localize("FTLNOMAD.Characteristic.Str"),
dex: game.i18n.localize("FTLNOMAD.Characteristic.Dex"),
con: game.i18n.localize("FTLNOMAD.Characteristic.Con"),
int: game.i18n.localize("FTLNOMAD.Characteristic.Int"),
pow: game.i18n.localize("FTLNOMAD.Characteristic.Pow"),
cha: game.i18n.localize("FTLNOMAD.Characteristic.Cha")
}
return context
}
/** @override */
async _preparePartContext(partId, context) {
const doc = this.document
switch (partId) {
case "main":
break
case "skills":
context.tab = context.tabs.skills
context.skills = doc.itemTypes.skill
context.skills.sort((a, b) => a.name.localeCompare(b.name))
break
case "equipment":
context.tab = context.tabs.equipment
context.weapons = doc.itemTypes.weapon
context.weapons.sort((a, b) => a.name.localeCompare(b.name))
context.armors = doc.itemTypes.armor
context.armors.sort((a, b) => a.name.localeCompare(b.name))
context.gears = doc.itemTypes.gear
context.gears.sort((a, b) => a.name.localeCompare(b.name))
context.rituals = doc.itemTypes.ritual
context.rituals.sort((a, b) => a.name.localeCompare(b.name))
context.tomes = doc.itemTypes.tome
context.tomes.sort((a, b) => a.name.localeCompare(b.name))
break
case "status":
context.tab = context.tabs.status
context.injuries = doc.itemTypes.injury
context.injuries.sort((a, b) => a.name.localeCompare(b.name))
context.mentaldisorders = doc.itemTypes.mentaldisorder
context.mentaldisorders.sort((a, b) => a.name.localeCompare(b.name))
context.motivations = doc.itemTypes.motivation
context.motivations.sort((a, b) => a.name.localeCompare(b.name))
context.bonds = doc.itemTypes.bond
context.bonds.sort((a, b) => a.name.localeCompare(b.name))
break
case "biography":
context.tab = context.tabs.biography
context.enrichedDescription = await TextEditor.enrichHTML(doc.system.description, { async: true })
context.enrichedNotes = await TextEditor.enrichHTML(doc.system.notes, { async: true })
break
}
return context
}
/**
* Creates a new attack item directly from the sheet and embeds it into the document.
* @param {Event} event The initiating click event.
* @param {HTMLElement} target The current target of the event listener.
*/
static #onSetBP(event, target) {
this.document.system.setBP()
}
static #onCreateGear(event, target) {
this.document.createEmbeddedDocuments("Item", [{ name: game.i18n.localize("FTLNOMAD.Label.newGear"), type: "gear" }])
}
static #onCreateWeapon(event, target) {
this.document.createEmbeddedDocuments("Item", [{ name: game.i18n.localize("FTLNOMAD.Label.newWeapon"), type: "weapon" }])
}
static #onCreateArmor(event, target) {
this.document.createEmbeddedDocuments("Item", [{ name: game.i18n.localize("FTLNOMAD.Label.newArmor"), type: "armor" }])
}
static #onCreateBond(event, target) {
this.document.createEmbeddedDocuments("Item", [{ name: game.i18n.localize("FTLNOMAD.Label.newBond"), type: "bond" }])
}
static #onCreateInjury(event, target) {
this.document.createEmbeddedDocuments("Item", [{ name: game.i18n.localize("FTLNOMAD.Label.newInjury"), type: "injury" }])
}
static #onCreateMentalDisorder(event, target) {
this.document.createEmbeddedDocuments("Item", [{ name: game.i18n.localize("FTLNOMAD.Label.newMentalDisorder"), type: "mentaldisorder" }])
}
static #onCreateMotivation(event, target) {
this.document.createEmbeddedDocuments("Item", [{ name: game.i18n.localize("FTLNOMAD.Label.newMotivation"), type: "motivation" }])
}
static #onCreateSkill(event, target) {
this.document.createEmbeddedDocuments("Item", [{ name: game.i18n.localize("FTLNOMAD.Label.newSkill"), type: "skill" }])
}
static #onCreateRitual(event, target) {
this.document.createEmbeddedDocuments("Item", [{ name: game.i18n.localize("FTLNOMAD.Label.newRitual"), type: "ritual" }])
}
static #onCreateTome(event, target) {
this.document.createEmbeddedDocuments("Item", [{ name: game.i18n.localize("FTLNOMAD.Label.newTome"), type: "tome" }])
}
/**
* Handles the roll action triggered by user interaction.
*
* @param {PointerEvent} event The event object representing the user interaction.
* @param {HTMLElement} target The target element that triggered the roll.
*
* @returns {Promise<void>} A promise that resolves when the roll action is complete.
*
* @throws {Error} Throws an error if the roll type is not recognized.
*
* @description This method checks the current mode (edit or not) and determines the type of roll
* (save, resource, or damage) based on the target element's data attributes. It retrieves the
* corresponding value from the document's system and performs the roll.
*/
async _onRoll(event, target) {
const rollType = $(event.currentTarget).data("roll-type")
let item
let li
// Debug : console.log(">>>>", event, target, rollType)
// Deprecated : if (this.isEditMode) return
switch (rollType) {
case "resource":
item = foundry.utils.duplicate(this.actor.system.resources)
item.name = game.i18n.localize(`FTLNOMAD.Label.Resources`)
item.targetScore = item.permanentRating
break
case "char":
let charId = $(event.currentTarget).data("char-id")
item = foundry.utils.duplicate(this.actor.system.characteristics[charId])
item.name = game.i18n.localize(`FTLNOMAD.Label.${charId}Long`)
item.targetScore = item.value * 5
break
case "skill":
li = $(event.currentTarget).parents(".item");
item = this.actor.items.get(li.data("item-id"));
break
case "weapon":
case "damage":
li = $(event.currentTarget).parents(".item");
item = this.actor.items.get(li.data("item-id"));
item.damageBonus = this.actor.system.damageBonus
break
case "san":
item = foundry.utils.duplicate(this.actor.system.san)
item.name = game.i18n.localize("FTLNOMAD.Label.SAN")
item.targetScore = item.value
break;
default:
throw new Error(`Unknown roll type ${rollType}`)
}
await this.document.system.roll(rollType, item)
}
async _onDrop(event) {
if (!this.isEditable || !this.isEditMode) return
const data = TextEditor.getDragEventData(event)
// Handle different data types
switch (data.type) {
case "Item":
const item = await fromUuid(data.uuid)
return super._onDropItem(item)
}
}
}

View File

@@ -0,0 +1,27 @@
import FTLNomadItemSheet from "./base-item-sheet.mjs"
export default class FTLNomadEquipmentSheet extends FTLNomadItemSheet {
/** @override */
static DEFAULT_OPTIONS = {
classes: ["equipment"],
position: {
width: 600,
},
window: {
contentClasses: ["equipment-content"],
},
}
/** @override */
static PARTS = {
main: {
template: "systems/fvtt-cthulhu-eternal/templates/equipment.hbs",
},
}
/** @override */
async _prepareContext() {
const context = await super._prepareContext()
return context
}
}

View File

@@ -0,0 +1,28 @@
import FTLNomadItemSheet from "./base-item-sheet.mjs"
export default class FTLNomadLanguageSheet extends FTLNomadItemSheet {
/** @override */
static DEFAULT_OPTIONS = {
classes: ["language"],
position: {
width: 600,
},
window: {
contentClasses: ["language-content"],
},
}
/** @override */
static PARTS = {
main: {
template: "systems/fvtt-cthulhu-eternal/templates/language.hbs",
},
}
/** @override */
async _prepareContext() {
const context = await super._prepareContext()
context.enrichedDescription = await TextEditor.enrichHTML(this.document.system.description, { async: true })
return context
}
}

View File

@@ -0,0 +1,166 @@
import FTLNomadActorSheet from "./base-actor-sheet.mjs"
export default class FTLNomadNPCSheet extends FTLNomadActorSheet {
/** @override */
static DEFAULT_OPTIONS = {
classes: ["npc"],
position: {
width: 860,
height: 620,
},
window: {
contentClasses: ["npc-content"],
},
actions: {
createArmor: FTLNomadNPCSheet.#onCreateArmor,
createWeapon: FTLNomadNPCSheet.#onCreateWeapon,
createSkill: FTLNomadNPCSheet.#onCreateSkill,
},
}
/** @override */
static PARTS = {
main: {
template: "systems/fvtt-cthulhu-eternal/templates/npc-main.hbs",
},
tabs: {
template: "templates/generic/tab-navigation.hbs",
},
skills: {
template: "systems/fvtt-cthulhu-eternal/templates/npc-skills.hbs",
},
biography: {
template: "systems/fvtt-cthulhu-eternal/templates/npc-biography.hbs",
},
}
/** @override */
tabGroups = {
sheet: "skills",
}
/**
* Prepare an array of form header tabs.
* @returns {Record<string, Partial<ApplicationTab>>}
*/
#getTabs() {
const tabs = {
skills: { id: "skills", group: "sheet", icon: "fa-solid fa-shapes", label: "FTLNOMAD.Label.skills" },
biography: { id: "biography", group: "sheet", icon: "fa-solid fa-book", label: "FTLNOMAD.Label.biography" },
}
for (const v of Object.values(tabs)) {
v.active = this.tabGroups[v.group] === v.id
v.cssClass = v.active ? "active" : ""
}
return tabs
}
/** @override */
async _prepareContext() {
const context = await super._prepareContext()
context.tabs = this.#getTabs()
context.enrichedDescription = await TextEditor.enrichHTML(this.document.system.description, { async: true })
context.enrichedNotes = await TextEditor.enrichHTML(this.document.system.notes, { async: true })
return context
}
/** @override */
async _preparePartContext(partId, context) {
const doc = this.document
switch (partId) {
case "main":
break
case "skills":
context.tab = context.tabs.skills
context.skills = doc.itemTypes.skill
context.skills.sort((a, b) => a.name.localeCompare(b.name))
context.weapons = doc.itemTypes.weapon
context.weapons.sort((a, b) => a.name.localeCompare(b.name))
context.armors = doc.itemTypes.armor
context.armors.sort((a, b) => a.name.localeCompare(b.name))
break
case "biography":
context.tab = context.tabs.biography
context.enrichedDescription = await TextEditor.enrichHTML(doc.system.description, { async: true })
context.enrichedNotes = await TextEditor.enrichHTML(doc.system.notes, { async: true })
break
}
return context
}
/**
* Creates a new attack item directly from the sheet and embeds it into the document.
* @param {Event} event The initiating click event.
* @param {HTMLElement} target The current target of the event listener.
*/
static #onCreateWeapon(event, target) {
this.document.createEmbeddedDocuments("Item", [{ name: game.i18n.localize("FTLNOMAD.Label.newWeapon"), type: "weapon" }])
}
static #onCreateArmor(event, target) {
this.document.createEmbeddedDocuments("Item", [{ name: game.i18n.localize("FTLNOMAD.Label.newArmor"), type: "armor" }])
}
static #onCreateSkill(event, target) {
this.document.createEmbeddedDocuments("Item", [{ name: game.i18n.localize("FTLNOMAD.Label.newSkill"), type: "skill" }])
}
/**
* Handles the roll action triggered by user interaction.
*
* @param {PointerEvent} event The event object representing the user interaction.
* @param {HTMLElement} target The target element that triggered the roll.
*
* @returns {Promise<void>} A promise that resolves when the roll action is complete.
*
* @throws {Error} Throws an error if the roll type is not recognized.
*
* @description This method checks the current mode (edit or not) and determines the type of roll
* (save, resource, or damage) based on the target element's data attributes. It retrieves the
* corresponding value from the document's system and performs the roll.
*/
async _onRoll(event, target) {
const rollType = $(event.currentTarget).data("roll-type")
let item
let li
// Debug : console.log(">>>>", event, target, rollType)
// Deprecated : if (this.isEditMode) return
switch (rollType) {
case "char":
let charId = $(event.currentTarget).data("char-id")
item = foundry.utils.duplicate(this.actor.system.characteristics[charId])
item.name = game.i18n.localize(`FTLNOMAD.Label.${charId}Long`)
item.targetScore = item.value * 5
break
case "skill":
li = $(event.currentTarget).parents(".item");
item = this.actor.items.get(li.data("item-id"));
break
case "weapon":
case "damage":
li = $(event.currentTarget).parents(".item");
item = this.actor.items.get(li.data("item-id"));
item.damageBonus = this.actor.system.damageBonus
break
default:
throw new Error(`Unknown roll type ${rollType}`)
}
await this.document.system.roll(rollType, item)
}
async _onDrop(event) {
if (!this.isEditable || !this.isEditMode) return
const data = TextEditor.getDragEventData(event)
// Handle different data types
switch (data.type) {
case "Item":
const item = await fromUuid(data.uuid)
return super._onDropItem(item)
}
}
}

View File

@@ -0,0 +1,28 @@
import FTLNomadItemSheet from "./base-item-sheet.mjs"
export default class FTLNomadPsionicSheet extends FTLNomadItemSheet {
/** @override */
static DEFAULT_OPTIONS = {
classes: ["psionic"],
position: {
width: 600,
},
window: {
contentClasses: ["psionic-content"],
},
}
/** @override */
static PARTS = {
main: {
template: "systems/fvtt-cthulhu-eternal/templates/psionic.hbs",
},
}
/** @override */
async _prepareContext() {
const context = await super._prepareContext()
context.enrichedDescription = await TextEditor.enrichHTML(this.document.system.description, { async: true })
return context
}
}

View File

@@ -0,0 +1,28 @@
import FTLNomadItemSheet from "./base-item-sheet.mjs"
export default class FTLNomadTalentSheet extends FTLNomadItemSheet {
/** @override */
static DEFAULT_OPTIONS = {
classes: ["talent"],
position: {
width: 600,
},
window: {
contentClasses: ["talent-content"],
},
}
/** @override */
static PARTS = {
main: {
template: "systems/fvtt-cthulhu-eternal/templates/talent.hbs",
},
}
/** @override */
async _prepareContext() {
const context = await super._prepareContext()
context.enrichedDescription = await TextEditor.enrichHTML(this.document.system.description, { async: true })
return context
}
}

View File

@@ -0,0 +1,117 @@
import FTLNomadActorSheet from "./base-actor-sheet.mjs"
export default class FTLNomadVehicleSheet extends FTLNomadActorSheet {
/** @override */
static DEFAULT_OPTIONS = {
classes: ["vehicle"],
position: {
width: 680,
height: 540,
},
window: {
contentClasses: ["vehicle-content"],
},
actions: {
createGear: CthulhuEternalVehicleSheet.#onCreateGear,
createWeapon: CthulhuEternalVehicleSheet.#onCreateWeapon,
},
}
/** @override */
static PARTS = {
main: {
template: "systems/fvtt-cthulhu-eternal/templates/vehicle-main.hbs",
},
tabs: {
template: "templates/generic/tab-navigation.hbs",
},
equipment: {
template: "systems/fvtt-cthulhu-eternal/templates/vehicle-equipment.hbs",
},
description: {
template: "systems/fvtt-cthulhu-eternal/templates/vehicle-description.hbs",
},
}
/** @override */
tabGroups = {
sheet: "equipment",
}
/**
* Prepare an array of form header tabs.
* @returns {Record<string, Partial<ApplicationTab>>}
*/
#getTabs() {
const tabs = {
equipment: { id: "equipment", group: "sheet", icon: "fa-solid fa-shapes", label: "CTHULHUETERNAL.Label.equipment" },
description: { id: "description", group: "sheet", icon: "fa-solid fa-book", label: "CTHULHUETERNAL.Label.description" },
}
for (const v of Object.values(tabs)) {
v.active = this.tabGroups[v.group] === v.id
v.cssClass = v.active ? "active" : ""
}
return tabs
}
/** @override */
async _prepareContext() {
const context = await super._prepareContext()
context.tabs = this.#getTabs()
context.enrichedDescription = await TextEditor.enrichHTML(this.document.system.description, { async: true })
context.enrichedNotes = await TextEditor.enrichHTML(this.document.system.notes, { async: true })
return context
}
_generateTooltip(type, target) {
}
/** @override */
async _preparePartContext(partId, context) {
const doc = this.document
switch (partId) {
case "main":
break
case "equipment":
context.tab = context.tabs.equipment
context.weapons = doc.itemTypes.weapon
context.gears = doc.itemTypes.gear
break
case "description":
context.tab = context.tabs.description
context.enrichedDescription = await TextEditor.enrichHTML(doc.system.description, { async: true })
context.enrichedNotes = await TextEditor.enrichHTML(doc.system.notes, { async: true })
break
}
return context
}
/**
* Creates a new attack item directly from the sheet and embeds it into the document.
* @param {Event} event The initiating click event.
* @param {HTMLElement} target The current target of the event listener.
*/
static #onCreateGear(event, target) {
this.document.createEmbeddedDocuments("Item", [{ name: game.i18n.localize("CTHULHUETERNAL.Label.newGear"), type: "gear" }])
}
static #onCreateWeapon(event, target) {
this.document.createEmbeddedDocuments("Item", [{ name: game.i18n.localize("CTHULHUETERNAL.Label.newWeapon"), type: "weapon" }])
}
async _onDrop(event) {
if (!this.isEditable || !this.isEditMode) return
const data = TextEditor.getDragEventData(event)
// Handle different data types
switch (data.type) {
case "Item":
const item = await fromUuid(data.uuid)
return super._onDropItem(item)
}
}
}

View File

@@ -0,0 +1,21 @@
import FTLNomadItemSheet from "./base-item-sheet.mjs"
export default class FTLNomadWeaponSheet extends FTLNomadItemSheet {
/** @override */
static DEFAULT_OPTIONS = {
classes: ["weapon"],
position: {
width: 620,
},
window: {
contentClasses: ["weapon-content"],
},
}
/** @override */
static PARTS = {
main: {
template: "systems/fvtt-cthulhu-eternal/templates/weapon.hbs",
},
}
}

33
module/config/system.mjs Normal file
View File

@@ -0,0 +1,33 @@
export const SYSTEM_ID = "fvtt-ftl-nomad"
export const ASCII = `
▄████▄ ▄▄▄█████▓ ██░ ██ █ ██ ██▓ ██░ ██ █ ██ ▓█████▄▄▄█████▓▓█████ ██▀███ ███▄ █ ▄▄▄ ██▓
▒██▀ ▀█ ▓ ██▒ ▓▒▓██░ ██▒ ██ ▓██▒▓██▒ ▓██░ ██▒ ██ ▓██▒ ▓█ ▀▓ ██▒ ▓▒▓█ ▀ ▓██ ▒ ██▒ ██ ▀█ █ ▒████▄ ▓██▒
▒▓█ ▄ ▒ ▓██░ ▒░▒██▀▀██░▓██ ▒██░▒██░ ▒██▀▀██░▓██ ▒██░ ▒███ ▒ ▓██░ ▒░▒███ ▓██ ░▄█ ▒▓██ ▀█ ██▒▒██ ▀█▄ ▒██░
▒▓▓▄ ▄██▒░ ▓██▓ ░ ░▓█ ░██ ▓▓█ ░██░▒██░ ░▓█ ░██ ▓▓█ ░██░ ▒▓█ ▄░ ▓██▓ ░ ▒▓█ ▄ ▒██▀▀█▄ ▓██▒ ▐▌██▒░██▄▄▄▄██ ▒██░
▒ ▓███▀ ░ ▒██▒ ░ ░▓█▒░██▓▒▒█████▓ ░██████▒░▓█▒░██▓▒▒█████▓ ░▒████▒ ▒██▒ ░ ░▒████▒░██▓ ▒██▒▒██░ ▓██░ ▓█ ▓██▒░██████▒
░ ░▒ ▒ ░ ▒ ░░ ▒ ░░▒░▒░▒▓▒ ▒ ▒ ░ ▒░▓ ░ ▒ ░░▒░▒░▒▓▒ ▒ ▒ ░░ ▒░ ░ ▒ ░░ ░░ ▒░ ░░ ▒▓ ░▒▓░░ ▒░ ▒ ▒ ▒▒ ▓▒█░░ ▒░▓ ░
░ ▒ ░ ▒ ░▒░ ░░░▒░ ░ ░ ░ ░ ▒ ░ ▒ ░▒░ ░░░▒░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░▒ ░ ▒░░ ░░ ░ ▒░ ▒ ▒▒ ░░ ░ ▒ ░
░ ░ ░ ░░ ░ ░░░ ░ ░ ░ ░ ░ ░░ ░ ░░░ ░ ░ ░ ░ ░ ░░ ░ ░ ░ ░ ░ ▒ ░ ░
░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░
`
export const SKILLS = {
"combat": { id: "combat", label: "FTLNOMAD.Skill.Combat" },
"knowledge": { id: "knowledge", label: "FTLNOMAD.Skill.Knowledge" },
"social": { id: "social", label: "FTLNOMAD.Skill.Social" },
"physical": { id: "physical", label: "FTLNOMAD.Skill.Physical" },
"stealth": { id: "stealth", label: "FTLNOMAD.Skill.Stealth" },
"vehicles": { id: "vehicle", label: "FTLNOMAD.Skill.Vehicles" },
"technology": { id: "technology", label: "FTLNOMAD.Skill.Technology" }
}
/**
* Include all constant definitions within the SYSTEM global export
* @type {Object}
*/
export const SYSTEM = {
id: SYSTEM_ID,
SKILLS: SKILLS,
ASCII
}

View File

@@ -0,0 +1,4 @@
export { default as CthulhuEternalActor } from "./actor.mjs"
export { default as CthulhuEternalItem } from "./item.mjs"
export { default as CthulhuEternalRoll } from "./roll.mjs"
export { default as CthulhuEternalChatMessage } from "./chat-message.mjs"

View File

@@ -0,0 +1,86 @@
import CthulhuEternalUtils from "../utils.mjs"
export default class CthulhuEternalActor extends Actor {
static async create(data, options) {
// Case of compendium global import
if (data instanceof Array) {
return super.create(data, options);
}
// If the created actor has items (only applicable to duplicated actors) bypass the new actor creation logic
if (data.items) {
let actor = super.create(data, options);
return actor;
}
if (data.type === 'protagonist') {
let era = game.settings.get("fvtt-cthulhu-eternal", "settings-era")
const skills = await CthulhuEternalUtils.loadCompendium("fvtt-cthulhu-eternal.skills")
data.items = data.items || []
for (let skill of skills) {
if (skill.system.settings === era) {
data.items.push(skill.toObject())
}
}
data.items.push({ type:"weapon", img: "systems/fvtt-cthulhu-eternal/assets/icons/icon_fist.svg",
name: game.i18n.localize("CTHULHUETERNAL.Label.Unarmed"), system: { damage: "1d4-1", weaponType: "unarmed" } })
}
return super.create(data, options);
}
_onUpdate(changed, options, userId) {
// DEBUG : console.log("CthulhuEternalActor.update", changed, options, userId)
if (changed?.system?.wp?.exhausted) {
ChatMessage.create({
user: userId,
speaker: { alias: this.name },
rollMode: "selfroll",
content: game.i18n.localize("CTHULHUETERNAL.ChatMessage.exhausted"),
type: CONST.CHAT_MESSAGE_STYLES.OTHER
})
}
return super._onUpdate(changed, options, userId)
}
async createEmbeddedDocuments(embeddedName, data, operation) {
let newData = []
if (embeddedName === "Item") {
for (let i of data) {
if (i.type === "skill") {
if (this.items.find(item => item.name.toLowerCase() === i.name.toLowerCase())) {
ui.notifications.warn(game.i18n.localize("CTHULHUETERNAL.Notifications.skillAlreadyExists"))
continue
}
}
if (i.type === "bond") {
if (i.system.bondType === "individual") {
i.system.value = this.system.characteristics.cha.value
} else {
i.system.value = Math.floor(this.system.resources.permanentRating / 2)
}
}
newData.push(i)
}
return super.createEmbeddedDocuments(embeddedName, newData, operation)
}
return super.createEmbeddedDocuments(embeddedName, data, operation)
}
async _preCreate(data, options, user) {
await super._preCreate(data, options, user)
// Configure prototype token settings
const prototypeToken = {}
if (this.type === "protagonist") {
Object.assign(prototypeToken, {
sight: { enabled: true },
actorLink: true,
disposition: CONST.TOKEN_DISPOSITIONS.FRIENDLY,
})
this.updateSource({ prototypeToken })
}
}
}

View File

@@ -0,0 +1,21 @@
import CthulhuEternalRoll from "./roll.mjs"
export default class CthulhuEternalChatMessage extends ChatMessage {
async _renderRollContent(messageData) {
const data = messageData.message
if (this.rolls[0] instanceof CthulhuEternalRoll) {
const isPrivate = !this.isContentVisible
// _renderRollHTML va appeler render sur tous les rolls
const rollHTML = await this._renderRollHTML(isPrivate)
if (isPrivate) {
data.flavor = game.i18n.format("CHAT.PrivateRollContent", { user: this.user.name })
messageData.isWhisper = false
messageData.alias = this.user.name
}
data.content = `<section class="dice-rolls">${rollHTML}</section>`
return
}
return super._renderRollContent(messageData)
}
}

23
module/documents/item.mjs Normal file
View File

@@ -0,0 +1,23 @@
export const defaultItemImg = {
weapon: "systems/fvtt-cthulhu-eternal/assets/icons/icon_weapon.svg",
armor: "systems/fvtt-cthulhu-eternal/assets/icons/icon_armor.svg",
gear: "systems/fvtt-cthulhu-eternal/assets/icons/icon_equipment.svg",
skill: "systems/fvtt-cthulhu-eternal/assets/icons/icon_skill.svg",
archetype: "systems/fvtt-cthulhu-eternal/assets/icons/icon_archetype.svg",
bond: "systems/fvtt-cthulhu-eternal/assets/icons/icon_bond.svg",
mentaldisorder: "systems/fvtt-cthulhu-eternal/assets/icons/icon_mental_disorder.svg",
arcane: "systems/fvtt-cthulhu-eternal/assets/icons/icon_arcane.svg",
injury: "systems/fvtt-cthulhu-eternal/assets/icons/icon_injury.svg",
motivation: "systems/fvtt-cthulhu-eternal/assets/icons/icon_motivation.svg",
ritual: "systems/fvtt-cthulhu-eternal/assets/icons/icon_ritual.svg",
tome: "systems/fvtt-cthulhu-eternal/assets/icons/icon_tome.svg",
}
export default class CthulhuEternalItem extends Item {
constructor(data, context) {
if (!data.img) {
data.img = defaultItemImg[data.type];
}
super(data, context);
}
}

455
module/documents/roll.mjs Normal file
View File

@@ -0,0 +1,455 @@
import { SYSTEM } from "../config/system.mjs"
export default class CthulhuEternalRoll extends Roll {
/**
* The HTML template path used to render dice checks of this type
* @type {string}
*/
static CHAT_TEMPLATE = "systems/fvtt-cthulhu-eternal/templates/chat-message.hbs"
get type() {
return this.options.type
}
get isDamage() {
return this.type === ROLL_TYPE.DAMAGE
}
get target() {
return this.options.target
}
get value() {
return this.options.value
}
get actorId() {
return this.options.actorId
}
get actorName() {
return this.options.actorName
}
get actorImage() {
return this.options.actorImage
}
get help() {
return this.options.help
}
get gene() {
return this.options.gene
}
get modifier() {
return this.options.modifier
}
get resultType() {
return this.options.resultType
}
get isFailure() {
return this.resultType === "failure"
}
get hasTarget() {
return this.options.hasTarget
}
get realDamage() {
return this.options.realDamage
}
get weapon() {
return this.options.weapon
}
get isLowWP() {
return this.options.isLowWP
}
get isZeroWP() {
return this.options.isZeroWP
}
get isExhausted() {
return this.options.isExhausted
}
get isNudgedRoll() {
return this.options.isNudgedRoll
}
get wpCost() {
return this.options.wpCost
}
static updateResourceDialog(options) {
let rating = 0
if (options.rollItem.enableHand) {
rating += options.rollItem.hand
}
if (options.rollItem.enableStowed) {
rating += options.rollItem.stowed
}
if (options.rollItem.enableStorage) {
rating += options.rollItem.storage
}
let multiplier = Number($(`.roll-skill-multiplier`).val())
options.initialScore = rating
options.percentScore = rating * multiplier
$(".resource-score").text(`${rating} (${options.percentScore}%)`)
}
/**
* Prompt the user with a dialog to configure and execute a roll.
*
* @param {Object} options Configuration options for the roll.
* @param {string} options.rollType The type of roll being performed.
* @param {string} options.rollTarget The target of the roll.
* @param {string} options.actorId The ID of the actor performing the roll.
* @param {string} options.actorName The name of the actor performing the roll.
* @param {string} options.actorImage The image of the actor performing the roll.
* @param {boolean} options.hasTarget Whether the roll has a target.
* @param {Object} options.data Additional data for the roll.
*
* @returns {Promise<Object|null>} The roll result or null if the dialog was cancelled.
*/
static async prompt(options = {}) {
let formula = "1d100"
let hasModifier = true
let hasMultiplier = false
options.isNudge = true
switch (options.rollType) {
case "skill":
console.log(options.rollItem)
options.initialScore = options.rollItem.system.computeScore()
break
case "san":
case "char":
options.initialScore = options.rollItem.targetScore
options.isNudge = (options.rollType !== "san")
break
case "resource":
hasModifier = false
hasMultiplier = true
options.initialScore = options.rollItem.targetScore
options.totalRating = options.rollItem.targetScore
options.percentScore = options.rollItem.targetScore * 5
options.rollItem.enableHand = true
options.rollItem.enableStowed = true
options.rollItem.enableStorage = true
options.isNudge = false
break
case "damage":
let formula = options.rollItem.system.damage
if ( options.rollItem.system.weaponType === "melee" || options.rollItem.system.weaponType === "unarmed") {
formula += ` + ${options.rollItem.damageBonus}`
}
let damageRoll = new Roll(formula)
await damageRoll.evaluate()
await damageRoll.toMessage({
flavor: `${options.rollItem.name} - Damage Roll`
});
let isLethal = false
options.isNudge = false
if (options.rollItem.system.lethality > 0) {
let lethalityRoll = new Roll("1d100")
await lethalityRoll.evaluate()
isLethal = (lethalityRoll.total <= options.rollItem.system.lethality)
await lethalityRoll.toMessage({
flavor: `${options.rollItem.name} - Lethality Roll : ${lethalityRoll.total} <= ${options.rollItem.system.lethality} => ${isLethal}`
});
}
return
case "weapon":
let era = game.settings.get("fvtt-cthulhu-eternal", "settings-era")
if (era !== options.rollItem.system.settings) {
ui.notifications.error(game.i18n.localize("CTHULHUETERNAL.Notifications.WrongEra"))
console.log("WP Wrong Era", era, options.rollItem.system.weaponType)
return
}
if (!SYSTEM.WEAPON_SKILL_MAPPING[era] || !SYSTEM.WEAPON_SKILL_MAPPING[era][options.rollItem.system.weaponType]) {
ui.notifications.error(game.i18n.localize("CTHULHUETERNAL.Notifications.NoWeaponType"))
console.log("WP Not found", era, options.rollItem.system.weaponType)
return
}
options.weapon = options.rollItem
if (options.rollItem.system.hasDirectSkill) {
let skillName = options.rollItem.name
options.rollItem = {type: "skill", name: skillName, system: {base: 0, bonus: options.weapon.system.directSkillValue} }
options.initialScore = options.weapon.system.directSkillValue
} else {
let skillName = game.i18n.localize(SYSTEM.WEAPON_SKILL_MAPPING[era][options.rollItem.system.weaponType])
let actor = game.actors.get(options.actorId)
options.rollItem = actor.items.find(i => i.type === "skill" && i.name.toLowerCase() === skillName.toLowerCase())
if (!options.rollItem) {
ui.notifications.error(game.i18n.localize("CTHULHUETERNAL.Notifications.NoWeaponSkill"))
return
}
options.initialScore = options.rollItem.system.computeScore()
console.log("WEAPON", skillName, era, options.rollItem)
}
break
default:
options.initialScore = 50
break
}
const rollModes = Object.fromEntries(Object.entries(CONFIG.Dice.rollModes).map(([key, value]) => [key, game.i18n.localize(value)]))
const fieldRollMode = new foundry.data.fields.StringField({
choices: rollModes,
blank: false,
default: "public",
})
const choiceModifier = SYSTEM.MODIFIER_CHOICES
const choiceMultiplier = SYSTEM.MULTIPLIER_CHOICES
let modifier = "+0"
let multiplier = "5"
let dialogContext = {
rollType: options.rollType,
rollItem: foundry.utils.duplicate(options.rollItem), // Object only, no class
weapon: options?.weapon,
initialScore: options.initialScore,
targetScore: options.initialScore,
isLowWP: options.isLowWP,
isZeroWP: options.isZeroWP,
isExhausted: options.isExhausted,
enableHand: options.rollItem.enableHand,
enableStowed: options.rollItem.enableStowed,
enableStorage: options.rollItem.enableStorage,
rollModes,
fieldRollMode,
choiceModifier,
choiceMultiplier,
formula,
hasTarget: options.hasTarget,
hasModifier,
hasMultiplier,
modifier,
multiplier
}
const content = await renderTemplate("systems/fvtt-cthulhu-eternal/templates/roll-dialog.hbs", dialogContext)
const title = CthulhuEternalRoll.createTitle(options.rollType, options.rollTarget)
const label = game.i18n.localize("CTHULHUETERNAL.Roll.roll")
const rollContext = await foundry.applications.api.DialogV2.wait({
window: { title: title },
classes: ["fvtt-cthulhu-eternal"],
content,
buttons: [
{
label: label,
callback: (event, button, dialog) => {
const output = Array.from(button.form.elements).reduce((obj, input) => {
if (input.name) obj[input.name] = input.value
return obj
}, {})
return output
},
},
],
actions: {
"selectHand": (event, button, dialog) => {
options.rollItem.enableHand = !options.rollItem.enableHand
this.updateResourceDialog(options)
},
"selectStowed": (event, button, dialog) => {
options.rollItem.enableStowed = !options.rollItem.enableStowed
this.updateResourceDialog(options)
},
"selectStorage": (event, button, dialog) => {
options.rollItem.enableStorage = !options.rollItem.enableStorage
this.updateResourceDialog(options)
}
},
rejectClose: false, // Click on Close button will not launch an error
render: (event, dialog) => {
$(".roll-skill-multiplier").change(event => {
options.multiplier = Number(event.target.value)
this.updateResourceDialog(options)
})
}
})
// If the user cancels the dialog, exit
if (rollContext === null) return
let rollData = foundry.utils.mergeObject(foundry.utils.duplicate(options), rollContext)
rollData.rollMode = rollContext.visibility
// Update target score
console.log("Rolldata", rollData, options)
if (options.rollType === "resource") {
rollData.targetScore = options.initialScore * Number(rollContext.multiplier)
} else {
rollData.targetScore = Math.min(Math.max(options.initialScore + Number(rollData.modifier), 0), 100)
if (rollData.isLowWP || rollData.isExhausted) {
rollData.targetScore -= 20
}
if (rollData.isZeroWP) {
rollData.targetScore = 0
}
rollData.targetScore = Math.min(Math.max(rollData.targetScore, 0), 100)
}
if (Hooks.call("fvtt-cthulhu-eternal.preRoll", options, rollData) === false) return
const roll = new this(formula, options.data, rollData)
await roll.evaluate()
roll.displayRollResult(roll, options, rollData)
if (Hooks.call("fvtt-cthulhu-eternal.Roll", options, rollData, roll) === false) return
return roll
}
displayRollResult(formula, options, rollData) {
// Compute the result quality
let resultType = "failure"
let dec = Math.floor(this.total / 10)
let unit = this.total - (dec * 10)
if (this.total <= rollData.targetScore) {
resultType = "success"
// Detect if decimal == unit in the dire total result
if (dec === unit || this.total === 1) {
resultType = "successCritical"
}
} else {
// Detect if decimal == unit in the dire total result
if (dec === unit || this.total === 100) {
resultType = "failureCritical"
}
}
this.options.resultType = resultType
if (this.options.isNudgedRoll) {
this.options.isSuccess = resultType === "success" || resultType === "successCritical"
this.options.isFailure = resultType === "failure" || resultType === "failureCritical"
this.options.isCritical = false
} else {
this.options.isSuccess = resultType === "success" || resultType === "successCritical"
this.options.isFailure = resultType === "failure" || resultType === "failureCritical"
this.options.isCritical = resultType === "successCritical" || resultType === "failureCritical"
}
this.options.isLowWP = rollData.isLowWP
this.options.isZeroWP = rollData.isZeroWP
this.options.isExhausted = rollData.isExhausted
this.options.rollData = foundry.utils.duplicate(rollData)
}
/**
* Creates a title based on the given type.
*
* @param {string} type The type of the roll.
* @param {string} target The target of the roll.
* @returns {string} The generated title.
*/
static createTitle(type, target) {
switch (type) {
case "skill":
return `${game.i18n.localize("CTHULHUETERNAL.Label.titleSkill")}`
case "weapon":
return `${game.i18n.localize("CTHULHUETERNAL.Label.titleWeapon")}`
case "char":
return `${game.i18n.localize("CTHULHUETERNAL.Label.titleCharacteristic")}`
case "san":
return `${game.i18n.localize("CTHULHUETERNAL.Label.titleSAN")}`
default:
return game.i18n.localize("CTHULHUETERNAL.Label.titleStandard")
}
}
/** @override */
async render(chatOptions = {}) {
let chatData = await this._getChatCardData(chatOptions.isPrivate)
return await renderTemplate(this.constructor.CHAT_TEMPLATE, chatData)
}
/**
* Generates the data required for rendering a roll chat card.
*
* @param {boolean} isPrivate Indicates if the chat card is private.
* @returns {Promise<Object>} A promise that resolves to an object containing the chat card data.
* @property {Array<string>} css - CSS classes for the chat card.
* @property {Object} data - The data associated with the roll.
* @property {number} diceTotal - The total value of the dice rolled.
* @property {boolean} isGM - Indicates if the user is a Game Master.
* @property {string} formula - The formula used for the roll.
* @property {number} total - The total result of the roll.
* @property {boolean} isFailure - Indicates if the roll is a failure.
* @property {string} actorId - The ID of the actor performing the roll.
* @property {string} actingCharName - The name of the character performing the roll.
* @property {string} actingCharImg - The image of the character performing the roll.
* @property {string} resultType - The type of result (e.g., success, failure).
* @property {boolean} hasTarget - Indicates if the roll has a target.
* @property {string} targetName - The name of the target.
* @property {number} targetArmor - The armor value of the target.
* @property {number} realDamage - The real damage dealt.
* @property {boolean} isPrivate - Indicates if the chat card is private.
* @property {string} cssClass - The combined CSS classes as a single string.
* @property {string} tooltip - The tooltip text for the chat card.
*/
async _getChatCardData(isPrivate) {
let cardData = foundry.utils.duplicate(this.options)
cardData.css = [SYSTEM.id, "dice-roll"]
cardData.data = this.data
cardData.diceTotal = this.dice.reduce((t, d) => t + d.total, 0)
cardData.isGM = game.user.isGM
cardData.formula = this.formula
cardData.total = this.total
cardData.actorId = this.actorId
cardData.actingCharName = this.actorName
cardData.actingCharImg = this.actorImage
cardData.resultType = this.resultType
cardData.hasTarget = this.hasTarget
cardData.targetName = this.targetName
cardData.targetArmor = this.targetArmor
cardData.realDamage = this.realDamage
cardData.isPrivate = isPrivate
cardData.weapon = this.weapon
cardData.isLowWP = this.isLowWP
cardData.isZeroWP = this.isZeroWP
cardData.isExhausted = this.isExhausted
cardData.isNudgedRoll = this.isNudgedRoll
cardData.wpCost = this.wpCost
cardData.cssClass = cardData.css.join(" ")
cardData.tooltip = isPrivate ? "" : await this.getTooltip()
return cardData
}
/**
* Converts the roll result to a chat message.
*
* @param {Object} [messageData={}] Additional data to include in the message.
* @param {Object} options Options for message creation.
* @param {string} options.rollMode The mode of the roll (e.g., public, private).
* @param {boolean} [options.create=true] Whether to create the message.
* @returns {Promise} - A promise that resolves when the message is created.
*/
async toMessage(messageData = {}, { rollMode, create = true } = {}) {
super.toMessage(
{
isFailure: this.resultType === "failure",
actingCharName: this.actorName,
actingCharImg: this.actorImage,
hasTarget: this.hasTarget,
realDamage: this.realDamage,
...messageData,
},
{ rollMode: rollMode },
)
}
}

10
module/models/_module.mjs Normal file
View File

@@ -0,0 +1,10 @@
export { default as FTLNomadWeapon } from "./weapon.mjs"
export { default as FTLNomadPsionic } from "./psionic.mjs"
export { default as FTLNomadLanguage } from "./language.mjs"
export { default as FTLNomadTalent } from "./talent.mjs"
export { default as FTLNomadArmor } from "./armor.mjs"
export { default as FTLNomadNPC } from "./npc.mjs"
export { default as FTLNomadVehicle } from "./vehicle.mjs"
export { default as FTLNomadCharacter } from "./character.mjs"
export { default as FTLNomadEquipment } from "./equipment.mjs"

21
module/models/armor.mjs Normal file
View File

@@ -0,0 +1,21 @@
import { SYSTEM } from "../config/system.mjs"
export default class FTLNomadArmor extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields
const schema = {}
const requiredInteger = { required: true, nullable: false, integer: true }
schema.description = new fields.HTMLField({ required: true, textSearch: true })
let setting = game.settings.get("fvtt-cthulhu-eternal", "settings-era") || "modern"
schema.settings = new fields.StringField({ required: true, initial: setting, choices: SYSTEM.AVAILABLE_SETTINGS })
schema.protection = new fields.NumberField({ ...requiredInteger, required: true, initial: 0, min: 0 })
schema.resourceLevel = new fields.NumberField({ required: true, initial: 0, min: 0 })
return schema
}
/** @override */
static LOCALIZATION_PREFIXES = ["FTLNOMAD.Armor"]
}

232
module/models/character.mjs Normal file
View File

@@ -0,0 +1,232 @@
import { SYSTEM } from "../config/system.mjs"
import FTLNomadRoll from "../documents/roll.mjs"
export default class FTLNomadProtagonist extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields
const requiredInteger = { required: true, nullable: false, integer: true }
const schema = {}
schema.description = new fields.HTMLField({ required: true, textSearch: true })
schema.notes = new fields.HTMLField({ required: true, textSearch: true })
schema.name = new fields.StringField({ required: true, nullable: false, initial: "" })
schema.concept = new fields.StringField({ required: true, nullable: false, initial: "" })
schema.species = new fields.StringField({ required: true, nullable: false, initial: "" })
schema.archetype = new fields.StringField({ required: true, nullable: false, initial: "" })
// Carac
const skillField = (label) => {
const schema = {
value: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }),
}
return new fields.SchemaField(schema, { label })
}
schema.skills = new fields.SchemaField(
Object.values(SYSTEM.SKILLS).reduce((obj, characteristic) => {
obj[characteristic.id] = skillField(characteristic.label)
return obj
}, {}),
)
schema.wp = new fields.SchemaField({
value: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }),
max: new fields.NumberField({ ...requiredInteger, initial: 3, min: 0 }),
exhausted: new fields.BooleanField({ required: true, initial: false })
})
schema.hp = new fields.SchemaField({
value: new fields.NumberField({ ...requiredInteger, initial: 1, min: 0 }),
max: new fields.NumberField({ ...requiredInteger, initial: 1, min: 0 }),
stunned: new fields.BooleanField({ required: true, initial: false })
})
schema.san = new fields.SchemaField({
value: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }),
max: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }),
recovery: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }),
violence: new fields.ArrayField(new fields.BooleanField(), { required: true, initial: [false, false, false], min:3, max:3}),
helplessness: new fields.ArrayField(new fields.BooleanField(), { required: true, initial: [false, false, false], min:3, max:3 }),
breakingPoint: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }),
insanity: new fields.StringField({ required: true, nullable: false, initial: "none", choices:SYSTEM.INSANITY }),
})
schema.damageBonus = new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 })
schema.resources = new fields.SchemaField({
value: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }), // Unused but kept for compatibility
permanentRating: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }),
hand: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }),
currentHand: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }),
stowed: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }),
currentStowed: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }),
storage: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }),
currentStorage: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }),
checks: new fields.ArrayField(new fields.BooleanField(), { required: true, initial: [false, false, false], min:3, max:3 }),
nbValidChecks: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 })
})
schema.biodata = new fields.SchemaField({
age: new fields.NumberField({ ...requiredInteger, initial: 15, min: 6 }),
archetype: new fields.StringField({ required: true, nullable: false, initial: "" }),
height: new fields.NumberField({ ...requiredInteger, initial: 170, min: 50 }),
gender: new fields.StringField({ required: true, nullable: false, initial: "" }),
home: new fields.StringField({ required: true, nullable: false, initial: "" }),
birthplace: new fields.StringField({ required: true, nullable: false, initial: "" }),
eyes: new fields.StringField({ required: true, nullable: false, initial: "" }),
hair: new fields.StringField({ required: true, nullable: false, initial: "" }),
harshness: new fields.StringField({ required: true, nullable: false, initial: "normal", choices:SYSTEM.HARSHNESS }),
adaptedToViolence: new fields.BooleanField({ required: true, initial: false }),
adaptedToHelplessness: new fields.BooleanField({ required: true, initial: false })
})
return schema
}
/** @override */
static LOCALIZATION_PREFIXES = ["FTLNOMAD.Protagonist"]
prepareDerivedData() {
super.prepareDerivedData();
let updates = {}
if ( this.wp.max !== this.characteristics.pow.value) {
updates[`system.wp.max`] = this.characteristics.pow.value
}
let hpMax = Math.round((this.characteristics.con.value + this.characteristics.str.value) / 2)
if ( this.hp.max !== hpMax) {
updates[`system.hp.max`] = hpMax
}
// Get Unnatural skill for MAX SAN
let unnatural = this.parent.items.find(i => i.type === "skill" && i.name.toLowerCase() === game.i18n.localize("CTHULHUETERNAL.Skill.Unnatural").toLowerCase())
let minus = 0
if (unnatural) {
minus = unnatural.system.skillTotal
}
let maxSan = Math.max(99 - minus, 0)
if ( this.san.max !== maxSan) {
updates[`system.san.max`] = maxSan
}
let recoverySan = this.characteristics.pow.value * 5
if (recoverySan > this.san.max) {
recoverySan = this.san.max
}
if ( this.san.recovery !== recoverySan) {
updates[`system.san.recovery`] = recoverySan
}
let dmgBonus = 0
if (this.characteristics.str.value <= 4) {
dmgBonus = -2
} else if (this.characteristics.str.value <= 8) {
dmgBonus = -1
} else if (this.characteristics.str.value <= 12) {
dmgBonus = 0
} else if (this.characteristics.str.value <= 16) {
dmgBonus = 1
} else if (this.characteristics.str.value <= 20) {
dmgBonus = 2
}
if ( this.damageBonus !== dmgBonus) {
updates[`system.damageBonus`] = dmgBonus
}
// Sanity check
if (this.san.value > this.san.max) {
updates[`system.san.value`] = this.san.max
}
if (this.wp.value > this.wp.max) {
updates[`system.wp.value`] = this.wp.max
}
if (this.hp.value > this.hp.max) {
updates[`system.hp.value`] = this.hp.max
}
if (this.resources.permanentRating < 0) {
updates[`system.resources.permanentRating`] = 0
}
let resourceIndex = Math.max(Math.min(this.resources.permanentRating, 20), 0)
let breakdown = SYSTEM.RESOURCE_BREAKDOWN[resourceIndex]
if (this.resources.hand !== breakdown.hand) {
updates[`system.resources.hand`] = breakdown.hand
}
if (this.resources.stowed !== breakdown.stowed) {
updates[`system.resources.stowed`] = breakdown.stowed
}
if (this.resources.storage !== breakdown.storage) {
updates[`system.resources.storage`] = breakdown.storage + (this.resources.permanentRating - resourceIndex)
}
if (this.resources.nbValidChecks !== breakdown.checks) {
updates[`system.resources.nbValidChecks`] = breakdown.checks
}
if (Object.keys(updates).length > 0) {
this.parent.update(updates)
}
}
isLowWP() {
return this.wp.value <= 2
}
isZeroWP() {
return this.wp.value === 0
}
isExhausted() {
return this.wp.exhausted
}
modifyWP(value) {
let updates = {}
let wp = Math.max(Math.min(this.wp.value + value, this.wp.max), 0)
if ( this.wp.value !== wp) {
updates[`system.wp.value`] = wp
}
if (Object.keys(updates).length > 0) {
this.parent.update(updates)
}
}
setBP() {
let updates = {}
let bp = Math.max(this.san.value - this.characteristics.pow.value, 0)
if ( this.san.breakingPoint !== bp) {
updates[`system.san.breakingPoint`] = bp
}
if (Object.keys(updates).length > 0) {
this.parent.update(updates)
}
}
/** */
/**
* Rolls a dice for a character.
* @param {("save"|"resource|damage")} rollType The type of the roll.
* @param {number} rollItem The target value for the roll. Which caracteristic or resource. If the roll is a damage roll, this is the id of the item.
* @returns {Promise<null>} - A promise that resolves to null if the roll is cancelled.
*/
async roll(rollType, rollItem) {
let opponentTarget
const hasTarget = opponentTarget !== undefined
let roll = await CthulhuEternalRoll.prompt({
rollType,
rollItem,
isLowWP: this.isLowWP(),
isZeroWP: this.isZeroWP(),
isExhausted: this.isExhausted(),
actorId: this.parent.id,
actorName: this.parent.name,
actorImage: this.parent.img,
hasTarget,
target: opponentTarget
})
if (!roll) return null
await roll.toMessage({}, { rollMode: roll.options.rollMode })
}
}

View File

@@ -0,0 +1,23 @@
import { SYSTEM } from "../config/system.mjs"
export default class FTLNomadEquipment extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields
const schema = {}
const requiredInteger = { required: true, nullable: false, integer: true }
schema.description = new fields.HTMLField({ required: true, textSearch: true })
let setting = game.settings.get("fvtt-cthulhu-eternal", "settings-era") || "modern"
schema.settings = new fields.StringField({ required: true, initial: setting, choices: SYSTEM.AVAILABLE_SETTINGS })
schema.resourceLevel = new fields.NumberField({ required: true, initial: 0, min: 0 })
schema.state = new fields.StringField({ required: true, initial: "pristine", choices: SYSTEM.EQUIPMENT_STATES })
return schema
}
/** @override */
static LOCALIZATION_PREFIXES = ["FTLNOMAD.Equipment"]
}

View File

@@ -0,0 +1,20 @@
import { SYSTEM } from "../config/system.mjs"
export default class FTLNomadLanguage extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields
const requiredInteger = { required: true, nullable: false, integer: true }
const schema = {}
schema.description = new fields.HTMLField({
required: false,
blank: true,
initial: "",
textSearch: true,
})
return schema
}
/** @override */
static LOCALIZATION_PREFIXES = ["FTLNOMAD.Language"]
}

80
module/models/npc.mjs Normal file
View File

@@ -0,0 +1,80 @@
import { SYSTEM } from "../config/system.mjs"
import FTLNomadRoll from "../documents/roll.mjs"
export default class FTLNomadNPC extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields
const requiredInteger = { required: true, nullable: false, integer: true }
const schema = {}
// Carac
const characteristicField = (label) => {
const schema = {
value: new fields.NumberField({ ...requiredInteger, initial: 3, min: 0 }),
feature: new fields.StringField({ required: true, nullable: false, initial: "" })
}
return new fields.SchemaField(schema, { label })
}
schema.characteristics = new fields.SchemaField(
Object.values(SYSTEM.CHARACTERISTICS).reduce((obj, characteristic) => {
obj[characteristic.id] = characteristicField(characteristic.label)
return obj
}, {}),
)
schema.wp = new fields.SchemaField({
value: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }),
max: new fields.NumberField({ ...requiredInteger, initial: 3, min: 0 }),
exhausted: new fields.BooleanField({ required: true, initial: false })
})
schema.hp = new fields.SchemaField({
value: new fields.NumberField({ ...requiredInteger, initial: 1, min: 0 }),
max: new fields.NumberField({ ...requiredInteger, initial: 1, min: 0 })
})
schema.movement = new fields.StringField({ required: true, initial: "" })
schema.sanLoss = new fields.StringField({ required: true, initial: "1/1D6" })
schema.armor = new fields.StringField({ required: true, initial: "" })
schema.size = new fields.StringField({ required: true, initial: "medium", choices: SYSTEM.CREATURE_SIZE })
schema.damageBonus = new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 })
schema.description = new fields.HTMLField({ required: true, textSearch: true })
schema.notes = new fields.HTMLField({ required: true, textSearch: true })
return schema
}
/** @override */
static LOCALIZATION_PREFIXES = ["FTLNOMAD.NPC"]
/** */
/**
* Rolls a dice for a character.
* @param {("save"|"resource|damage")} rollType The type of the roll.
* @param {number} rollItem The target value for the roll. Which caracteristic or resource. If the roll is a damage roll, this is the id of the item.
* @returns {Promise<null>} - A promise that resolves to null if the roll is cancelled.
*/
async roll(rollType, rollItem) {
let opponentTarget
const hasTarget = opponentTarget !== undefined
let roll = await CthulhuEternalRoll.prompt({
rollType,
rollItem,
isLowWP: false,
isZeroWP: false,
isExhausted: false,
actorId: this.parent.id,
actorName: this.parent.name,
actorImage: this.parent.img,
hasTarget,
target: opponentTarget
})
if (!roll) return null
await roll.toMessage({}, { rollMode: roll.options.rollMode })
}
}

26
module/models/psionic.mjs Normal file
View File

@@ -0,0 +1,26 @@
import { SYSTEM } from "../config/system.mjs"
export default class FTLNomadPsionic extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields
const schema = {}
const requiredInteger = { required: true, nullable: false, integer: true }
schema.description = new fields.HTMLField({ required: true, textSearch: true })
let setting = game.settings.get("fvtt-cthulhu-eternal", "settings-era") || "modern"
schema.settings = new fields.StringField({ required: true, initial: setting, choices: SYSTEM.AVAILABLE_SETTINGS })
schema.base = new fields.StringField({ required: true, initial: "0" })
schema.bonus = new fields.NumberField({ ...requiredInteger, required: true, initial: 0, min: 0 })
schema.diceEvolved = new fields.BooleanField({ required: true, initial: true })
schema.rollFailed = new fields.BooleanField({ required: true, initial: false })
schema.isAdversary = new fields.BooleanField({ required: true, initial: false })
return schema
}
/** @override */
static LOCALIZATION_PREFIXES = ["FTLNOMAD.Psionic"]
}

64
module/models/talent.mjs Normal file
View File

@@ -0,0 +1,64 @@
import { SYSTEM } from "../config/system.mjs";
export default class FTLNomadTalent extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
const schema = {};
let setting = game.settings.get("fvtt-cthulhu-eternal", "settings-era") || "modern"
schema.minimumEra = new fields.StringField({ required: true, initial: setting, choices: SYSTEM.AVAILABLE_SETTINGS })
schema.creationDate = new fields.StringField({
required: true,
initial: "",
textSearch: true
});
// Language field
schema.language = new fields.StringField({
required: true,
initial: "Latin",
textSearch: true
});
// studyTime field
schema.studyTime = new fields.StringField({
required: true,
initial: "X days",
textSearch: true
});
// SAN loss field
schema.sanLoss = new fields.StringField({
required: true,
initial: "1d4",
textSearch: true
});
// Unnatural skill field
schema.unnaturalSkill = new fields.StringField({
required: true,
initial: "1d4",
textSearch: true
});
schema.rituals = new fields.StringField({
required: true,
initial: "",
textSearch: true
});
schema.otherBenefits = new fields.StringField({
required: true,
initial: "",
textSearch: true
});
schema.description = new fields.HTMLField({ required: true, textSearch: true })
return schema;
}
/** @override */
static LOCALIZATION_PREFIXES = ["FTLNomad.Talent"];
}

35
module/models/vehicle.mjs Normal file
View File

@@ -0,0 +1,35 @@
import { SYSTEM } from "../config/system.mjs"
import FTLNomadRoll from "../documents/roll.mjs"
export default class CthulhuEternalVehicle extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields
const requiredInteger = { required: true, nullable: false, integer: true }
const schema = {}
let setting = game.settings.get("fvtt-cthulhu-eternal", "settings-era") || "modern"
schema.settings = new fields.StringField({ required: true, initial: setting, choices: SYSTEM.AVAILABLE_SETTINGS })
schema.hp = new fields.SchemaField({
value: new fields.NumberField({ ...requiredInteger, initial: 1, min: 0 }),
max: new fields.NumberField({ ...requiredInteger, initial: 1, min: 0 })
})
schema.armor = new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 })
schema.surfaceSpeed = new fields.StringField({ required: true, initial: "slow", choices: SYSTEM.VEHICLE_SPEED })
schema.airSpeed = new fields.StringField({ required: true, initial: "none", choices: SYSTEM.VEHICLE_SPEED })
schema.state = new fields.StringField({ required: true, initial: "pristine", choices: SYSTEM.EQUIPMENT_STATES })
schema.crew = new fields.ArrayField(new fields.StringField(), { required: false, initial: [], min:0 })
schema.resourceLevel = new fields.NumberField({ required: true, initial: 0, min: 0 })
schema.description = new fields.HTMLField({ required: true, textSearch: true })
schema.notes = new fields.HTMLField({ required: true, textSearch: true })
return schema
}
/** @override */
static LOCALIZATION_PREFIXES = ["FTLNomad.Vehicle"]
}

35
module/models/weapon.mjs Normal file
View File

@@ -0,0 +1,35 @@
import { SYSTEM } from "../config/system.mjs"
export default class FTLNomadWeapon extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields
const schema = {}
const requiredInteger = { required: true, nullable: false, integer: true }
schema.description = new fields.HTMLField({ required: true, textSearch: true })
let setting = game.settings.get("fvtt-cthulhu-eternal", "settings-era") || "modern"
schema.settings = new fields.StringField({ required: true, initial: setting, choices: SYSTEM.AVAILABLE_SETTINGS })
schema.weaponType = new fields.StringField({ required: true, initial: "melee", choices: SYSTEM.WEAPON_TYPE })
schema.hasDirectSkill = new fields.BooleanField({ required: true, initial: false })
schema.directSkillValue = new fields.NumberField({ required: true, initial: 0, min: 0, max:99 })
schema.damage = new fields.StringField({required: true, initial: "1d6"})
schema.baseRange = new fields.StringField({required: true, initial: ""})
schema.rangeUnit = new fields.StringField({ required: true, initial: "yard", choices: SYSTEM.WEAPON_RANGE_UNIT })
schema.lethality = new fields.NumberField({ required: true, initial: 0, min: 0 })
schema.killRadius = new fields.NumberField({ required: true, initial: 0, min: 0 })
schema.armorPiercing = new fields.NumberField({ required: true, initial: 0, min: 0 })
schema.weaponSubtype = new fields.StringField({ required: true, initial: "basicfirearm", choices: SYSTEM.WEAPON_SUBTYPE })
schema.state = new fields.StringField({ required: true, initial: "pristine", choices: SYSTEM.EQUIPMENT_STATES })
schema.resourceLevel = new fields.NumberField({ required: true, initial: 0, min: 0 })
return schema
}
/** @override */
static LOCALIZATION_PREFIXES = ["FTLNOMAD.Weapon"]
}

13
module/socket.mjs Normal file
View File

@@ -0,0 +1,13 @@
/**
* Handles socket events based on the provided action.
*
* @param {Object} [params={}] The parameters for the socket event.
* @param {string|null} [params.action=null] The action to be performed.
* @param {Object} [params.data={}] The data associated with the action.
* @returns {*} The result of the action handler, if applicable.
*/
export function handleSocketEvent({ action = null, data = {} } = {}) {
console.debug("handleSocketEvent", action, data)
}

200
module/utils.mjs Normal file
View File

@@ -0,0 +1,200 @@
import CthulhuEternalRoll from "./documents/roll.mjs"
import { SystemManager } from './applications/hud/system-manager.js'
import { SYSTEM } from "./config/system.mjs"
export default class FTLNomadUtils {
static registerSettings() {
game.settings.register("fvtt-ftl-nomad", "settings-era", {
name: game.i18n.localize("FTLNOMAD.Settings.era"),
hint: game.i18n.localize("FTLNOMAD.Settings.eraHint"),
default: "jazz",
scope: "world",
type: String,
choices: SYSTEM.AVAILABLE_SETTINGS,
config: true,
onChange: _ => window.location.reload()
});
}
static async loadCompendiumData(compendium) {
const pack = game.packs.get(compendium)
return await pack?.getDocuments() ?? []
}
static async loadCompendium(compendium, filter = item => true) {
let compendiumData = await FTLNomadUtils.loadCompendiumData(compendium)
return compendiumData.filter(filter)
}
static registerHandlebarsHelpers() {
Handlebars.registerHelper('isNull', function (val) {
return val == null;
});
Handlebars.registerHelper('exists', function (val) {
return val != null && val !== undefined;
});
Handlebars.registerHelper('isEmpty', function (list) {
if (list) return list.length === 0;
else return false;
});
Handlebars.registerHelper('notEmpty', function (list) {
return list.length > 0;
});
Handlebars.registerHelper('isNegativeOrNull', function (val) {
return val <= 0;
});
Handlebars.registerHelper('isNegative', function (val) {
return val < 0;
});
Handlebars.registerHelper('isPositive', function (val) {
return val > 0;
});
Handlebars.registerHelper('equals', function (val1, val2) {
return val1 === val2;
});
Handlebars.registerHelper('neq', function (val1, val2) {
return val1 !== val2;
});
Handlebars.registerHelper('gt', function (val1, val2) {
return val1 > val2;
})
Handlebars.registerHelper('lt', function (val1, val2) {
return val1 < val2;
})
Handlebars.registerHelper('gte', function (val1, val2) {
return val1 >= val2;
})
Handlebars.registerHelper('lte', function (val1, val2) {
return val1 <= val2;
})
Handlebars.registerHelper('and', function (val1, val2) {
return val1 && val2;
})
Handlebars.registerHelper('or', function (val1, val2) {
return val1 || val2;
})
Handlebars.registerHelper('or3', function (val1, val2, val3) {
return val1 || val2 || val3;
})
Handlebars.registerHelper('for', function (from, to, incr, block) {
let accum = '';
for (let i = from; i < to; i += incr)
accum += block.fn(i);
return accum;
})
Handlebars.registerHelper('not', function (cond) {
return !cond;
})
Handlebars.registerHelper('count', function (list) {
return list.length;
})
Handlebars.registerHelper('countKeys', function (obj) {
return Object.keys(obj).length;
})
Handlebars.registerHelper('isEnabled', function (configKey) {
return game.settings.get("bol", configKey);
})
Handlebars.registerHelper('split', function (str, separator, keep) {
return str.split(separator)[keep];
})
// If you need to add Handlebars helpers, here are a few useful examples:
Handlebars.registerHelper('concat', function () {
let outStr = '';
for (let arg in arguments) {
if (typeof arguments[arg] != 'object') {
outStr += arguments[arg];
}
}
return outStr;
})
Handlebars.registerHelper('add', function (a, b) {
return parseInt(a) + parseInt(b);
});
Handlebars.registerHelper('mul', function (a, b) {
return parseInt(a) * parseInt(b);
})
Handlebars.registerHelper('sub', function (a, b) {
return parseInt(a) - parseInt(b);
})
Handlebars.registerHelper('abbrev2', function (a) {
return a.substring(0, 2);
})
Handlebars.registerHelper('abbrev3', function (a) {
return a.substring(0, 3);
})
Handlebars.registerHelper('valueAtIndex', function (arr, idx) {
return arr[idx];
})
Handlebars.registerHelper('includesKey', function (items, type, key) {
return items.filter(i => i.type === type).map(i => i.system.key).includes(key);
})
Handlebars.registerHelper('includes', function (array, val) {
return array.includes(val);
})
Handlebars.registerHelper('eval', function (expr) {
return eval(expr);
})
Handlebars.registerHelper('isOwnerOrGM', function (actor) {
console.log("Testing actor", actor.isOwner, game.userId)
return actor.isOwner || game.isGM;
})
Handlebars.registerHelper('upperFirst', function (text) {
if (typeof text !== 'string') return text
return text.charAt(0).toUpperCase() + text.slice(1)
})
Handlebars.registerHelper('upperFirstOnly', function (text) {
if (typeof text !== 'string') return text
return text.charAt(0).toUpperCase()
})
Handlebars.registerHelper('isCreature', function (key) {
return key === "creature" || key === "daemon";
})
// Handle v12 removal of this helper
Handlebars.registerHelper('select', function (selected, options) {
const escapedValue = RegExp.escape(Handlebars.escapeExpression(selected));
const rgx = new RegExp(' value=[\"\']' + escapedValue + '[\"\']');
const html = options.fn(this);
return html.replace(rgx, "$& selected");
});
}
static setupCSSRootVariables() {
const era = game.settings.get("fvtt-cthulhu-eternal", "settings-era")
let eraCSS = SYSTEM.ERA_CSS[era];
if (!eraCSS) eraCSS = SYSTEM.ERA_CSS["jazz"];
document.documentElement.style.setProperty('--font-size-standard', eraCSS.baseFontSize);
document.documentElement.style.setProperty('--font-size-title', eraCSS.titleFontSize);
document.documentElement.style.setProperty('--font-size-result', eraCSS.titleFontSize);
document.documentElement.style.setProperty('--font-primary', eraCSS.primaryFont);
document.documentElement.style.setProperty('--font-secondary', eraCSS.secondaryFont);
document.documentElement.style.setProperty('--font-title', eraCSS.titleFont);
document.documentElement.style.setProperty('--img-icon-color-filter', eraCSS.imgFilter);
document.documentElement.style.setProperty('--background-image-base', `linear-gradient(rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0.8)), url("../assets/ui/${era}_background_main.webp")`);
}
}

1
node_modules/.bin/acorn generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../acorn/bin/acorn

1
node_modules/.bin/errno generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../errno/cli.js

1
node_modules/.bin/eslint generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../eslint/bin/eslint.js

1
node_modules/.bin/eslint-config-prettier generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../eslint-config-prettier/bin/cli.js

1
node_modules/.bin/fvtt generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../@foundryvtt/foundryvtt-cli/fvtt.mjs

1
node_modules/.bin/gulp generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../gulp/bin/gulp.js

1
node_modules/.bin/image-size generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../image-size/bin/image-size.js

1
node_modules/.bin/js-yaml generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../js-yaml/bin/js-yaml.js

1
node_modules/.bin/lessc generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../less/bin/lessc

1
node_modules/.bin/mime generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../mime/cli.js

1
node_modules/.bin/mkdirp generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../mkdirp/dist/cjs/src/bin.js

1
node_modules/.bin/needle generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../needle/bin/needle

1
node_modules/.bin/node-gyp-build generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../node-gyp-build/bin.js

1
node_modules/.bin/node-gyp-build-optional generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../node-gyp-build/optional.js

1
node_modules/.bin/node-gyp-build-test generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../node-gyp-build/build-test.js

1
node_modules/.bin/node-which generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../which/bin/node-which

Some files were not shown because too many files have changed in this diff Show More