diff --git a/assets/sheet/Character_Sheet_PG1.png b/assets/sheet/Character_Sheet_PG1.png new file mode 100644 index 0000000..15d913b Binary files /dev/null and b/assets/sheet/Character_Sheet_PG1.png differ diff --git a/assets/sheet/Character_Sheet_PG2.png b/assets/sheet/Character_Sheet_PG2.png new file mode 100644 index 0000000..b180e9d Binary files /dev/null and b/assets/sheet/Character_Sheet_PG2.png differ diff --git a/assets/sheet/Character_Sheet_PG3_Indigo.png b/assets/sheet/Character_Sheet_PG3_Indigo.png new file mode 100644 index 0000000..ef5e96e Binary files /dev/null and b/assets/sheet/Character_Sheet_PG3_Indigo.png differ diff --git a/css/fvtt-prism-rpg.css b/css/fvtt-prism-rpg.css index 5019a6f..2f8f3b4 100644 --- a/css/fvtt-prism-rpg.css +++ b/css/fvtt-prism-rpg.css @@ -421,54 +421,176 @@ i.prismrpg { } .prismrpg .tab.character-skills .main-div .skills { display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 4px; + grid-template-columns: repeat(2, 1fr); + gap: 6px; } .prismrpg .tab.character-skills .main-div .skills .skill { display: flex; align-items: center; - gap: 4px; + gap: 8px; + padding: 6px 10px; + background: rgba(255, 255, 255, 0.3); + border: 2px solid #6b6b6b; + border-radius: 6px; + transition: all 0.2s; +} +.prismrpg .tab.character-skills .main-div .skills .skill:hover { + background: rgba(255, 255, 255, 0.5); + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15); +} +.prismrpg .tab.character-skills .main-div .skills .skill.is-core-skill { + background: rgba(255, 235, 180, 0.4); + border-color: #d4a017; +} +.prismrpg .tab.character-skills .main-div .skills .skill.is-core-skill:hover { + background: rgba(255, 235, 180, 0.6); } .prismrpg .tab.character-skills .main-div .skills .skill .item-img { - width: 24px; - height: 24px; + width: 32px; + height: 32px; + border: 2px solid #6b6b6b; + border-radius: 4px; + object-fit: cover; + flex-shrink: 0; } .prismrpg .tab.character-skills .main-div .skills .skill .name { - min-width: 12rem; + flex: 1; + min-width: 0; +} +.prismrpg .tab.character-skills .main-div .skills .skill .name a { + font-family: "Cinzel", serif; + font-size: 14px; + font-weight: 600; + color: #2c2c2c; +} +.prismrpg .tab.character-skills .main-div .skills .skill .name a i { + margin-right: 6px; + color: #6b6b6b; +} +.prismrpg .tab.character-skills .main-div .skills .skill .name a:hover { + color: #000; +} +.prismrpg .tab.character-skills .main-div .skills .skill .score { + font-family: "Cinzel", serif; + font-size: 16px; + font-weight: bold; + color: #2c2c2c; + min-width: 50px; + text-align: center; +} +.prismrpg .tab.character-skills .main-div .skills .skill .score .advanced-icon { + color: #d4a017; + font-size: 18px; + margin-left: 4px; +} +.prismrpg .tab.character-skills .main-div .skills .skill .controls { + display: flex; + gap: 8px; + flex-shrink: 0; +} +.prismrpg .tab.character-skills .main-div .skills .skill .controls a { + color: #6b6b6b; + font-size: 14px; + transition: color 0.2s; +} +.prismrpg .tab.character-skills .main-div .skills .skill .controls a:hover { + color: #2c2c2c; } .prismrpg .tab.character-skills .main-div .racial-abilities { display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 4px; + grid-template-columns: repeat(2, 1fr); + gap: 6px; } .prismrpg .tab.character-skills .main-div .racial-abilities .racial-ability { display: flex; align-items: center; - gap: 4px; + gap: 8px; + padding: 6px 10px; + background: rgba(200, 255, 200, 0.2); + border: 2px solid #6b9b6b; + border-radius: 6px; + transition: all 0.2s; +} +.prismrpg .tab.character-skills .main-div .racial-abilities .racial-ability:hover { + background: rgba(200, 255, 200, 0.4); + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15); } .prismrpg .tab.character-skills .main-div .racial-abilities .racial-ability .item-img { - width: 24px; - height: 24px; + width: 32px; + height: 32px; + border: 2px solid #6b9b6b; + border-radius: 4px; + object-fit: cover; + flex-shrink: 0; } .prismrpg .tab.character-skills .main-div .racial-abilities .racial-ability .name { - min-width: 12rem; + flex: 1; + min-width: 0; + font-family: "Cinzel", serif; + font-size: 14px; + font-weight: 600; + color: #2c2c2c; +} +.prismrpg .tab.character-skills .main-div .racial-abilities .racial-ability .controls { + display: flex; + gap: 8px; + flex-shrink: 0; +} +.prismrpg .tab.character-skills .main-div .racial-abilities .racial-ability .controls a { + color: #6b9b6b; + font-size: 14px; + transition: color 0.2s; +} +.prismrpg .tab.character-skills .main-div .racial-abilities .racial-ability .controls a:hover { + color: #3c6b3c; } .prismrpg .tab.character-skills .main-div .vulnerabilities { display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 4px; + grid-template-columns: repeat(2, 1fr); + gap: 6px; } .prismrpg .tab.character-skills .main-div .vulnerabilities .vulnerability { display: flex; align-items: center; - gap: 4px; + gap: 8px; + padding: 6px 10px; + background: rgba(255, 200, 200, 0.2); + border: 2px solid #9b6b6b; + border-radius: 6px; + transition: all 0.2s; +} +.prismrpg .tab.character-skills .main-div .vulnerabilities .vulnerability:hover { + background: rgba(255, 200, 200, 0.4); + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15); } .prismrpg .tab.character-skills .main-div .vulnerabilities .vulnerability .item-img { - width: 24px; - height: 24px; + width: 32px; + height: 32px; + border: 2px solid #9b6b6b; + border-radius: 4px; + object-fit: cover; + flex-shrink: 0; } .prismrpg .tab.character-skills .main-div .vulnerabilities .vulnerability .name { - min-width: 12rem; + flex: 1; + min-width: 0; + font-family: "Cinzel", serif; + font-size: 14px; + font-weight: 600; + color: #2c2c2c; +} +.prismrpg .tab.character-skills .main-div .vulnerabilities .vulnerability .controls { + display: flex; + gap: 8px; + flex-shrink: 0; +} +.prismrpg .tab.character-skills .main-div .vulnerabilities .vulnerability .controls a { + color: #9b6b6b; + font-size: 14px; + transition: color 0.2s; +} +.prismrpg .tab.character-skills .main-div .vulnerabilities .vulnerability .controls a:hover { + color: #6b3c3c; } .prismrpg .tab.character-equipment .main-div { display: grid; @@ -756,6 +878,505 @@ i.prismrpg { .prismrpg .tab.character-miracles .main-div prose-mirror.active { min-height: 150px; } +.prismrpg .character-main-v2 { + font-family: var(--font-primary); + font-size: calc(var(--font-size-standard) * 1); + color: var(--color-dark-1); + background-image: var(--background-image-base); + background-repeat: no-repeat; + background-size: 100% 100%; + padding: 0; + margin: 0; +} +.prismrpg .character-main-v2 nav.tabs [data-tab] { + color: #636060; +} +.prismrpg .character-main-v2 nav.tabs [data-tab].active { + color: #252424; +} +.prismrpg .character-main-v2 input:disabled, +.prismrpg .character-main-v2 select:disabled { + background-color: rgba(0, 0, 0, 0.2); + border-color: transparent; + color: var(--color-dark-3); +} +.prismrpg .character-main-v2 input, +.prismrpg .character-main-v2 select { + height: 1.5rem; + background-color: rgba(0, 0, 0, 0.1); + border-color: var(--color-dark-6); + color: var(--color-dark-2); +} +.prismrpg .character-main-v2 input[name="name"] { + height: 2.5rem; + margin-right: 4px; + font-family: var(--font-secondary); + font-size: calc(var(--font-size-standard) * 1.2); + font-weight: bold; + border: none; +} +.prismrpg .character-main-v2 fieldset { + margin-bottom: 4px; + border-radius: 4px; +} +.prismrpg .character-main-v2 .form-fields input, +.prismrpg .character-main-v2 .form-fields select { + text-align: center; + font-size: calc(var(--font-size-standard) * 1); +} +.prismrpg .character-main-v2 .form-fields select { + font-family: var(--font-secondary); + font-size: calc(var(--font-size-standard) * 1); +} +.prismrpg .character-main-v2 legend { + font-family: var(--font-secondary); + font-size: calc(var(--font-size-standard) * 1.2); + font-weight: bold; + letter-spacing: 1px; +} +.prismrpg .character-main-v2 .character-sheet-wrapper { + background-image: url("../assets/sheet/character-bg.png"); + background-size: cover; + background-position: center; + padding: 8px 10px; + min-height: auto; +} +.prismrpg .character-main-v2 .character-header { + position: relative; + margin-bottom: 5px; +} +.prismrpg .character-main-v2 .character-header .character-name-banner { + background-image: url("../assets/sheet/banner-bg.png"); + background-size: contain; + background-repeat: no-repeat; + background-position: center; + height: 60px; + display: flex; + align-items: center; + justify-content: center; +} +.prismrpg .character-main-v2 .character-header .character-name-banner input { + background: transparent; + border: none; + text-align: center; + font-family: "Cinzel", serif; + font-size: 24px; + font-weight: bold; + color: #2c2c2c; + width: 500px; + text-shadow: 1px 1px 2px rgba(255, 255, 255, 0.5); +} +.prismrpg .character-main-v2 .character-header .character-toggle-controls { + position: absolute; + top: 10px; + right: 10px; +} +.prismrpg .character-main-v2 .character-main-grid { + display: grid; + grid-template-columns: 1fr 300px; + gap: 12px; + align-items: start; +} +.prismrpg .character-main-v2 .character-left-column { + display: flex; + flex-direction: row; + gap: 12px; + align-items: flex-start; +} +.prismrpg .character-main-v2 .character-left-column .portrait-hp-column { + display: flex; + flex-direction: column; + gap: 12px; + width: 200px; + flex-shrink: 0; +} +.prismrpg .character-main-v2 .character-left-column .character-portrait { + width: 200px; + height: 200px; + border: 3px solid #6b6b6b; + border-radius: 8px; + overflow: hidden; + background: #d4d4d4; +} +.prismrpg .character-main-v2 .character-left-column .character-portrait img { + width: 100%; + height: 100%; + object-fit: cover; +} +.prismrpg .character-main-v2 .character-left-column .hp-shields-section { + width: 200px; +} +.prismrpg .character-main-v2 .character-left-column .hp-shields-section .hp-shields { + display: flex; + flex-direction: column; + gap: 6px; +} +.prismrpg .character-main-v2 .character-left-column .hp-shields-section .hp-shields .hp-item { + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; +} +.prismrpg .character-main-v2 .character-left-column .hp-shields-section .hp-shields .hp-item .hp-label { + font-family: "Cinzel", serif; + font-size: 12px; + font-weight: bold; + color: #2c2c2c; + text-transform: uppercase; + width: 55px; + flex-shrink: 0; +} +.prismrpg .character-main-v2 .character-left-column .hp-shields-section .hp-shields .hp-item .hp-value input { + width: 40px; + height: 28px; + text-align: center; + font-size: 14px; + font-weight: bold; + background: rgba(255, 255, 255, 0.9); + border: 2px solid #6b6b6b; + border-radius: 4px; +} +.prismrpg .character-main-v2 .character-left-column .hp-shields-section .hp-shields .hp-item .hp-separator { + font-family: "Cinzel", serif; + font-size: 14px; + font-weight: bold; + color: #2c2c2c; +} +.prismrpg .character-main-v2 .character-left-column .hp-shields-section .hp-shields .hp-item .hp-max input { + width: 40px; + height: 28px; + text-align: center; + font-size: 14px; + font-weight: bold; + background: rgba(200, 220, 255, 0.5); + border: 2px solid #6b6b6b; + border-radius: 4px; +} +.prismrpg .character-main-v2 .character-left-column .character-attributes { + display: flex; + flex-direction: column; + gap: 6px; + flex: 1; + min-width: 0; + max-width: 280px; +} +.prismrpg .character-main-v2 .character-left-column .character-attributes .attribute-shield { + position: relative; + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; + padding: 6px 12px; + background: rgba(255, 255, 255, 0.3); + border: 2px solid #6b6b6b; + border-radius: 4px; + height: auto; +} +.prismrpg .character-main-v2 .character-left-column .character-attributes .attribute-shield .attribute-label { + font-family: "Cinzel", serif; + font-size: 14px; + font-weight: bold; + color: #2c2c2c; + text-transform: uppercase; + margin: 0; + min-width: 40px; +} +.prismrpg .character-main-v2 .character-left-column .character-attributes .attribute-shield .attribute-label a.rollable { + display: flex; + align-items: center; + gap: 4px; + color: #2c2c2c; + text-decoration: none; + cursor: pointer; + transition: all 0.2s; +} +.prismrpg .character-main-v2 .character-left-column .character-attributes .attribute-shield .attribute-label a.rollable i { + font-size: 12px; + color: #6b6b6b; +} +.prismrpg .character-main-v2 .character-left-column .character-attributes .attribute-shield .attribute-label a.rollable:hover { + color: #000; + text-shadow: 0 0 4px rgba(0, 0, 0, 0.3); +} +.prismrpg .character-main-v2 .character-left-column .character-attributes .attribute-shield .attribute-label a.rollable:hover i { + color: #2c2c2c; +} +.prismrpg .character-main-v2 .character-left-column .character-attributes .attribute-shield .attribute-value input { + width: 45px; + height: 32px; + text-align: center; + font-size: 16px; + font-weight: bold; + background: rgba(255, 255, 255, 0.9); + border: 2px solid #6b6b6b; + border-radius: 4px; +} +.prismrpg .character-main-v2 .character-left-column .character-attributes .attribute-shield .attribute-save { + margin-left: auto; +} +.prismrpg .character-main-v2 .character-left-column .character-attributes .attribute-shield .attribute-save input { + width: 45px; + height: 32px; + text-align: center; + font-size: 14px; + font-weight: bold; + background: rgba(200, 220, 255, 0.5); + border: 2px solid #6b6b6b; + border-radius: 4px; + color: #2c2c2c; +} +.prismrpg .character-main-v2 .character-right-column { + display: flex; + flex-direction: column; + gap: 15px; +} +.prismrpg .character-main-v2 .character-right-column .section-title { + font-family: "Cinzel", serif; + font-size: 14px; + font-weight: bold; + color: #2c2c2c; + text-transform: uppercase; + margin-bottom: 8px; + padding: 5px 10px; + background: rgba(255, 255, 255, 0.6); + border: 2px solid #6b6b6b; + border-radius: 4px; + text-align: center; +} +.prismrpg .character-main-v2 .character-right-column .race-section .race-box { + padding: 10px; + background: rgba(255, 255, 255, 0.5); + border: 3px solid #6b6b6b; + border-radius: 8px; + min-height: 60px; +} +.prismrpg .character-main-v2 .character-right-column .race-section .race-box .section-title { + font-family: "Cinzel", serif; + font-size: 12px; + font-weight: bold; + color: #2c2c2c; + text-transform: uppercase; + margin: 0 0 8px 0; + padding: 5px; + background: rgba(255, 255, 255, 0.6); + border: 2px solid #6b6b6b; + border-radius: 4px; + text-align: center; +} +.prismrpg .character-main-v2 .character-right-column .race-section .race-box .race-item { + display: flex; + align-items: center; + gap: 8px; +} +.prismrpg .character-main-v2 .character-right-column .race-section .race-box .race-item .item-img { + width: 36px; + height: 36px; + border: 2px solid #6b6b6b; + border-radius: 4px; + object-fit: cover; + cursor: pointer; +} +.prismrpg .character-main-v2 .character-right-column .race-section .race-box .race-item .race-name { + flex: 1; + font-family: "Cinzel", serif; + font-size: 12px; + font-weight: bold; + color: #2c2c2c; + text-align: center; +} +.prismrpg .character-main-v2 .character-right-column .race-section .race-box .race-item .controls { + display: flex; + gap: 6px; +} +.prismrpg .character-main-v2 .character-right-column .race-section .race-box .race-item .controls a { + color: #6b6b6b; + cursor: pointer; + transition: color 0.2s; +} +.prismrpg .character-main-v2 .character-right-column .race-section .race-box .race-item .controls a:hover { + color: #2c2c2c; +} +.prismrpg .character-main-v2 .character-right-column .race-section .race-box .race-item .controls a i { + font-size: 14px; +} +.prismrpg .character-main-v2 .character-right-column .race-section .race-box .no-race { + text-align: center; + font-family: "Crimson Text", serif; + font-size: 13px; + color: #6b6b6b; + font-style: italic; + padding: 10px; +} +.prismrpg .character-main-v2 .character-right-column .race-section .race-box input { + width: 100%; + background: transparent; + border: none; + font-family: "Cinzel", serif; + font-size: 14px; + text-align: center; +} +.prismrpg .character-main-v2 .character-right-column .classes-section { + display: flex; + flex-direction: column; + gap: 12px; +} +.prismrpg .character-main-v2 .character-right-column .classes-section .class-box { + padding: 10px; + background: rgba(255, 255, 255, 0.5); + border: 3px solid #6b6b6b; + border-radius: 8px; +} +.prismrpg .character-main-v2 .character-right-column .classes-section .class-box .class-label { + font-family: "Cinzel", serif; + font-size: 11px; + color: #6b6b6b; + text-align: center; + margin-bottom: 5px; +} +.prismrpg .character-main-v2 .character-right-column .classes-section .class-box .class-content .class-item { + display: flex; + align-items: center; + gap: 8px; +} +.prismrpg .character-main-v2 .character-right-column .classes-section .class-box .class-content .class-item .item-img { + width: 32px; + height: 32px; + border: 2px solid #6b6b6b; + border-radius: 4px; + object-fit: cover; + cursor: pointer; +} +.prismrpg .character-main-v2 .character-right-column .classes-section .class-box .class-content .class-item .class-name { + flex: 1; + font-family: "Cinzel", serif; + font-size: 11px; + font-weight: bold; + color: #2c2c2c; + text-align: center; +} +.prismrpg .character-main-v2 .character-right-column .classes-section .class-box .class-content .class-item .controls { + display: flex; + gap: 6px; +} +.prismrpg .character-main-v2 .character-right-column .classes-section .class-box .class-content .class-item .controls a { + color: #6b6b6b; + cursor: pointer; + transition: color 0.2s; +} +.prismrpg .character-main-v2 .character-right-column .classes-section .class-box .class-content .class-item .controls a:hover { + color: #2c2c2c; +} +.prismrpg .character-main-v2 .character-right-column .classes-section .class-box .class-content .class-item .controls a i { + font-size: 12px; +} +.prismrpg .character-main-v2 .character-right-column .classes-section .class-box .class-content .no-class { + text-align: center; + font-family: "Crimson Text", serif; + font-size: 11px; + color: #6b6b6b; + font-style: italic; + padding: 5px; +} +.prismrpg .character-main-v2 .character-right-column .classes-section .class-box .class-input input { + width: 100%; + background: transparent; + border: none; + font-family: "Cinzel", serif; + font-size: 14px; + text-align: center; +} +.prismrpg .character-main-v2 .character-right-column .origin-section .origin-box { + padding: 15px; + background: rgba(255, 255, 255, 0.5); + border: 3px solid #6b6b6b; + border-radius: 8px; + min-height: 350px; +} +.prismrpg .character-main-v2 .character-right-column .origin-section .origin-box textarea { + width: 100%; + min-height: 320px; + background: transparent; + border: none; + font-family: "Crimson Text", serif; + font-size: 13px; + line-height: 1.6; + resize: vertical; +} +.prismrpg .character-subattributes.tab .subattributes-content { + padding: 1rem; +} +.prismrpg .character-subattributes.tab .subattributes-content .subattributes-list { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 1rem; +} +.prismrpg .character-subattributes.tab .subattributes-content .subattribute-item { + background: rgba(0, 0, 0, 0.1); + border: 1px solid var(--color-border-dark-secondary); + border-radius: 4px; + padding: 0.75rem; + display: flex; + flex-direction: column; + gap: 0.5rem; +} +.prismrpg .character-subattributes.tab .subattributes-content .subattribute-item .subattribute-header { + display: flex; + justify-content: space-between; + align-items: center; + gap: 0.5rem; +} +.prismrpg .character-subattributes.tab .subattributes-content .subattribute-item .subattribute-name { + font-weight: bold; + font-size: 1.1em; + color: var(--color-text-dark-primary); +} +.prismrpg .character-subattributes.tab .subattributes-content .subattribute-item .subattribute-value input { + width: 3em; + text-align: center; + font-weight: bold; + font-size: 1.2em; + background: rgba(255, 255, 255, 0.1); + border: 1px solid var(--color-border-dark-tertiary); + border-radius: 3px; + padding: 0.25rem; + color: var(--color-text-dark-primary); +} +.prismrpg .character-subattributes.tab .subattributes-content .subattribute-item .subattribute-value input:disabled { + opacity: 0.9; + cursor: default; +} +.prismrpg .character-subattributes.tab .subattributes-content .subattribute-item .subattribute-details { + display: flex; + flex-direction: column; + gap: 0.25rem; + font-size: 0.9em; + color: var(--color-text-dark-secondary); +} +.prismrpg .character-subattributes.tab .subattributes-content .subattribute-item .subattribute-parents { + font-style: italic; +} +.prismrpg .character-subattributes.tab .subattributes-content .subattribute-item .subattribute-parents .parent-char { + display: inline-block; + margin-right: 0.5rem; +} +.prismrpg .character-subattributes.tab .subattributes-content .subattribute-item .subattribute-parents .parent-char .parent-name { + font-weight: 600; + color: var(--color-text-dark-primary); +} +.prismrpg .character-subattributes.tab .subattributes-content .subattribute-item .subattribute-parents .parent-char .parent-value { + color: var(--color-text-dark-secondary); +} +.prismrpg .character-subattributes.tab .subattributes-content .subattribute-item .subattribute-description { + padding-top: 0.25rem; + border-top: 1px solid var(--color-border-dark-tertiary); + font-size: 0.85em; + line-height: 1.3; +} +@media (max-width: 768px) { + .prismrpg .character-subattributes.tab .subattributes-content .subattributes-list { + grid-template-columns: 1fr; + } +} .prismrpg .monster-content { font-family: var(--font-primary); font-size: calc(var(--font-size-standard) * 1); diff --git a/lang/en.json b/lang/en.json index aeaa7d8..8229c42 100644 --- a/lang/en.json +++ b/lang/en.json @@ -78,6 +78,66 @@ "cha": { "label": "Charisma" }, + "prowess": { + "label": "Prowess", + "description": "Physical power and combat capability (STR+DEX)" + }, + "vigor": { + "label": "Vigor", + "description": "Endurance and physical resilience (STR+CON)" + }, + "competence": { + "label": "Competence", + "description": "Practical application of knowledge (STR+INT)" + }, + "authority": { + "label": "Authority", + "description": "Command and leadership presence (STR+WIS)" + }, + "presence": { + "label": "Presence", + "description": "Physical charisma and intimidation (STR+CHA)" + }, + "stamina": { + "label": "Stamina", + "description": "Athletic endurance (DEX+CON)" + }, + "initiative": { + "label": "Initiative", + "description": "Reaction speed and alertness (DEX+INT)" + }, + "wit": { + "label": "Wit", + "description": "Mental agility and cleverness (DEX+WIS)" + }, + "grace": { + "label": "Grace", + "description": "Social finesse and charm (DEX+CHA)" + }, + "tenacity": { + "label": "Tenacity", + "description": "Mental and physical toughness (CON+INT)" + }, + "willpower": { + "label": "Willpower", + "description": "Inner strength and determination (CON+WIS)" + }, + "resilience": { + "label": "Resilience", + "description": "Ability to endure hardship (CON+CHA)" + }, + "cunning": { + "label": "Cunning", + "description": "Strategic thinking (INT+WIS)" + }, + "guile": { + "label": "Guile", + "description": "Deceptive intellect (INT+CHA)" + }, + "sovereignty": { + "label": "Sovereignty", + "description": "Diplomatic wisdom and influence (WIS+CHA)" + }, "FIELDS": { "moneys": { "tinbit": { @@ -454,6 +514,7 @@ "skill": "Skill", "skillBonus": "Skill bonus", "skills": "Skills", + "subattributes": "Sub-Attributes", "spells": "Spells", "str": "STR", "titleChallenge": "Challenge", @@ -743,7 +804,9 @@ "racialAbilities": "Racial Abilities from your character's race and sub-race" }, "Message": { - "selectCoreSkill": "You must select a Core Skill for your character. Each character chooses one Core Skill at creation." + "selectCoreSkill": "You must select a Core Skill for your character. Each character chooses one Core Skill at creation.", + "dropRace": "Drag and drop a Race item here", + "dropClass": "Drag and drop a Class item here" }, "Miracle": { "FIELDS": { @@ -842,7 +905,7 @@ "save": "Save roll {save}", "success": "Success", "visibility": "Visibility", - "favorDisfavor": "Favor/Disfavor" + "advantageDisadvantage": "Advantage/Disadvantage" }, "Save": { "FIELDS": { diff --git a/module/applications/sheets/character-sheet.mjs b/module/applications/sheets/character-sheet.mjs index bee8ef3..0428bd5 100644 --- a/module/applications/sheets/character-sheet.mjs +++ b/module/applications/sheets/character-sheet.mjs @@ -1,12 +1,13 @@ import PrismRPGActorSheet from "./base-actor-sheet.mjs" import PrismRPGRoll from "../../documents/roll.mjs" +import { SYSTEM } from "../../config/system.mjs" export default class PrismRPGCharacterSheet extends PrismRPGActorSheet { /** @override */ static DEFAULT_OPTIONS = { classes: ["character"], position: { - width: 972, + width: 750, height: 780, }, window: { @@ -36,6 +37,9 @@ export default class PrismRPGCharacterSheet extends PrismRPGActorSheet { skills: { template: "systems/fvtt-prism-rpg/templates/character-skills.hbs", }, + subattributes: { + template: "systems/fvtt-prism-rpg/templates/character-subattributes.hbs", + }, combat: { template: "systems/fvtt-prism-rpg/templates/character-combat.hbs", }, @@ -67,6 +71,7 @@ export default class PrismRPGCharacterSheet extends PrismRPGActorSheet { #getTabs() { let tabs = { skills: { id: "skills", group: "sheet", icon: "fa-solid fa-shapes", label: "PRISMRPG.Label.skills" }, + subattributes: { id: "subattributes", group: "sheet", icon: "fa-solid fa-diagram-project", label: "PRISMRPG.Label.subattributes" }, combat: { id: "combat", group: "sheet", icon: "fa-solid fa-swords", label: "PRISMRPG.Label.combat" }, equipment: { id: "equipment", group: "sheet", icon: "fa-solid fa-backpack", label: "PRISMRPG.Label.equipment" }, biography: { id: "biography", group: "sheet", icon: "fa-solid fa-book", label: "PRISMRPG.Label.biography" }, @@ -90,6 +95,7 @@ export default class PrismRPGCharacterSheet extends PrismRPGActorSheet { async _prepareContext() { const context = await super._prepareContext() context.tabs = this.#getTabs() + context.config = SYSTEM return context } @@ -99,6 +105,14 @@ export default class PrismRPGCharacterSheet extends PrismRPGActorSheet { const doc = this.document switch (partId) { case "main": + context.race = doc.itemTypes.race?.[0] || null + const classes = doc.itemTypes.class || [] + // Create 3 class slots + context.classSlots = [ + classes[0] || null, + classes[1] || null, + classes[2] || null + ] break case "skills": context.tab = context.tabs.skills @@ -106,6 +120,9 @@ export default class PrismRPGCharacterSheet extends PrismRPGActorSheet { context.racialAbilities = doc.itemTypes["racial-ability"] context.vulnerabilities = doc.itemTypes.vulnerability break + case "subattributes": + context.tab = context.tabs.subattributes + break case "spells": context.tab = context.tabs.spells context.spells = doc.itemTypes.spell diff --git a/module/config/character.mjs b/module/config/character.mjs index 9fdfe86..64ea617 100644 --- a/module/config/character.mjs +++ b/module/config/character.mjs @@ -135,28 +135,28 @@ export const CHALLENGES = Object.freeze({ }) export const SAVES = Object.freeze({ - will: { - id: "will", - label: "PRISMRPG.Character.will.label" + str: { + id: "str", + label: "PRISMRPG.Character.str.label" }, - dodge: { - id: "dodge", - label: "PRISMRPG.Character.dodge.label" + dex: { + id: "dex", + label: "PRISMRPG.Character.dex.label" }, - toughness: { - id: "toughness", - label: "PRISMRPG.Character.toughness.label" + con: { + id: "con", + label: "PRISMRPG.Character.con.label" }, - contagion: { - id: "contagion", - label: "PRISMRPG.Character.contagion.label" + int: { + id: "int", + label: "PRISMRPG.Character.int.label" }, - poison: { - id: "poison", - label: "PRISMRPG.Character.poison.label" + wis: { + id: "wis", + label: "PRISMRPG.Character.wis.label" }, - pain: { - id: "pain", - label: "PRISMRPG.Character.pain.label" + cha: { + id: "cha", + label: "PRISMRPG.Character.cha.label" } }) diff --git a/module/config/system.mjs b/module/config/system.mjs index 3bde296..294bfb3 100644 --- a/module/config/system.mjs +++ b/module/config/system.mjs @@ -56,10 +56,10 @@ export const MORTAL_CHOICES = { "halflings": { label: "Halfling", id: "halflings", defenseBonus: 2 } } -export const FAVOR_CHOICES = { +export const ADVANTAGE_CHOICES = { "none": { label: "None", value: "none" }, - "favor": { label: "Favor", value: "favor" }, - "disfavor": { label: "Disfavor", value: "disfavor" } + "advantage": { label: "Advantage", value: "advantage" }, + "disadvantage": { label: "Disadvantage", value: "disadvantage" } } export const MOVEMENT_CHOICES = { @@ -327,7 +327,7 @@ export const SYSTEM = { MOVE_DIRECTION_CHOICES, SIZE_CHOICES, RANGE_CHOICES, - FAVOR_CHOICES, + ADVANTAGE_CHOICES, ATTACKER_AIM_CHOICES, MORTAL_CHOICES, MIRACLE_TYPES, diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index d2091d6..46567a1 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -79,17 +79,28 @@ export default class PrismRPGActor extends Actor { } /* *************************************************/ - async prepareRoll(rollType, rollKey, rollDice ) { + async prepareRoll(rollType, rollKey, rollDice) { console.log("Preparing roll", rollType, rollKey, rollDice) let rollTarget switch (rollType) { + case "characteristic": + if (!this.system.characteristics || !this.system.characteristics[rollKey]) { + ui.notifications.error(`Characteristic ${rollKey} not found`) + return + } + rollTarget = { + ...foundry.utils.duplicate(this.system.characteristics[rollKey]), + rollKey: rollKey, + name: game.i18n.localize(`PRISMRPG.Label.${rollKey}`) + } + break case "granted": rollTarget = { name: rollKey, formula: foundry.utils.duplicate(this.system.granted[rollKey]), rollKey: rollKey } - if ( rollTarget.formula === "" || rollTarget.formula === undefined) { + if (rollTarget.formula === "" || rollTarget.formula === undefined) { rollTarget.formula = 0 } break; @@ -126,70 +137,75 @@ export default class PrismRPGActor extends Actor { rollTarget.rollKey = rollKey break case "shield-roll": { - rollTarget = this.items.find((i) => i.type === "shield" && i.id === rollKey) - let shieldSkill = this.items.find((i) => i.type === "skill" && i.name.toLowerCase() === rollTarget.name.toLowerCase()) - rollTarget.skill = shieldSkill - rollTarget.rollKey = rollKey - } + rollTarget = this.items.find((i) => i.type === "shield" && i.id === rollKey) + let shieldSkill = this.items.find((i) => i.type === "skill" && i.name.toLowerCase() === rollTarget.name.toLowerCase()) + rollTarget.skill = shieldSkill + rollTarget.rollKey = rollKey + } break; case "weapon-damage-small": case "weapon-damage-medium": case "weapon-attack": case "weapon-defense": { - let weapon = this.items.find((i) => i.type === "weapon" && i.id === rollKey) - let skill - let skills = this.items.filter((i) => i.type === "skill" && i.name.toLowerCase() === weapon.name.toLowerCase()) + let weapon = this.items.find((i) => i.type === "weapon" && i.id === rollKey) + let skill + let skills = this.items.filter((i) => i.type === "skill" && i.name.toLowerCase() === weapon.name.toLowerCase()) + if (skills.length > 0) { + skill = this.getBestWeaponClassSkill(skills, rollType, 1.0) + } else { + skills = this.items.filter((i) => i.type === "skill" && i.name.toLowerCase().replace(" skill", "") === weapon.name.toLowerCase()) if (skills.length > 0) { skill = this.getBestWeaponClassSkill(skills, rollType, 1.0) } else { - skills = this.items.filter((i) => i.type === "skill" && i.name.toLowerCase().replace(" skill", "") === weapon.name.toLowerCase()) + skills = this.items.filter((i) => i.type === "skill" && i.system.weaponClass === weapon.system.weaponClass) if (skills.length > 0) { - skill = this.getBestWeaponClassSkill(skills, rollType, 1.0) + skill = this.getBestWeaponClassSkill(skills, rollType, 0.5) } else { - skills = this.items.filter((i) => i.type === "skill" && i.system.weaponClass === weapon.system.weaponClass) + skills = this.items.filter((i) => i.type === "skill" && i.system.weaponClass.includes(SYSTEM.WEAPON_CATEGORIES[weapon.system.weaponClass])) if (skills.length > 0) { - skill = this.getBestWeaponClassSkill(skills, rollType, 0.5) + skill = this.getBestWeaponClassSkill(skills, rollType, 0.25) } else { - skills = this.items.filter((i) => i.type === "skill" && i.system.weaponClass.includes(SYSTEM.WEAPON_CATEGORIES[weapon.system.weaponClass])) - if (skills.length > 0) { - skill = this.getBestWeaponClassSkill(skills, rollType, 0.25) - } else { - ui.notifications.warn(game.i18n.localize("PRISMRPG.Notifications.skillNotFound")) - return - } + ui.notifications.warn(game.i18n.localize("PRISMRPG.Notifications.skillNotFound")) + return } } } - if (!weapon || !skill) { - console.error("Weapon or skill not found", weapon, skill) - ui.notifications.warn(game.i18n.localize("PRISMRPG.Notifications.skillNotFound")) - return - } - rollTarget = skill - rollTarget.weapon = weapon - rollTarget.weaponSkillModifier = skill.weaponSkillModifier - rollTarget.rollKey = rollKey - rollTarget.combat = foundry.utils.duplicate(this.system.combat) - if ( rollType === "weapon-damage-small" || rollType === "weapon-damage-medium") { - rollTarget.grantedDice = this.system.granted.damageDice - } - if ( rollType === "weapon-attack") { - rollTarget.grantedDice = this.system.granted.attackDice - } - if ( rollType === "weapon-defense") { - rollTarget.grantedDice = this.system.granted.defenseDice - } } + if (!weapon || !skill) { + console.error("Weapon or skill not found", weapon, skill) + ui.notifications.warn(game.i18n.localize("PRISMRPG.Notifications.skillNotFound")) + return + } + rollTarget = skill + rollTarget.weapon = weapon + rollTarget.weaponSkillModifier = skill.weaponSkillModifier + rollTarget.rollKey = rollKey + rollTarget.combat = foundry.utils.duplicate(this.system.combat) + if (rollType === "weapon-damage-small" || rollType === "weapon-damage-medium") { + rollTarget.grantedDice = this.system.granted.damageDice + } + if (rollType === "weapon-attack") { + rollTarget.grantedDice = this.system.granted.attackDice + } + if (rollType === "weapon-defense") { + rollTarget.grantedDice = this.system.granted.defenseDice + } + } break default: ui.notifications.error(game.i18n.localize("PRISMRPG.Notifications.rollTypeNotFound") + String(rollType)) - break + return } - // In all cases - rollTarget.magicUser = this.system.biodata.magicUser - rollTarget.actorModifiers = foundry.utils.duplicate(this.system.modifiers) - rollTarget.actorLevel = this.system.biodata.level + // In all cases - verify rollTarget exists + if (!rollTarget) { + console.error("Roll target is undefined for rollType:", rollType) + return + } + + rollTarget.magicUser = this.system.biodata?.magicUser || false + rollTarget.actorModifiers = this.system.modifiers ? foundry.utils.duplicate(this.system.modifiers) : {} + rollTarget.actorLevel = this.system.biodata?.level || 1 await this.system.roll(rollType, rollTarget) } diff --git a/module/documents/roll-old.mjs b/module/documents/roll-old.mjs new file mode 100644 index 0000000..caa2cc5 --- /dev/null +++ b/module/documents/roll-old.mjs @@ -0,0 +1,1095 @@ +import { SYSTEM } from "../config/system.mjs" +import PrismRPGUtils from "../utils.mjs" + +export default class PrismRPGRoll extends Roll { + /** + * The HTML template path used to render dice checks of this type + * @type {string} + */ + static CHAT_TEMPLATE = "systems/fvtt-prism-rpg/templates/chat-message.hbs" + + get type() { + return this.options.type + } + + get titleFormula() { + return this.options.titleFormula + } + + get rollName() { + return this.options.rollName + } + + get target() { + return this.options.target + } + + get value() { + return this.options.value + } + + get treshold() { + return this.options.treshold + } + + get actorId() { + return this.options.actorId + } + + get actorName() { + return this.options.actorName + } + + get actorImage() { + return this.options.actorImage + } + + 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 targetName() { + return this.options.targetName + } + + get targetArmor() { + return this.options.targetArmor + } + + get targetMalus() { + return this.options.targetMalus + } + + get realDamage() { + return this.options.realDamage + } + + get rollTotal() { + return this.options.rollTotal + } + + get diceResults() { + return this.options.diceResults + } + + get rollTarget() { + return this.options.rollTarget + } + + get D30result() { + return this.options.D30result + } + + get badResult() { + return this.options.badResult + } + + get rollData() { + return this.options.rollData + } + + /** + * Prompt the user with a dialog to configure and execute a roll (D&D 5e style). + * + * @param {Object} options Configuration options for the roll. + * @param {string} options.rollType The type of roll being performed. + * @param {Object} 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} The roll result or null if the dialog was cancelled. + */ + static async prompt(options = {}) { + // D&D 5e style: simple 1d20 + modifier + let dice = "1d20" + let baseFormula = "1d20" + let hasModifier = true + let hasFavor = true // Allow advantage/disadvantage + let isDamageRoll = false + + // Determine roll specifics based on type + if (options.rollType === "characteristic") { + options.rollName = options.rollTarget.name + options.rollTarget.value = options.rollTarget.value + options.rollTarget.charModifier = options.rollTarget.value + + } else if (options.rollType === "challenge" || options.rollType === "save") { + options.rollName = game.i18n.localize(`PRISMRPG.Label.${options.rollTarget.rollKey}`) + // value already set in actor.mjs + + } else if (options.rollType === "skill") { + options.rollName = options.rollTarget.name + options.rollTarget.value = Math.floor(options.rollTarget.system.skillTotal / 10) + + } else if (options.rollType === "weapon-attack") { + options.rollName = options.rollTarget.name + if (options.rollTarget.weapon.system.weaponType === "melee") { + options.rollTarget.value = options.rollTarget.combat.attackModifier + options.rollTarget.weaponSkillModifier + options.rollTarget.weapon.system.bonuses.attackBonus + options.rollTarget.charModifier = options.rollTarget.combat.attackModifier + } else { + options.rollTarget.value = options.rollTarget.combat.rangedAttackModifier + options.rollTarget.weaponSkillModifier + options.rollTarget.weapon.system.bonuses.attackBonus + options.rollTarget.charModifier = options.rollTarget.combat.rangedAttackModifier + } + + } else if (options.rollType === "weapon-defense") { + options.rollName = options.rollTarget.name + options.rollTarget.value = options.rollTarget.combat.defenseModifier + options.rollTarget.weaponSkillModifier + options.rollTarget.weapon.system.bonuses.defenseBonus + options.rollTarget.charModifier = options.rollTarget.combat.defenseModifier + + } else if (options.rollType === "spell" || options.rollType === "spell-attack" || options.rollType === "spell-power") { + options.rollName = options.rollTarget.name + options.rollTarget.value = options.rollTarget.actorModifiers.levelSpellModifier + options.rollTarget.actorModifiers.intSpellModifier + options.rollTarget.charModifier = options.rollTarget.actorModifiers.intSpellModifier + + } else if (options.rollType === "miracle" || options.rollType === "miracle-attack" || options.rollType === "miracle-power") { + options.rollName = options.rollTarget.name + options.rollTarget.value = options.rollTarget.actorModifiers.levelMiracleModifier + options.rollTarget.actorModifiers.chaMiracleModifier + options.rollTarget.charModifier = options.rollTarget.actorModifiers.chaMiracleModifier + + } else if (options.rollType === "monster-attack" || options.rollType === "monster-defense") { + options.rollName = options.rollTarget.name + if (options.rollType === "monster-attack") { + options.rollTarget.value = options.rollTarget.attackModifier + } else { + options.rollTarget.value = options.rollTarget.defenseModifier + } + options.rollTarget.charModifier = 0 + + } else if (options.rollType === "monster-skill") { + options.rollName = game.i18n.localize(`PRISMRPG.Label.${options.rollTarget.rollKey}`) + // value already set + + } else if (options.rollType.includes("weapon-damage")) { + isDamageRoll = true + hasFavor = false + options.rollName = options.rollTarget.name + let damageBonus = options.rollTarget.combat.damageModifier + options.rollTarget.value = damageBonus + options.rollTarget.weaponSkillModifier + options.rollTarget.weapon.system.bonuses.damageBonus + options.rollTarget.charModifier = damageBonus + + if (options.rollType.includes("small")) { + dice = options.rollTarget.weapon.system.damage.damageS + } else { + dice = options.rollTarget.weapon.system.damage.damageM + } + dice = dice.replace("E", "").replace("e", "") + baseFormula = dice + + } else if (options.rollType.includes("monster-damage")) { + isDamageRoll = true + hasFavor = false + options.rollName = options.rollTarget.name + options.rollTarget.value = options.rollTarget.damageModifier + options.rollTarget.charModifier = 0 + dice = options.rollTarget.damageDice.replace("E", "").replace("e", "") + baseFormula = dice + + } else if (options.rollType === "granted") { + hasModifier = false + hasFavor = false + options.rollName = `Granted ${options.rollTarget.rollKey}` + dice = options.rollTarget.formula + baseFormula = options.rollTarget.formula + } + + // Setup roll modes + const rollModes = foundry.utils.duplicate(CONFIG.Dice.rollModes) + + const fieldRollMode = new foundry.data.fields.StringField({ + choices: rollModes, + blank: false, + default: "public", + }) + + // D&D 5e style: simple modifiers + const choiceModifier = SYSTEM.CHOICE_MODIFIERS + const choiceFavor = SYSTEM.FAVOR_CHOICES + + let dialogContext = { + rollType: options.rollType, + rollTarget: options.rollTarget, + rollName: options.rollName, + actorName: options.actorName, + rollModes, + hasModifier, + hasFavor, + isDamageRoll, + baseValue: options.rollTarget.value || 0, + baseFormula, + dice, + fieldRollMode, + choiceModifier, + choiceFavor, + hasTarget: options.hasTarget, + modifier: "+0", + favor: "none", + targetName: "" + } + + const content = await foundry.applications.handlebars.renderTemplate("systems/fvtt-prism-rpg/templates/roll-dialog.hbs", dialogContext) + + let position = game.user.getFlag(SYSTEM.id, "roll-dialog-pos") || { top: -1, left: -1 } + const label = game.i18n.localize("PRISMRPG.Roll.roll") + const rollContext = await foundry.applications.api.DialogV2.wait({ + window: { title: "Roll dialog" }, + classes: ["prismrpg"], + content, + position, + buttons: [ + { + label: label, + callback: (event, button, dialog) => { + console.log("Roll context", event, button, dialog) + let position = dialog.position + game.user.setFlag(SYSTEM.id, "roll-dialog-pos", foundry.utils.duplicate(position)) + const output = Array.from(button.form.elements).reduce((obj, input) => { + if (input.name) obj[input.name] = input.value + return obj + }, {}) + return output + }, + }, + ], + actions: { + "selectGranted": (event, button, dialog) => { + hasGrantedDice = event.target.checked + }, + "selectBeyondSkill": (event, button, dialog) => { + beyondSkill = button.checked + }, + "selectPointBlank": (event, button, dialog) => { + pointBlank = button.checked + }, + "selectLetItFly": (event, button, dialog) => { + letItFly = button.checked + }, + "saveSpellCheck": (event, button, dialog) => { + saveSpell = button.checked + }, + "gotoToken": (event, button, dialog) => { + let tokenId = $(button).data("tokenId") + let token = canvas.tokens?.get(tokenId) + if (token) { + canvas.animatePan({ x: token.x, y: token.y, duration: 200 }) + canvas.tokens.releaseAll(); + token.control({ releaseOthers: true }); + } + } + }, + rejectClose: false // Click on Close button will not launch an error + }) + + // If the user cancels the dialog, exit + if (rollContext === null) return + console.log("rollContext", rollContext, hasGrantedDice) + rollContext.saveSpell = saveSpell // Update fucking flag + + let fullModifier = 0 + let titleFormula = "" + dice = rollContext.changeDice || dice + if (hasModifier) { + let bonus = Number(options.rollTarget.value) + fullModifier = rollContext.modifier === "" ? 0 : parseInt(rollContext.modifier, 10) + bonus + fullModifier += (rollContext.saveSpell) ? options.rollTarget.actorModifiers.saveModifier : 0 + if (Number(rollContext.attackerAim) > 0) { + fullModifier += Number(rollContext.attackerAim) + } + + if (fullModifier === 0) { + modifierFormula = "0" + } else { + let modAbs = Math.abs(fullModifier) + modifierFormula = `D${modAbs + 1} - 1` + } + if (hasStaticModifier) { + modifierFormula += ` + ${options.rollTarget.staticModifier}` + } + let sign = fullModifier < 0 ? "-" : "+" + if (hasExplode) { + titleFormula = `${dice}E ${sign} ${modifierFormula}` + } else { + titleFormula = `${dice} ${sign} ${modifierFormula}` + } + } else { + modifierFormula = "0" + fullModifier = 0 + baseFormula = `${dice}` + if (hasExplode) { + titleFormula = `${dice}E` + } else { + titleFormula = `${dice}` + } + } + + // Latest addition : favor choice at point blank range + if (pointBlank) { + rollContext.favor = "favor" + } + if (beyondSkill) { + rollContext.favor = "disfavor" + } + + // Specific pain case + if (options.rollType === "save" && options.rollTarget.rollKey === "pain" || options.rollTarget.rollKey === "paincourage") { + baseFormula = options.rollTarget.rollDice + titleFormula = `${dice}` + modifierFormula = "0" + fullModifier = 0 + } + + // Specific pain/poison/contagion case + if (options.rollType === "save" && (options.rollTarget.rollKey === "poison" || options.rollTarget.rollKey === "contagion")) { + hasD30 = false + hasStaticModifier = true + modifierFormula = ` + ${Math.abs(fullModifier)}` + titleFormula = `${dice}E + ${Math.abs(fullModifier)}` + } + + if (letItFly) { + baseFormula = "1D20" + titleFormula = `1D20E` + modifierFormula = "0" + fullModifier = 0 + hasFavor = false + hasExplode = true + rollContext.favor = "none" + } + + maxValue = Number(baseFormula.match(/\d+$/)[0]) // Update the max value agains + + const rollData = { + type: options.rollType, + rollType: options.rollType, + target: options.rollTarget, + rollName: options.rollName, + actorId: options.actorId, + actorName: options.actorName, + actorImage: options.actorImage, + rollMode: rollContext.visibility, + hasTarget: options.hasTarget, + pointBlank, + beyondSkill, + letItFly, + hasGrantedDice, + titleFormula, + targetName, + ...rollContext, + } + + /** + * A hook event that fires before the roll is made. + * @function + * @memberof hookEvents + * @param {Object} options Options for the roll. + * @param {Object} rollData All data related to the roll. + * @returns {boolean} Explicitly return `false` to prevent roll to be made. + */ + if (Hooks.call("fvtt-prism-rpg.preRoll", options, rollData) === false) return + + let rollBase = new this(baseFormula, options.data, rollData) + const rollModifier = new Roll(modifierFormula, options.data, rollData) + await rollModifier.evaluate() + await rollBase.evaluate() + + let rollFavor + let badResult + if (rollContext.favor === "favor") { + rollFavor = new this(baseFormula, options.data, rollData) + await rollFavor.evaluate() + console.log("Rolling with favor", rollFavor) + if (game?.dice3d) { + game.dice3d.showForRoll(rollFavor, game.user, true) + } + if (Number(rollFavor.result) > Number(rollBase.result)) { + badResult = rollBase.result + rollBase = rollFavor + } else { + badResult = rollFavor.result + } + rollFavor = null + } + + if (rollContext.favor === "disfavor") { + rollFavor = new this(baseFormula, options.data, rollData) + await rollFavor.evaluate() + if (game?.dice3d) { + game.dice3d.showForRoll(rollFavor, game.user, true) + } + if (Number(rollFavor.result) < Number(rollBase.result)) { + badResult = rollBase.result + rollBase = rollFavor + } else { + badResult = rollFavor.result + } + rollFavor = null + } + + if (hasD30) { + let rollD30 = await new Roll("1D30").evaluate() + if (game?.dice3d) { + game.dice3d.showForRoll(rollD30, game.user, true) + } + options.D30result = rollD30.total + } + + let rollTotal = 0 + let diceResults = [] + let resultType + let diceSum = 0 + + let singleDice = `1D${maxValue}` + for (let i = 0; i < rollBase.dice.length; i++) { + for (let j = 0; j < rollBase.dice[i].results.length; j++) { + let diceResult = rollBase.dice[i].results[j].result + diceResults.push({ dice: `${singleDice.toUpperCase()}`, value: diceResult }) + diceSum += diceResult + if (hasMaxValue) { + while (diceResult === maxValue) { + let r = await new Roll(baseFormula).evaluate() + if (game?.dice3d) { + await game.dice3d.showForRoll(r, game.user, true) + } + diceResult = r.dice[0].results[0].result + diceResults.push({ dice: `${singleDice.toUpperCase()}-1`, value: diceResult - 1 }) + diceSum += (diceResult - 1) + } + } + } + } + + if (hasGrantedDice && options.rollTarget.grantedDice && options.rollTarget.grantedDice !== "") { + titleFormula += ` + ${options.rollTarget.grantedDice.toUpperCase()}` + let grantedRoll = new Roll(options.rollTarget.grantedDice) + await grantedRoll.evaluate() + if (game?.dice3d) { + await game.dice3d.showForRoll(grantedRoll, game.user, true) + } + diceResults.push({ dice: `${options.rollTarget.grantedDice.toUpperCase()}`, value: grantedRoll.total }) + rollTotal += grantedRoll.total + } + + if (fullModifier !== 0) { + diceResults.push({ dice: `${rollModifier.formula.toUpperCase()}`, value: rollModifier.total }) + if (fullModifier < 0) { + rollTotal += Math.max(diceSum - rollModifier.total, 0) + } else { + rollTotal += diceSum + rollModifier.total + } + } else { + rollTotal += diceSum + } + + rollBase.options.resultType = resultType + rollBase.options.rollTotal = rollTotal + rollBase.options.diceResults = diceResults + rollBase.options.rollTarget = options.rollTarget + rollBase.options.titleFormula = titleFormula + rollBase.options.D30result = options.D30result + rollBase.options.badResult = badResult + rollBase.options.rollData = foundry.utils.duplicate(rollData) + + /** + * A hook event that fires after the roll has been made. + * @function + * @memberof hookEvents + * @param {Object} options Options for the roll. + * @param {Object} rollData All data related to the roll. + @param {PrismRPGRoll} roll The resulting roll. + * @returns {boolean} Explicitly return `false` to prevent roll to be made. + */ + if (Hooks.call("fvtt-prism-rpg.Roll", options, rollData, rollBase) === false) return + + return rollBase + } + + /* ***********************************************************/ + static async promptInitiative(options = {}) { + const rollModes = foundry.utils.duplicate(CONFIG.Dice.rollModes); // v12 : 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", + }) + + if (SYSTEM.INITIATIVE_DICE_CHOICES_PER_CLASS[options.actorClass]) { + options.initiativeDiceChoice = SYSTEM.INITIATIVE_DICE_CHOICES_PER_CLASS[options.actorClass] + } else { + options.initiativeDiceChoice = SYSTEM.INITIATIVE_DICE_CHOICES_PER_CLASS["untrained"] + } + + let dialogContext = { + actorClass: options.actorClass, + initiativeDiceChoice: options.initiativeDiceChoice, + initiativeDice: "1D20", + maxInit: options.maxInit, + fieldRollMode, + rollModes + } + + const content = await foundry.applications.handlebars.renderTemplate("systems/fvtt-prism-rpg/templates/roll-initiative-dialog.hbs", dialogContext) + + const label = game.i18n.localize("PRISMRPG.Label.initiative") + const rollContext = await foundry.applications.api.DialogV2.wait({ + window: { title: "Initiative Roll" }, + classes: ["prismrpg"], + 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 + }, + }, + ], + rejectClose: false // Click on Close button will not launch an error + }) + + let initRoll = new Roll(`min(${rollContext.initiativeDice}, ${options.maxInit})`, options.data, rollContext) + await initRoll.evaluate() + let msg = await initRoll.toMessage({ flavor: `Initiative for ${options.actorName}` }, { rollMode: rollContext.visibility }) + if (game?.dice3d) { + await game.dice3d.waitFor3DAnimationByMessageID(msg.id) + } + + if (options.combatId && options.combatantId) { + let combat = game.combats.get(options.combatId) + combat.updateEmbeddedDocuments("Combatant", [{ _id: options.combatantId, initiative: initRoll.total, 'system.progressionCount': 0 }]); + } + } + + /* ***********************************************************/ + static async promptCombatAction(options = {}) { + + const rollModes = foundry.utils.duplicate(CONFIG.Dice.rollModes); // v12 : 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", + }) + + let combatant = game.combats.get(options.combatId)?.combatants?.get(options.combatantId) + if (!combatant) { + console.error("No combatant found for this combat") + return + } + let currentAction = combatant.getFlag(SYSTEM.id, "currentAction") + + let position = game.user.getFlag(SYSTEM.id, "combat-action-dialog-pos") || { top: -1, left: -1 } + + let dialogContext = { + progressionDiceId: "", + fieldRollMode, + rollModes, + currentAction, + ...options + } + + const content = await foundry.applications.handlebars.renderTemplate("systems/fvtt-prism-rpg/templates/combat-action-dialog.hbs", dialogContext) + + let buttons = [] + if (currentAction) { + if (currentAction.type === "weapon") { + buttons.push({ + action: "roll", + label: "Roll progression dice", + callback: (event, button, dialog) => { + let pos = $('#combat-action-dialog').position() + game.user.setFlag(SYSTEM.id, "combat-action-dialog-pos", pos) + return "rollProgressionDice" + }, + }) + } else if (currentAction.type === "spell" || currentAction.type === "miracle") { + let label = "" + if (currentAction.spellStatus === "castingTime") { + let pos = $('#combat-action-dialog').position() + game.user.setFlag(SYSTEM.id, "combat-action-dialog-pos", pos) + label = "Wait casting time" + } + if (currentAction.spellStatus === "toBeCasted") { + let pos = $('#combat-action-dialog').position() + game.user.setFlag(SYSTEM.id, "combat-action-dialog-pos", pos) + label = "Cast spell/miracle" + } + if (currentAction.spellStatus === "lethargy") { + let pos = $('#combat-action-dialog').position() + game.user.setFlag(SYSTEM.id, "combat-action-dialog-pos", pos) + label = "Roll lethargy dice" + } + buttons.push({ + action: "roll", + label: label, + callback: (event, button, dialog) => { + let pos = $('#combat-action-dialog').position() + game.user.setFlag(SYSTEM.id, "combat-action-dialog-pos", foundry.utils.duplicate(pos)) + return "rollLethargyDice" + }, + }) + } + } else { + buttons.push({ + action: "roll", + label: "Select action", + callback: (event, button, dialog) => { + let pos = $('#combat-action-dialog').position() + game.user.setFlag(SYSTEM.id, "combat-action-dialog-pos", foundry.utils.duplicate(pos)) + const output = Array.from(button.form.elements).reduce((obj, input) => { + if (input.name) obj[input.name] = input.value + return obj + }, {}) + return output + }, + }, + ) + } + buttons.push({ + action: "cancel", + label: "Other action, not listed here", + callback: (event, button, dialog) => { + let pos = $('#combat-action-dialog').position() + game.user.setFlag(SYSTEM.id, "combat-action-dialog-pos", foundry.utils.duplicate(pos)) + return null; + } + }) + + let rollContext = await foundry.applications.api.DialogV2.wait({ + window: { title: "Combat Action Dialog" }, + id: "combat-action-dialog", + classes: ["prismrpg"], + position, + content, + buttons, + rejectClose: false // Click on Close button will not launch an error + }) + + console.log("RollContext", dialogContext, rollContext) + // If action is cancelled, exit + if (rollContext === null || rollContext === "cancel") { + await combatant.setFlag(SYSTEM.id, "currentAction", "") + let message = `${combatant.name} : Other action, progression reset` + ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) + return + } + + // Setup the current action + if (!currentAction || currentAction === "") { + // Get the item from the returned selectedChoice value + let selectedChoice = rollContext.selectedChoice + let rangedMode + if (selectedChoice.match("simpleAim")) { + selectedChoice = selectedChoice.replace("simpleAim", "") + rangedMode = "simpleAim" + } + if (selectedChoice.match("carefulAim")) { + selectedChoice = selectedChoice.replace("carefulAim", "") + rangedMode = "carefulAim" + } + if (selectedChoice.match("focusedAim")) { + selectedChoice = selectedChoice.replace("focusedAim", "") + rangedMode = "focusedAim" + } + let selectedItem = combatant.actor.items.find(i => i.id === selectedChoice) + // Setup flag for combat action usage + let actionItem = foundry.utils.duplicate(selectedItem) + actionItem.progressionCount = 1 + actionItem.rangedMode = rangedMode + actionItem.castingTime = 1 + actionItem.spellStatus = "castingTime" + // Set the flag on the combatant + await combatant.setFlag(SYSTEM.id, "currentAction", actionItem) + let message = `${combatant.name} action : ${selectedItem.name}, start rolling progression dice or casting time` + ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) + rollContext = (actionItem.type === "weapon") ? "rollProgressionDice" : "rollLethargyDice" // Set the roll context to rollProgressionDice + currentAction = actionItem + } + + if (currentAction) { + if (rollContext === "rollLethargyDice") { + if (currentAction.spellStatus === "castingTime") { + let time = currentAction.type === "spell" ? currentAction.system.castingTime : currentAction.system.prayerTime + if (currentAction.castingTime < time) { + let message = `Casting time : ${currentAction.name}, count : ${currentAction.castingTime}/${time}` + ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) + currentAction.castingTime += 1 + await combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction)) + return + } else { + let message = `Spell/Miracle ${currentAction.name} ready to be cast on next second !` + ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) + currentAction.castingTime = 1 + currentAction.spellStatus = "toBeCasted" + await combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction)) + return + } + } + if (currentAction.spellStatus === "toBeCasted") { + combatant.actor.prepareRoll((currentAction.type === "spell") ? "spell-attack" : "miracle-attack", currentAction._id) + if (currentAction.type === "spell") { + currentAction.spellStatus = "lethargy" + await combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction)) + } else { + // No lethargy for miracle + await combatant.setFlag(SYSTEM.id, "currentAction", "") + } + return + } + if (currentAction.spellStatus === "lethargy") { + // Roll lethargy dice + let dice = PrismRPGUtils.getLethargyDice(currentAction.system.level) + let roll = new Roll(dice) + await roll.evaluate() + if (game?.dice3d) { + await game.dice3d.showForRoll(roll) + } + let max = roll.dice[0].faces - 1 + let toCompare = Math.min(currentAction.progressionCount, max) + if (roll.total <= toCompare) { + // Notify that the player can act now with a chat message + let message = game.i18n.format("PRISMRPG.Notifications.messageLethargyOK", { name: combatant.actor.name, spellName: currentAction.name, roll: roll.total }) + ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) + // Update the combatant progression count + await combatant.setFlag(SYSTEM.id, "currentAction", "") + // Display the action selection window again + combatant.actor.system.rollProgressionDice(options.combatId, options.combatantId) + } else { + // Notify that the player cannot act now with a chat message + currentAction.progressionCount += 1 + await combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction)) + let message = game.i18n.format("PRISMRPG.Notifications.messageLethargyKO", { name: combatant.actor.name, spellName: currentAction.name, roll: roll.total }) + ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) + } + } + } + + if (rollContext === "rollProgressionDice") { + let formula = currentAction.system.combatProgressionDice + if (currentAction?.rangedMode) { + let toSplit = currentAction.system.speed[currentAction.rangedMode] + let split = toSplit.split("+") + currentAction.rangedLoad = Number(split[0]) || 0 + formula = split[1] + console.log("Ranged Mode", currentAction.rangedMode, currentAction.rangedLoad, formula) + } + // Range weapon loading + if (!currentAction.weaponLoaded && currentAction.rangedLoad) { + if (currentAction.progressionCount <= currentAction.rangedLoad) { + let message = `Ranged weapon ${currentAction.name} is loading, loading count : ${currentAction.progressionCount}/${currentAction.rangedLoad}` + ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) + currentAction.progressionCount += 1 + await combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction)) + } else { + let message = `Ranged weapon ${currentAction.name} is loaded !` + ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) + currentAction.weaponLoaded = true + currentAction.progressionCount = 1 + await combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction)) + } + return + } + + // Melee mode + let isMonster = combatant.actor.type === "monster" + // Get the dice and roll it if + let roll = new Roll(formula) + await roll.evaluate() + + let max = roll.dice[0].faces - 1 + max = Math.min(currentAction.progressionCount, max) + let msg = await roll.toMessage({ flavor: `Progression Roll for ${currentAction.name}, progression count : ${currentAction.progressionCount}/${max}` }, { rollMode: rollContext.visibility }) + if (game?.dice3d) { + await game.dice3d.waitFor3DAnimationByMessageID(msg.id) + } + + if (roll.total <= max) { + // Notify that the player can act now with a chat message + let message = game.i18n.format("PRISMRPG.Notifications.messageProgressionOK", { isMonster, name: combatant.actor.name, weapon: currentAction.name, roll: roll.total }) + ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) + await combatant.setFlag(SYSTEM.id, "currentAction", "") + combatant.actor.prepareRoll(currentAction.type === "weapon" ? "weapon-attack" : "spell-attack", currentAction._id) + } else { + // Notify that the player cannot act now with a chat message + currentAction.progressionCount += 1 + combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction)) + let message = game.i18n.format("PRISMRPG.Notifications.messageProgressionKO", { isMonster, name: combatant.actor.name, weapon: currentAction.name, roll: roll.total }) + ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) + } + } + } + } + + /* ***********************************************************/ + static async promptRangedDefense(options = {}) { + + const rollModes = foundry.utils.duplicate(CONFIG.Dice.rollModes); + const fieldRollMode = new foundry.data.fields.StringField({ + choices: rollModes, + blank: false, + default: "public", + }) + + let dialogContext = { + movementChoices: SYSTEM.MOVEMENT_CHOICES, + moveDirectionChoices: SYSTEM.MOVE_DIRECTION_CHOICES, + sizeChoices: SYSTEM.SIZE_CHOICES, + rangeChoices: SYSTEM.RANGE_CHOICES, + attackerAimChoices: SYSTEM.ATTACKER_AIM_CHOICES, + movement: "none", + moveDirection: "none", + size: "+5", + range: "short", + attackerAim: "simple", + fieldRollMode, + rollModes + } + + const content = await foundry.applications.handlebars.renderTemplate("systems/fvtt-prism-rpg/templates/range-defense-dialog.hbs", dialogContext) + + const label = game.i18n.localize("PRISMRPG.Label.rangeDefenseRoll") + const rollContext = await foundry.applications.api.DialogV2.wait({ + window: { title: "Range Defense" }, + classes: ["prismrpg"], + 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 + }, + }, + ], + rejectClose: false // Click on Close button will not launch an error + }) + + // If the user cancels the dialog, exit + if (rollContext === null) return + + console.log("RollContext", rollContext) + // Add disfavor/favor option if point blank range + if (rollContext.range === "pointblank") { + rollContext.movement = rollContext.movement.replace("kh", "") + rollContext.movement = rollContext.movement.replace("kl", "") + rollContext.movement += "kl" // Add the kl to the movement (disfavor for point blank range) + rollContext.range = "0" + } + if (rollContext.range === "beyondskill") { + rollContext.movement = rollContext.movement.replace("kh", "") + rollContext.movement = rollContext.movement.replace("kl", "") + rollContext.movement += "kh" // Add the kl to the movement (favor for point blank range) + rollContext.range = "+11" + } + + // Build the final modifier + let fullModifier = Number(rollContext.moveDirection) + + Number(rollContext.size) + + Number(rollContext.range) + + Number(rollContext?.attackerAim || 0) + + let modifierFormula + if (fullModifier === 0) { + modifierFormula = "0" + } else { + let modAbs = Math.abs(fullModifier) + modifierFormula = `D${modAbs + 1} -1` + } + + let rollData = { ...rollContext } + // Merge rollContext object into options object + options = { ...options, ...rollContext } + options.rollName = "Ranged Defense" + + const rollBase = new this(rollContext.movement, options.data, rollData) + const rollModifier = new Roll(modifierFormula, options.data, rollData) + rollModifier.evaluate() + await rollBase.evaluate() + let rollD30 = await new Roll("1D30").evaluate() + options.D30result = rollD30.total + + let badResult = 0 + if (rollContext.movement.includes("kh")) { + rollData.favor = "favor" + badResult = Math.min(rollBase.terms[0].results[0].result, rollBase.terms[0].results[1]?.result || 20) + } + if (rollContext.movement.includes("kl")) { + rollData.favor = "disfavor" + badResult = Math.max(rollBase.terms[0].results[0].result, rollBase.terms[0].results[1]?.result || 1) + } + let dice = rollContext.movement + let maxValue = 20 // As per latest changes (was : Number(dice.match(/\d+$/)[0]) + let rollTotal = -1 + let diceResults = [] + let resultType + + let diceResult = rollBase.dice[0].results[0].result + diceResults.push({ dice: `${dice.toUpperCase()}`, value: diceResult }) + let diceSum = diceResult + while (diceResult === maxValue) { + let r = await new Roll(dice).evaluate() + diceResult = r.dice[0].results[0].result + diceResults.push({ dice: `${dice.toUpperCase()}-1`, value: diceResult - 1 }) + diceSum += (diceResult - 1) + } + if (fullModifier !== 0) { + diceResults.push({ dice: `${rollModifier.formula.toUpperCase()}`, value: rollModifier.total }) + if (fullModifier < 0) { + rollTotal = Math.max(diceSum - rollModifier.total, 0) + } else { + rollTotal = diceSum + rollModifier.total + } + } else { + rollTotal = diceSum + } + rollBase.options = { ...rollBase.options, ...options } + rollBase.options.resultType = resultType + rollBase.options.rollTotal = rollTotal + rollBase.options.diceResults = diceResults + rollBase.options.rollTarget = options.rollTarget + rollBase.options.titleFormula = `1D20E + ${modifierFormula}` + rollBase.options.D30result = options.D30result + rollBase.options.rollName = "Ranged Defense" + rollBase.options.badResult = badResult + rollBase.options.rollData = foundry.utils.duplicate(rollData) + /** + * A hook event that fires after the roll has been made. + * @function + * @memberof hookEvents + * @param {Object} options Options for the roll. + * @param {Object} rollData All data related to the roll. + @param {PrismRPGRoll} roll The resulting roll. + * @returns {boolean} Explicitly return `false` to prevent roll to be made. + */ + + return rollBase + } + + /** + * 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 "challenge": + return `${game.i18n.localize("PRISMRPG.Label.titleChallenge")}` + case "save": + return `${game.i18n.localize("PRISMRPG.Label.titleSave")}` + case "monster-skill": + case "skill": + return `${game.i18n.localize("PRISMRPG.Label.titleSkill")}` + case "weapon-attack": + return `${game.i18n.localize("PRISMRPG.Label.weapon-attack")}` + case "weapon-defense": + return `${game.i18n.localize("PRISMRPG.Label.weapon-defense")}` + case "weapon-damage-small": + return `${game.i18n.localize("PRISMRPG.Label.weapon-damage-small")}` + case "weapon-damage-medium": + return `${game.i18n.localize("PRISMRPG.Label.weapon-damage-medium")}` + case "spell": + case "spell-attack": + case "spell-power": + return `${game.i18n.localize("PRISMRPG.Label.spell")}` + case "miracle": + case "miracle-attack": + case "miracle-power": + return `${game.i18n.localize("PRISMRPG.Label.miracle")}` + default: + return game.i18n.localize("PRISMRPG.Label.titleStandard") + } + } + + /** @override */ + async render(chatOptions = {}) { + let chatData = await this._getChatCardData(chatOptions.isPrivate) + console.log("ChatData", chatData) + return await foundry.applications.handlebars.renderTemplate(this.constructor.CHAT_TEMPLATE, chatData) + } + + /* + * Generates the data required for rendering a roll chat card. + */ + async _getChatCardData(isPrivate) { + const cardData = { + css: [SYSTEM.id, "dice-roll"], + data: this.data, + diceTotal: this.dice.reduce((t, d) => t + d.total, 0), + isGM: game.user.isGM, + formula: this.formula, + titleFormula: this.titleFormula, + rollName: this.rollName, + rollType: this.type, + rollTarget: this.rollTarget, + total: this.rollTotal, + isFailure: this.isFailure, + actorId: this.actorId, + diceResults: this.diceResults, + actingCharName: this.actorName, + actingCharImg: this.actorImage, + resultType: this.resultType, + hasTarget: this.hasTarget, + targetName: this.targetName, + targetArmor: this.targetArmor, + D30result: this.D30result, + badResult: this.badResult, + rollData: this.rollData, + isPrivate: isPrivate + } + 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( + { + isSave: this.isSave, + isChallenge: this.isChallenge, + isFailure: this.resultType === "failure", + rollType: this.type, + rollTarget: this.rollTarget, + actingCharName: this.actorName, + actingCharImg: this.actorImage, + hasTarget: this.hasTarget, + targetName: this.targetName, + targetArmor: this.targetArmor, + targetMalus: this.targetMalus, + realDamage: this.realDamage, + rollData: this.rollData, + ...messageData, + }, + { rollMode: rollMode }, + ) + } + +} diff --git a/module/documents/roll.mjs b/module/documents/roll.mjs index 6a75b29..db88d53 100644 --- a/module/documents/roll.mjs +++ b/module/documents/roll.mjs @@ -1,13 +1,13 @@ import { SYSTEM } from "../config/system.mjs" -import PrismRPGUtils from "../utils.mjs" +/** + * D&D 5e style Roll system for Prism RPG + * Simple 1d20 + modifier, with advantage/disadvantage + */ export default class PrismRPGRoll extends Roll { - /** - * The HTML template path used to render dice checks of this type - * @type {string} - */ static CHAT_TEMPLATE = "systems/fvtt-prism-rpg/templates/chat-message.hbs" + // Getters for roll data get type() { return this.options.type } @@ -28,10 +28,6 @@ export default class PrismRPGRoll extends Roll { return this.options.value } - get treshold() { - return this.options.treshold - } - get actorId() { return this.options.actorId } @@ -64,250 +60,132 @@ export default class PrismRPGRoll extends Roll { return this.options.targetName } - get targetArmor() { - return this.options.targetArmor - } - - get targetMalus() { - return this.options.targetMalus - } - - get realDamage() { - return this.options.realDamage - } - get rollTotal() { return this.options.rollTotal } - get diceResults() { - return this.options.diceResults - } - get rollTarget() { return this.options.rollTarget } - get D30result() { - return this.options.D30result - } - - get badResult() { - return this.options.badResult - } - get rollData() { return this.options.rollData } /** - * 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 (e.g., RESOURCE, DAMAGE, ATTACK, SAVE). - * @param {string} options.rollValue The initial value or formula for the roll. - * @param {string} options.rollTarget The target of the roll. - * @param {"="|"+"|"++"|"-"|"--"} options.rollAdvantage If there is an avantage (+), a disadvantage (-), a double advantage (++), a double disadvantage (--) or a normal 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.target The target of the roll, if any. - * @param {Object} options.data Additional data for the roll. - * - * @returns {Promise} The roll result or null if the dialog was cancelled. + * D&D 5e style dice roll prompt + * Formula: 1d20 + modifier, or damage dice + modifier + * Advantage: 2d20kh, Disadvantage: 2d20kl + * @param {Object} options Roll options + * @returns {Promise} The roll result or null if cancelled */ static async prompt(options = {}) { - let dice = "1D20" - let maxValue = 20 - let baseFormula = "1D20" - let modifierFormula = "1D0" + let dice = "1d20" let hasModifier = true - let hasChangeDice = false - let hasD30 = false - let hasFavor = false - let hasMaxValue = true - let hasGrantedDice = false - let pointBlank = false - let letItFly = false - let saveSpell = false - let beyondSkill = false - let hasStaticModifier = false - let hasExplode = true + let hasAdvantage = true + let isDamageRoll = false - if (options.rollType === "challenge" || options.rollType === "save") { - options.rollName = game.i18n.localize(`PRISMRPG.Label.${options.rollTarget.rollKey}`) - hasD30 = options.rollType === "save" - if (options.rollTarget.rollKey === "dying") { - dice = options.rollTarget.value - hasModifier = false - hasChangeDice = true - hasFavor = true - } else { - dice = "1D20" - hasFavor = true - } + // Determine roll type and modifiers + switch (options.rollType) { + case "characteristic": + options.rollName = options.rollTarget.name + // Value already set in actor.mjs + break - } else if (options.rollType === "granted") { - hasD30 = false - options.rollName = `Granted ${options.rollTarget.rollKey}` - dice = options.rollTarget.formula - baseFormula = options.rollTarget.formula - hasModifier = false - hasMaxValue = false - hasChangeDice = false - hasFavor = false + case "challenge": + case "save": + options.rollName = game.i18n.localize(`PRISMRPG.Label.${options.rollTarget.rollKey}`) + break - } else if (options.rollType === "monster-attack" || options.rollType === "monster-defense") { - hasD30 = true - options.rollName = options.rollTarget.name - dice = "1D20" - baseFormula = "D20" - hasModifier = true - hasChangeDice = false - hasFavor = true - if (options.rollType === "monster-attack") { - options.rollTarget.value = options.rollTarget.attackModifier - options.rollTarget.charModifier = 0 - } else { - options.rollTarget.value = options.rollTarget.defenseModifier - options.rollTarget.charModifier = 0 - } + case "skill": + options.rollName = options.rollTarget.name + options.rollTarget.value = Math.floor(options.rollTarget.system.skillTotal / 10) + break - } else if (options.rollType === "monster-skill") { - options.rollName = game.i18n.localize(`PRISMRPG.Label.${options.rollTarget.rollKey}`) - dice = "1D20" - baseFormula = "D20" - hasModifier = true - hasFavor = true - hasChangeDice = false - - } else if (options.rollType === "skill") { - options.rollName = options.rollTarget.name - hasD30 = true - dice = "1D20" - baseFormula = "D20" - hasModifier = true - hasFavor = true - hasChangeDice = false - options.rollTarget.value = Math.floor(options.rollTarget.system.skillTotal / 10) - - } else if (options.rollType === "weapon-attack" || options.rollType === "weapon-defense") { - hasD30 = true - options.rollName = options.rollTarget.name - dice = "1D20" - baseFormula = "D20" - hasModifier = true - hasChangeDice = false - hasFavor = true - if (options.rollType === "weapon-attack") { + case "weapon-attack": + options.rollName = options.rollTarget.name if (options.rollTarget.weapon.system.weaponType === "melee") { - options.rollTarget.value = options.rollTarget.combat.attackModifier + options.rollTarget.weaponSkillModifier + options.rollTarget.weapon.system.bonuses.attackBonus - options.rollTarget.charModifier = options.rollTarget.combat.attackModifier + options.rollTarget.value = options.rollTarget.combat.attackModifier + + options.rollTarget.weaponSkillModifier + + options.rollTarget.weapon.system.bonuses.attackBonus } else { - options.rollTarget.value = options.rollTarget.combat.rangedAttackModifier + options.rollTarget.weaponSkillModifier + options.rollTarget.weapon.system.bonuses.attackBonus - options.rollTarget.charModifier = options.rollTarget.combat.rangedAttackModifier + options.rollTarget.value = options.rollTarget.combat.rangedAttackModifier + + options.rollTarget.weaponSkillModifier + + options.rollTarget.weapon.system.bonuses.attackBonus } - } else { - options.rollTarget.value = options.rollTarget.combat.defenseModifier + options.rollTarget.weaponSkillModifier + options.rollTarget.weapon.system.bonuses.defenseBonus - options.rollTarget.charModifier = options.rollTarget.combat.defenseModifier - } + break - } else if (options.rollType === "spell" || options.rollType === "spell-attack" || options.rollType === "spell-power") { - hasD30 = true - options.rollName = options.rollTarget.name - dice = "1D20" - baseFormula = "D20" - hasModifier = true - hasChangeDice = false - options.rollTarget.value = options.rollTarget.actorModifiers.levelSpellModifier + options.rollTarget.actorModifiers.intSpellModifier - options.rollTarget.charModifier = options.rollTarget.actorModifiers.intSpellModifier - hasStaticModifier = options.rollType === "spell-power" - //hasModifier = options.rollType !== "spell-attack" - if (hasStaticModifier) { - options.rollTarget.staticModifier = options.rollTarget.actorLevel - } else { - options.rollTarget.staticModifier = 0 - } + case "weapon-defense": + options.rollName = options.rollTarget.name + options.rollTarget.value = options.rollTarget.combat.defenseModifier + + options.rollTarget.weaponSkillModifier + + options.rollTarget.weapon.system.bonuses.defenseBonus + break - } else if (options.rollType === "miracle" || options.rollType === "miracle-attack" || options.rollType === "miracle-power") { - hasD30 = true - options.rollName = options.rollTarget.name - dice = "1D20" - baseFormula = "D20" - hasChangeDice = false - options.rollTarget.value = options.rollTarget.actorModifiers.levelMiracleModifier + options.rollTarget.actorModifiers.chaMiracleModifier - options.rollTarget.charModifier = options.rollTarget.actorModifiers.chaMiracleModifier - hasStaticModifier = options.rollType === "miracle-power" - //hasModifier = options.rollType !== "miracle-attack" - if (hasStaticModifier) { - options.rollTarget.staticModifier = options.rollTarget.actorLevel - } else { - options.rollTarget.staticModifier = 0 - } + case "spell": + case "spell-attack": + case "spell-power": + options.rollName = options.rollTarget.name + options.rollTarget.value = options.rollTarget.actorModifiers.levelSpellModifier + + options.rollTarget.actorModifiers.intSpellModifier + break - } else if (options.rollType === "shield-roll") { - // Legacy Lethal Fantasy - Shield Defense Roll (not used in PRISM RPG) - // In PRISM, shields use Block action with Shield Rating (SR) - hasD30 = false - options.rollName = "Shield Block" - dice = "1d20" // Placeholder - actual Block mechanic handled elsewhere - baseFormula = dice - hasModifier = false - hasChangeDice = false - hasMaxValue = false - hasExplode = false - options.rollTarget.value = 0 + case "miracle": + case "miracle-attack": + case "miracle-power": + options.rollName = options.rollTarget.name + options.rollTarget.value = options.rollTarget.actorModifiers.levelMiracleModifier + + options.rollTarget.actorModifiers.chaMiracleModifier + break - } else if (options.rollType.includes("weapon-damage")) { - options.rollName = options.rollTarget.name - hasModifier = true - hasChangeDice = false - // In PRISM, all weapons apply STR damage bonus - let damageBonus = options.rollTarget.combat.damageModifier - options.rollTarget.value = damageBonus + options.rollTarget.weaponSkillModifier + options.rollTarget.weapon.system.bonuses.damageBonus - options.rollTarget.charModifier = damageBonus - if (options.rollType.includes("small")) { - dice = options.rollTarget.weapon.system.damage.damageS - } else { - dice = options.rollTarget.weapon.system.damage.damageM - } - dice = dice.replace("E", "") - baseFormula = dice + case "monster-attack": + options.rollName = options.rollTarget.name + options.rollTarget.value = options.rollTarget.attackModifier + break - } else if (options.rollType.includes("monster-damage")) { - options.rollName = options.rollTarget.name - hasModifier = true - hasChangeDice = false - options.rollTarget.value = options.rollTarget.damageModifier - options.rollTarget.charModifier = 0 - dice = options.rollTarget.damageDice - dice = dice.replace("E", "") - baseFormula = dice + case "monster-defense": + options.rollName = options.rollTarget.name + options.rollTarget.value = options.rollTarget.defenseModifier + break + + case "monster-skill": + options.rollName = game.i18n.localize(`PRISMRPG.Label.${options.rollTarget.rollKey}`) + break + + default: + if (options.rollType.includes("weapon-damage")) { + isDamageRoll = true + hasAdvantage = false + options.rollName = options.rollTarget.name + let damageBonus = options.rollTarget.combat.damageModifier + options.rollTarget.value = damageBonus + + options.rollTarget.weaponSkillModifier + + options.rollTarget.weapon.system.bonuses.damageBonus + + if (options.rollType.includes("small")) { + dice = options.rollTarget.weapon.system.damage.damageS + } else { + dice = options.rollTarget.weapon.system.damage.damageM + } + dice = dice.replace(/E/gi, "") + } else if (options.rollType.includes("monster-damage")) { + isDamageRoll = true + hasAdvantage = false + options.rollName = options.rollTarget.name + options.rollTarget.value = options.rollTarget.damageModifier + dice = options.rollTarget.damageDice.replace(/E/gi, "") + } else if (options.rollType === "granted") { + hasModifier = false + hasAdvantage = false + options.rollName = `Granted ${options.rollTarget.rollKey}` + dice = options.rollTarget.formula + } } - - if (options.rollType === "save" && (options.rollTarget.rollKey === "pain" || options.rollTarget.rollKey === "paincourage")) { - dice = options.rollTarget.rollDice - baseFormula = options.rollTarget.rollDice - hasModifier = false - } - - const rollModes = foundry.utils.duplicate(CONFIG.Dice.rollModes); // v12 : Object.fromEntries(Object.entries(CONFIG.Dice.rollModes).map(([key, value]) => [key, game.i18n.localize(value)])) - console.log("Roll mode", rollModes) - - const fieldRollMode = new foundry.data.fields.StringField({ - choices: rollModes, - blank: false, - default: "public", - }) - + // Setup dialog + const rollModes = foundry.utils.duplicate(CONFIG.Dice.rollModes) const choiceModifier = SYSTEM.CHOICE_MODIFIERS - const choiceDice = SYSTEM.CHOICE_DICE - const choiceFavor = SYSTEM.FAVOR_CHOICES - - let modifier = "+0" - let targetName + const choiceAdvantage = SYSTEM.ADVANTAGE_CHOICES let dialogContext = { rollType: options.rollType, @@ -316,155 +194,68 @@ export default class PrismRPGRoll extends Roll { actorName: options.actorName, rollModes, hasModifier, - hasFavor, - hasChangeDice, - pointBlank, - baseValue: options.rollTarget.value, - attackerAimChoices: SYSTEM.ATTACKER_AIM_CHOICES, - attackerAim: "0", - changeDice: `${dice}`, - fieldRollMode, - choiceModifier, - choiceDice, - choiceFavor, - baseFormula, + hasAdvantage, + isDamageRoll, + baseValue: options.rollTarget.value || 0, dice, + choiceModifier, + choiceAdvantage, hasTarget: options.hasTarget, - modifier, - saveSpell: false, - favor: "none", - targetName + modifier: "+0", + advantage: "none" } - const content = await foundry.applications.handlebars.renderTemplate("systems/fvtt-prism-rpg/templates/roll-dialog.hbs", dialogContext) + + const content = await foundry.applications.handlebars.renderTemplate( + "systems/fvtt-prism-rpg/templates/roll-dialog.hbs", + dialogContext + ) let position = game.user.getFlag(SYSTEM.id, "roll-dialog-pos") || { top: -1, left: -1 } const label = game.i18n.localize("PRISMRPG.Roll.roll") + const rollContext = await foundry.applications.api.DialogV2.wait({ window: { title: "Roll dialog" }, classes: ["prismrpg"], content, position, - buttons: [ - { - label: label, - callback: (event, button, dialog) => { - console.log("Roll context", event, button, dialog) - let position = dialog.position - game.user.setFlag(SYSTEM.id, "roll-dialog-pos", foundry.utils.duplicate(position)) - const output = Array.from(button.form.elements).reduce((obj, input) => { - if (input.name) obj[input.name] = input.value - return obj - }, {}) - return output - }, + buttons: [{ + label: label, + callback: (event, button, dialog) => { + game.user.setFlag(SYSTEM.id, "roll-dialog-pos", foundry.utils.duplicate(dialog.position)) + const output = Array.from(button.form.elements).reduce((obj, input) => { + if (input.name) obj[input.name] = input.value + return obj + }, {}) + return output }, - ], - actions: { - "selectGranted": (event, button, dialog) => { - hasGrantedDice = event.target.checked - }, - "selectBeyondSkill": (event, button, dialog) => { - beyondSkill = button.checked - }, - "selectPointBlank": (event, button, dialog) => { - pointBlank = button.checked - }, - "selectLetItFly": (event, button, dialog) => { - letItFly = button.checked - }, - "saveSpellCheck": (event, button, dialog) => { - saveSpell = button.checked - }, - "gotoToken": (event, button, dialog) => { - let tokenId = $(button).data("tokenId") - let token = canvas.tokens?.get(tokenId) - if (token) { - canvas.animatePan({ x: token.x, y: token.y, duration: 200 }) - canvas.tokens.releaseAll(); - token.control({ releaseOthers: true }); - } - } - }, - rejectClose: false // Click on Close button will not launch an error + }], + rejectClose: false }) - // If the user cancels the dialog, exit if (rollContext === null) return - console.log("rollContext", rollContext, hasGrantedDice) - rollContext.saveSpell = saveSpell // Update fucking flag - let fullModifier = 0 - let titleFormula = "" - dice = rollContext.changeDice || dice + // Build D&D 5e formula: 1d20 + modifier + let finalFormula = dice + let totalModifier = 0 + if (hasModifier) { - let bonus = Number(options.rollTarget.value) - fullModifier = rollContext.modifier === "" ? 0 : parseInt(rollContext.modifier, 10) + bonus - fullModifier += (rollContext.saveSpell) ? options.rollTarget.actorModifiers.saveModifier : 0 - if (Number(rollContext.attackerAim) > 0) { - fullModifier += Number(rollContext.attackerAim) - } + let bonus = Number(options.rollTarget.value) || 0 + let extraModifier = rollContext.modifier === "" ? 0 : parseInt(rollContext.modifier, 10) + totalModifier = bonus + extraModifier - if (fullModifier === 0) { - modifierFormula = "0" - } else { - let modAbs = Math.abs(fullModifier) - modifierFormula = `D${modAbs + 1} - 1` - } - if (hasStaticModifier) { - modifierFormula += ` + ${options.rollTarget.staticModifier}` - } - let sign = fullModifier < 0 ? "-" : "+" - if (hasExplode) { - titleFormula = `${dice}E ${sign} ${modifierFormula}` - } else { - titleFormula = `${dice} ${sign} ${modifierFormula}` - } - } else { - modifierFormula = "0" - fullModifier = 0 - baseFormula = `${dice}` - if (hasExplode) { - titleFormula = `${dice}E` - } else { - titleFormula = `${dice}` + if (totalModifier !== 0) { + finalFormula = totalModifier > 0 ? + `${dice} + ${totalModifier}` : + `${dice} - ${Math.abs(totalModifier)}` } } - // Latest addition : favor choice at point blank range - if (pointBlank) { - rollContext.favor = "favor" + // Apply advantage/disadvantage + if (rollContext.advantage === "advantage" && !isDamageRoll) { + finalFormula = finalFormula.replace(dice, `2${dice}kh`) + } else if (rollContext.advantage === "disadvantage" && !isDamageRoll) { + finalFormula = finalFormula.replace(dice, `2${dice}kl`) } - if (beyondSkill) { - rollContext.favor = "disfavor" - } - - // Specific pain case - if (options.rollType === "save" && options.rollTarget.rollKey === "pain" || options.rollTarget.rollKey === "paincourage") { - baseFormula = options.rollTarget.rollDice - titleFormula = `${dice}` - modifierFormula = "0" - fullModifier = 0 - } - - // Specific pain/poison/contagion case - if (options.rollType === "save" && (options.rollTarget.rollKey === "poison" || options.rollTarget.rollKey === "contagion")) { - hasD30 = false - hasStaticModifier = true - modifierFormula = ` + ${Math.abs(fullModifier)}` - titleFormula = `${dice}E + ${Math.abs(fullModifier)}` - } - - if (letItFly) { - baseFormula = "1D20" - titleFormula = `1D20E` - modifierFormula = "0" - fullModifier = 0 - hasFavor = false - hasExplode = true - rollContext.favor = "none" - } - - maxValue = Number(baseFormula.match(/\d+$/)[0]) // Update the max value agains const rollData = { type: options.rollType, @@ -476,654 +267,34 @@ export default class PrismRPGRoll extends Roll { actorImage: options.actorImage, rollMode: rollContext.visibility, hasTarget: options.hasTarget, - pointBlank, - beyondSkill, - letItFly, - hasGrantedDice, - titleFormula, - targetName, + titleFormula: finalFormula, ...rollContext, } - /** - * A hook event that fires before the roll is made. - * @function - * @memberof hookEvents - * @param {Object} options Options for the roll. - * @param {Object} rollData All data related to the roll. - * @returns {boolean} Explicitly return `false` to prevent roll to be made. - */ if (Hooks.call("fvtt-prism-rpg.preRoll", options, rollData) === false) return - let rollBase = new this(baseFormula, options.data, rollData) - const rollModifier = new Roll(modifierFormula, options.data, rollData) - await rollModifier.evaluate() - await rollBase.evaluate() + // Execute the roll + let roll = new this(finalFormula, options.data, rollData) + await roll.evaluate() - let rollFavor - let badResult - if (rollContext.favor === "favor") { - rollFavor = new this(baseFormula, options.data, rollData) - await rollFavor.evaluate() - console.log("Rolling with favor", rollFavor) - if (game?.dice3d) { - game.dice3d.showForRoll(rollFavor, game.user, true) - } - if (Number(rollFavor.result) > Number(rollBase.result)) { - badResult = rollBase.result - rollBase = rollFavor - } else { - badResult = rollFavor.result - } - rollFavor = null - } + // Store results + roll.options.resultType = "success" + roll.options.rollTotal = roll.total + roll.options.rollTarget = options.rollTarget + roll.options.titleFormula = finalFormula + roll.options.rollData = foundry.utils.duplicate(rollData) - if (rollContext.favor === "disfavor") { - rollFavor = new this(baseFormula, options.data, rollData) - await rollFavor.evaluate() - if (game?.dice3d) { - game.dice3d.showForRoll(rollFavor, game.user, true) - } - if (Number(rollFavor.result) < Number(rollBase.result)) { - badResult = rollBase.result - rollBase = rollFavor - } else { - badResult = rollFavor.result - } - rollFavor = null - } + if (Hooks.call("fvtt-prism-rpg.Roll", options, rollData, roll) === false) return - if (hasD30) { - let rollD30 = await new Roll("1D30").evaluate() - if (game?.dice3d) { - game.dice3d.showForRoll(rollD30, game.user, true) - } - options.D30result = rollD30.total - } - - let rollTotal = 0 - let diceResults = [] - let resultType - let diceSum = 0 - - let singleDice = `1D${maxValue}` - for (let i = 0; i < rollBase.dice.length; i++) { - for (let j = 0; j < rollBase.dice[i].results.length; j++) { - let diceResult = rollBase.dice[i].results[j].result - diceResults.push({ dice: `${singleDice.toUpperCase()}`, value: diceResult }) - diceSum += diceResult - if (hasMaxValue) { - while (diceResult === maxValue) { - let r = await new Roll(baseFormula).evaluate() - if (game?.dice3d) { - await game.dice3d.showForRoll(r, game.user, true) - } - diceResult = r.dice[0].results[0].result - diceResults.push({ dice: `${singleDice.toUpperCase()}-1`, value: diceResult - 1 }) - diceSum += (diceResult - 1) - } - } - } - } - - if (hasGrantedDice && options.rollTarget.grantedDice && options.rollTarget.grantedDice !== "") { - titleFormula += ` + ${options.rollTarget.grantedDice.toUpperCase()}` - let grantedRoll = new Roll(options.rollTarget.grantedDice) - await grantedRoll.evaluate() - if (game?.dice3d) { - await game.dice3d.showForRoll(grantedRoll, game.user, true) - } - diceResults.push({ dice: `${options.rollTarget.grantedDice.toUpperCase()}`, value: grantedRoll.total }) - rollTotal += grantedRoll.total - } - - if (fullModifier !== 0) { - diceResults.push({ dice: `${rollModifier.formula.toUpperCase()}`, value: rollModifier.total }) - if (fullModifier < 0) { - rollTotal += Math.max(diceSum - rollModifier.total, 0) - } else { - rollTotal += diceSum + rollModifier.total - } - } else { - rollTotal += diceSum - } - - rollBase.options.resultType = resultType - rollBase.options.rollTotal = rollTotal - rollBase.options.diceResults = diceResults - rollBase.options.rollTarget = options.rollTarget - rollBase.options.titleFormula = titleFormula - rollBase.options.D30result = options.D30result - rollBase.options.badResult = badResult - rollBase.options.rollData = foundry.utils.duplicate(rollData) - - /** - * A hook event that fires after the roll has been made. - * @function - * @memberof hookEvents - * @param {Object} options Options for the roll. - * @param {Object} rollData All data related to the roll. - @param {PrismRPGRoll} roll The resulting roll. - * @returns {boolean} Explicitly return `false` to prevent roll to be made. - */ - if (Hooks.call("fvtt-prism-rpg.Roll", options, rollData, rollBase) === false) return - - return rollBase - } - - /* ***********************************************************/ - static async promptInitiative(options = {}) { - const rollModes = foundry.utils.duplicate(CONFIG.Dice.rollModes); // v12 : 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", - }) - - if (SYSTEM.INITIATIVE_DICE_CHOICES_PER_CLASS[options.actorClass]) { - options.initiativeDiceChoice = SYSTEM.INITIATIVE_DICE_CHOICES_PER_CLASS[options.actorClass] - } else { - options.initiativeDiceChoice = SYSTEM.INITIATIVE_DICE_CHOICES_PER_CLASS["untrained"] - } - - let dialogContext = { - actorClass: options.actorClass, - initiativeDiceChoice: options.initiativeDiceChoice, - initiativeDice: "1D20", - maxInit: options.maxInit, - fieldRollMode, - rollModes - } - - const content = await foundry.applications.handlebars.renderTemplate("systems/fvtt-prism-rpg/templates/roll-initiative-dialog.hbs", dialogContext) - - const label = game.i18n.localize("PRISMRPG.Label.initiative") - const rollContext = await foundry.applications.api.DialogV2.wait({ - window: { title: "Initiative Roll" }, - classes: ["prismrpg"], - 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 - }, - }, - ], - rejectClose: false // Click on Close button will not launch an error - }) - - let initRoll = new Roll(`min(${rollContext.initiativeDice}, ${options.maxInit})`, options.data, rollContext) - await initRoll.evaluate() - let msg = await initRoll.toMessage({ flavor: `Initiative for ${options.actorName}` }, { rollMode: rollContext.visibility }) - if (game?.dice3d) { - await game.dice3d.waitFor3DAnimationByMessageID(msg.id) - } - - if (options.combatId && options.combatantId) { - let combat = game.combats.get(options.combatId) - combat.updateEmbeddedDocuments("Combatant", [{ _id: options.combatantId, initiative: initRoll.total, 'system.progressionCount': 0 }]); - } - } - - /* ***********************************************************/ - static async promptCombatAction(options = {}) { - - const rollModes = foundry.utils.duplicate(CONFIG.Dice.rollModes); // v12 : 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", - }) - - let combatant = game.combats.get(options.combatId)?.combatants?.get(options.combatantId) - if (!combatant) { - console.error("No combatant found for this combat") - return - } - let currentAction = combatant.getFlag(SYSTEM.id, "currentAction") - - let position = game.user.getFlag(SYSTEM.id, "combat-action-dialog-pos") || { top: -1, left: -1 } - - let dialogContext = { - progressionDiceId: "", - fieldRollMode, - rollModes, - currentAction, - ...options - } - - const content = await foundry.applications.handlebars.renderTemplate("systems/fvtt-prism-rpg/templates/combat-action-dialog.hbs", dialogContext) - - let buttons = [] - if (currentAction) { - if (currentAction.type === "weapon") { - buttons.push({ - action: "roll", - label: "Roll progression dice", - callback: (event, button, dialog) => { - let pos = $('#combat-action-dialog').position() - game.user.setFlag(SYSTEM.id, "combat-action-dialog-pos", pos) - return "rollProgressionDice" - }, - }) - } else if (currentAction.type === "spell" || currentAction.type === "miracle") { - let label = "" - if (currentAction.spellStatus === "castingTime") { - let pos = $('#combat-action-dialog').position() - game.user.setFlag(SYSTEM.id, "combat-action-dialog-pos", pos) - label = "Wait casting time" - } - if (currentAction.spellStatus === "toBeCasted") { - let pos = $('#combat-action-dialog').position() - game.user.setFlag(SYSTEM.id, "combat-action-dialog-pos", pos) - label = "Cast spell/miracle" - } - if (currentAction.spellStatus === "lethargy") { - let pos = $('#combat-action-dialog').position() - game.user.setFlag(SYSTEM.id, "combat-action-dialog-pos", pos) - label = "Roll lethargy dice" - } - buttons.push({ - action: "roll", - label: label, - callback: (event, button, dialog) => { - let pos = $('#combat-action-dialog').position() - game.user.setFlag(SYSTEM.id, "combat-action-dialog-pos", foundry.utils.duplicate(pos)) - return "rollLethargyDice" - }, - }) - } - } else { - buttons.push({ - action: "roll", - label: "Select action", - callback: (event, button, dialog) => { - let pos = $('#combat-action-dialog').position() - game.user.setFlag(SYSTEM.id, "combat-action-dialog-pos", foundry.utils.duplicate(pos)) - const output = Array.from(button.form.elements).reduce((obj, input) => { - if (input.name) obj[input.name] = input.value - return obj - }, {}) - return output - }, - }, - ) - } - buttons.push({ - action: "cancel", - label: "Other action, not listed here", - callback: (event, button, dialog) => { - let pos = $('#combat-action-dialog').position() - game.user.setFlag(SYSTEM.id, "combat-action-dialog-pos", foundry.utils.duplicate(pos)) - return null; - } - }) - - let rollContext = await foundry.applications.api.DialogV2.wait({ - window: { title: "Combat Action Dialog" }, - id: "combat-action-dialog", - classes: ["prismrpg"], - position, - content, - buttons, - rejectClose: false // Click on Close button will not launch an error - }) - - console.log("RollContext", dialogContext, rollContext) - // If action is cancelled, exit - if (rollContext === null || rollContext === "cancel") { - await combatant.setFlag(SYSTEM.id, "currentAction", "") - let message = `${combatant.name} : Other action, progression reset` - ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) - return - } - - // Setup the current action - if (!currentAction || currentAction === "") { - // Get the item from the returned selectedChoice value - let selectedChoice = rollContext.selectedChoice - let rangedMode - if (selectedChoice.match("simpleAim")) { - selectedChoice = selectedChoice.replace("simpleAim", "") - rangedMode = "simpleAim" - } - if (selectedChoice.match("carefulAim")) { - selectedChoice = selectedChoice.replace("carefulAim", "") - rangedMode = "carefulAim" - } - if (selectedChoice.match("focusedAim")) { - selectedChoice = selectedChoice.replace("focusedAim", "") - rangedMode = "focusedAim" - } - let selectedItem = combatant.actor.items.find(i => i.id === selectedChoice) - // Setup flag for combat action usage - let actionItem = foundry.utils.duplicate(selectedItem) - actionItem.progressionCount = 1 - actionItem.rangedMode = rangedMode - actionItem.castingTime = 1 - actionItem.spellStatus = "castingTime" - // Set the flag on the combatant - await combatant.setFlag(SYSTEM.id, "currentAction", actionItem) - let message = `${combatant.name} action : ${selectedItem.name}, start rolling progression dice or casting time` - ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) - rollContext = (actionItem.type === "weapon") ? "rollProgressionDice" : "rollLethargyDice" // Set the roll context to rollProgressionDice - currentAction = actionItem - } - - if (currentAction) { - if (rollContext === "rollLethargyDice") { - if (currentAction.spellStatus === "castingTime") { - let time = currentAction.type === "spell" ? currentAction.system.castingTime : currentAction.system.prayerTime - if (currentAction.castingTime < time) { - let message = `Casting time : ${currentAction.name}, count : ${currentAction.castingTime}/${time}` - ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) - currentAction.castingTime += 1 - await combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction)) - return - } else { - let message = `Spell/Miracle ${currentAction.name} ready to be cast on next second !` - ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) - currentAction.castingTime = 1 - currentAction.spellStatus = "toBeCasted" - await combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction)) - return - } - } - if (currentAction.spellStatus === "toBeCasted") { - combatant.actor.prepareRoll((currentAction.type === "spell") ? "spell-attack" : "miracle-attack", currentAction._id) - if (currentAction.type === "spell") { - currentAction.spellStatus = "lethargy" - await combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction)) - } else { - // No lethargy for miracle - await combatant.setFlag(SYSTEM.id, "currentAction", "") - } - return - } - if (currentAction.spellStatus === "lethargy") { - // Roll lethargy dice - let dice = PrismRPGUtils.getLethargyDice(currentAction.system.level) - let roll = new Roll(dice) - await roll.evaluate() - if (game?.dice3d) { - await game.dice3d.showForRoll(roll) - } - let max = roll.dice[0].faces - 1 - let toCompare = Math.min(currentAction.progressionCount, max) - if (roll.total <= toCompare) { - // Notify that the player can act now with a chat message - let message = game.i18n.format("PRISMRPG.Notifications.messageLethargyOK", { name: combatant.actor.name, spellName: currentAction.name, roll: roll.total }) - ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) - // Update the combatant progression count - await combatant.setFlag(SYSTEM.id, "currentAction", "") - // Display the action selection window again - combatant.actor.system.rollProgressionDice(options.combatId, options.combatantId) - } else { - // Notify that the player cannot act now with a chat message - currentAction.progressionCount += 1 - await combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction)) - let message = game.i18n.format("PRISMRPG.Notifications.messageLethargyKO", { name: combatant.actor.name, spellName: currentAction.name, roll: roll.total }) - ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) - } - } - } - - if (rollContext === "rollProgressionDice") { - let formula = currentAction.system.combatProgressionDice - if (currentAction?.rangedMode) { - let toSplit = currentAction.system.speed[currentAction.rangedMode] - let split = toSplit.split("+") - currentAction.rangedLoad = Number(split[0]) || 0 - formula = split[1] - console.log("Ranged Mode", currentAction.rangedMode, currentAction.rangedLoad, formula) - } - // Range weapon loading - if (!currentAction.weaponLoaded && currentAction.rangedLoad) { - if (currentAction.progressionCount <= currentAction.rangedLoad) { - let message = `Ranged weapon ${currentAction.name} is loading, loading count : ${currentAction.progressionCount}/${currentAction.rangedLoad}` - ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) - currentAction.progressionCount += 1 - await combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction)) - } else { - let message = `Ranged weapon ${currentAction.name} is loaded !` - ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) - currentAction.weaponLoaded = true - currentAction.progressionCount = 1 - await combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction)) - } - return - } - - // Melee mode - let isMonster = combatant.actor.type === "monster" - // Get the dice and roll it if - let roll = new Roll(formula) - await roll.evaluate() - - let max = roll.dice[0].faces - 1 - max = Math.min(currentAction.progressionCount, max) - let msg = await roll.toMessage({ flavor: `Progression Roll for ${currentAction.name}, progression count : ${currentAction.progressionCount}/${max}` }, { rollMode: rollContext.visibility }) - if (game?.dice3d) { - await game.dice3d.waitFor3DAnimationByMessageID(msg.id) - } - - if (roll.total <= max) { - // Notify that the player can act now with a chat message - let message = game.i18n.format("PRISMRPG.Notifications.messageProgressionOK", { isMonster, name: combatant.actor.name, weapon: currentAction.name, roll: roll.total }) - ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) - await combatant.setFlag(SYSTEM.id, "currentAction", "") - combatant.actor.prepareRoll(currentAction.type === "weapon" ? "weapon-attack" : "spell-attack", currentAction._id) - } else { - // Notify that the player cannot act now with a chat message - currentAction.progressionCount += 1 - combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction)) - let message = game.i18n.format("PRISMRPG.Notifications.messageProgressionKO", { isMonster, name: combatant.actor.name, weapon: currentAction.name, roll: roll.total }) - ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) - } - } - } - } - - /* ***********************************************************/ - static async promptRangedDefense(options = {}) { - - const rollModes = foundry.utils.duplicate(CONFIG.Dice.rollModes); - const fieldRollMode = new foundry.data.fields.StringField({ - choices: rollModes, - blank: false, - default: "public", - }) - - let dialogContext = { - movementChoices: SYSTEM.MOVEMENT_CHOICES, - moveDirectionChoices: SYSTEM.MOVE_DIRECTION_CHOICES, - sizeChoices: SYSTEM.SIZE_CHOICES, - rangeChoices: SYSTEM.RANGE_CHOICES, - attackerAimChoices: SYSTEM.ATTACKER_AIM_CHOICES, - movement: "none", - moveDirection: "none", - size: "+5", - range: "short", - attackerAim: "simple", - fieldRollMode, - rollModes - } - - const content = await foundry.applications.handlebars.renderTemplate("systems/fvtt-prism-rpg/templates/range-defense-dialog.hbs", dialogContext) - - const label = game.i18n.localize("PRISMRPG.Label.rangeDefenseRoll") - const rollContext = await foundry.applications.api.DialogV2.wait({ - window: { title: "Range Defense" }, - classes: ["prismrpg"], - 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 - }, - }, - ], - rejectClose: false // Click on Close button will not launch an error - }) - - // If the user cancels the dialog, exit - if (rollContext === null) return - - console.log("RollContext", rollContext) - // Add disfavor/favor option if point blank range - if (rollContext.range === "pointblank") { - rollContext.movement = rollContext.movement.replace("kh", "") - rollContext.movement = rollContext.movement.replace("kl", "") - rollContext.movement += "kl" // Add the kl to the movement (disfavor for point blank range) - rollContext.range = "0" - } - if (rollContext.range === "beyondskill") { - rollContext.movement = rollContext.movement.replace("kh", "") - rollContext.movement = rollContext.movement.replace("kl", "") - rollContext.movement += "kh" // Add the kl to the movement (favor for point blank range) - rollContext.range = "+11" - } - - // Build the final modifier - let fullModifier = Number(rollContext.moveDirection) + - Number(rollContext.size) + - Number(rollContext.range) + - Number(rollContext?.attackerAim || 0) - - let modifierFormula - if (fullModifier === 0) { - modifierFormula = "0" - } else { - let modAbs = Math.abs(fullModifier) - modifierFormula = `D${modAbs + 1} -1` - } - - let rollData = { ...rollContext } - // Merge rollContext object into options object - options = { ...options, ...rollContext } - options.rollName = "Ranged Defense" - - const rollBase = new this(rollContext.movement, options.data, rollData) - const rollModifier = new Roll(modifierFormula, options.data, rollData) - rollModifier.evaluate() - await rollBase.evaluate() - let rollD30 = await new Roll("1D30").evaluate() - options.D30result = rollD30.total - - let badResult = 0 - if (rollContext.movement.includes("kh")) { - rollData.favor = "favor" - badResult = Math.min(rollBase.terms[0].results[0].result, rollBase.terms[0].results[1]?.result || 20) - } - if (rollContext.movement.includes("kl")) { - rollData.favor = "disfavor" - badResult = Math.max(rollBase.terms[0].results[0].result, rollBase.terms[0].results[1]?.result || 1) - } - let dice = rollContext.movement - let maxValue = 20 // As per latest changes (was : Number(dice.match(/\d+$/)[0]) - let rollTotal = -1 - let diceResults = [] - let resultType - - let diceResult = rollBase.dice[0].results[0].result - diceResults.push({ dice: `${dice.toUpperCase()}`, value: diceResult }) - let diceSum = diceResult - while (diceResult === maxValue) { - let r = await new Roll(dice).evaluate() - diceResult = r.dice[0].results[0].result - diceResults.push({ dice: `${dice.toUpperCase()}-1`, value: diceResult - 1 }) - diceSum += (diceResult - 1) - } - if (fullModifier !== 0) { - diceResults.push({ dice: `${rollModifier.formula.toUpperCase()}`, value: rollModifier.total }) - if (fullModifier < 0) { - rollTotal = Math.max(diceSum - rollModifier.total, 0) - } else { - rollTotal = diceSum + rollModifier.total - } - } else { - rollTotal = diceSum - } - rollBase.options = { ...rollBase.options, ...options } - rollBase.options.resultType = resultType - rollBase.options.rollTotal = rollTotal - rollBase.options.diceResults = diceResults - rollBase.options.rollTarget = options.rollTarget - rollBase.options.titleFormula = `1D20E + ${modifierFormula}` - rollBase.options.D30result = options.D30result - rollBase.options.rollName = "Ranged Defense" - rollBase.options.badResult = badResult - rollBase.options.rollData = foundry.utils.duplicate(rollData) - /** - * A hook event that fires after the roll has been made. - * @function - * @memberof hookEvents - * @param {Object} options Options for the roll. - * @param {Object} rollData All data related to the roll. - @param {PrismRPGRoll} roll The resulting roll. - * @returns {boolean} Explicitly return `false` to prevent roll to be made. - */ - - return rollBase - } - - /** - * 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 "challenge": - return `${game.i18n.localize("PRISMRPG.Label.titleChallenge")}` - case "save": - return `${game.i18n.localize("PRISMRPG.Label.titleSave")}` - case "monster-skill": - case "skill": - return `${game.i18n.localize("PRISMRPG.Label.titleSkill")}` - case "weapon-attack": - return `${game.i18n.localize("PRISMRPG.Label.weapon-attack")}` - case "weapon-defense": - return `${game.i18n.localize("PRISMRPG.Label.weapon-defense")}` - case "weapon-damage-small": - return `${game.i18n.localize("PRISMRPG.Label.weapon-damage-small")}` - case "weapon-damage-medium": - return `${game.i18n.localize("PRISMRPG.Label.weapon-damage-medium")}` - case "spell": - case "spell-attack": - case "spell-power": - return `${game.i18n.localize("PRISMRPG.Label.spell")}` - case "miracle": - case "miracle-attack": - case "miracle-power": - return `${game.i18n.localize("PRISMRPG.Label.miracle")}` - default: - return game.i18n.localize("PRISMRPG.Label.titleStandard") - } + return roll } /** @override */ async render(chatOptions = {}) { let chatData = await this._getChatCardData(chatOptions.isPrivate) - console.log("ChatData", chatData) return await foundry.applications.handlebars.renderTemplate(this.constructor.CHAT_TEMPLATE, chatData) } - /* - * Generates the data required for rendering a roll chat card. - */ async _getChatCardData(isPrivate) { const cardData = { css: [SYSTEM.id, "dice-roll"], @@ -1138,15 +309,11 @@ export default class PrismRPGRoll extends Roll { total: this.rollTotal, isFailure: this.isFailure, actorId: this.actorId, - diceResults: this.diceResults, actingCharName: this.actorName, actingCharImg: this.actorImage, resultType: this.resultType, hasTarget: this.hasTarget, targetName: this.targetName, - targetArmor: this.targetArmor, - D30result: this.D30result, - badResult: this.badResult, rollData: this.rollData, isPrivate: isPrivate } @@ -1155,20 +322,9 @@ export default class PrismRPGRoll extends Roll { 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( { - isSave: this.isSave, - isChallenge: this.isChallenge, isFailure: this.resultType === "failure", rollType: this.type, rollTarget: this.rollTarget, @@ -1176,14 +332,10 @@ export default class PrismRPGRoll extends Roll { actingCharImg: this.actorImage, hasTarget: this.hasTarget, targetName: this.targetName, - targetArmor: this.targetArmor, - targetMalus: this.targetMalus, - realDamage: this.realDamage, rollData: this.rollData, ...messageData, }, { rollMode: rollMode }, ) } - } diff --git a/module/documents/roll.mjs.backup b/module/documents/roll.mjs.backup new file mode 100644 index 0000000..caa2cc5 --- /dev/null +++ b/module/documents/roll.mjs.backup @@ -0,0 +1,1095 @@ +import { SYSTEM } from "../config/system.mjs" +import PrismRPGUtils from "../utils.mjs" + +export default class PrismRPGRoll extends Roll { + /** + * The HTML template path used to render dice checks of this type + * @type {string} + */ + static CHAT_TEMPLATE = "systems/fvtt-prism-rpg/templates/chat-message.hbs" + + get type() { + return this.options.type + } + + get titleFormula() { + return this.options.titleFormula + } + + get rollName() { + return this.options.rollName + } + + get target() { + return this.options.target + } + + get value() { + return this.options.value + } + + get treshold() { + return this.options.treshold + } + + get actorId() { + return this.options.actorId + } + + get actorName() { + return this.options.actorName + } + + get actorImage() { + return this.options.actorImage + } + + 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 targetName() { + return this.options.targetName + } + + get targetArmor() { + return this.options.targetArmor + } + + get targetMalus() { + return this.options.targetMalus + } + + get realDamage() { + return this.options.realDamage + } + + get rollTotal() { + return this.options.rollTotal + } + + get diceResults() { + return this.options.diceResults + } + + get rollTarget() { + return this.options.rollTarget + } + + get D30result() { + return this.options.D30result + } + + get badResult() { + return this.options.badResult + } + + get rollData() { + return this.options.rollData + } + + /** + * Prompt the user with a dialog to configure and execute a roll (D&D 5e style). + * + * @param {Object} options Configuration options for the roll. + * @param {string} options.rollType The type of roll being performed. + * @param {Object} 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} The roll result or null if the dialog was cancelled. + */ + static async prompt(options = {}) { + // D&D 5e style: simple 1d20 + modifier + let dice = "1d20" + let baseFormula = "1d20" + let hasModifier = true + let hasFavor = true // Allow advantage/disadvantage + let isDamageRoll = false + + // Determine roll specifics based on type + if (options.rollType === "characteristic") { + options.rollName = options.rollTarget.name + options.rollTarget.value = options.rollTarget.value + options.rollTarget.charModifier = options.rollTarget.value + + } else if (options.rollType === "challenge" || options.rollType === "save") { + options.rollName = game.i18n.localize(`PRISMRPG.Label.${options.rollTarget.rollKey}`) + // value already set in actor.mjs + + } else if (options.rollType === "skill") { + options.rollName = options.rollTarget.name + options.rollTarget.value = Math.floor(options.rollTarget.system.skillTotal / 10) + + } else if (options.rollType === "weapon-attack") { + options.rollName = options.rollTarget.name + if (options.rollTarget.weapon.system.weaponType === "melee") { + options.rollTarget.value = options.rollTarget.combat.attackModifier + options.rollTarget.weaponSkillModifier + options.rollTarget.weapon.system.bonuses.attackBonus + options.rollTarget.charModifier = options.rollTarget.combat.attackModifier + } else { + options.rollTarget.value = options.rollTarget.combat.rangedAttackModifier + options.rollTarget.weaponSkillModifier + options.rollTarget.weapon.system.bonuses.attackBonus + options.rollTarget.charModifier = options.rollTarget.combat.rangedAttackModifier + } + + } else if (options.rollType === "weapon-defense") { + options.rollName = options.rollTarget.name + options.rollTarget.value = options.rollTarget.combat.defenseModifier + options.rollTarget.weaponSkillModifier + options.rollTarget.weapon.system.bonuses.defenseBonus + options.rollTarget.charModifier = options.rollTarget.combat.defenseModifier + + } else if (options.rollType === "spell" || options.rollType === "spell-attack" || options.rollType === "spell-power") { + options.rollName = options.rollTarget.name + options.rollTarget.value = options.rollTarget.actorModifiers.levelSpellModifier + options.rollTarget.actorModifiers.intSpellModifier + options.rollTarget.charModifier = options.rollTarget.actorModifiers.intSpellModifier + + } else if (options.rollType === "miracle" || options.rollType === "miracle-attack" || options.rollType === "miracle-power") { + options.rollName = options.rollTarget.name + options.rollTarget.value = options.rollTarget.actorModifiers.levelMiracleModifier + options.rollTarget.actorModifiers.chaMiracleModifier + options.rollTarget.charModifier = options.rollTarget.actorModifiers.chaMiracleModifier + + } else if (options.rollType === "monster-attack" || options.rollType === "monster-defense") { + options.rollName = options.rollTarget.name + if (options.rollType === "monster-attack") { + options.rollTarget.value = options.rollTarget.attackModifier + } else { + options.rollTarget.value = options.rollTarget.defenseModifier + } + options.rollTarget.charModifier = 0 + + } else if (options.rollType === "monster-skill") { + options.rollName = game.i18n.localize(`PRISMRPG.Label.${options.rollTarget.rollKey}`) + // value already set + + } else if (options.rollType.includes("weapon-damage")) { + isDamageRoll = true + hasFavor = false + options.rollName = options.rollTarget.name + let damageBonus = options.rollTarget.combat.damageModifier + options.rollTarget.value = damageBonus + options.rollTarget.weaponSkillModifier + options.rollTarget.weapon.system.bonuses.damageBonus + options.rollTarget.charModifier = damageBonus + + if (options.rollType.includes("small")) { + dice = options.rollTarget.weapon.system.damage.damageS + } else { + dice = options.rollTarget.weapon.system.damage.damageM + } + dice = dice.replace("E", "").replace("e", "") + baseFormula = dice + + } else if (options.rollType.includes("monster-damage")) { + isDamageRoll = true + hasFavor = false + options.rollName = options.rollTarget.name + options.rollTarget.value = options.rollTarget.damageModifier + options.rollTarget.charModifier = 0 + dice = options.rollTarget.damageDice.replace("E", "").replace("e", "") + baseFormula = dice + + } else if (options.rollType === "granted") { + hasModifier = false + hasFavor = false + options.rollName = `Granted ${options.rollTarget.rollKey}` + dice = options.rollTarget.formula + baseFormula = options.rollTarget.formula + } + + // Setup roll modes + const rollModes = foundry.utils.duplicate(CONFIG.Dice.rollModes) + + const fieldRollMode = new foundry.data.fields.StringField({ + choices: rollModes, + blank: false, + default: "public", + }) + + // D&D 5e style: simple modifiers + const choiceModifier = SYSTEM.CHOICE_MODIFIERS + const choiceFavor = SYSTEM.FAVOR_CHOICES + + let dialogContext = { + rollType: options.rollType, + rollTarget: options.rollTarget, + rollName: options.rollName, + actorName: options.actorName, + rollModes, + hasModifier, + hasFavor, + isDamageRoll, + baseValue: options.rollTarget.value || 0, + baseFormula, + dice, + fieldRollMode, + choiceModifier, + choiceFavor, + hasTarget: options.hasTarget, + modifier: "+0", + favor: "none", + targetName: "" + } + + const content = await foundry.applications.handlebars.renderTemplate("systems/fvtt-prism-rpg/templates/roll-dialog.hbs", dialogContext) + + let position = game.user.getFlag(SYSTEM.id, "roll-dialog-pos") || { top: -1, left: -1 } + const label = game.i18n.localize("PRISMRPG.Roll.roll") + const rollContext = await foundry.applications.api.DialogV2.wait({ + window: { title: "Roll dialog" }, + classes: ["prismrpg"], + content, + position, + buttons: [ + { + label: label, + callback: (event, button, dialog) => { + console.log("Roll context", event, button, dialog) + let position = dialog.position + game.user.setFlag(SYSTEM.id, "roll-dialog-pos", foundry.utils.duplicate(position)) + const output = Array.from(button.form.elements).reduce((obj, input) => { + if (input.name) obj[input.name] = input.value + return obj + }, {}) + return output + }, + }, + ], + actions: { + "selectGranted": (event, button, dialog) => { + hasGrantedDice = event.target.checked + }, + "selectBeyondSkill": (event, button, dialog) => { + beyondSkill = button.checked + }, + "selectPointBlank": (event, button, dialog) => { + pointBlank = button.checked + }, + "selectLetItFly": (event, button, dialog) => { + letItFly = button.checked + }, + "saveSpellCheck": (event, button, dialog) => { + saveSpell = button.checked + }, + "gotoToken": (event, button, dialog) => { + let tokenId = $(button).data("tokenId") + let token = canvas.tokens?.get(tokenId) + if (token) { + canvas.animatePan({ x: token.x, y: token.y, duration: 200 }) + canvas.tokens.releaseAll(); + token.control({ releaseOthers: true }); + } + } + }, + rejectClose: false // Click on Close button will not launch an error + }) + + // If the user cancels the dialog, exit + if (rollContext === null) return + console.log("rollContext", rollContext, hasGrantedDice) + rollContext.saveSpell = saveSpell // Update fucking flag + + let fullModifier = 0 + let titleFormula = "" + dice = rollContext.changeDice || dice + if (hasModifier) { + let bonus = Number(options.rollTarget.value) + fullModifier = rollContext.modifier === "" ? 0 : parseInt(rollContext.modifier, 10) + bonus + fullModifier += (rollContext.saveSpell) ? options.rollTarget.actorModifiers.saveModifier : 0 + if (Number(rollContext.attackerAim) > 0) { + fullModifier += Number(rollContext.attackerAim) + } + + if (fullModifier === 0) { + modifierFormula = "0" + } else { + let modAbs = Math.abs(fullModifier) + modifierFormula = `D${modAbs + 1} - 1` + } + if (hasStaticModifier) { + modifierFormula += ` + ${options.rollTarget.staticModifier}` + } + let sign = fullModifier < 0 ? "-" : "+" + if (hasExplode) { + titleFormula = `${dice}E ${sign} ${modifierFormula}` + } else { + titleFormula = `${dice} ${sign} ${modifierFormula}` + } + } else { + modifierFormula = "0" + fullModifier = 0 + baseFormula = `${dice}` + if (hasExplode) { + titleFormula = `${dice}E` + } else { + titleFormula = `${dice}` + } + } + + // Latest addition : favor choice at point blank range + if (pointBlank) { + rollContext.favor = "favor" + } + if (beyondSkill) { + rollContext.favor = "disfavor" + } + + // Specific pain case + if (options.rollType === "save" && options.rollTarget.rollKey === "pain" || options.rollTarget.rollKey === "paincourage") { + baseFormula = options.rollTarget.rollDice + titleFormula = `${dice}` + modifierFormula = "0" + fullModifier = 0 + } + + // Specific pain/poison/contagion case + if (options.rollType === "save" && (options.rollTarget.rollKey === "poison" || options.rollTarget.rollKey === "contagion")) { + hasD30 = false + hasStaticModifier = true + modifierFormula = ` + ${Math.abs(fullModifier)}` + titleFormula = `${dice}E + ${Math.abs(fullModifier)}` + } + + if (letItFly) { + baseFormula = "1D20" + titleFormula = `1D20E` + modifierFormula = "0" + fullModifier = 0 + hasFavor = false + hasExplode = true + rollContext.favor = "none" + } + + maxValue = Number(baseFormula.match(/\d+$/)[0]) // Update the max value agains + + const rollData = { + type: options.rollType, + rollType: options.rollType, + target: options.rollTarget, + rollName: options.rollName, + actorId: options.actorId, + actorName: options.actorName, + actorImage: options.actorImage, + rollMode: rollContext.visibility, + hasTarget: options.hasTarget, + pointBlank, + beyondSkill, + letItFly, + hasGrantedDice, + titleFormula, + targetName, + ...rollContext, + } + + /** + * A hook event that fires before the roll is made. + * @function + * @memberof hookEvents + * @param {Object} options Options for the roll. + * @param {Object} rollData All data related to the roll. + * @returns {boolean} Explicitly return `false` to prevent roll to be made. + */ + if (Hooks.call("fvtt-prism-rpg.preRoll", options, rollData) === false) return + + let rollBase = new this(baseFormula, options.data, rollData) + const rollModifier = new Roll(modifierFormula, options.data, rollData) + await rollModifier.evaluate() + await rollBase.evaluate() + + let rollFavor + let badResult + if (rollContext.favor === "favor") { + rollFavor = new this(baseFormula, options.data, rollData) + await rollFavor.evaluate() + console.log("Rolling with favor", rollFavor) + if (game?.dice3d) { + game.dice3d.showForRoll(rollFavor, game.user, true) + } + if (Number(rollFavor.result) > Number(rollBase.result)) { + badResult = rollBase.result + rollBase = rollFavor + } else { + badResult = rollFavor.result + } + rollFavor = null + } + + if (rollContext.favor === "disfavor") { + rollFavor = new this(baseFormula, options.data, rollData) + await rollFavor.evaluate() + if (game?.dice3d) { + game.dice3d.showForRoll(rollFavor, game.user, true) + } + if (Number(rollFavor.result) < Number(rollBase.result)) { + badResult = rollBase.result + rollBase = rollFavor + } else { + badResult = rollFavor.result + } + rollFavor = null + } + + if (hasD30) { + let rollD30 = await new Roll("1D30").evaluate() + if (game?.dice3d) { + game.dice3d.showForRoll(rollD30, game.user, true) + } + options.D30result = rollD30.total + } + + let rollTotal = 0 + let diceResults = [] + let resultType + let diceSum = 0 + + let singleDice = `1D${maxValue}` + for (let i = 0; i < rollBase.dice.length; i++) { + for (let j = 0; j < rollBase.dice[i].results.length; j++) { + let diceResult = rollBase.dice[i].results[j].result + diceResults.push({ dice: `${singleDice.toUpperCase()}`, value: diceResult }) + diceSum += diceResult + if (hasMaxValue) { + while (diceResult === maxValue) { + let r = await new Roll(baseFormula).evaluate() + if (game?.dice3d) { + await game.dice3d.showForRoll(r, game.user, true) + } + diceResult = r.dice[0].results[0].result + diceResults.push({ dice: `${singleDice.toUpperCase()}-1`, value: diceResult - 1 }) + diceSum += (diceResult - 1) + } + } + } + } + + if (hasGrantedDice && options.rollTarget.grantedDice && options.rollTarget.grantedDice !== "") { + titleFormula += ` + ${options.rollTarget.grantedDice.toUpperCase()}` + let grantedRoll = new Roll(options.rollTarget.grantedDice) + await grantedRoll.evaluate() + if (game?.dice3d) { + await game.dice3d.showForRoll(grantedRoll, game.user, true) + } + diceResults.push({ dice: `${options.rollTarget.grantedDice.toUpperCase()}`, value: grantedRoll.total }) + rollTotal += grantedRoll.total + } + + if (fullModifier !== 0) { + diceResults.push({ dice: `${rollModifier.formula.toUpperCase()}`, value: rollModifier.total }) + if (fullModifier < 0) { + rollTotal += Math.max(diceSum - rollModifier.total, 0) + } else { + rollTotal += diceSum + rollModifier.total + } + } else { + rollTotal += diceSum + } + + rollBase.options.resultType = resultType + rollBase.options.rollTotal = rollTotal + rollBase.options.diceResults = diceResults + rollBase.options.rollTarget = options.rollTarget + rollBase.options.titleFormula = titleFormula + rollBase.options.D30result = options.D30result + rollBase.options.badResult = badResult + rollBase.options.rollData = foundry.utils.duplicate(rollData) + + /** + * A hook event that fires after the roll has been made. + * @function + * @memberof hookEvents + * @param {Object} options Options for the roll. + * @param {Object} rollData All data related to the roll. + @param {PrismRPGRoll} roll The resulting roll. + * @returns {boolean} Explicitly return `false` to prevent roll to be made. + */ + if (Hooks.call("fvtt-prism-rpg.Roll", options, rollData, rollBase) === false) return + + return rollBase + } + + /* ***********************************************************/ + static async promptInitiative(options = {}) { + const rollModes = foundry.utils.duplicate(CONFIG.Dice.rollModes); // v12 : 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", + }) + + if (SYSTEM.INITIATIVE_DICE_CHOICES_PER_CLASS[options.actorClass]) { + options.initiativeDiceChoice = SYSTEM.INITIATIVE_DICE_CHOICES_PER_CLASS[options.actorClass] + } else { + options.initiativeDiceChoice = SYSTEM.INITIATIVE_DICE_CHOICES_PER_CLASS["untrained"] + } + + let dialogContext = { + actorClass: options.actorClass, + initiativeDiceChoice: options.initiativeDiceChoice, + initiativeDice: "1D20", + maxInit: options.maxInit, + fieldRollMode, + rollModes + } + + const content = await foundry.applications.handlebars.renderTemplate("systems/fvtt-prism-rpg/templates/roll-initiative-dialog.hbs", dialogContext) + + const label = game.i18n.localize("PRISMRPG.Label.initiative") + const rollContext = await foundry.applications.api.DialogV2.wait({ + window: { title: "Initiative Roll" }, + classes: ["prismrpg"], + 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 + }, + }, + ], + rejectClose: false // Click on Close button will not launch an error + }) + + let initRoll = new Roll(`min(${rollContext.initiativeDice}, ${options.maxInit})`, options.data, rollContext) + await initRoll.evaluate() + let msg = await initRoll.toMessage({ flavor: `Initiative for ${options.actorName}` }, { rollMode: rollContext.visibility }) + if (game?.dice3d) { + await game.dice3d.waitFor3DAnimationByMessageID(msg.id) + } + + if (options.combatId && options.combatantId) { + let combat = game.combats.get(options.combatId) + combat.updateEmbeddedDocuments("Combatant", [{ _id: options.combatantId, initiative: initRoll.total, 'system.progressionCount': 0 }]); + } + } + + /* ***********************************************************/ + static async promptCombatAction(options = {}) { + + const rollModes = foundry.utils.duplicate(CONFIG.Dice.rollModes); // v12 : 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", + }) + + let combatant = game.combats.get(options.combatId)?.combatants?.get(options.combatantId) + if (!combatant) { + console.error("No combatant found for this combat") + return + } + let currentAction = combatant.getFlag(SYSTEM.id, "currentAction") + + let position = game.user.getFlag(SYSTEM.id, "combat-action-dialog-pos") || { top: -1, left: -1 } + + let dialogContext = { + progressionDiceId: "", + fieldRollMode, + rollModes, + currentAction, + ...options + } + + const content = await foundry.applications.handlebars.renderTemplate("systems/fvtt-prism-rpg/templates/combat-action-dialog.hbs", dialogContext) + + let buttons = [] + if (currentAction) { + if (currentAction.type === "weapon") { + buttons.push({ + action: "roll", + label: "Roll progression dice", + callback: (event, button, dialog) => { + let pos = $('#combat-action-dialog').position() + game.user.setFlag(SYSTEM.id, "combat-action-dialog-pos", pos) + return "rollProgressionDice" + }, + }) + } else if (currentAction.type === "spell" || currentAction.type === "miracle") { + let label = "" + if (currentAction.spellStatus === "castingTime") { + let pos = $('#combat-action-dialog').position() + game.user.setFlag(SYSTEM.id, "combat-action-dialog-pos", pos) + label = "Wait casting time" + } + if (currentAction.spellStatus === "toBeCasted") { + let pos = $('#combat-action-dialog').position() + game.user.setFlag(SYSTEM.id, "combat-action-dialog-pos", pos) + label = "Cast spell/miracle" + } + if (currentAction.spellStatus === "lethargy") { + let pos = $('#combat-action-dialog').position() + game.user.setFlag(SYSTEM.id, "combat-action-dialog-pos", pos) + label = "Roll lethargy dice" + } + buttons.push({ + action: "roll", + label: label, + callback: (event, button, dialog) => { + let pos = $('#combat-action-dialog').position() + game.user.setFlag(SYSTEM.id, "combat-action-dialog-pos", foundry.utils.duplicate(pos)) + return "rollLethargyDice" + }, + }) + } + } else { + buttons.push({ + action: "roll", + label: "Select action", + callback: (event, button, dialog) => { + let pos = $('#combat-action-dialog').position() + game.user.setFlag(SYSTEM.id, "combat-action-dialog-pos", foundry.utils.duplicate(pos)) + const output = Array.from(button.form.elements).reduce((obj, input) => { + if (input.name) obj[input.name] = input.value + return obj + }, {}) + return output + }, + }, + ) + } + buttons.push({ + action: "cancel", + label: "Other action, not listed here", + callback: (event, button, dialog) => { + let pos = $('#combat-action-dialog').position() + game.user.setFlag(SYSTEM.id, "combat-action-dialog-pos", foundry.utils.duplicate(pos)) + return null; + } + }) + + let rollContext = await foundry.applications.api.DialogV2.wait({ + window: { title: "Combat Action Dialog" }, + id: "combat-action-dialog", + classes: ["prismrpg"], + position, + content, + buttons, + rejectClose: false // Click on Close button will not launch an error + }) + + console.log("RollContext", dialogContext, rollContext) + // If action is cancelled, exit + if (rollContext === null || rollContext === "cancel") { + await combatant.setFlag(SYSTEM.id, "currentAction", "") + let message = `${combatant.name} : Other action, progression reset` + ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) + return + } + + // Setup the current action + if (!currentAction || currentAction === "") { + // Get the item from the returned selectedChoice value + let selectedChoice = rollContext.selectedChoice + let rangedMode + if (selectedChoice.match("simpleAim")) { + selectedChoice = selectedChoice.replace("simpleAim", "") + rangedMode = "simpleAim" + } + if (selectedChoice.match("carefulAim")) { + selectedChoice = selectedChoice.replace("carefulAim", "") + rangedMode = "carefulAim" + } + if (selectedChoice.match("focusedAim")) { + selectedChoice = selectedChoice.replace("focusedAim", "") + rangedMode = "focusedAim" + } + let selectedItem = combatant.actor.items.find(i => i.id === selectedChoice) + // Setup flag for combat action usage + let actionItem = foundry.utils.duplicate(selectedItem) + actionItem.progressionCount = 1 + actionItem.rangedMode = rangedMode + actionItem.castingTime = 1 + actionItem.spellStatus = "castingTime" + // Set the flag on the combatant + await combatant.setFlag(SYSTEM.id, "currentAction", actionItem) + let message = `${combatant.name} action : ${selectedItem.name}, start rolling progression dice or casting time` + ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) + rollContext = (actionItem.type === "weapon") ? "rollProgressionDice" : "rollLethargyDice" // Set the roll context to rollProgressionDice + currentAction = actionItem + } + + if (currentAction) { + if (rollContext === "rollLethargyDice") { + if (currentAction.spellStatus === "castingTime") { + let time = currentAction.type === "spell" ? currentAction.system.castingTime : currentAction.system.prayerTime + if (currentAction.castingTime < time) { + let message = `Casting time : ${currentAction.name}, count : ${currentAction.castingTime}/${time}` + ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) + currentAction.castingTime += 1 + await combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction)) + return + } else { + let message = `Spell/Miracle ${currentAction.name} ready to be cast on next second !` + ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) + currentAction.castingTime = 1 + currentAction.spellStatus = "toBeCasted" + await combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction)) + return + } + } + if (currentAction.spellStatus === "toBeCasted") { + combatant.actor.prepareRoll((currentAction.type === "spell") ? "spell-attack" : "miracle-attack", currentAction._id) + if (currentAction.type === "spell") { + currentAction.spellStatus = "lethargy" + await combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction)) + } else { + // No lethargy for miracle + await combatant.setFlag(SYSTEM.id, "currentAction", "") + } + return + } + if (currentAction.spellStatus === "lethargy") { + // Roll lethargy dice + let dice = PrismRPGUtils.getLethargyDice(currentAction.system.level) + let roll = new Roll(dice) + await roll.evaluate() + if (game?.dice3d) { + await game.dice3d.showForRoll(roll) + } + let max = roll.dice[0].faces - 1 + let toCompare = Math.min(currentAction.progressionCount, max) + if (roll.total <= toCompare) { + // Notify that the player can act now with a chat message + let message = game.i18n.format("PRISMRPG.Notifications.messageLethargyOK", { name: combatant.actor.name, spellName: currentAction.name, roll: roll.total }) + ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) + // Update the combatant progression count + await combatant.setFlag(SYSTEM.id, "currentAction", "") + // Display the action selection window again + combatant.actor.system.rollProgressionDice(options.combatId, options.combatantId) + } else { + // Notify that the player cannot act now with a chat message + currentAction.progressionCount += 1 + await combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction)) + let message = game.i18n.format("PRISMRPG.Notifications.messageLethargyKO", { name: combatant.actor.name, spellName: currentAction.name, roll: roll.total }) + ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) + } + } + } + + if (rollContext === "rollProgressionDice") { + let formula = currentAction.system.combatProgressionDice + if (currentAction?.rangedMode) { + let toSplit = currentAction.system.speed[currentAction.rangedMode] + let split = toSplit.split("+") + currentAction.rangedLoad = Number(split[0]) || 0 + formula = split[1] + console.log("Ranged Mode", currentAction.rangedMode, currentAction.rangedLoad, formula) + } + // Range weapon loading + if (!currentAction.weaponLoaded && currentAction.rangedLoad) { + if (currentAction.progressionCount <= currentAction.rangedLoad) { + let message = `Ranged weapon ${currentAction.name} is loading, loading count : ${currentAction.progressionCount}/${currentAction.rangedLoad}` + ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) + currentAction.progressionCount += 1 + await combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction)) + } else { + let message = `Ranged weapon ${currentAction.name} is loaded !` + ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) + currentAction.weaponLoaded = true + currentAction.progressionCount = 1 + await combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction)) + } + return + } + + // Melee mode + let isMonster = combatant.actor.type === "monster" + // Get the dice and roll it if + let roll = new Roll(formula) + await roll.evaluate() + + let max = roll.dice[0].faces - 1 + max = Math.min(currentAction.progressionCount, max) + let msg = await roll.toMessage({ flavor: `Progression Roll for ${currentAction.name}, progression count : ${currentAction.progressionCount}/${max}` }, { rollMode: rollContext.visibility }) + if (game?.dice3d) { + await game.dice3d.waitFor3DAnimationByMessageID(msg.id) + } + + if (roll.total <= max) { + // Notify that the player can act now with a chat message + let message = game.i18n.format("PRISMRPG.Notifications.messageProgressionOK", { isMonster, name: combatant.actor.name, weapon: currentAction.name, roll: roll.total }) + ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) + await combatant.setFlag(SYSTEM.id, "currentAction", "") + combatant.actor.prepareRoll(currentAction.type === "weapon" ? "weapon-attack" : "spell-attack", currentAction._id) + } else { + // Notify that the player cannot act now with a chat message + currentAction.progressionCount += 1 + combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction)) + let message = game.i18n.format("PRISMRPG.Notifications.messageProgressionKO", { isMonster, name: combatant.actor.name, weapon: currentAction.name, roll: roll.total }) + ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) + } + } + } + } + + /* ***********************************************************/ + static async promptRangedDefense(options = {}) { + + const rollModes = foundry.utils.duplicate(CONFIG.Dice.rollModes); + const fieldRollMode = new foundry.data.fields.StringField({ + choices: rollModes, + blank: false, + default: "public", + }) + + let dialogContext = { + movementChoices: SYSTEM.MOVEMENT_CHOICES, + moveDirectionChoices: SYSTEM.MOVE_DIRECTION_CHOICES, + sizeChoices: SYSTEM.SIZE_CHOICES, + rangeChoices: SYSTEM.RANGE_CHOICES, + attackerAimChoices: SYSTEM.ATTACKER_AIM_CHOICES, + movement: "none", + moveDirection: "none", + size: "+5", + range: "short", + attackerAim: "simple", + fieldRollMode, + rollModes + } + + const content = await foundry.applications.handlebars.renderTemplate("systems/fvtt-prism-rpg/templates/range-defense-dialog.hbs", dialogContext) + + const label = game.i18n.localize("PRISMRPG.Label.rangeDefenseRoll") + const rollContext = await foundry.applications.api.DialogV2.wait({ + window: { title: "Range Defense" }, + classes: ["prismrpg"], + 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 + }, + }, + ], + rejectClose: false // Click on Close button will not launch an error + }) + + // If the user cancels the dialog, exit + if (rollContext === null) return + + console.log("RollContext", rollContext) + // Add disfavor/favor option if point blank range + if (rollContext.range === "pointblank") { + rollContext.movement = rollContext.movement.replace("kh", "") + rollContext.movement = rollContext.movement.replace("kl", "") + rollContext.movement += "kl" // Add the kl to the movement (disfavor for point blank range) + rollContext.range = "0" + } + if (rollContext.range === "beyondskill") { + rollContext.movement = rollContext.movement.replace("kh", "") + rollContext.movement = rollContext.movement.replace("kl", "") + rollContext.movement += "kh" // Add the kl to the movement (favor for point blank range) + rollContext.range = "+11" + } + + // Build the final modifier + let fullModifier = Number(rollContext.moveDirection) + + Number(rollContext.size) + + Number(rollContext.range) + + Number(rollContext?.attackerAim || 0) + + let modifierFormula + if (fullModifier === 0) { + modifierFormula = "0" + } else { + let modAbs = Math.abs(fullModifier) + modifierFormula = `D${modAbs + 1} -1` + } + + let rollData = { ...rollContext } + // Merge rollContext object into options object + options = { ...options, ...rollContext } + options.rollName = "Ranged Defense" + + const rollBase = new this(rollContext.movement, options.data, rollData) + const rollModifier = new Roll(modifierFormula, options.data, rollData) + rollModifier.evaluate() + await rollBase.evaluate() + let rollD30 = await new Roll("1D30").evaluate() + options.D30result = rollD30.total + + let badResult = 0 + if (rollContext.movement.includes("kh")) { + rollData.favor = "favor" + badResult = Math.min(rollBase.terms[0].results[0].result, rollBase.terms[0].results[1]?.result || 20) + } + if (rollContext.movement.includes("kl")) { + rollData.favor = "disfavor" + badResult = Math.max(rollBase.terms[0].results[0].result, rollBase.terms[0].results[1]?.result || 1) + } + let dice = rollContext.movement + let maxValue = 20 // As per latest changes (was : Number(dice.match(/\d+$/)[0]) + let rollTotal = -1 + let diceResults = [] + let resultType + + let diceResult = rollBase.dice[0].results[0].result + diceResults.push({ dice: `${dice.toUpperCase()}`, value: diceResult }) + let diceSum = diceResult + while (diceResult === maxValue) { + let r = await new Roll(dice).evaluate() + diceResult = r.dice[0].results[0].result + diceResults.push({ dice: `${dice.toUpperCase()}-1`, value: diceResult - 1 }) + diceSum += (diceResult - 1) + } + if (fullModifier !== 0) { + diceResults.push({ dice: `${rollModifier.formula.toUpperCase()}`, value: rollModifier.total }) + if (fullModifier < 0) { + rollTotal = Math.max(diceSum - rollModifier.total, 0) + } else { + rollTotal = diceSum + rollModifier.total + } + } else { + rollTotal = diceSum + } + rollBase.options = { ...rollBase.options, ...options } + rollBase.options.resultType = resultType + rollBase.options.rollTotal = rollTotal + rollBase.options.diceResults = diceResults + rollBase.options.rollTarget = options.rollTarget + rollBase.options.titleFormula = `1D20E + ${modifierFormula}` + rollBase.options.D30result = options.D30result + rollBase.options.rollName = "Ranged Defense" + rollBase.options.badResult = badResult + rollBase.options.rollData = foundry.utils.duplicate(rollData) + /** + * A hook event that fires after the roll has been made. + * @function + * @memberof hookEvents + * @param {Object} options Options for the roll. + * @param {Object} rollData All data related to the roll. + @param {PrismRPGRoll} roll The resulting roll. + * @returns {boolean} Explicitly return `false` to prevent roll to be made. + */ + + return rollBase + } + + /** + * 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 "challenge": + return `${game.i18n.localize("PRISMRPG.Label.titleChallenge")}` + case "save": + return `${game.i18n.localize("PRISMRPG.Label.titleSave")}` + case "monster-skill": + case "skill": + return `${game.i18n.localize("PRISMRPG.Label.titleSkill")}` + case "weapon-attack": + return `${game.i18n.localize("PRISMRPG.Label.weapon-attack")}` + case "weapon-defense": + return `${game.i18n.localize("PRISMRPG.Label.weapon-defense")}` + case "weapon-damage-small": + return `${game.i18n.localize("PRISMRPG.Label.weapon-damage-small")}` + case "weapon-damage-medium": + return `${game.i18n.localize("PRISMRPG.Label.weapon-damage-medium")}` + case "spell": + case "spell-attack": + case "spell-power": + return `${game.i18n.localize("PRISMRPG.Label.spell")}` + case "miracle": + case "miracle-attack": + case "miracle-power": + return `${game.i18n.localize("PRISMRPG.Label.miracle")}` + default: + return game.i18n.localize("PRISMRPG.Label.titleStandard") + } + } + + /** @override */ + async render(chatOptions = {}) { + let chatData = await this._getChatCardData(chatOptions.isPrivate) + console.log("ChatData", chatData) + return await foundry.applications.handlebars.renderTemplate(this.constructor.CHAT_TEMPLATE, chatData) + } + + /* + * Generates the data required for rendering a roll chat card. + */ + async _getChatCardData(isPrivate) { + const cardData = { + css: [SYSTEM.id, "dice-roll"], + data: this.data, + diceTotal: this.dice.reduce((t, d) => t + d.total, 0), + isGM: game.user.isGM, + formula: this.formula, + titleFormula: this.titleFormula, + rollName: this.rollName, + rollType: this.type, + rollTarget: this.rollTarget, + total: this.rollTotal, + isFailure: this.isFailure, + actorId: this.actorId, + diceResults: this.diceResults, + actingCharName: this.actorName, + actingCharImg: this.actorImage, + resultType: this.resultType, + hasTarget: this.hasTarget, + targetName: this.targetName, + targetArmor: this.targetArmor, + D30result: this.D30result, + badResult: this.badResult, + rollData: this.rollData, + isPrivate: isPrivate + } + 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( + { + isSave: this.isSave, + isChallenge: this.isChallenge, + isFailure: this.resultType === "failure", + rollType: this.type, + rollTarget: this.rollTarget, + actingCharName: this.actorName, + actingCharImg: this.actorImage, + hasTarget: this.hasTarget, + targetName: this.targetName, + targetArmor: this.targetArmor, + targetMalus: this.targetMalus, + realDamage: this.realDamage, + rollData: this.rollData, + ...messageData, + }, + { rollMode: rollMode }, + ) + } + +} diff --git a/module/models/character.mjs b/module/models/character.mjs index e426c5c..71f1f8d 100644 --- a/module/models/character.mjs +++ b/module/models/character.mjs @@ -43,6 +43,20 @@ export default class PrismRPGCharacter extends foundry.abstract.TypeDataModel { }, {}), ) + // Sub-Attributes (derived from two parent characteristics) + const subAttributeField = (label) => { + const schema = { + value: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }) + } + return new fields.SchemaField(schema, { label }) + } + schema.subAttributes = new fields.SchemaField( + Object.values(SYSTEM.SUB_ATTRIBUTES).reduce((obj, subAttr) => { + obj[subAttr.id] = subAttributeField(subAttr.label) + return obj + }, {}), + ) + // Challenges const challengeField = (label) => { const schema = { @@ -67,50 +81,21 @@ export default class PrismRPGCharacter extends foundry.abstract.TypeDataModel { schema.hp = new fields.SchemaField({ value: new fields.NumberField({ ...requiredInteger, initial: 1, min: 0 }), max: new fields.NumberField({ ...requiredInteger, initial: 1, min: 0 }), - painDamage: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }), - wounds: new fields.ArrayField(new fields.SchemaField(woundFieldSchema), { - initial: [{ description: "", value: 0, duration: 0 }, { description: "", value: 0, duration: 0 }, - { description: "", value: 0, duration: 0 }, { description: "", value: 0, duration: 0 }, { description: "", value: 0, duration: 0 }, { description: "", value: 0, duration: 0 }, - { description: "", value: 0, duration: 0 }, { description: "", value: 0, duration: 0 }], min: 8 - }), - damageResistance: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }) + temp: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }) }) - schema.perception = new fields.SchemaField({ + schema.magicPoints = new fields.SchemaField({ value: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }), - bonus: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }) - }) - schema.grit = new fields.SchemaField({ - starting: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }), - earned: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }), - current: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }) - }) - schema.luck = new fields.SchemaField({ - earned: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }), - current: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }) - }) - schema.granted = new fields.SchemaField({ - attackDice: new fields.StringField({ required: true, nullable: false, initial: "0", choices: SYSTEM.GRANTED_DICE_CHOICES }), - defenseDice: new fields.StringField({ required: true, nullable: false, initial: "0", choices: SYSTEM.GRANTED_DICE_CHOICES }), - damageDice: new fields.StringField({ required: true, nullable: false, initial: "0", choices: SYSTEM.GRANTED_DICE_CHOICES }) + max: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }) }) - schema.movement = new fields.SchemaField({ - walk: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }), - jog: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }), - sprint: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }), - run: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }), - armorAdjust: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }), - }) - schema.jump = new fields.SchemaField({ - broad: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }), - running: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }), - vertical: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }), + schema.armorPoints = new fields.SchemaField({ + value: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }), + max: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }) }) + schema.biodata = new fields.SchemaField({ - class: new fields.StringField({ required: true, initial: "untrained", choices: SYSTEM.CHAR_CLASSES }), level: new fields.NumberField({ ...requiredInteger, initial: 1, min: 1 }), - mortal: new fields.StringField({ required: true, initial: "mankind", choices: SYSTEM.MORTAL_CHOICES }), alignment: new fields.StringField({ required: true, nullable: false, initial: "" }), age: new fields.NumberField({ ...requiredInteger, initial: 15, min: 6 }), height: new fields.NumberField({ ...requiredInteger, initial: 170, min: 10 }), @@ -118,16 +103,7 @@ export default class PrismRPGCharacter extends foundry.abstract.TypeDataModel { eyes: new fields.StringField({ required: true, nullable: false, initial: "" }), hair: new fields.StringField({ required: true, nullable: false, initial: "" }), magicUser: new fields.BooleanField({ initial: false }), - clericUser: new fields.BooleanField({ initial: false }), - hpPerLevel: new fields.StringField({ required: true, nullable: false, initial: "" }), - }) - - schema.modifiers = new fields.SchemaField({ - levelSpellModifier: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }), - saveModifier: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }), - levelMiracleModifier: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }), - intSpellModifier: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }), - chaMiracleModifier: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }), + clericUser: new fields.BooleanField({ initial: false }) }) schema.developmentPoints = new fields.SchemaField({ @@ -171,14 +147,16 @@ export default class PrismRPGCharacter extends foundry.abstract.TypeDataModel { // Core Skill system (Prism RPG) schema.coreSkill = new fields.SchemaField({ skill: new fields.StringField({ - required: true, - initial: "", + required: false, + nullable: true, + initial: null, choices: SYSTEM.CORE_SKILLS_CHOICES, label: "Selected Core Skill" }), attributeChoice: new fields.StringField({ - required: true, - initial: "", + required: false, + nullable: true, + initial: null, label: "Attribute Choice for +2 Bonus" }) }) @@ -216,43 +194,34 @@ export default class PrismRPGCharacter extends foundry.abstract.TypeDataModel { prepareDerivedData() { super.prepareDerivedData(); - let grit = 0 - for (let c in this.characteristics) { - if (SYSTEM.CHARACTERISTICS_MAJOR[c.id]) { - grit += this.characteristics[c].value - } + + // Calculate sub-attributes from parent characteristics + // Sub-attribute = lowest ability modifier between the two parent characteristics + for (let subAttrKey in SYSTEM.SUB_ATTRIBUTES) { + const subAttr = SYSTEM.SUB_ATTRIBUTES[subAttrKey] + const parent1Value = this.characteristics[subAttr.parents[0]].value + const parent2Value = this.characteristics[subAttr.parents[1]].value + // Calculate D&D 5e style ability modifiers: (ability - 10) / 2 + const parent1Bonus = Math.floor((parent1Value - 10) / 2) + const parent2Bonus = Math.floor((parent2Value - 10) / 2) + // Take the lowest modifier + this.subAttributes[subAttrKey].value = Math.min(parent1Bonus, parent2Bonus) } - this.modifiers.saveModifier = Math.floor((Number(this.biodata.level) / 5)) - this.modifiers.levelSpellModifier = Math.floor((Number(this.biodata.level) / 5)) - this.modifiers.levelMiracleModifier = Math.floor((Number(this.biodata.level) / 5)) - - this.grit.starting = Math.round(grit / 6) + // Calculate save modifier locally (not stored) + const saveModifier = Math.floor((Number(this.biodata.level) / 5)) let strDef = SYSTEM.CHARACTERISTICS_TABLES.str.find(s => s.value === this.characteristics.str.value) this.challenges.str.value = strDef.challenge - let intDef = SYSTEM.CHARACTERISTICS_TABLES.int.find(s => s.value === this.characteristics.int.value) - this.modifiers.intSpellModifier = intDef.arkane_casting_mod - let dexDef = SYSTEM.CHARACTERISTICS_TABLES.dex.find(s => s.value === this.characteristics.dex.value) this.challenges.agility.value = dexDef.challenge - this.saves.dodge.value = dexDef.dodge + this.modifiers.saveModifier let wisDef = SYSTEM.CHARACTERISTICS_TABLES.wis.find(s => s.value === this.characteristics.wis.value) - this.saves.will.value = wisDef.willpower_save + this.modifiers.saveModifier - - let chaDef = SYSTEM.CHARACTERISTICS_TABLES.cha.find(s => s.value === this.characteristics.cha.value) - this.modifiers.chaMiracleModifier = chaDef.divine_miracle_bonus let conDef = SYSTEM.CHARACTERISTICS_TABLES.con.find(s => s.value === this.characteristics.con.value) - this.saves.pain.value = conDef.pain_save + this.modifiers.saveModifier - this.saves.toughness.value = conDef.toughness_save + this.modifiers.saveModifier this.challenges.dying.value = conDef.stabilization_dice - this.saves.contagion.value = this.characteristics.con.value;// + this.modifiers.saveModifier - this.saves.poison.value = this.characteristics.con.value; // + this.modifiers.saveModifier - this.combat.attackModifier = 0 for (let chaKey of SYSTEM.CHARACTERISTIC_ATTACK) { let chaDef = SYSTEM.CHARACTERISTICS_TABLES[chaKey].find(s => s.value === this.characteristics[chaKey].value) diff --git a/module/utils.mjs b/module/utils.mjs index c2f4e55..f42beee 100644 --- a/module/utils.mjs +++ b/module/utils.mjs @@ -170,6 +170,12 @@ export default class PrismRPGUtils { Handlebars.registerHelper('countKeys', function (obj) { return Object.keys(obj).length; }) + Handlebars.registerHelper('entries', function (obj) { + return Object.entries(obj); + }) + Handlebars.registerHelper('uppercase', function (str) { + return str ? str.toUpperCase() : ''; + }) Handlebars.registerHelper('isEnabled', function (configKey) { return game.settings.get("bol", configKey); diff --git a/styles/character-main-v2.less b/styles/character-main-v2.less new file mode 100644 index 0000000..2c068c4 --- /dev/null +++ b/styles/character-main-v2.less @@ -0,0 +1,447 @@ +// Character Main Sheet V2 - Based on PNG character sheet design +.character-main-v2 { + .sheet-common(); + padding: 0; + margin: 0; + + .character-sheet-wrapper { + background-image: url("../assets/sheet/character-bg.png"); + background-size: cover; + background-position: center; + padding: 8px 10px; + min-height: auto; + } + + // Character Header with Banner + .character-header { + position: relative; + margin-bottom: 5px; + + .character-name-banner { + background-image: url("../assets/sheet/banner-bg.png"); + background-size: contain; + background-repeat: no-repeat; + background-position: center; + height: 60px; + display: flex; + align-items: center; + justify-content: center; + + input { + background: transparent; + border: none; + text-align: center; + font-family: "Cinzel", serif; + font-size: 24px; + font-weight: bold; + color: #2c2c2c; + width: 500px; + text-shadow: 1px 1px 2px rgba(255, 255, 255, 0.5); + } + } + + .character-toggle-controls { + position: absolute; + top: 10px; + right: 10px; + } + } + + // Main Grid Layout + .character-main-grid { + display: grid; + grid-template-columns: 1fr 300px; + gap: 12px; + align-items: start; + } + + // Left Column - Portrait, Attributes, HP + .character-left-column { + display: flex; + flex-direction: row; + gap: 12px; + align-items: flex-start; + + .portrait-hp-column { + display: flex; + flex-direction: column; + gap: 12px; + width: 200px; + flex-shrink: 0; + } + + .character-portrait { + width: 200px; + height: 200px; + border: 3px solid #6b6b6b; + border-radius: 8px; + overflow: hidden; + background: #d4d4d4; + + img { + width: 100%; + height: 100%; + object-fit: cover; + } + } + + // HP Shields - below portrait, same width + .hp-shields-section { + width: 200px; + + .hp-shields { + display: flex; + flex-direction: column; + gap: 6px; + + .hp-item { + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; + + .hp-label { + font-family: "Cinzel", serif; + font-size: 12px; + font-weight: bold; + color: #2c2c2c; + text-transform: uppercase; + width: 55px; + flex-shrink: 0; + } + + .hp-value { + input { + width: 40px; + height: 28px; + text-align: center; + font-size: 14px; + font-weight: bold; + background: rgba(255, 255, 255, 0.9); + border: 2px solid #6b6b6b; + border-radius: 4px; + } + } + + .hp-separator { + font-family: "Cinzel", serif; + font-size: 14px; + font-weight: bold; + color: #2c2c2c; + } + + .hp-max { + input { + width: 40px; + height: 28px; + text-align: center; + font-size: 14px; + font-weight: bold; + background: rgba(200, 220, 255, 0.5); + border: 2px solid #6b6b6b; + border-radius: 4px; + } + } + } + } + } + + .character-attributes { + display: flex; + flex-direction: column; + gap: 6px; + flex: 1; + min-width: 0; + max-width: 280px; + + .attribute-shield { + position: relative; + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; + padding: 6px 12px; + background: rgba(255, 255, 255, 0.3); + border: 2px solid #6b6b6b; + border-radius: 4px; + height: auto; + + .attribute-label { + font-family: "Cinzel", serif; + font-size: 14px; + font-weight: bold; + color: #2c2c2c; + text-transform: uppercase; + margin: 0; + min-width: 40px; + + a.rollable { + display: flex; + align-items: center; + gap: 4px; + color: #2c2c2c; + text-decoration: none; + cursor: pointer; + transition: all 0.2s; + + i { + font-size: 12px; + color: #6b6b6b; + } + + &:hover { + color: #000; + text-shadow: 0 0 4px rgba(0, 0, 0, 0.3); + + i { + color: #2c2c2c; + } + } + } + } + + .attribute-value { + input { + width: 45px; + height: 32px; + text-align: center; + font-size: 16px; + font-weight: bold; + background: rgba(255, 255, 255, 0.9); + border: 2px solid #6b6b6b; + border-radius: 4px; + } + } + + .attribute-save { + margin-left: auto; + + input { + width: 45px; + height: 32px; + text-align: center; + font-size: 14px; + font-weight: bold; + background: rgba(200, 220, 255, 0.5); + border: 2px solid #6b6b6b; + border-radius: 4px; + color: #2c2c2c; + } + } + } + } + } + + // Right Column - Race, Classes + .character-right-column { + display: flex; + flex-direction: column; + gap: 15px; + + .section-title { + font-family: "Cinzel", serif; + font-size: 14px; + font-weight: bold; + color: #2c2c2c; + text-transform: uppercase; + margin-bottom: 8px; + padding: 5px 10px; + background: rgba(255, 255, 255, 0.6); + border: 2px solid #6b6b6b; + border-radius: 4px; + text-align: center; + } + + .race-section { + .race-box { + padding: 10px; + background: rgba(255, 255, 255, 0.5); + border: 3px solid #6b6b6b; + border-radius: 8px; + min-height: 60px; + + .section-title { + font-family: "Cinzel", serif; + font-size: 12px; + font-weight: bold; + color: #2c2c2c; + text-transform: uppercase; + margin: 0 0 8px 0; + padding: 5px; + background: rgba(255, 255, 255, 0.6); + border: 2px solid #6b6b6b; + border-radius: 4px; + text-align: center; + } + + .race-item { + display: flex; + align-items: center; + gap: 8px; + + .item-img { + width: 36px; + height: 36px; + border: 2px solid #6b6b6b; + border-radius: 4px; + object-fit: cover; + cursor: pointer; + } + + .race-name { + flex: 1; + font-family: "Cinzel", serif; + font-size: 12px; + font-weight: bold; + color: #2c2c2c; + text-align: center; + } + + .controls { + display: flex; + gap: 6px; + + a { + color: #6b6b6b; + cursor: pointer; + transition: color 0.2s; + + &:hover { + color: #2c2c2c; + } + + i { + font-size: 14px; + } + } + } + } + + .no-race { + text-align: center; + font-family: "Crimson Text", serif; + font-size: 13px; + color: #6b6b6b; + font-style: italic; + padding: 10px; + } + + input { + width: 100%; + background: transparent; + border: none; + font-family: "Cinzel", serif; + font-size: 14px; + text-align: center; + } + } + } + + .classes-section { + display: flex; + flex-direction: column; + gap: 12px; + + .class-box { + padding: 10px; + background: rgba(255, 255, 255, 0.5); + border: 3px solid #6b6b6b; + border-radius: 8px; + + .class-label { + font-family: "Cinzel", serif; + font-size: 11px; + color: #6b6b6b; + text-align: center; + margin-bottom: 5px; + } + + .class-content { + .class-item { + display: flex; + align-items: center; + gap: 8px; + + .item-img { + width: 32px; + height: 32px; + border: 2px solid #6b6b6b; + border-radius: 4px; + object-fit: cover; + cursor: pointer; + } + + .class-name { + flex: 1; + font-family: "Cinzel", serif; + font-size: 11px; + font-weight: bold; + color: #2c2c2c; + text-align: center; + } + + .controls { + display: flex; + gap: 6px; + + a { + color: #6b6b6b; + cursor: pointer; + transition: color 0.2s; + + &:hover { + color: #2c2c2c; + } + + i { + font-size: 12px; + } + } + } + } + + .no-class { + text-align: center; + font-family: "Crimson Text", serif; + font-size: 11px; + color: #6b6b6b; + font-style: italic; + padding: 5px; + } + } + + .class-input { + input { + width: 100%; + background: transparent; + border: none; + font-family: "Cinzel", serif; + font-size: 14px; + text-align: center; + } + } + } + } + + .origin-section { + .origin-box { + padding: 15px; + background: rgba(255, 255, 255, 0.5); + border: 3px solid #6b6b6b; + border-radius: 8px; + min-height: 350px; + + textarea { + width: 100%; + min-height: 320px; + background: transparent; + border: none; + font-family: "Crimson Text", serif; + font-size: 13px; + line-height: 1.6; + resize: vertical; + } + } + } + } +} diff --git a/styles/character-subattributes.less b/styles/character-subattributes.less new file mode 100644 index 0000000..81cc5cc --- /dev/null +++ b/styles/character-subattributes.less @@ -0,0 +1,99 @@ +// Sub-attributes tab styling + +.character-subattributes.tab { + .subattributes-content { + padding: 1rem; + + .subattributes-list { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 1rem; + } + + .subattribute-item { + background: rgba(0, 0, 0, 0.1); + border: 1px solid var(--color-border-dark-secondary); + border-radius: 4px; + padding: 0.75rem; + display: flex; + flex-direction: column; + gap: 0.5rem; + + .subattribute-header { + display: flex; + justify-content: space-between; + align-items: center; + gap: 0.5rem; + } + + .subattribute-name { + font-weight: bold; + font-size: 1.1em; + color: var(--color-text-dark-primary); + } + + .subattribute-value { + input { + width: 3em; + text-align: center; + font-weight: bold; + font-size: 1.2em; + background: rgba(255, 255, 255, 0.1); + border: 1px solid var(--color-border-dark-tertiary); + border-radius: 3px; + padding: 0.25rem; + color: var(--color-text-dark-primary); + + &:disabled { + opacity: 0.9; + cursor: default; + } + } + } + + .subattribute-details { + display: flex; + flex-direction: column; + gap: 0.25rem; + font-size: 0.9em; + color: var(--color-text-dark-secondary); + } + + .subattribute-parents { + font-style: italic; + + .parent-char { + display: inline-block; + margin-right: 0.5rem; + + .parent-name { + font-weight: 600; + color: var(--color-text-dark-primary); + } + + .parent-value { + color: var(--color-text-dark-secondary); + } + } + } + + .subattribute-description { + padding-top: 0.25rem; + border-top: 1px solid var(--color-border-dark-tertiary); + font-size: 0.85em; + line-height: 1.3; + } + } + } +} + +// Responsive adjustments +@media (max-width: 768px) { + .character-subattributes.tab { + .subattributes-content { + .subattributes-list { + grid-template-columns: 1fr; + } + } + } +} diff --git a/styles/character.less b/styles/character.less index f541880..f205c61 100644 --- a/styles/character.less +++ b/styles/character.less @@ -263,52 +263,199 @@ } .skills { display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 4px; + grid-template-columns: repeat(2, 1fr); + gap: 6px; .skill { display: flex; align-items: center; - gap: 4px; - .item-img { - width: 24px; - height: 24px; + gap: 8px; + padding: 6px 10px; + background: rgba(255, 255, 255, 0.3); + border: 2px solid #6b6b6b; + border-radius: 6px; + transition: all 0.2s; + + &:hover { + background: rgba(255, 255, 255, 0.5); + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15); } + + &.is-core-skill { + background: rgba(255, 235, 180, 0.4); + border-color: #d4a017; + + &:hover { + background: rgba(255, 235, 180, 0.6); + } + } + + .item-img { + width: 32px; + height: 32px; + border: 2px solid #6b6b6b; + border-radius: 4px; + object-fit: cover; + flex-shrink: 0; + } + .name { - min-width: 12rem; + flex: 1; + min-width: 0; + + a { + font-family: "Cinzel", serif; + font-size: 14px; + font-weight: 600; + color: #2c2c2c; + + i { + margin-right: 6px; + color: #6b6b6b; + } + + &:hover { + color: #000; + } + } + } + + .score { + font-family: "Cinzel", serif; + font-size: 16px; + font-weight: bold; + color: #2c2c2c; + min-width: 50px; + text-align: center; + + .advanced-icon { + color: #d4a017; + font-size: 18px; + margin-left: 4px; + } + } + + .controls { + display: flex; + gap: 8px; + flex-shrink: 0; + + a { + color: #6b6b6b; + font-size: 14px; + transition: color 0.2s; + + &:hover { + color: #2c2c2c; + } + } } } } .racial-abilities { display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 4px; + grid-template-columns: repeat(2, 1fr); + gap: 6px; .racial-ability { display: flex; align-items: center; - gap: 4px; - .item-img { - width: 24px; - height: 24px; + gap: 8px; + padding: 6px 10px; + background: rgba(200, 255, 200, 0.2); + border: 2px solid #6b9b6b; + border-radius: 6px; + transition: all 0.2s; + + &:hover { + background: rgba(200, 255, 200, 0.4); + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15); } + + .item-img { + width: 32px; + height: 32px; + border: 2px solid #6b9b6b; + border-radius: 4px; + object-fit: cover; + flex-shrink: 0; + } + .name { - min-width: 12rem; + flex: 1; + min-width: 0; + font-family: "Cinzel", serif; + font-size: 14px; + font-weight: 600; + color: #2c2c2c; + } + + .controls { + display: flex; + gap: 8px; + flex-shrink: 0; + + a { + color: #6b9b6b; + font-size: 14px; + transition: color 0.2s; + + &:hover { + color: #3c6b3c; + } + } } } } .vulnerabilities { display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 4px; + grid-template-columns: repeat(2, 1fr); + gap: 6px; .vulnerability { display: flex; align-items: center; - gap: 4px; - .item-img { - width: 24px; - height: 24px; + gap: 8px; + padding: 6px 10px; + background: rgba(255, 200, 200, 0.2); + border: 2px solid #9b6b6b; + border-radius: 6px; + transition: all 0.2s; + + &:hover { + background: rgba(255, 200, 200, 0.4); + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15); } + + .item-img { + width: 32px; + height: 32px; + border: 2px solid #9b6b6b; + border-radius: 4px; + object-fit: cover; + flex-shrink: 0; + } + .name { - min-width: 12rem; + flex: 1; + min-width: 0; + font-family: "Cinzel", serif; + font-size: 14px; + font-weight: 600; + color: #2c2c2c; + } + + .controls { + display: flex; + gap: 8px; + flex-shrink: 0; + + a { + color: #9b6b6b; + font-size: 14px; + transition: color 0.2s; + + &:hover { + color: #6b3c3c; + } + } } } } diff --git a/styles/fvtt-prism-rpg.less b/styles/fvtt-prism-rpg.less index aeb5f0d..d5a7f48 100644 --- a/styles/fvtt-prism-rpg.less +++ b/styles/fvtt-prism-rpg.less @@ -4,6 +4,8 @@ .prismrpg { @import "mixins.less"; @import "character.less"; + @import "character-main-v2.less"; + @import "character-subattributes.less"; @import "monster.less"; @import "skill.less"; @import "racial-ability.less"; diff --git a/templates/character-main.hbs b/templates/character-main.hbs index 563de3c..3456ebc 100644 --- a/templates/character-main.hbs +++ b/templates/character-main.hbs @@ -1,461 +1,298 @@ -
- {{log "character-main" this}} - -
- {{localize "PRISMRPG.Label.pc"}} -
-
-
- -
- -
-
- {{localize "PRISMRPG.Label.HP"}} - {{formInput - systemFields.hp.fields.value - value=system.hp.value - disabled=isPlayMode - classes="character-hp-value" - }} -  /  - {{formInput - systemFields.hp.fields.max - value=system.hp.max - disabled=isPlayMode - classes="character-hp-value" - }} -
-
- {{localize "PRISMRPG.Label.grit"}} - {{formInput - systemFields.grit.fields.current - value=system.grit.current - disabled=isPlayMode - classes="character-hp" - }} - {{localize "PRISMRPG.Label.earned"}} - {{formInput - systemFields.grit.fields.earned - value=system.grit.earned - disabled=isPlayMode - classes="character-hp" - }} -
-
- {{localize "PRISMRPG.Label.luck"}} - {{formInput - systemFields.luck.fields.current - value=system.luck.current - disabled=isPlayMode - classes="character-hp" - }} - {{localize "PRISMRPG.Label.earned"}} - {{formInput - systemFields.luck.fields.earned - value=system.luck.earned - disabled=isPlayMode - classes="character-hp" - }} -
- -
- {{localize - "PRISMRPG.Label.damageResistanceShort" - }} - {{formInput - systemFields.hp.fields.damageResistance - value=system.hp.fields.damageResistance - disabled=isPlayMode - classes="character-hp" - }} -
- -
+
+ {{log "character-main-v2" this}} +
+ {{! Character Header with Name }} +
+
+ {{formInput + fields.name + value=source.name + rootId=partId + disabled=isPlayMode + placeholder="Character Name" + }}
-
-
- {{formInput - fields.name - value=source.name - rootId=partId - disabled=isPlayMode - }} - - - -
- -
+ - {{localize "PRISMRPG.Label.Saves"}} -
-
- - {{localize "PRISMRPG.Label.saves.will"}} - - {{formField - systemFields.saves.fields.will.fields.value - value=system.saves.will.value - disabled=true - }} - - - {{localize "PRISMRPG.Label.saves.dodge"}} - - - {{formField - systemFields.saves.fields.dodge.fields.value - value=system.saves.dodge.value - disabled=true - }} - - - {{localize "PRISMRPG.Label.saves.toughness"}} - - - {{formField - systemFields.saves.fields.toughness.fields.value - value=system.saves.toughness.value - disabled=true - }} -
-
- - - {{localize "PRISMRPG.Label.saves.contagion"}} - - - {{formField - systemFields.saves.fields.contagion.fields.value - value=system.saves.contagion.value - disabled=true - }} - - - - {{localize "PRISMRPG.Label.saves.poison"}} - - - {{formField - systemFields.saves.fields.poison.fields.value - value=system.saves.poison.value - disabled=true - }} - - - -
-
-
- -
- {{localize "PRISMRPG.Label.Challenges"}} -
-
- {{localize - "PRISMRPG.Label.challenges.strength" - }} - {{formField - systemFields.challenges.fields.str.fields.value - value=system.challenges.str.value - disabled=true - }} - {{localize - "PRISMRPG.Label.challenges.agility" - }} - {{formField - systemFields.challenges.fields.agility.fields.value - value=system.challenges.agility.value - disabled=true - }} - {{localize - "PRISMRPG.Label.challenges.dying" - }} - {{formField - systemFields.challenges.fields.dying.fields.value - value=system.challenges.dying.value - disabled=true - }} -
-
-
- -
- {{localize "PRISMRPG.Label.Movement"}} -
-
- {{localize - "PRISMRPG.Label.movement.walk" - }} - {{formField - systemFields.movement.fields.walk - value=system.movement.walk - disabled=isPlayMode - }} - {{localize - "PRISMRPG.Label.movement.jog" - }} - {{formField - systemFields.movement.fields.jog - value=system.movement.jog - disabled=isPlayMode - }} - {{localize - "PRISMRPG.Label.movement.run" - }} - {{formField - systemFields.movement.fields.run - value=system.movement.run - disabled=isPlayMode - }} - {{localize - "PRISMRPG.Label.movement.sprint" - }} - {{formField - systemFields.movement.fields.sprint - value=system.movement.sprint - disabled=isPlayMode - }} -
-
- {{localize - "PRISMRPG.Label.movement.jumpBroad" - }} - {{formField - systemFields.jump.fields.broad - value=system.jump.broad - disabled=isPlayMode - }} - {{localize - "PRISMRPG.Label.movement.jumpRunning" - }} - {{formField - systemFields.jump.fields.running - value=system.jump.running - disabled=isPlayMode - }} - {{localize - "PRISMRPG.Label.movement.jumpVertical" - }} - {{formField - systemFields.jump.fields.vertical - value=system.jump.vertical - disabled=isPlayMode - }} -
-
-
- + +
-
-
- {{localize "PRISMRPG.Label.characteristics"}} -
- {{localize "PRISMRPG.Label.str"}} - {{formField - systemFields.characteristics.fields.str.fields.value - value=system.characteristics.str.value - disabled=isPlayMode - data-char-id="str" - }} - {{formField - systemFields.characteristics.fields.str.fields.percent - value=system.characteristics.str.percent - disabled=isPlayMode - type="number" - }} -
-
- {{localize "PRISMRPG.Label.int"}} - {{formField - systemFields.characteristics.fields.int.fields.value - value=system.characteristics.int.value - disabled=isPlayMode - data-char-id="int" - }} +
+ {{! Left Column - Portrait, Attributes & HP }} +
+ {{! Portrait + HP column }} +
+ {{! Portrait }} +
+ +
- {{formField - systemFields.characteristics.fields.int.fields.percent - value=system.characteristics.int.percent - disabled=isPlayMode - type="number" - }} -
-
- {{localize "PRISMRPG.Label.wis"}} - {{formField - systemFields.characteristics.fields.wis.fields.value - value=system.characteristics.wis.value - disabled=isPlayMode - data-char-id="wis" - }} - - {{formField - systemFields.characteristics.fields.wis.fields.percent - value=system.characteristics.wis.percent - disabled=isPlayMode - type="number" - }} -
-
- {{localize "PRISMRPG.Label.dex"}} - {{formField - systemFields.characteristics.fields.dex.fields.value - value=system.characteristics.dex.value - disabled=isPlayMode - data-char-id="wis" - }} - - {{formField - systemFields.characteristics.fields.dex.fields.percent - value=system.characteristics.dex.percent - disabled=isPlayMode - type="number" - }} -
-
- {{localize "PRISMRPG.Label.con"}} - {{formField - systemFields.characteristics.fields.con.fields.value - value=system.characteristics.con.value - disabled=isPlayMode - data-char-id="con" - }} - - {{formField - systemFields.characteristics.fields.con.fields.percent - value=system.characteristics.con.percent - disabled=isPlayMode - type="number" - }} -
-
- {{localize "PRISMRPG.Label.cha"}} - {{formField - systemFields.characteristics.fields.cha.fields.value - value=system.characteristics.cha.value - disabled=isPlayMode - data-char-id="cha" - }} - - {{formField - systemFields.characteristics.fields.cha.fields.percent - value=system.characteristics.cha.percent - disabled=isPlayMode - type="number" - }} -
-
- - {{!-- Sub-Attributes (Prism RPG) --}} -
- {{localize "PRISMRPG.Label.subAttributes"}} -
- {{#each config.SUB_ATTRIBUTES as |subAttr|}} -
- {{localize subAttr.label}} - {{lookup (lookup ../system.subAttributes subAttr.id) 'value'}} - ({{#each subAttr.parents}}{{localize (concat "PRISMRPG.Label." this)}}{{#unless @last}}/{{/unless}}{{/each}}) + {{! HP Shields (3 shields) - Below portrait }} +
+
+
+
HP
+
+ {{formInput + systemFields.hp.fields.value + value=system.hp.value + disabled=isPlayMode + }} +
+
/
+
+ {{formInput + systemFields.hp.fields.max + value=system.hp.max + disabled=isPlayMode + }} +
+
+
+
MAGIC
+
+ {{formInput + systemFields.magicPoints.fields.value + value=system.magicPoints.value + disabled=isPlayMode + }} +
+
/
+
+ {{formInput + systemFields.magicPoints.fields.max + value=system.magicPoints.max + disabled=isPlayMode + }} +
+
+
+
ARMOR
+
+ {{formInput + systemFields.armorPoints.fields.value + value=system.armorPoints.value + disabled=isPlayMode + }} +
+
/
+
+ {{formInput + systemFields.armorPoints.fields.max + value=system.armorPoints.max + disabled=isPlayMode + }} +
+
+
+
- {{/each}} + + {{! Core Attributes (STR, DEX, CON, INT, WIS, CHA) }} +
+
+ +
+ {{formInput + systemFields.characteristics.fields.str.fields.value + value=system.characteristics.str.value + disabled=isPlayMode + }} +
+
+ {{formInput + systemFields.saves.fields.str.fields.value + value=system.saves.str.value + disabled=true + }} +
+
+
+ +
+ {{formInput + systemFields.characteristics.fields.dex.fields.value + value=system.characteristics.dex.value + disabled=isPlayMode + }} +
+
+ {{formInput + systemFields.saves.fields.dex.fields.value + value=system.saves.dex.value + disabled=true + }} +
+
+
+ +
+ {{formInput + systemFields.characteristics.fields.con.fields.value + value=system.characteristics.con.value + disabled=isPlayMode + }} +
+
+ {{formInput + systemFields.saves.fields.con.fields.value + value=system.saves.con.value + disabled=true + }} +
+
+
+ +
+ {{formInput + systemFields.characteristics.fields.int.fields.value + value=system.characteristics.int.value + disabled=isPlayMode + }} +
+
+ {{formInput + systemFields.saves.fields.int.fields.value + value=system.saves.int.value + disabled=true + }} +
+
+
+ +
+ {{formInput + systemFields.characteristics.fields.wis.fields.value + value=system.characteristics.wis.value + disabled=isPlayMode + }} +
+
+ {{formInput + systemFields.saves.fields.wis.fields.value + value=system.saves.wis.value + disabled=true + }} +
+
+
+ +
+ {{formInput + systemFields.characteristics.fields.cha.fields.value + value=system.characteristics.cha.value + disabled=isPlayMode + }} +
+
+ {{formInput + systemFields.saves.fields.cha.fields.value + value=system.saves.cha.value + disabled=true + }} +
+
+
+
+ + {{! Right Column - Race, Classes }} +
+ {{! Race }} +
+
+

Race

+ {{#if race}} +
+ +
{{race.name}}
+
+ + +
+
+ {{else}} +
+

{{localize "PRISMRPG.Message.dropRace"}}

+
+ {{/if}} +
+
+ + {{! Classes (Three boxes) }} +
+ {{#each classSlots as |classItem index|}} +
+

Class {{add index 1}}

+
+ {{#if classItem}} +
+ +
{{classItem.name}}
+
+ + +
+
+ {{else}} +
+

{{localize "PRISMRPG.Message.dropClass"}}

+
+ {{/if}} +
+
+ {{/each}} +
+
-
+ +
\ No newline at end of file diff --git a/templates/character-skills.hbs b/templates/character-skills.hbs index c9ce0c7..c5d7eba 100644 --- a/templates/character-skills.hbs +++ b/templates/character-skills.hbs @@ -1,68 +1,10 @@
- {{!-- Core Skill Selection (Prism RPG) --}} -
- - {{localize "PRISMRPG.Label.coreSkill"}} - -
- {{#if system.coreSkill.skill}} -
- {{localize (concat "PRISMRPG.CoreSkill." system.coreSkill.skill)}} - +5 {{localize "PRISMRPG.Label.basicChecks"}} - {{#if system.coreSkill.attributeChoice}} - +2 {{localize (concat "PRISMRPG.Label." system.coreSkill.attributeChoice)}} - {{/if}} - {{localize "PRISMRPG.Label.advancedChecksEnabled"}} -
- {{else}} -
-

{{localize "PRISMRPG.Message.selectCoreSkill"}}

- -
- {{/if}} -
-
- - {{!-- Available Core Skills Reference --}} -
- - {{localize "PRISMRPG.Label.availableCoreSkills"}} - -
- {{#each config.CORE_SKILLS as |skill skillId|}} -
-
- {{localize skill.label}} - {{#if (eq ../system.coreSkill.skill skillId)}} - {{localize "PRISMRPG.Label.yourCoreSkill"}} - {{/if}} -
-
- {{localize "PRISMRPG.Label.attributeChoices"}}: - {{#each skill.attributeChoices as |attr|}} - - {{localize (concat "PRISMRPG.Label." attr)}} - - {{#unless @last}}/{{/unless}} - {{/each}} -
-
- {{/each}} -
-
- - {{!-- Skills Items (if any) --}} + {{!-- Skills Items --}}
- {{localize "PRISMRPG.Label.customSkills"}} + {{localize "PRISMRPG.Label.skills"}}
{{#each skills as |item|}} diff --git a/templates/character-subattributes-old.hbs b/templates/character-subattributes-old.hbs new file mode 100644 index 0000000..6755e71 --- /dev/null +++ b/templates/character-subattributes-old.hbs @@ -0,0 +1,257 @@ +
+ {{log "character-subattributes" this}} + +
+

+ + Sub-Attributes +

+

+ Sub-attributes are derived from the average of two primary characteristics. +

+ +
+ {{#each (entries config.SUB_ATTRIBUTES) as |entry|}} + {{#with entry.1 as |subAttr|}} +
+
+
+ + {{localize subAttr.label}} +
+
+ +
+
+
+
+ From: + {{#each subAttr.parents as |parentKey|}} + + {{uppercase parentKey}} + ({{lookup ../../system.characteristics parentKey 'value'}}) + + {{/each}} +
+
+ {{localize subAttr.description}} +
+
+
+ {{/with}} + {{/each}} +
+
+
+ +
+ {{! Character Header with Age, Length, Weight, Sex, Skin, Hair }} +
+
+
+ + {{formInput + systemFields.bio.fields.age + value=system.bio.age + disabled=isPlayMode + }} +
+
+ + {{formInput + systemFields.bio.fields.length + value=system.bio.length + disabled=isPlayMode + }} +
+
+ + {{formInput + systemFields.bio.fields.weight + value=system.bio.weight + disabled=isPlayMode + }} +
+
+
+
+ + {{formInput + systemFields.bio.fields.sex + value=system.bio.sex + disabled=isPlayMode + }} +
+
+ + {{formInput + systemFields.bio.fields.skin + value=system.bio.skin + disabled=isPlayMode + }} +
+
+ + {{formInput + systemFields.bio.fields.hair + value=system.bio.hair + disabled=isPlayMode + }} +
+
+
+ + {{! Sub-Attributes Table }} +
+

Sub-Attribute

+
+
+
Prowess
+
+ {{formInput + systemFields.subattributes.fields.prowess + value=system.subattributes.prowess + disabled=isPlayMode + }} +
+
Vigor
+
+ {{formInput + systemFields.subattributes.fields.vigor + value=system.subattributes.vigor + disabled=isPlayMode + }} +
+
Competence
+
+ {{formInput + systemFields.subattributes.fields.competence + value=system.subattributes.competence + disabled=isPlayMode + }} +
+
Authority
+
+ {{formInput + systemFields.subattributes.fields.authority + value=system.subattributes.authority + disabled=isPlayMode + }} +
+
Presence
+
+ {{formInput + systemFields.subattributes.fields.presence + value=system.subattributes.presence + disabled=isPlayMode + }} +
+
+ +
+
Willpower
+
+ {{formInput + systemFields.subattributes.fields.willpower + value=system.subattributes.willpower + disabled=isPlayMode + }} +
+
Resilience
+
+ {{formInput + systemFields.subattributes.fields.resilience + value=system.subattributes.resilience + disabled=isPlayMode + }} +
+
Cunning
+
+ {{formInput + systemFields.subattributes.fields.cunning + value=system.subattributes.cunning + disabled=isPlayMode + }} +
+
Guile
+
+ {{formInput + systemFields.subattributes.fields.guile + value=system.subattributes.guile + disabled=isPlayMode + }} +
+
Sovereignty
+
+ {{formInput + systemFields.subattributes.fields.sovereignty + value=system.subattributes.sovereignty + disabled=isPlayMode + }} +
+
+ +
+
Stamina
+
+ {{formInput + systemFields.subattributes.fields.stamina + value=system.subattributes.stamina + disabled=isPlayMode + }} +
+
Initiative
+
+ {{formInput + systemFields.subattributes.fields.initiative + value=system.subattributes.initiative + disabled=isPlayMode + }} +
+
Wit
+
+ {{formInput + systemFields.subattributes.fields.wit + value=system.subattributes.wit + disabled=isPlayMode + }} +
+
Grace
+
+ {{formInput + systemFields.subattributes.fields.grace + value=system.subattributes.grace + disabled=isPlayMode + }} +
+
Tenacity
+
+ {{formInput + systemFields.subattributes.fields.tenacity + value=system.subattributes.tenacity + disabled=isPlayMode + }} +
+
+
+
+ + {{! Proficiencies Section }} +
+

Proficiencies

+
+ {{formInput + systemFields.proficiencies + value=system.proficiencies + disabled=isPlayMode + type="textarea" + }} +
+
+
+
\ No newline at end of file diff --git a/templates/character-subattributes.hbs b/templates/character-subattributes.hbs new file mode 100644 index 0000000..1532d99 --- /dev/null +++ b/templates/character-subattributes.hbs @@ -0,0 +1,48 @@ +
+
+

+ + Sub-Attributes +

+

+ Sub-attributes are derived from the average of two primary characteristics. +

+ +
+ {{#each (entries config.SUB_ATTRIBUTES) as |entry|}} + {{#with entry.[1] as |subAttr|}} +
+
+
+ + {{localize subAttr.label}} +
+
+ +
+
+
+
+ From: + {{#each subAttr.parents as |parentKey|}} + + {{uppercase parentKey}} + ({{lookup ../../system.characteristics parentKey 'value'}}) + + {{/each}} +
+
+ {{localize subAttr.description}} +
+
+
+ {{/with}} + {{/each}} +
+
+
diff --git a/templates/roll-dialog-old.hbs b/templates/roll-dialog-old.hbs new file mode 100644 index 0000000..626b376 --- /dev/null +++ b/templates/roll-dialog-old.hbs @@ -0,0 +1,119 @@ +
+ +
+ {{localize (concat "PRISMRPG.Label." rollType)}} - {{actorName}} + + {{#if rollTarget.tokenId}} + + {{/if}} + + {{#if (match rollType "attack")}} +
Attack roll ! - {{rollTarget.name}}
+ {{/if}} + {{#if (match rollType "defense")}} +
Defense roll ! - {{rollTarget.name}}
+ {{/if}} + + {{#if hasModifier}} +
{{upperFirst rollName}} : {{baseFormula}} + {{baseValue}}
+ {{else}} +
{{upperFirst rollName}} : {{baseFormula}}
+ {{/if}} + {{#if rollTarget.weapon}} +
{{localize "PRISMRPG.Label.baseModifier"}} : {{rollTarget.charModifier}}
+
{{localize "PRISMRPG.Label.weapon"}} : {{rollTarget.weapon.name}}
+
{{localize "PRISMRPG.Label.skill"}} : {{rollTarget.name}}
+
{{localize "PRISMRPG.Label.skillBonus"}} : {{rollTarget.weaponSkillModifier}}
+ {{/if}} + + {{#if (match rollType "attack")}} +
Add Granted Attack Dice + +
+ {{#if rollTarget.weapon}} + {{#if (eq rollTarget.weapon.system.weaponType "melee")}} + {{else}} +
Point Blank Range Attack + +
+
Beyond Skill Range Attack + +
+
Let it Fly (Pure D20E) + +
+
Aiming + +
+ {{/if}} + {{/if}} + + {{/if}} + {{#if (match rollType "defense")}} +
Add Granted Defense Dice + +
+ {{/if}} + {{#if (match rollType "damage")}} +
Add Granted Damage Dice + +
+ {{/if}} + + {{#if rollTarget.staticModifier}} +
Static modifier : +{{rollTarget.staticModifier}}
+ {{/if}} + +
+ + + {{#if hasFavor}} +
+ {{localize "PRISMRPG.Roll.favorDisfavor"}} + +
+ {{/if}} + + {{#if hasModifier}} +
+ {{localize "PRISMRPG.Roll.modifierBonusMalus"}} + + + {{#if (eq rollType "save")}} + {{#if rollTarget.magicUser}} +
+ Save against spell (+{{rollTarget.actorModifiers.saveModifier}}) ? + +
+ {{/if}} + {{/if}} +
+ {{/if}} + + {{#if hasChangeDice}} +
+ {{localize "PRISMRPG.Roll.changeDice"}} + +
+ {{/if}} + +
+ {{localize "PRISMRPG.Roll.visibility"}} + +
+ + +
\ No newline at end of file diff --git a/templates/roll-dialog.hbs b/templates/roll-dialog.hbs index 626b376..6a11e44 100644 --- a/templates/roll-dialog.hbs +++ b/templates/roll-dialog.hbs @@ -1,111 +1,59 @@
- -
- {{localize (concat "PRISMRPG.Label." rollType)}} - {{actorName}} - - {{#if rollTarget.tokenId}} - - {{/if}} - - {{#if (match rollType "attack")}} -
Attack roll ! - {{rollTarget.name}}
- {{/if}} - {{#if (match rollType "defense")}} -
Defense roll ! - {{rollTarget.name}}
- {{/if}} +
+ {{localize (concat "PRISMRPG.Label." rollType)}} + - + {{actorName}} {{#if hasModifier}} -
{{upperFirst rollName}} : {{baseFormula}} + {{baseValue}}
+
+ {{upperFirst rollName}} + : + {{dice}} + + + {{baseValue}} +
{{else}} -
{{upperFirst rollName}} : {{baseFormula}}
- {{/if}} - {{#if rollTarget.weapon}} -
{{localize "PRISMRPG.Label.baseModifier"}} : {{rollTarget.charModifier}}
-
{{localize "PRISMRPG.Label.weapon"}} : {{rollTarget.weapon.name}}
-
{{localize "PRISMRPG.Label.skill"}} : {{rollTarget.name}}
-
{{localize "PRISMRPG.Label.skillBonus"}} : {{rollTarget.weaponSkillModifier}}
+
+ {{upperFirst rollName}} + : + {{dice}} +
{{/if}} - {{#if (match rollType "attack")}} -
Add Granted Attack Dice - -
{{#if rollTarget.weapon}} - {{#if (eq rollTarget.weapon.system.weaponType "melee")}} - {{else}} -
Point Blank Range Attack - -
-
Beyond Skill Range Attack - -
-
Let it Fly (Pure D20E) - -
-
Aiming - + {{selectOptions choiceAdvantage selected=advantage}} -
- {{/if}} - {{/if}} - - {{/if}} - {{#if (match rollType "defense")}} -
Add Granted Defense Dice - -
- {{/if}} - {{#if (match rollType "damage")}} -
Add Granted Damage Dice - -
- {{/if}} - - {{#if rollTarget.staticModifier}} -
Static modifier : +{{rollTarget.staticModifier}}
- {{/if}} - -
- - - {{#if hasFavor}} -
- {{localize "PRISMRPG.Roll.favorDisfavor"}} - -
+
{{/if}} {{#if hasModifier}} -
- {{localize "PRISMRPG.Roll.modifierBonusMalus"}} - - - {{#if (eq rollType "save")}} - {{#if rollTarget.magicUser}} -
- Save against spell (+{{rollTarget.actorModifiers.saveModifier}}) ? - -
- {{/if}} - {{/if}} -
- {{/if}} - - {{#if hasChangeDice}} -
- {{localize "PRISMRPG.Roll.changeDice"}} - -
+
+ {{localize "PRISMRPG.Roll.modifierBonusMalus"}} + +
{{/if}}
@@ -114,6 +62,4 @@ {{selectOptions rollModes selected=visibility localize=true}}
- -
\ No newline at end of file