From adae5a5d5664b0919065f3360c23971de5f6d3bf Mon Sep 17 00:00:00 2001 From: Aaron Lutomski Date: Fri, 29 May 2026 20:20:40 -0700 Subject: [PATCH] Restyled character creation dialog. Added dialog factory. --- CHANGELOG.md | 2 + css/deltagreen.css | 783 +++------------- css/dg-dialog.css | 846 ++++++++++++++++++ lang/en.json | 25 +- module/actor/actor.js | 2 + module/applications/add-profession-dialog.js | 240 +++-- module/applications/assign-stats-dialog.js | 24 +- module/applications/dg-dialog.js | 103 +++ module/applications/pick-statistics-dialog.js | 8 +- module/applications/profession-setup-flow.js | 6 +- module/applications/roll-stats-dialog.js | 24 +- module/chat/dg-chat-card.js | 33 +- module/data/item/profession.js | 9 +- module/documents/dg-active-effect.js | 4 +- module/roll/roll-classes.js | 36 +- module/roll/roll-dialogs.js | 11 +- module/sheets/agent-sheet.js | 19 +- module/sheets/mixins/profession-item-mixin.js | 15 +- module/sheets/mixins/skill-prep-mixin.js | 123 +-- module/utils/active-effect-derived.js | 84 +- module/utils/profession-skills.js | 194 +++- module/utils/profession-stat-setup.js | 4 +- module/utils/register-helpers.js | 15 - module/utils/resource-chat.js | 32 +- module/utils/roll-target-tooltip.js | 209 +++++ module/utils/skill-layout.js | 18 + module/utils/skill-tooltip.js | 169 ++++ system.json | 2 +- .../partials/custom-skills-partial-agent.html | 15 +- templates/actor/partials/sanity-agent.html | 4 +- templates/actor/parts/left-bar.html | 6 +- templates/actor/parts/skills-tab-agent.html | 2 +- templates/dialog/add-profession-skill.html | 14 +- templates/dialog/add-profession.html | 287 +++--- .../dialog/apply-skill-improvements.html | 6 +- templates/dialog/assign-stats.html | 8 +- templates/dialog/exhaust-agent.html | 8 +- templates/dialog/modify-damage-roll.html | 4 +- templates/dialog/modify-percentile-roll.html | 10 +- templates/dialog/pick-statistics.html | 6 +- templates/dialog/roll-stats.html | 6 +- templates/dialog/take-stimulants.html | 34 +- .../item/parts/profession-skills-tab.html | 2 +- 43 files changed, 2340 insertions(+), 1112 deletions(-) create mode 100644 css/dg-dialog.css create mode 100644 module/applications/dg-dialog.js create mode 100644 module/utils/roll-target-tooltip.js create mode 100644 module/utils/skill-tooltip.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 9590e04..327739f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,10 +23,12 @@ - [#209](https://github.com/deltagreen-foundryvtt/delta-green-foundry-vtt-system/issues/209) - Added **Profession** item type with partial chargen automation ### **Bug Fixes:** + - [#392](https://github.com/deltagreen-foundryvtt/delta-green-foundry-vtt-system/issues/392) - Fixed sheet rolls - [#394](https://github.com/deltagreen-foundryvtt/delta-green-foundry-vtt-system/issues/394) - Unified actor and item description fields on schema with shared ProseMirror preparation ### **Under the Hood:** + - [#323](https://github.com/deltagreen-foundryvtt/delta-green-foundry-vtt-system/issues/323) - Refactored roll logic and split into util functions ## Version 1.6.6 - 2026-04-15 diff --git a/css/deltagreen.css b/css/deltagreen.css index 01283eb..6395fc2 100644 --- a/css/deltagreen.css +++ b/css/deltagreen.css @@ -38,15 +38,23 @@ format("truetype"); /* Chrome 4+, Firefox 3.5, Opera 10+, Safari 3—5 */ } +/* Color tokens — near-black ink and dark surfaces (avoid pure #000) */ +:root { + --dg-ink: #212121; + --dg-surface-dark: #1a1a1a; + --dg-surface-dark-hover: #2a2a2a; + --dg-on-dark: #f5f5f0; +} + /* Global styles */ .deltagreen section.window-content { scrollbar-color: #215112 #111 !important; padding: 0.5rem; - color: black; /* Override for when in Foundry's dark mode */ + color: var(--dg-ink); /* Override for when in Foundry's dark mode */ input[type="checkbox"]:checked::before { - color: black; /* Override for when in Foundry's dark mode */ + color: var(--dg-ink); /* Override for when in Foundry's dark mode */ } input[type="text"], @@ -79,7 +87,8 @@ font-family: "PublicSans", sans-serif !important; } -.outlaw-style .cowboy-style .tabs [data-action="tab"] { +.outlaw-style .tabs [data-action="tab"], +.cowboy-style .tabs [data-action="tab"] { background: rgba(173, 151, 101, 0.3); } @@ -121,7 +130,7 @@ .rollable:hover, .rollable:focus { - color: #000; + color: var(--dg-ink); text-shadow: 0 0 5px #374e13; cursor: pointer; } @@ -332,7 +341,7 @@ explicitly define a number of rows, rather than columns. */ cursor: var(--cursor-pointer); /* Variable name from Foundry */ - border: 1px solid black; + border: 1px solid var(--dg-ink); margin-right: 5px; } @@ -383,7 +392,7 @@ explicitly define a number of rows, rather than columns. */ line-height: inherit; padding-inline: 5px; border-radius: 5px 5px 0px 0px; - border-color: #000; + border-color: var(--dg-ink); border: gray; border-width: 1px 1px 0px 1px; border-style: solid; @@ -392,7 +401,7 @@ explicitly define a number of rows, rather than columns. */ .deltagreen .tabs [data-action="tab"].active { text-shadow: none; font-style: italic; - border: #000; + border-color: var(--dg-ink); border-width: 2px 2px 0px 2px; border-style: solid; } @@ -401,7 +410,7 @@ explicitly define a number of rows, rather than columns. */ height: 100%; overflow-y: auto; - border-color: #000; + border-color: var(--dg-ink); border-width: 2px; border-style: solid; border-radius: 3px; @@ -410,13 +419,13 @@ explicitly define a number of rows, rather than columns. */ } .deltagreen .program-style .tabs [data-action="tab"].active { - background-color: #000000; - color: #ffffff; + background-color: var(--dg-surface-dark); + color: var(--dg-on-dark); } .deltagreen .cowboy-style .tabs [data-action="tab"].active { background-color: rgba(156, 115, 18, 0.3); - color: #000000; + color: var(--dg-ink); } .deltagreen .items-list { @@ -608,6 +617,10 @@ input.percentile-skill-input { margin: 0; } +.deltagreen .left-bar .stats .agent-left-bar-stat-grid { + grid-template-columns: auto 1fr; +} + /* Hide input text only when an AE overlay is showing the effective value */ .deltagreen .ae-input-host:has(.ae-input-display):not(:focus-within) input { color: transparent; @@ -880,7 +893,7 @@ textarea.wounds { grid-template-columns: 100%; align-items: center; justify-content: center; - border: 1.5px solid #000000; + border: 1.5px solid var(--dg-ink); padding: 5px; grid-gap: 3px; text-align: center; @@ -908,67 +921,77 @@ div.max-resource-value { .physical-stats-and-feature { border-radius: 2px; - border: 1.5px solid #000000; + border: 1.5px solid var(--dg-ink); } .attributes-wrapper { margin-block-end: 12px; } -div.horizontal-bar-section-divider { - display: grid; - grid-template-columns: 100%; - justify-content: center; - align-items: center; - text-align: center; - +div.horizontal-bar-section-divider, +div.horizontal-bar-section-divider.skills-header, +div.horizontal-bar-section-header { + box-sizing: border-box; margin-bottom: 3px; + background: var(--dg-surface-dark); + color: var(--dg-on-dark); + border-radius: 3px; + line-height: 1.2; + min-height: 30px; + height: auto; + padding: 6px 8px; + text-align: center; +} - background: #000; - color: #fff; - height: 30px; - - border-radius: 3px 3px 3px 3px; +div.horizontal-bar-section-divider { + display: flex; + align-items: center; + justify-content: center; } div.horizontal-bar-section-divider.skills-header { display: grid; - grid-template-columns: repeat(3, 1fr); - justify-content: center; + grid-template-columns: 1fr auto 1fr; align-items: center; - text-align: center; +} - margin-bottom: 3px; - - background: #000; - color: #fff; - height: 30px; - - border-radius: 3px 3px 3px 3px; +/* Agent sheet: only the title — center in a single column (not grid column 2 of 3). */ +div.horizontal-bar-section-divider.skills-header:not(:has(.toggle-untrained)) { + grid-template-columns: 1fr; } .horizontal-bar-section-divider .skills-header-title { grid-column: 2; + margin: 0; + line-height: 1.2; +} + +div.horizontal-bar-section-divider.skills-header:not(:has(.toggle-untrained)) + .skills-header-title { + grid-column: 1; + justify-self: center; } .horizontal-bar-section-divider .toggle-untrained { grid-column: 3; padding-right: 0.5rem; justify-self: end; + align-self: center; + line-height: 1.2; } div.horizontal-bar-section-header { - display: grid; - grid-template-columns: 100%; - justify-content: center; + display: flex; align-items: center; - text-align: center; + justify-content: center; + min-height: 50px; + padding: 10px 8px; +} - margin-bottom: 3px; - - background: #000; - color: #fff; - height: 50px; +/* Special Elite cap height sits high in the em box; nudge section headers down slightly. */ +.cowboy-style div.horizontal-bar-section-divider:not(.custom-skill-group-label), +.cowboy-style div.horizontal-bar-section-header { + padding-block: 8px 4px; } .physical-description-label { @@ -1162,7 +1185,7 @@ div.horizontal-bar-section-header { -ms-flex: 0 0 100px; flex: 0 0 100px; height: 100px; - border: 0px solid black; + border: 0px solid var(--dg-ink); } input.item-sheet-short-input { @@ -1577,14 +1600,36 @@ a[data-action="applySkillImprovements"] { font-weight: bold; } +.deltagreen input.percentile-skill-input.ae-mod-increase, +.deltagreen input.centered.ae-mod-increase { + color: #00a82d; + -webkit-text-fill-color: #00a82d; + font-weight: bold; +} + .deltagreen .max-resource-value.ae-mod-increase > div, .deltagreen .max-resource-value.ae-mod-decrease > div { font-weight: bold; } +table.dg-roll-target-tooltip { + margin-top: 0.25em; + border-collapse: collapse; +} + +table.dg-roll-target-tooltip td { + padding: 1px 6px 1px 0; + vertical-align: top; +} + +table.dg-roll-target-tooltip td:last-child { + text-align: right; + white-space: nowrap; +} + /* Delta Green stylized chat cards */ .deltagreen.chat-card.dg-chat-card { - border: 2px solid #000; + border: 2px solid var(--dg-ink); border-radius: 3px; overflow: hidden; margin: 0; @@ -1595,8 +1640,8 @@ a[data-action="applySkillImprovements"] { flex-direction: row; align-items: center; gap: 0; - background: #1a1a1a; - color: #f5f5f0; + background: var(--dg-surface-dark); + color: var(--dg-on-dark); padding: 6px 0; } @@ -1636,19 +1681,19 @@ a[data-action="applySkillImprovements"] { margin: 0; font-size: 0.95rem; font-weight: bold; - color: #f5f5f0; + color: var(--dg-on-dark); line-height: 1.2; } .dg-chat-card__label { font-size: 0.8rem; - color: #f5f5f0; + color: var(--dg-on-dark); line-height: 1.25; margin-top: 2px; } .dg-chat-card__body { - border-color: #000; + border-color: var(--dg-ink); border-width: 0; border-style: solid; border-radius: 0; @@ -1656,6 +1701,10 @@ a[data-action="applySkillImprovements"] { background: inherit; } +.dg-chat-card__body .dice-roll { + margin-top: 0.5em; +} + .chat-message:has(.dg-chat-card) .message-header, .chat-message.dg-chat-card-message .message-header { display: none; @@ -1800,14 +1849,27 @@ a[data-action="applySkillImprovements"] { } } - /* Opaque inactive tabs so overlapping borders only show on the tab in front */ - .window-content.program-style .tabs [data-action="tab"]:not(.active) { - background-color: rgb(186, 184, 181); + /* Opaque tab fills so stacked borders only show on the tab in front (all themes) */ + .window-content.program-style { + --dg-agent-tab-inactive-bg: rgb(186, 184, 181); + --dg-agent-tab-active-bg: var(--dg-surface-dark); + --dg-agent-tab-active-color: var(--dg-on-dark); } - .window-content.cowboy-style .tabs [data-action="tab"]:not(.active), - .window-content.outlaw-style .tabs [data-action="tab"]:not(.active) { - background-color: rgb(210, 190, 150); + .window-content.cowboy-style, + .window-content.outlaw-style { + --dg-agent-tab-inactive-bg: rgb(210, 190, 150); + --dg-agent-tab-active-bg: rgb(194, 168, 110); + --dg-agent-tab-active-color: var(--dg-ink); + } + + .window-content .tabs [data-action="tab"]:not(.active) { + background: var(--dg-agent-tab-inactive-bg); + } + + .window-content .tabs [data-action="tab"].active { + background: var(--dg-agent-tab-active-bg); + color: var(--dg-agent-tab-active-color); } /* Pin the about tab to the right without opening a gap before it */ @@ -1969,10 +2031,6 @@ a[data-action="applySkillImprovements"] { } } - .horizontal-bar-section-divider.skills-header { - display: flex; - } - .skill-util-buttons { display: flex; flex-direction: row; @@ -2023,12 +2081,12 @@ a[data-action="applySkillImprovements"] { &.custom-skills-edit-mode .custom-skills-edit-toggle { background-color: rgba(0, 0, 0, 0.45); - outline: 1px solid #000; + outline: 1px solid var(--dg-ink); } &.custom-skills-edit-mode .program-style .custom-skills-edit-toggle { - background-color: #000; - color: #fff; + background-color: var(--dg-surface-dark); + color: var(--dg-on-dark); } .typed-skill-row { @@ -2200,7 +2258,7 @@ a[data-action="applySkillImprovements"] { } .adaptation-block { - border: 1px solid black; + border: 1px solid var(--dg-ink); padding: 3px; } @@ -2280,7 +2338,7 @@ a[data-action="applySkillImprovements"] { border-radius: 50%; background: var(--color-border-dark, #444); - color: #fff; + color: var(--dg-on-dark); font-size: 12px; @@ -2402,134 +2460,6 @@ a[data-action="applySkillImprovements"] { white-space: nowrap; } -.stimulants-dialog { - display: grid; - grid-template-columns: 10rem 1fr; - gap: 12px; - align-items: center; -} - -.stimulants-dialog .stimulants-option { - display: contents; -} - -.stimulants-dialog .stimulants-choice-btn { - width: 100%; - box-sizing: border-box; - padding: 4px 8px; - border-radius: 3px; - font-size: 0.8rem; - font-weight: bold; - background: var(--color-control-bg, rgba(255, 255, 255, 0.14)); - color: var(--color-text-light-primary, #eee); - border: 1px solid var(--color-border-light-2, rgba(255, 255, 255, 0.35)); - cursor: pointer; - white-space: nowrap; - text-align: center; -} - -.stimulants-dialog .stimulants-choice-btn:hover { - background: var(--color-control-bg-hover, rgba(255, 255, 255, 0.22)); - border-color: var(--color-border-light-highlight, rgba(255, 255, 255, 0.5)); -} - -.stimulants-dialog .stimulants-explanation { - margin: 0; - font-size: var(--font-size-12); - color: var(--color-text-light-secondary, rgba(255, 255, 255, 0.75)); - line-height: 1.35; -} - -.stimulants-dialog-app .form-footer { - display: none; -} - -.modify-roll-dialog-app .form-footer { - display: none; -} - -.modify-roll-dialog .form-group { - margin-bottom: 0.5rem; -} - -.modify-roll-dialog .modify-roll-readonly { - flex: 1; - font-weight: bold; -} - -.modify-roll-dialog .modify-roll-section { - margin-top: 1rem; - padding-top: 0.75rem; - border-top: 1px solid var(--color-border-light-2, rgba(255, 255, 255, 0.2)); -} - -.modify-roll-dialog .modify-roll-section h3 { - margin: 0 0 0.35rem; - font-size: var(--font-size-14, 0.875rem); - font-weight: bold; -} - -.modify-roll-dialog .modify-roll-hint { - margin: 0 0 0.65rem; - font-size: var(--font-size-12); - color: var(--color-text-light-secondary, rgba(255, 255, 255, 0.75)); - line-height: 1.35; -} - -.modify-roll-dialog .modify-roll-quick-grid { - display: grid; - grid-template-columns: 5rem 1fr; - gap: 10px 12px; - align-items: center; -} - -.modify-roll-dialog .modify-roll-quick-option { - display: contents; -} - -.modify-roll-dialog .modify-roll-quick-btn, -.modify-roll-dialog .modify-roll-submit-btn { - box-sizing: border-box; - padding: 4px 8px; - border-radius: 3px; - font-size: 0.8rem; - font-weight: bold; - background: var(--color-control-bg, rgba(255, 255, 255, 0.14)); - color: var(--color-text-light-primary, #eee); - border: 1px solid var(--color-border-light-2, rgba(255, 255, 255, 0.35)); - cursor: pointer; - white-space: nowrap; - text-align: center; -} - -.modify-roll-dialog .modify-roll-quick-btn:hover, -.modify-roll-dialog .modify-roll-submit-btn:hover { - background: var(--color-control-bg-hover, rgba(255, 255, 255, 0.22)); - border-color: var(--color-border-light-highlight, rgba(255, 255, 255, 0.5)); -} - -.modify-roll-dialog .modify-roll-quick-explanation { - margin: 0; - font-size: var(--font-size-12); - color: var(--color-text-light-secondary, rgba(255, 255, 255, 0.75)); - line-height: 1.35; -} - -.modify-roll-dialog .modify-roll-custom-row { - display: flex; - gap: 0.5rem; - align-items: center; -} - -.modify-roll-dialog .modify-roll-custom-row input[type="number"] { - flex: 0 0 5rem; - text-align: center; -} - -.modify-roll-dialog .modify-roll-submit-btn { - flex: 0 0 auto; - min-width: 5rem; -} .deltagreen .effects-list .effect-controls { display: flex; @@ -2640,454 +2570,6 @@ a[data-action="applySkillImprovements"] { text-align: right; } -.add-profession-skill-dialog-app .add-profession-skill-dialog .profession-typed-label-group.hidden, -.deltagreen .add-profession-skill-dialog .profession-typed-label-group.hidden { - display: none; -} - -.add-profession-skill-dialog-app .add-profession-skill-dialog .profession-typed-label-group, -.add-profession-skill-dialog-app .standard-form .add-profession-skill-dialog .profession-typed-label-group, -.deltagreen .add-profession-skill-dialog .profession-typed-label-group, -.standard-form .add-profession-skill-dialog .profession-typed-label-group { - flex-direction: column; - align-items: stretch; - gap: 0.5rem; -} - -.add-profession-skill-dialog-app .add-profession-skill-dialog .profession-typed-label-group > *, -.add-profession-skill-dialog-app .standard-form .add-profession-skill-dialog .profession-typed-label-group > *, -.deltagreen .add-profession-skill-dialog .profession-typed-label-group > *, -.standard-form .add-profession-skill-dialog .profession-typed-label-group > * { - flex: 0 0 auto; - width: 100%; - max-width: 100%; -} - -.add-profession-skill-dialog-app .add-profession-skill-dialog .profession-typed-name-field, -.add-profession-skill-dialog-app .add-profession-skill-dialog .profession-choose-one-field, -.add-profession-skill-dialog-app .standard-form .add-profession-skill-dialog .profession-typed-name-field, -.add-profession-skill-dialog-app .standard-form .add-profession-skill-dialog .profession-choose-one-field, -.deltagreen .add-profession-skill-dialog .profession-typed-name-field, -.deltagreen .add-profession-skill-dialog .profession-choose-one-field, -.standard-form .add-profession-skill-dialog .profession-typed-name-field, -.standard-form .add-profession-skill-dialog .profession-choose-one-field { - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 0.2rem; -} - -.add-profession-skill-dialog-app .add-profession-skill-dialog .profession-typed-name-field > label, -.add-profession-skill-dialog-app .standard-form .add-profession-skill-dialog .profession-typed-name-field > label, -.deltagreen .add-profession-skill-dialog .profession-typed-name-field > label, -.standard-form .add-profession-skill-dialog .profession-typed-name-field > label { - flex: none; - width: 100%; - line-height: 1.3; -} - -.deltagreen .add-profession-skill-dialog .profession-typed-name-field input { - width: 100%; -} - -.deltagreen .add-profession-skill-dialog .profession-choose-one-field { - padding-top: 0.25rem; -} - -/* Add Profession dialog */ -.add-profession-dialog-app h3 { - font-size: var(--font-size-14); - font-weight: bold; - margin: 0.5rem 0 0.35rem; - line-height: 1.2; -} - -.add-profession-skill-dialog-app .add-profession-skill-dialog .profession-choose-one-checkbox, -.deltagreen .add-profession-skill-dialog .profession-choose-one-checkbox { - display: flex; - align-items: center; - gap: 0.35rem; - margin: 0; -} - -.add-profession-skill-dialog-app .add-profession-skill-dialog .profession-choose-one-hint, -.deltagreen .add-profession-skill-dialog .profession-choose-one-hint { - margin: 0.25rem 0 0; -} - -.add-profession-dialog-app .add-profession-choose-one-section { - margin-bottom: 0.75rem; -} - -.add-profession-dialog-app .add-profession-choose-one-list { - display: flex; - flex-direction: column; - gap: 0.35rem; -} - -.add-profession-dialog-app .add-profession-choose-one-row { - display: flex; - flex-direction: column; - gap: 0.2rem; -} - -.add-profession-dialog-app .add-profession-choose-one-row input { - width: 100%; -} - -.add-profession-dialog-app .add-profession-option-row .option-choose-one-label { - width: 100%; -} - -.add-profession-dialog-app .add-profession-skills-section { - margin-bottom: 0.75rem; -} - -.add-profession-dialog-app .add-profession-skills-columns-header { - display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 0.75rem; - margin-bottom: 0.25rem; - font-size: var(--font-size-12); - font-weight: bold; -} - -.add-profession-dialog-app .add-profession-skill-col-header { - display: grid; - grid-template-columns: 1fr auto; - gap: 0.5rem; - padding: 0 0.15rem; -} - -.add-profession-dialog-app .add-profession-skills-grid { - display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 0.2rem 0.75rem; - max-height: 16rem; - overflow-y: auto; -} - -.add-profession-dialog-app .add-profession-skill-entry { - display: grid; - grid-template-columns: 1fr auto; - gap: 0.25rem 0.5rem; - align-items: center; - min-width: 0; -} - -.add-profession-dialog-app .add-profession-skill-label { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.add-profession-dialog-app .add-profession-skill-value { - text-align: right; - min-width: 2.5rem; -} - -.add-profession-dialog-app .add-profession-skill-entry.is-modified .add-profession-skill-label, -.add-profession-dialog-app .add-profession-skill-entry.is-modified .add-profession-skill-value { - color: var(--color-text-success, #2d8a3e); -} - -.add-profession-dialog-app .add-profession-options-section { - margin-bottom: 0.75rem; -} - -.add-profession-dialog-app .add-profession-option-list { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 0.35rem 0.75rem; - margin: 0 0 0.75rem; - padding: 0; -} - -.add-profession-dialog-app .add-profession-option-row { - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 0.25rem; - min-width: 0; -} - -.add-profession-dialog-app .add-profession-option-row > label { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 0.35rem; - width: 100%; - min-width: 0; -} - -.add-profession-dialog-app .add-profession-option-row .option-skill-label { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - min-width: 0; - flex: 1 1 auto; -} - -.add-profession-dialog-app .add-profession-option-row .option-typed-label { - width: 100%; -} - -.add-profession-dialog-app .add-profession-bonus-grid { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 0.5rem 0.75rem; - margin-bottom: 0.75rem; -} - -.add-profession-dialog-app .add-profession-bonus-slot { - display: flex; - flex-direction: row; - align-items: center; - gap: 0.35rem; - min-width: 0; -} - -.add-profession-dialog-app .add-profession-bonus-slot select { - flex: 0 1 11rem; - width: 11rem; - max-width: 42%; - min-width: 7rem; -} - -.add-profession-dialog-app .add-profession-bonus-slot .bonus-typed-label { - flex: 1 1 0; - min-width: 5rem; - width: auto; -} - -.add-profession-dialog-app .add-profession-bonus-slot .bonus-typed-label.hidden { - display: none; -} - -.add-profession-dialog-app .cap-warning, -.add-profession-dialog-app .validation-error { - color: var(--color-text-error, #c00); - font-weight: bold; - margin: 0.25rem 0; -} - -.add-profession-dialog-app .add-profession-bonds-grid { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 0.75rem; -} - -.add-profession-dialog-app .add-profession-bond-row { - display: grid; - grid-template-columns: auto 1fr 1fr 3.5rem; - gap: 0.35rem 0.5rem; - align-items: end; - min-width: 0; -} - -.add-profession-dialog-app .add-profession-bond-row .bond-row-label { - font-weight: bold; - white-space: nowrap; - padding-bottom: 0.15rem; -} - -.add-profession-dialog-app .add-profession-bond-field { - display: flex; - flex-direction: column; - gap: 0.15rem; - min-width: 0; - font-size: var(--font-size-12); -} - -.add-profession-dialog-app .add-profession-bond-field-label { - white-space: nowrap; -} - -.add-profession-dialog-app .add-profession-bond-field input { - width: 100%; - min-width: 0; -} - -.add-profession-dialog-app .add-profession-bond-field-score input { - text-align: center; -} - -.add-profession-dialog-app .form-footer { - display: flex; - flex-direction: row; - flex-wrap: nowrap; - align-items: stretch; - gap: 0.5rem; - margin-top: 0.75rem; -} - -.add-profession-dialog-app .form-footer > button { - flex: 1 1 0; - min-width: 0; -} - -.deltagreen .profession-assigned-row { - margin-bottom: 0.75rem; -} - -.pick-statistics-dialog-app .pick-statistics-intro { - margin: 0 0 0.5rem; - line-height: 1.4; -} - -.pick-statistics-dialog-app .form-footer { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 0.5rem; - margin-top: 0.75rem; -} - -.pick-statistics-dialog-app .form-footer > button[data-action="skip"] { - grid-column: 1 / -1; -} - -.roll-stats-dialog-app .roll-stats-layout { - display: grid; - grid-template-columns: 1fr auto; - gap: 1rem; - align-items: start; -} - -.roll-stats-dialog-app .roll-stats-pool-section { - min-width: 5.5rem; -} - -.roll-stats-dialog-app .roll-stats-pool-hint { - margin: 0.75rem 0 0; - font-size: var(--font-size-12, 0.75rem); - text-align: center; - color: var(--color-text-dark-secondary, rgba(255, 255, 255, 0.65)); -} - -.roll-stats-dialog-app .roll-stats-pool { - display: flex; - flex-direction: column; - align-items: center; - gap: 0.5rem; - width: 3rem; - min-height: 3rem; - padding: 0.5rem; - border: 1px dashed var(--color-border-light-2, rgba(255, 255, 255, 0.25)); - border-radius: 4px; -} - -.roll-stats-dialog-app .roll-stats-pool.drag-over { - border-color: var(--color-border-highlight, #ff6400); - background: rgba(255, 100, 0, 0.08); -} - -.roll-stats-dialog-app .roll-stat-token { - display: inline-flex; - align-items: center; - justify-content: center; - width: 2.5rem; - height: 2.5rem; - padding: 0; - font-weight: bold; - font-size: var(--font-size-16, 1rem); - border: 1px solid var(--color-border-light-2, rgba(255, 255, 255, 0.25)); - border-radius: 4px; - background: var(--color-bg-option, rgba(0, 0, 0, 0.15)); - cursor: grab; - user-select: none; - box-sizing: border-box; -} - -.roll-stats-dialog-app .roll-stat-token.is-dragging { - opacity: 0.5; - cursor: grabbing; -} - -.roll-stats-dialog-app .roll-stats-assignment-section { - display: flex; - flex-direction: column; - gap: 0.35rem; -} - -.roll-stats-dialog-app .roll-stat-row { - display: grid; - grid-template-columns: 3rem auto; - align-items: center; - gap: 0.75rem; -} - -.roll-stats-dialog-app .roll-stat-label { - font-weight: bold; -} - -.roll-stats-dialog-app .roll-stat-slot { - display: flex; - align-items: center; - justify-content: center; - width: 2.5rem; - height: 2.5rem; - flex: 0 0 auto; - padding: 0; - border: 1px dashed var(--color-border-light-2, rgba(255, 255, 255, 0.25)); - border-radius: 4px; - box-sizing: border-box; -} - -.roll-stats-dialog-app .roll-stat-slot.drag-over { - border-color: var(--color-border-highlight, #ff6400); - background: rgba(255, 100, 0, 0.08); -} - -.roll-stats-dialog-app .roll-stat-slot .roll-stat-token { - cursor: grab; - width: 100%; - height: 100%; -} - -.roll-stats-dialog-app .form-footer { - display: flex; - flex-direction: row; - gap: 0.5rem; - margin-top: 0.75rem; -} - -.assign-stats-dialog-app .assign-stats-remaining { - margin: 0 0 0.75rem; - font-weight: bold; -} - -.assign-stats-dialog-app .assign-stats-remaining.is-invalid { - color: var(--color-text-warning, #ff9200); -} - -.assign-stats-dialog-app .assign-stats-rows { - display: flex; - flex-direction: column; - gap: 0.35rem; -} - -.assign-stats-dialog-app .assign-stats-row { - display: grid; - grid-template-columns: 3rem 1fr; - align-items: center; - gap: 0.75rem; -} - -.assign-stats-dialog-app .assign-stats-label { - font-weight: bold; -} - -.assign-stats-dialog-app .assign-stats-row input { - width: 100%; - text-align: center; -} - -.assign-stats-dialog-app .form-footer { - display: flex; - flex-direction: row; - gap: 0.5rem; - margin-top: 0.75rem; -} .roll-stats-chat-table { width: auto; @@ -3095,7 +2577,12 @@ a[data-action="applySkillImprovements"] { border-collapse: collapse; } -.roll-stats-chat-table td { +/* Override Foundry row striping; alternate per cell in reading order (checkerboard in 3 cols) */ +.dg-chat-card__body .roll-stats-chat-table tbody tr { + background-color: transparent; +} + +.dg-chat-card__body .roll-stats-chat-table td { padding: 0.35rem 1rem; text-align: center; font-weight: bold; @@ -3103,6 +2590,14 @@ a[data-action="applySkillImprovements"] { border: 1px solid var(--color-border-light-2, rgba(255, 255, 255, 0.2)); } -.roll-stats-chat-table .roll-stats-chat-value { +.dg-chat-card__body .roll-stats-chat-table td.roll-stats-chat-value--a { + background-color: var(--table-row-color-odd, transparent); +} + +.dg-chat-card__body .roll-stats-chat-table td.roll-stats-chat-value--b { + background-color: var(--table-row-color-even, rgba(255, 255, 255, 0.1)); +} + +.dg-chat-card__body .roll-stats-chat-table .roll-stats-chat-value { min-width: 2rem; } diff --git a/css/dg-dialog.css b/css/dg-dialog.css new file mode 100644 index 0000000..e32f930 --- /dev/null +++ b/css/dg-dialog.css @@ -0,0 +1,846 @@ +/* Delta Green — shared DialogV2 styling (.dg-dialog-app) */ + +.dg-dialog-app section.window-content { + scrollbar-color: #215112 #111 !important; + padding: 0.5rem; + color: var(--dg-ink); +} + +/* Foundry sets explicit light-theme tokens on headings/labels; inherit parchment body color. */ +.dg-dialog-app section.window-content :is(h1, h2, h3, h4, h5, h6), +.dg-dialog-app .standard-form .form-group > label, +.dg-dialog-app .form-group > label, +.dg-dialog-app .dg-dialog__field-label, +.dg-dialog-app .dg-dialog .dg-dialog__heading, +.dg-dialog-app .dg-dialog__intro { + color: inherit; +} + +.dg-dialog-app section.window-content input[type="checkbox"]:checked::before { + color: var(--dg-ink); +} + +.dg-dialog-app section.window-content input[type="text"], +.dg-dialog-app section.window-content input[type="number"], +.dg-dialog-app section.window-content input[type="password"], +.dg-dialog-app section.window-content select { + height: unset; + line-height: 1.5rem; +} + +.dg-dialog-app .dg-dialog input[type="text"], +.dg-dialog-app .dg-dialog input[type="number"], +.dg-dialog-app .dg-dialog input[type="password"], +.dg-dialog-app .dg-dialog select { + background-color: rgba(0, 0, 0, 0.05); + border: 1px solid rgb(122, 121, 113); + border-radius: 3px; + color: inherit; +} + +.dg-dialog-app .dg-dialog__field-label, +.dg-dialog-app .dg-dialog .dg-dialog__heading { + font-weight: bold; +} + +.dg-dialog-app .dg-dialog__intro { + margin: 0 0 0.5rem; + line-height: 1.4; +} + +.dg-dialog-app .dg-dialog__messages { + margin-top: 0.5rem; +} + +.dg-dialog-app .dg-dialog__message--error, +.dg-dialog-app .dg-dialog .validation-error { + color: var(--color-text-error, #c00); + font-weight: bold; + margin: 0.25rem 0; +} + +.dg-dialog-app .dg-dialog__message--warning, +.dg-dialog-app .dg-dialog .cap-warning { + color: var(--color-text-warning, #c60); + font-weight: bold; + margin: 0.25rem 0; +} + +.dg-dialog-app .form-footer { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + align-items: stretch; + gap: 0.5rem; + margin-top: 0.75rem; +} + +.dg-dialog-app .form-footer > button { + flex: 1 1 0; + min-width: 0; + background-color: var(--dg-surface-dark); + color: var(--dg-on-dark); + border: 1px solid var(--dg-surface-dark); + font-weight: bold; +} + +.dg-dialog-app .form-footer > button:hover:not(:disabled) { + background-color: var(--dg-surface-dark-hover); + color: var(--dg-on-dark); + border-color: var(--dg-surface-dark-hover); +} + +.dg-dialog-app .form-footer > button:disabled { + background-color: #555; + color: #f0f0f0; + border-color: #555; + opacity: 1; + cursor: not-allowed; +} + +.dg-dialog-app--pick-statistics .form-footer { + display: grid; + grid-template-columns: 1fr 1fr; +} + +.dg-dialog-app--pick-statistics .form-footer > button[data-action="skip"] { + grid-column: 1 / -1; +} + +.dg-dialog-app--stimulants .form-footer, +.dg-dialog-app--modify-roll .form-footer { + display: none; +} + +/* Assign stats */ +.dg-dialog--assign-stats .assign-stats-remaining { + margin: 0 0 0.75rem; + font-weight: bold; +} + +.dg-dialog--assign-stats .assign-stats-remaining.is-invalid { + color: var(--color-text-warning, #ff9200); +} + +.dg-dialog--assign-stats .assign-stats-rows { + display: flex; + flex-direction: column; + gap: 0.35rem; +} + +.dg-dialog--assign-stats .assign-stats-row { + display: grid; + grid-template-columns: 3rem 1fr; + align-items: center; + gap: 0.75rem; +} + +.dg-dialog--assign-stats .assign-stats-label { + font-weight: bold; +} + +.dg-dialog--assign-stats .assign-stats-row input { + width: 100%; + text-align: center; +} + +/* Roll stats */ +.dg-dialog--roll-stats .roll-stats-layout { + display: grid; + grid-template-columns: 1fr auto; + gap: 1rem; + align-items: start; +} + +.dg-dialog--roll-stats .roll-stats-pool-section { + min-width: 5.5rem; +} + +.dg-dialog--roll-stats .roll-stats-pool-hint { + margin: 0.75rem 0 0; + font-size: var(--font-size-12, 0.75rem); + text-align: center; + color: var(--color-text-dark-secondary, rgba(0, 0, 0, 0.65)); +} + +.dg-dialog--roll-stats .roll-stats-pool { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; + width: 3rem; + min-height: 3rem; + padding: 0.5rem; + border: 1px dashed rgb(122, 121, 113); + border-radius: 4px; +} + +.dg-dialog--roll-stats .roll-stats-pool.drag-over { + border-color: var(--color-border-highlight, #ff6400); + background: rgba(255, 100, 0, 0.08); +} + +.dg-dialog--roll-stats .roll-stat-token { + display: inline-flex; + align-items: center; + justify-content: center; + width: 2.5rem; + height: 2.5rem; + padding: 0; + font-weight: bold; + font-size: var(--font-size-16, 1rem); + border: 1px solid rgb(122, 121, 113); + border-radius: 4px; + background: rgba(0, 0, 0, 0.05); + cursor: grab; + user-select: none; + box-sizing: border-box; +} + +.dg-dialog--roll-stats .roll-stat-token.is-dragging { + opacity: 0.5; + cursor: grabbing; +} + +.dg-dialog--roll-stats .roll-stats-assignment-section { + display: flex; + flex-direction: column; + gap: 0.35rem; +} + +.dg-dialog--roll-stats .roll-stat-row { + display: grid; + grid-template-columns: 3rem auto; + align-items: center; + gap: 0.75rem; +} + +.dg-dialog--roll-stats .roll-stat-label { + font-weight: bold; +} + +.dg-dialog--roll-stats .roll-stat-slot { + display: flex; + align-items: center; + justify-content: center; + width: 2.5rem; + height: 2.5rem; + flex: 0 0 auto; + padding: 0; + border: 1px dashed rgb(122, 121, 113); + border-radius: 4px; + box-sizing: border-box; +} + +.dg-dialog--roll-stats .roll-stat-slot.drag-over { + border-color: var(--color-border-highlight, #ff6400); + background: rgba(255, 100, 0, 0.08); +} + +.dg-dialog--roll-stats .roll-stat-slot .roll-stat-token { + cursor: grab; + width: 100%; + height: 100%; +} + +/* Add profession skill (item editor) */ +.dg-dialog-app--add-profession-skill .profession-typed-label-group.hidden, +.dg-dialog--add-profession-skill .profession-typed-label-group.hidden { + display: none; +} + +.dg-dialog-app--add-profession-skill .profession-typed-label-group, +.dg-dialog--add-profession-skill .profession-typed-label-group { + flex-direction: column; + align-items: stretch; + gap: 0.5rem; +} + +.dg-dialog-app--add-profession-skill .profession-typed-label-group > *, +.dg-dialog--add-profession-skill .profession-typed-label-group > * { + flex: 0 0 auto; + width: 100%; + max-width: 100%; +} + +.dg-dialog-app--add-profession-skill .profession-typed-name-field, +.dg-dialog-app--add-profession-skill .profession-choose-one-field, +.dg-dialog--add-profession-skill .profession-typed-name-field, +.dg-dialog--add-profession-skill .profession-choose-one-field { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 0.2rem; +} + +.dg-dialog-app--add-profession-skill .profession-typed-name-field > label, +.dg-dialog--add-profession-skill .profession-typed-name-field > label { + flex: none; + width: 100%; + line-height: 1.3; +} + +.dg-dialog--add-profession-skill .profession-typed-name-field input { + width: 100%; +} + +.dg-dialog--add-profession-skill .profession-choose-one-field { + padding-top: 0.25rem; +} + +.dg-dialog--add-profession-skill .profession-choose-one-checkbox { + display: flex; + align-items: center; + gap: 0.35rem; + margin: 0; +} + +.dg-dialog--add-profession-skill .profession-choose-one-hint { + margin: 0.25rem 0 0; +} + +/* Modify roll — keep layout in deltagreen.css under .dg-dialog--modify-roll */ + +/* Stimulants */ +.dg-dialog--stimulants .stimulants-dialog { + display: grid; + grid-template-columns: 10rem 1fr; + gap: 12px; + align-items: center; +} + +.dg-dialog--stimulants .stimulants-option { + display: contents; +} + +.dg-dialog--stimulants .stimulants-choice-btn { + width: 100%; + box-sizing: border-box; + padding: 4px 8px; + border-radius: 3px; + font-size: 0.8rem; + font-weight: bold; + background-color: var(--dg-surface-dark); + color: var(--dg-on-dark); + border: 1px solid var(--dg-surface-dark); + cursor: pointer; + white-space: nowrap; + text-align: center; +} + +.dg-dialog--stimulants .stimulants-choice-btn:hover { + background-color: var(--dg-surface-dark-hover); + color: var(--dg-on-dark); + border-color: var(--dg-surface-dark-hover); +} + +.dg-dialog--stimulants .stimulants-explanation { + margin: 0; + font-size: var(--font-size-12); + line-height: 1.35; +} + +/* Modify percentile roll */ +.dg-dialog--modify-roll .form-group { + margin-bottom: 0.5rem; +} + +.dg-dialog--modify-roll .modify-roll-readonly { + flex: 1; + font-weight: bold; +} + +.dg-dialog--modify-roll .modify-roll-section { + margin-top: 1rem; + padding-top: 0.75rem; + border-top: 1px solid rgb(122, 121, 113); +} + +.dg-dialog--modify-roll .modify-roll-section h3 { + margin: 0 0 0.35rem; + font-size: var(--font-size-14, 0.875rem); + font-weight: bold; +} + +.dg-dialog--modify-roll .modify-roll-hint { + margin: 0 0 0.65rem; + font-size: var(--font-size-12); + line-height: 1.35; +} + +.dg-dialog--modify-roll .modify-roll-quick-grid { + display: grid; + grid-template-columns: 5rem 1fr; + gap: 10px 12px; + align-items: center; +} + +.dg-dialog--modify-roll .modify-roll-quick-option { + display: contents; +} + +.dg-dialog--modify-roll .modify-roll-quick-btn, +.dg-dialog--modify-roll .modify-roll-submit-btn { + box-sizing: border-box; + padding: 4px 8px; + border-radius: 3px; + font-size: 0.8rem; + font-weight: bold; + background-color: var(--dg-surface-dark); + color: var(--dg-on-dark); + border: 1px solid var(--dg-surface-dark); + cursor: pointer; + white-space: nowrap; + text-align: center; +} + +.dg-dialog--modify-roll .modify-roll-quick-btn:hover, +.dg-dialog--modify-roll .modify-roll-submit-btn:hover { + background-color: var(--dg-surface-dark-hover); + color: var(--dg-on-dark); + border-color: var(--dg-surface-dark-hover); +} + +.dg-dialog--modify-roll .modify-roll-quick-explanation { + margin: 0; + font-size: var(--font-size-12); + line-height: 1.35; +} + +.dg-dialog--modify-roll .modify-roll-custom-row { + display: flex; + gap: 0.5rem; + align-items: center; +} + +.dg-dialog--modify-roll .modify-roll-custom-row input[type="number"] { + flex: 0 0 5rem; + text-align: center; +} + +.dg-dialog--modify-roll .modify-roll-submit-btn { + flex: 0 0 auto; + min-width: 5rem; +} + +/* Character creation — flex chain keeps footer visible; skill totals scroll */ +.dg-dialog-app--character-creation.application { + display: flex; + flex-direction: column; +} + +.dg-dialog-app--character-creation.application > .window-content { + display: flex; + flex-direction: column; + flex: 1 1 auto; + min-height: 0; + overflow: hidden; +} + +.dg-dialog-app--character-creation .dialog-form { + display: flex; + flex-direction: column; + flex: 1 1 auto; + min-height: 0; + max-height: 100%; + overflow: hidden; +} + +.dg-dialog-app--character-creation .dialog-form > .dialog-content { + display: flex; + flex-direction: column; + flex: 1 1 auto; + min-height: 0; + overflow: hidden; +} + +.dg-dialog-app--character-creation .dialog-form > .form-footer { + flex: 0 0 auto; +} + +.dg-dialog-app--character-creation section.window-content { + min-height: 38rem; +} + +.dg-dialog--character-creation.dg-dialog { + display: flex; + flex-direction: column; + flex: 1 1 auto; + min-height: 0; + overflow: hidden; +} + +.dg-dialog--character-creation .dg-dialog__layout--two-column { + display: flex; + flex-direction: row; + align-items: stretch; + gap: 40px; + flex: 1 1 auto; + min-height: 0; + min-width: 0; + overflow: hidden; +} + +.dg-dialog--character-creation .dg-dialog__column--left, +.dg-dialog--character-creation .dg-dialog__column--right { + display: flex; + flex-direction: column; + flex: 1 1 0; + min-width: 0; + min-height: 0; + overflow: hidden; +} + +.dg-dialog--character-creation .dg-dialog__column--right > .dg-dialog__messages { + flex: 0 0 auto; + margin-top: 0.5rem; +} + +/* Character creation tabs — same equal-width overlap as agent sheet */ +.dg-dialog--character-creation nav.character-creation-tabs { + display: flex; + flex-flow: row nowrap; + justify-content: flex-start; + align-items: center; + gap: 0; + width: 100%; + min-width: 0; + box-sizing: border-box; + margin: 0; + padding: 0; + border-top: none; + border-bottom: transparent; + height: 1.5rem; + line-height: 1.5rem; + position: relative; + z-index: 2; + overflow-x: auto; + overflow-y: hidden; +} + +.dg-dialog-app--character-creation section.window-content.program-style { + --dg-agent-tab-inactive-bg: rgb(186, 184, 181); + --dg-agent-tab-active-bg: var(--dg-surface-dark); + --dg-agent-tab-active-color: var(--dg-on-dark); +} + +.dg-dialog-app--character-creation section.window-content.cowboy-style, +.dg-dialog-app--character-creation section.window-content.outlaw-style { + --dg-agent-tab-inactive-bg: rgb(210, 190, 150); + --dg-agent-tab-active-bg: rgb(194, 168, 110); + --dg-agent-tab-active-color: var(--dg-ink); +} + +.dg-dialog--character-creation nav.character-creation-tabs [data-action="tab"] { + flex: 1 1 0; + min-width: 0; + position: relative; + text-align: center; + margin-left: -14px; + padding-inline: 3px; + font-size: 11px; + overflow: visible; + box-shadow: + 0 1px 2px rgba(0, 0, 0, 0.16), + -1px 0 2px rgba(0, 0, 0, 0.1), + 1px 0 2px rgba(0, 0, 0, 0.1); +} + +.dg-dialog--character-creation nav.character-creation-tabs [data-action="tab"]:first-child { + margin-left: 0; +} + +.dg-dialog--character-creation nav.character-creation-tabs [data-action="tab"]:nth-child(1) { + z-index: 1; +} + +.dg-dialog--character-creation nav.character-creation-tabs [data-action="tab"]:nth-child(2) { + z-index: 2; +} + +.dg-dialog--character-creation nav.character-creation-tabs [data-action="tab"]:nth-child(3) { + z-index: 3; +} + +.dg-dialog-app--character-creation section.window-content .character-creation-tabs [data-action="tab"]:not(.active) { + background: var(--dg-agent-tab-inactive-bg); +} + +.dg-dialog-app--character-creation section.window-content .character-creation-tabs [data-action="tab"].active { + z-index: 10; + margin-bottom: -2px; + padding-bottom: 2px; + background: var(--dg-agent-tab-active-bg); + color: var(--dg-agent-tab-active-color); + box-shadow: + 0 2px 6px rgba(0, 0, 0, 0.22), + -2px 0 5px rgba(0, 0, 0, 0.14), + 2px 0 5px rgba(0, 0, 0, 0.14); +} + +.dg-dialog--character-creation .dg-dialog__tab-panels > .tab.active { + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +.dg-dialog--character-creation .dg-dialog__tab-panels { + flex: 1; + min-height: 0; +} + +.dg-dialog--character-creation .dg-dialog__tab-panels > .tab { + display: none; + overflow-y: auto; + max-height: 28rem; +} + +.dg-dialog--character-creation .dg-dialog__tab-panels > .tab.active { + display: block; +} + +.dg-dialog--character-creation .character-creation-skills-section { + display: flex; + flex-direction: column; + flex: 1 1 auto; + min-height: 0; + overflow: hidden; + margin-bottom: 0.5rem; +} + +.dg-dialog--character-creation .character-creation-skill-totals-header { + margin-bottom: 0.5rem; + flex: 0 0 auto; +} + +.dg-dialog--character-creation .character-creation-skill-totals-header .dg-dialog__intro { + margin: 0; + line-height: 1.4; +} + +.dg-dialog--character-creation .character-creation-section-heading { + font-size: var(--font-size-16); + margin: 0 0 0.5rem; + line-height: 1.35; +} + +.dg-dialog--character-creation .character-creation-skills-columns-header { + flex: 0 0 auto; + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 0.75rem; + margin-bottom: 0.25rem; + font-size: var(--font-size-12); + font-weight: bold; +} + +.dg-dialog--character-creation .character-creation-skill-col-header { + display: grid; + grid-template-columns: 1fr auto; + gap: 0.5rem; + padding: 0 0.15rem; +} + +.dg-dialog--character-creation .character-creation-skills-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 0.2rem 0.75rem; + flex: 1 1 auto; + min-height: 0; + overflow-y: auto; + scrollbar-color: #215112 #111 !important; +} + +.dg-dialog--character-creation .dg-dialog__skill-entry { + display: grid; + grid-template-columns: 1fr auto; + gap: 0.25rem 0.5rem; + align-items: center; + min-width: 0; +} + +.dg-dialog--character-creation .dg-dialog__skill-label { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.dg-dialog--character-creation .dg-dialog__skill-value { + text-align: right; + min-width: 2.5rem; +} + +.dg-dialog--character-creation .dg-dialog__skill-entry.is-modified .dg-dialog__skill-label, +.dg-dialog--character-creation .dg-dialog__skill-entry.is-modified .dg-dialog__skill-value { + color: var(--color-text-success, #2d8a3e); +} + +.dg-dialog--character-creation .character-creation-choose-one-section { + flex: 0 0 auto; + margin: 0.5rem 0; +} + + +.dg-dialog--character-creation .character-creation-choose-one-list { + display: flex; + flex-direction: column; + gap: 0.35rem; +} + +.dg-dialog--character-creation .character-creation-choose-one-row { + display: flex; + flex-direction: column; + gap: 0.2rem; +} + +.dg-dialog--character-creation .character-creation-choose-one-row input { + width: 100%; +} + +.dg-dialog--character-creation .character-creation-option-list { + display: flex; + flex-direction: row; + align-items: flex-start; + gap: 0.75rem; + margin: 0; + padding: 0; +} + +.dg-dialog--character-creation .character-creation-option-column { + flex: 1 1 0; + min-width: 0; + display: flex; + flex-direction: column; + gap: 0.35rem; +} + +.dg-dialog--character-creation .character-creation-option-row { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 0.25rem; + min-width: 0; +} + +.dg-dialog--character-creation .character-creation-option-row > label { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.35rem; + width: 100%; + min-width: 0; +} + +.dg-dialog--character-creation .character-creation-option-row .option-skill-label { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + min-width: 0; + flex: 1 1 auto; +} + +.dg-dialog--character-creation .character-creation-option-row .option-choose-one-label { + width: 100%; +} + +.dg-dialog--character-creation .character-creation-option-row .option-choose-one-label:disabled { + opacity: 0.65; +} + +.dg-dialog--character-creation .dg-dialog__tab-panels > .tab[data-tab="bonus-skills"].active { + display: flex; + flex-direction: column; + min-height: 28rem; + box-sizing: border-box; +} + +.dg-dialog--character-creation .character-creation-bonus-intro { + flex: 0 0 auto; + margin: 0 0 0.5rem; + line-height: 1.4; +} + +.dg-dialog--character-creation .character-creation-bonus-grid { + flex: 1 1 auto; + display: flex; + flex-direction: column; + justify-content: space-evenly; + min-height: 0; + margin: 0; + padding: 0; +} + +.dg-dialog--character-creation .character-creation-bonus-slot { + display: flex; + flex-direction: row; + align-items: center; + gap: 0.35rem; + min-width: 0; + flex: 0 0 auto; +} + +.dg-dialog--character-creation .character-creation-bonus-slot select { + flex: 0 1 11rem; + min-width: 7rem; +} + +.dg-dialog--character-creation .character-creation-bonus-slot .bonus-typed-label { + flex: 1 1 0; + min-width: 5rem; +} + +.dg-dialog--character-creation .character-creation-bonus-slot .bonus-typed-label.hidden { + display: none; +} + +.dg-dialog--character-creation .dg-dialog__tab-panels > .tab[data-tab="bonds"].active { + display: flex; + flex-direction: column; + min-height: 28rem; + box-sizing: border-box; +} + +.dg-dialog--character-creation .character-creation-bonds-list { + flex: 1 1 auto; + display: flex; + flex-direction: column; + justify-content: space-evenly; + min-height: 0; + margin: 0; + padding: 0; +} + +.dg-dialog--character-creation .character-creation-bonds-intro { + flex: 0 0 auto; + margin: 0 0 0.5rem; + line-height: 1.4; +} + +.dg-dialog--character-creation .character-creation-bond-row { + display: grid; + grid-template-columns: 1fr 1fr 3.5rem; + gap: 0.35rem 0.5rem; + align-items: end; + min-width: 0; + flex: 0 0 auto; +} + +.dg-dialog--character-creation .character-creation-bond-field { + display: flex; + flex-direction: column; + gap: 0.15rem; + min-width: 0; + font-size: var(--font-size-12); +} + +.dg-dialog--character-creation .character-creation-bond-field-label { + white-space: nowrap; +} + +.dg-dialog--character-creation .character-creation-bond-field input { + width: 100%; + min-width: 0; +} + +.dg-dialog--character-creation .character-creation-bond-field-score input { + text-align: center; +} diff --git a/lang/en.json b/lang/en.json index b038f93..37ecb85 100644 --- a/lang/en.json +++ b/lang/en.json @@ -185,8 +185,9 @@ "DG.Physical.StimulantsHardExplanation": "Hard drugs, while illegal, suppress exhaustion for 2D6 hours.", "DG.Physical.Tooltip.SuppressExhaustion": "While checked, exhaustion penalties do not apply to rolls.", "DG.Physical.StimulantsNotExhaustedWarning": "{name} is not exhausted. Taking stimulants would have no effect!", - "DG.Physical.Chat.Exhausted": "{name} is exhausted. They lose {loss} Willpower, bringing them to {current}/{max}.", - "DG.Physical.Chat.Rested": "{name} gets a full night's rest and regains {gain} Willpower, bringing them to {current}/{max}.", + "DG.Physical.Chat.Exhausted": "{name} is exhausted. They lose {loss} Willpower.

New total: {current}/{max}", + "DG.Physical.Chat.Rested": "{name} gets a full night's rest and regains {gain} Willpower.

New total: {current}/{max}", + "DG.Physical.Chat.RestedNoLongerExhausted": "
{name} is no longer exhausted.", "DG.Physical.Chat.StimulantsRegular": "{name} takes regular stimulants, suppressing their exhaustion for {hours} hours. They cannot sleep during this time.", "DG.Physical.Chat.StimulantsHard": "{name} takes hard drugs, suppressing their exhaustion for {hours} hours. They cannot sleep during this time.", "DG.Generic.AddButtonLabel": "Add", @@ -263,6 +264,9 @@ "DG.Tooltip.SkillFailCheckbox": "After failing a test on this skill (if proficiency is at least 1%), check this box. At the end of the session, add 1D4 percentage points to the skill and uncheck the box. Does not apply to the Unnatural or Ritual skill. See Page 29 of the Agent's Handbook.", "DG.Tooltip.DistinguishingFeature": "Distinguishing Feature (fill out if score is 13 or higher, or 8 or lower).", "DG.Tooltip.x5AttributeLabel": "Click to roll a x5 check.", + "DG.Tooltip.RollTarget.Target": "Target: {value}%", + "DG.Tooltip.RollTarget.BaseTarget": "Base Target", + "DG.Tooltip.RollTarget.FinalTarget": "Final Target", "DG.Tooltip.FirstAidAttempted": "Once an agent receives first aid, they cannot benefit from it again until they receive further damage from a different source.", "DG.Tooltip.AdaptedToViolence": "Being adapted to violence means your Agent’s empathy suffers as they come to view violence as nothing but another tool to be wielded. Agent permanently loses 1D6 CHA and the same amount from each of their Bonds.", "DG.Tooltip.AdaptedToHelplessness": "Being adapted to helplessness means your Agent’s personal drive suffers as they pull back from the world around them. As soon as your Agent becomes adapted to helplessness, agent permanently loses 1D6 POW.", @@ -390,20 +394,27 @@ "TYPES.Item.profession": "Profession", "DG.Profession.AlreadyAssigned": "This agent already has a profession. Another cannot be assigned.", "DG.Profession.AssignedHeader": "ASSIGNED PROFESSION ITEM", - "DG.Profession.Dialog.Title": "Pick Skills", + "DG.Profession.Dialog.Title": "Character Creation", + "DG.Profession.Dialog.Tab.SkillOptions": "Skill Options", + "DG.Profession.Dialog.Tab.BonusSkills": "Bonus Skills", + "DG.Profession.Dialog.Tab.Bonds": "Bonds", "DG.Profession.Dialog.Skill": "Skill", "DG.Profession.Dialog.Value": "Value", + "DG.Profession.Dialog.SkillTotalsHeader": "Skill Totals", + "DG.Profession.Dialog.SkillTotalsHint": "Skills in green have been increased by your profession. Hover over a skill to see what it does or consult the Agent's Handbook for the full description.", "DG.Profession.Dialog.SkillsOptionsHeader": "Skills Options ({picks} picks)", - "DG.Profession.Dialog.BonusSkillsHeader": "Bonus Skills", - "DG.Profession.Dialog.BondsHeader": "Bonds", - "DG.Profession.Dialog.Bond": "Bond", + "DG.Profession.Dialog.BonusSkillsIntro": "Select skills from the dropdowns to add 20% to their total. A skill may be selected multiple times. Unnatural cannot be selected, nor can any skill be increased above 80% at character creation.", + "DG.Profession.Dialog.BonusSkillAlreadyAtCap": "{skill} may not be increased further. Please select a different bonus skill.", + "DG.Profession.Dialog.BonusSkillWasteTooHigh": "Bonus increases to {skill} would waste {waste} skill points (at most {maxWaste} may be wasted per skill).", + "DG.Profession.Dialog.BondsIntro": "Bonds are the important relationships in your agent's life, such as family, friends, and other loved ones. They can provide a buffer against SAN loss, at the cost of that relationship. Initial relationship score is equal to CHA.", + "DG.Profession.Dialog.BondFieldLabel": "Bond{number}", "DG.Profession.Dialog.SelectSkill": "Select a skill…", "DG.Profession.Dialog.SkillTypePlaceholder": "Type or specialization", "DG.Profession.Dialog.Submit": "Submit", "DG.Profession.Dialog.CapWarning": "{skill} is being raised to {attempted} but is capped at 80 during agent creation. This is wasting {waste} skill points.", "DG.Profession.Dialog.DuplicateTypedName": "Duplicate {type} ({name}) detected. Please choose unique names.", "DG.Profession.ChooseOnePlaceholder": "choose one", - "DG.Profession.Dialog.ChooseOneNamesHeader": "Name your skills", + "DG.Profession.Dialog.ChooseOneNamesHeader": "Name Skill Specializations", "DG.Profession.Dialog.ChooseOneNameLabel": "{group} — specialization", "DG.ProfessionSetup.PickStatistics.Title": "Pick Statistics", "DG.ProfessionSetup.PickStatistics.Intro": "Choose how to set this agent's statistics before picking skills.", diff --git a/module/actor/actor.js b/module/actor/actor.js index d3e2238..e424442 100644 --- a/module/actor/actor.js +++ b/module/actor/actor.js @@ -1,4 +1,5 @@ import { refreshDerivedAfterActiveEffects } from "../utils/active-effect-derived.js"; +import { prepareAgentStatSanityTooltips } from "../utils/roll-target-tooltip.js"; import { syncExhaustionEffect, updateTouchesExhaustionPhysical, @@ -15,6 +16,7 @@ export default class DeltaGreenActor extends Actor { prepareData() { super.prepareData(); refreshDerivedAfterActiveEffects(this); + if (this.type === "agent") prepareAgentStatSanityTooltips(this); } /** @override */ diff --git a/module/applications/add-profession-dialog.js b/module/applications/add-profession-dialog.js index 7538faf..8f868de 100644 --- a/module/applications/add-profession-dialog.js +++ b/module/applications/add-profession-dialog.js @@ -1,7 +1,12 @@ import { BASE_TEMPLATE_PATH } from "../config.js"; +import { + bindDialogTabs, + getDialogContentRoot, + showDgDialog, +} from "./dg-dialog.js"; import { BONUS_SKILL_COUNT, - buildSkillCatalog, + buildBonusSkillCatalog, buildSortedFixedSkillRows, buildSortedTypedSkillRows, computeSkillValues, @@ -11,10 +16,17 @@ import { prepareProfessionSkillRows, splitProfessionSkillMap, } from "../utils/profession-skills.js"; -import { reorderForColumnSorting } from "../utils/skill-layout.js"; +import { + reorderForColumnSorting, + splitIntoColumns, +} from "../utils/skill-layout.js"; +import { + applySkillTooltipDisplayMode, + buildSkillTooltip, +} from "../utils/skill-tooltip.js"; -const { DialogV2 } = foundry.applications.api; -const ADD_PROFESSION_SKILL_COLUMNS = 4; +const CHARACTER_CREATION_OPTION_COLUMNS = 2; +const ADD_PROFESSION_SKILL_COLUMNS = 2; const { renderTemplate } = foundry.applications.handlebars; /** @@ -29,8 +41,8 @@ export async function showPickSkillsDialog(professionItem, actor) { } /** - * @param {{ sortLabel: string, rowId: string, label: string, value: number, isModified: boolean }[]} rows - * @returns {{ rowId: string, label: string, value: number, isModified: boolean }[]} + * @param {{ sortLabel: string, rowId: string, label: string, value: number, isModified: boolean, tooltip?: string }[]} rows + * @returns {{ rowId: string, label: string, value: number, isModified: boolean, tooltip: string }[]} */ function orderSkillRowsForDisplay(rows) { const sorted = [...rows].sort((a, b) => @@ -41,11 +53,12 @@ function orderSkillRowsForDisplay(rows) { ? reorderForColumnSorting(sorted, ADD_PROFESSION_SKILL_COLUMNS) : sorted; - return ordered.map(({ rowId, label, value, isModified }) => ({ + return ordered.map(({ rowId, label, value, isModified, tooltip }) => ({ rowId, label, value, isModified: Boolean(isModified), + tooltip: tooltip ?? "", })); } @@ -82,7 +95,7 @@ class AddProfessionDialogController { /** @type {string[]} */ this.bondRelationships = []; - /** @type {DialogV2 | null} */ + /** @type {import("foundry.applications.api.DialogV2").default | null} */ this.dialog = null; this.submitted = false; } @@ -94,16 +107,21 @@ class AddProfessionDialogController { const content = await this.#buildTemplate(); this.submitted = false; - return DialogV2.wait({ + return showDgDialog({ + modifier: "character-creation", content, window: { title: game.i18n.localize("DG.Profession.Dialog.Title"), }, - position: { width: 960 }, - classes: ["add-profession-dialog-app"], + position: { width: 1080, height: 780 }, form: { closeOnSubmit: false }, - render: async (_event, dialog) => { + onRender: async (dialog) => { this.dialog = dialog; + const root = this.#contentRoot(); + if (root) { + bindDialogTabs(root); + applySkillTooltipDisplayMode(root); + } this.#bindListeners(); await this.#refreshUi(); }, @@ -144,7 +162,10 @@ class AddProfessionDialogController { } #bondCount() { - return Number(this.professionItem.system.bonds) || 0; + return Math.max( + 1, + Math.trunc(Number(this.professionItem.system.bonds) || 1), + ); } #computeContext() { @@ -181,7 +202,7 @@ class AddProfessionDialogController { /** * @param {ReturnType} computed - * @returns {{ rowId: string, label: string, value: number }[]} + * @returns {{ rowId: string, label: string, value: number, isModified: boolean, tooltip: string, sortLabel: string }[]} */ #buildSkillDisplayRows(computed) { const modifiedFixed = new Set(computed.modifiedFixedKeys ?? []); @@ -195,14 +216,26 @@ class AddProfessionDialogController { value: r.value, sortLabel: r.sortLabel, isModified: modifiedFixed.has(r.key), + tooltip: buildSkillTooltip("dialog", null, { + kind: "fixed", + key: r.key, + }), })), - ...typedRows.map((r) => ({ - rowId: r.storageKey, - label: r.label, - value: r.value, - sortLabel: r.sortLabel, - isModified: modifiedTyped.has(r.storageKey), - })), + ...typedRows.map((r) => { + const data = computed.typedValues[r.storageKey]; + return { + rowId: r.storageKey, + label: r.label, + value: r.value, + sortLabel: r.sortLabel, + isModified: modifiedTyped.has(r.storageKey), + tooltip: buildSkillTooltip("dialog", null, { + kind: "typed", + group: data?.group ?? "", + label: r.label, + }), + }; + }), ]); } @@ -221,18 +254,14 @@ class AddProfessionDialogController { ...row, isChooseOne: Boolean(this.optionSkillMeta[row.key]?.chooseOne), })); + const optionColumns = splitIntoColumns( + optionRows, + CHARACTER_CREATION_OPTION_COLUMNS, + ); const automaticChooseOneRows = this.#buildAutomaticChooseOneRows(); - const capWarnings = computed.capWarnings.map((w) => ({ - warning: game.i18n.format("DG.Profession.Dialog.CapWarning", { - skill: w.label, - attempted: w.attempted, - waste: w.waste, - }), - })); - - const bondCount = Number(this.professionItem.system.bonds) || 0; + const bondCount = this.#bondCount(); const bondRows = Array.from({ length: bondCount }, (_, index) => ({ index, number: index + 1, @@ -246,26 +275,25 @@ class AddProfessionDialogController { }), ); + const showSkillOptionsTab = this.optionPicks > 0; + return renderTemplate(`${BASE_TEMPLATE_PATH}/dialog/add-profession.html`, { skillRows, - showOptionSkills: this.optionPicks > 0, + showSkillOptionsTab, + defaultTabSkillOptions: showSkillOptionsTab, + defaultTabBonus: !showSkillOptionsTab, optionPicks: this.optionPicks, - optionRows, + optionColumns, automaticChooseOneRows, - catalog: buildSkillCatalog(), + catalog: buildBonusSkillCatalog(), bonusSlots, - validationMessages: formatProfessionValidationMessages( - computed.validationErrors, - { optionPicks: this.optionPicks }, - ), - capWarnings, - bondCount, + ...this.#getDisplayMessages(computed), bondRows, }); } #contentRoot() { - return this.dialog?.element?.querySelector(".dialog-content"); + return this.dialog ? getDialogContentRoot(this.dialog) : null; } #bindListeners() { @@ -304,7 +332,6 @@ class AddProfessionDialogController { #onOptionCheckChange(checkbox) { const key = checkbox.value; const isChooseOne = checkbox.dataset.isChooseOne === "true"; - const root = this.#contentRoot(); if (checkbox.checked) { if (this.checkedOptionKeys.size >= this.optionPicks) { @@ -312,18 +339,14 @@ class AddProfessionDialogController { return; } this.checkedOptionKeys.add(key); - if (isChooseOne && root) { - const nameInput = root.querySelector( - `[data-choose-one-key="${CSS.escape(key)}"]`, - ); + if (isChooseOne) { + const nameInput = this.#getOptionChooseOneInput(key); if (nameInput) nameInput.disabled = false; } } else { this.checkedOptionKeys.delete(key); - if (isChooseOne && root) { - const nameInput = root.querySelector( - `[data-choose-one-key="${CSS.escape(key)}"]`, - ); + if (isChooseOne) { + const nameInput = this.#getOptionChooseOneInput(key); if (nameInput) { nameInput.disabled = true; nameInput.value = ""; @@ -334,6 +357,20 @@ class AddProfessionDialogController { this.#refreshUi(); } + /** + * @param {string} key + * @returns {HTMLInputElement | null} + */ + #getOptionChooseOneInput(key) { + const root = this.#contentRoot(); + if (!root) return null; + return root.querySelector( + `[data-option-choose-one="true"][data-choose-one-key="${CSS.escape( + key, + )}"]`, + ); + } + /** * @param {HTMLSelectElement} select */ @@ -392,9 +429,7 @@ class AddProfessionDialogController { ); if (cb) cb.checked = true; if (!this.optionSkillMeta[key]?.chooseOne) continue; - const nameInput = root.querySelector( - `[data-choose-one-key="${CSS.escape(key)}"]`, - ); + const nameInput = this.#getOptionChooseOneInput(key); if (nameInput) { nameInput.disabled = false; nameInput.value = this.chooseOneLabels[key] ?? ""; @@ -403,7 +438,9 @@ class AddProfessionDialogController { for (const row of this.#buildAutomaticChooseOneRows()) { const nameInput = root.querySelector( - `[data-choose-one-key="${CSS.escape(row.key)}"]`, + `.character-creation-choose-one-section [data-choose-one-key="${CSS.escape( + row.key, + )}"]`, ); if (nameInput) nameInput.value = this.chooseOneLabels[row.key] ?? ""; } @@ -420,43 +457,87 @@ class AddProfessionDialogController { } } + /** + * @param {ReturnType} computed + * @returns {{ + * validationMessages: string[], + * capWarnings: { warning: string }[], + * atCapMessages: string[], + * }} + */ + #getDisplayMessages(computed) { + const atCapPrefix = "bonusAtCap:"; + const mainErrors = computed.validationErrors.filter( + (code) => !code.startsWith(atCapPrefix), + ); + const atCapErrors = computed.validationErrors.filter((code) => + code.startsWith(atCapPrefix), + ); + + return { + validationMessages: formatProfessionValidationMessages(mainErrors, { + optionPicks: this.optionPicks, + }), + capWarnings: computed.capWarnings.map((w) => ({ + warning: game.i18n.format("DG.Profession.Dialog.CapWarning", { + skill: w.label, + attempted: w.attempted, + waste: w.waste, + }), + })), + atCapMessages: formatProfessionValidationMessages(atCapErrors, { + optionPicks: this.optionPicks, + }), + }; + } + /** * @param {ReturnType} computed * @returns {string} */ #buildMessagesHtml(computed) { - const validationHtml = formatProfessionValidationMessages( - computed.validationErrors, - { optionPicks: this.optionPicks }, - ) - .map( - (message) => - `

${foundry.utils.escapeHTML( - message, - )}

`, - ) - .join(""); + const { validationMessages, capWarnings, atCapMessages } = + this.#getDisplayMessages(computed); - const capHtml = computed.capWarnings + const toErrorHtml = (messages) => + messages + .map( + (message) => + `

${foundry.utils.escapeHTML( + message, + )}

`, + ) + .join(""); + + const capHtml = capWarnings .map( (w) => - `

${foundry.utils.escapeHTML( - game.i18n.format("DG.Profession.Dialog.CapWarning", { - skill: w.label, - attempted: w.attempted, - waste: w.waste, - }), + `

${foundry.utils.escapeHTML( + w.warning, )}

`, ) .join(""); - return validationHtml + capHtml; + return ( + toErrorHtml(validationMessages) + capHtml + toErrorHtml(atCapMessages) + ); + } + + /** + * @param {string} tooltip + * @returns {string} + */ + #tooltipAttr(tooltip) { + if (!tooltip) return ""; + return ` data-tooltip="${foundry.utils.escapeHTML(tooltip)}"`; } async #refreshUi() { const root = this.#contentRoot(); if (!root) return; + this.#syncFormStateFromDom(); + const computed = this.#compute(); const rows = this.#buildSkillDisplayRows(computed); @@ -466,15 +547,18 @@ class AddProfessionDialogController { body.innerHTML = rows .map((r) => { const modifiedClass = r.isModified ? " is-modified" : ""; - return `
${foundry.utils.escapeHTML( + )}">${foundry.utils.escapeHTML( r.label, - )}${ + )}${ r.value }%
`; }) .join(""); + applySkillTooltipDisplayMode(body); } const messagesEl = root.querySelector("[data-profession-messages]"); @@ -537,13 +621,9 @@ class AddProfessionDialogController { }); } - if (bondDocs.length) { - await this.actor.createEmbeddedDocuments("Item", bondDocs); - } + await this.actor.createEmbeddedDocuments("Item", bondDocs); this.submitted = true; await this.dialog?.close(); } } - -export default { showPickSkillsDialog }; diff --git a/module/applications/assign-stats-dialog.js b/module/applications/assign-stats-dialog.js index 4b8d57f..91fbdcd 100644 --- a/module/applications/assign-stats-dialog.js +++ b/module/applications/assign-stats-dialog.js @@ -8,26 +8,18 @@ import { getDefaultPointBuyValues, validatePointBuyValues, } from "../utils/profession-stat-setup.js"; +import { getDialogContentRoot, showDgDialog } from "./dg-dialog.js"; -const { DialogV2 } = foundry.applications.api; const { renderTemplate } = foundry.applications.handlebars; /** @typedef {'submitted' | 'cancelled'} AssignStatsResult */ -/** - * @param {DialogV2} dialog - * @returns {HTMLElement|null} - */ -function contentRoot(dialog) { - return dialog.element?.querySelector(".dialog-content") ?? null; -} - /** * @param {DialogV2} dialog * @param {Record} values */ function syncValuesFromDom(dialog, values) { - const root = contentRoot(dialog); + const root = getDialogContentRoot(dialog); if (!root) return; for (const key of STAT_KEYS) { @@ -47,7 +39,7 @@ function refreshAssignStatsUi(dialog, values) { syncValuesFromDom(dialog, values); const { isValid, remaining } = validatePointBuyValues(values); - const root = contentRoot(dialog); + const root = getDialogContentRoot(dialog); const remainingEl = root?.querySelector("[data-points-remaining]"); if (remainingEl) { remainingEl.textContent = game.i18n.format( @@ -68,7 +60,7 @@ function refreshAssignStatsUi(dialog, values) { * @param {Record} values */ function bindAssignStatsListeners(dialog, values) { - const root = contentRoot(dialog); + const root = getDialogContentRoot(dialog); if (!root) return; root.querySelectorAll("[data-stat-key]").forEach((input) => { @@ -111,15 +103,15 @@ export async function showAssignStatsDialog(actor) { }, ); - return DialogV2.wait({ + return showDgDialog({ + modifier: "assign-stats", content, window: { title: game.i18n.localize("DG.ProfessionSetup.AssignStats.Title"), }, position: { width: 420 }, - classes: ["assign-stats-dialog-app"], form: { closeOnSubmit: false }, - render: (_event, dialog) => { + onRender: (dialog) => { bindAssignStatsListeners(dialog, values); refreshAssignStatsUi(dialog, values); }, @@ -152,5 +144,3 @@ export async function showAssignStatsDialog(actor) { ], }); } - -export default { showAssignStatsDialog }; diff --git a/module/applications/dg-dialog.js b/module/applications/dg-dialog.js new file mode 100644 index 0000000..792cd97 --- /dev/null +++ b/module/applications/dg-dialog.js @@ -0,0 +1,103 @@ +import DG from "../config.js"; + +const { DialogV2 } = foundry.applications.api; + +/** @type {readonly string[]} */ +export const DG_DIALOG_APP_CLASSES = ["deltagreen", "dg-dialog-app"]; + +/** + * @returns {"program-style" | "cowboy-style" | "outlaw-style"} + */ +export function getCharacterSheetThemeClass() { + const theme = game.settings.get(DG.ID, "characterSheetStyle"); + return `${theme}-style`; +} + +/** + * @param {DialogV2} dialog + * @returns {HTMLElement | null} + */ +export function getDialogContentRoot(dialog) { + return ( + dialog.element?.querySelector(".dialog-content") ?? + dialog.element?.querySelector(".dialog-form") ?? + null + ); +} + +/** + * @param {DialogV2} dialog + * @returns {void} + */ +export function applyDialogTheme(dialog) { + const windowContent = dialog.element?.querySelector("section.window-content"); + if (!windowContent) return; + windowContent.classList.add(getCharacterSheetThemeClass()); +} + +/** + * @param {string} [modifier] + * @returns {string[]} + */ +export function buildDialogAppClasses(modifier) { + const classes = [...DG_DIALOG_APP_CLASSES]; + if (modifier) classes.push(`dg-dialog-app--${modifier}`); + return classes; +} + +/** + * @param {HTMLElement} root + * @param {string} tabId + * @returns {void} + */ +export function activateDialogTab(root, tabId) { + root.querySelectorAll(".tabs [data-action='tab']").forEach((el) => { + el.classList.toggle("active", el.dataset.tab === tabId); + }); + root.querySelectorAll(".tab[data-tab]").forEach((el) => { + el.classList.toggle("active", el.dataset.tab === tabId); + }); +} + +/** + * @param {HTMLElement} root + * @returns {void} + */ +export function bindDialogTabs(root) { + root.querySelectorAll(".tabs [data-action='tab']").forEach((tab) => { + tab.addEventListener("click", (event) => { + event.preventDefault(); + const tabId = tab.dataset.tab; + if (tabId) activateDialogTab(root, tabId); + }); + }); +} + +/** + * @param {object} options — DialogV2.wait options plus DG extensions + * @param {string} [options.modifier] — app class modifier: dg-dialog-app--{modifier} + * @param {(dialog: DialogV2, event: Event) => void | Promise} [options.onRender] + * @param {string[]} [options.classes] — extra app classes merged after base + * @returns {Promise} + */ +export function showDgDialog(options) { + const { + modifier, + onRender, + classes: extraClasses = [], + render: userRender, + ...dialogOptions + } = options; + + const classes = [...buildDialogAppClasses(modifier), ...extraClasses]; + + return DialogV2.wait({ + ...dialogOptions, + classes, + render: async (event, dialog) => { + applyDialogTheme(dialog); + if (userRender) await userRender(event, dialog); + if (onRender) await onRender(dialog, event); + }, + }); +} diff --git a/module/applications/pick-statistics-dialog.js b/module/applications/pick-statistics-dialog.js index 9e29d1f..31da8ef 100644 --- a/module/applications/pick-statistics-dialog.js +++ b/module/applications/pick-statistics-dialog.js @@ -1,6 +1,6 @@ import { BASE_TEMPLATE_PATH } from "../config.js"; +import { showDgDialog } from "./dg-dialog.js"; -const { DialogV2 } = foundry.applications.api; const { renderTemplate } = foundry.applications.handlebars; /** @typedef {'roll' | 'assign' | 'skip'} PickStatisticsChoice */ @@ -14,13 +14,13 @@ export async function showPickStatisticsDialog() { {}, ); - return DialogV2.wait({ + return showDgDialog({ + modifier: "pick-statistics", content, window: { title: game.i18n.localize("DG.ProfessionSetup.PickStatistics.Title"), }, position: { width: 480 }, - classes: ["pick-statistics-dialog-app"], close: () => null, buttons: [ { @@ -51,5 +51,3 @@ export async function showPickStatisticsDialog() { ], }); } - -export default { showPickStatisticsDialog }; diff --git a/module/applications/profession-setup-flow.js b/module/applications/profession-setup-flow.js index d825d10..e3055b4 100644 --- a/module/applications/profession-setup-flow.js +++ b/module/applications/profession-setup-flow.js @@ -16,13 +16,13 @@ function createProfessionStub(actor, itemData) { } /** - * Run the full profession setup flow: Pick Statistics → optional stat setup → Pick Skills. + * Run the full profession setup flow: Pick Statistics → optional stat setup → Character Creation. * * @param {Item} professionItem Profession item or unsaved stub on the actor * @param {Actor} actor * @param {object} [options] * @param {TokenDocument|null} [options.token] - * @returns {Promise} True if Pick Skills was submitted successfully. + * @returns {Promise} True if Character Creation was submitted successfully. */ export async function runProfessionSetupFlow( professionItem, @@ -79,5 +79,3 @@ export async function assignProfessionToAgent( const created = await actor.createEmbeddedDocuments("Item", [createData]); return created[0] ?? null; } - -export default { runProfessionSetupFlow, assignProfessionToAgent }; diff --git a/module/applications/roll-stats-dialog.js b/module/applications/roll-stats-dialog.js index 4a4f67c..a854a8b 100644 --- a/module/applications/roll-stats-dialog.js +++ b/module/applications/roll-stats-dialog.js @@ -7,20 +7,12 @@ import { buildStatisticRows, rollStatisticScores, } from "../utils/profession-stat-setup.js"; +import { getDialogContentRoot, showDgDialog } from "./dg-dialog.js"; -const { DialogV2 } = foundry.applications.api; const { renderTemplate } = foundry.applications.handlebars; /** @typedef {'submitted' | 'cancelled'} RollStatsResult */ -/** - * @param {DialogV2} dialog - * @returns {HTMLElement|null} - */ -function contentRoot(dialog) { - return dialog.element?.querySelector(".dialog-content") ?? null; -} - /** * @param {object} state * @param {DialogV2} dialog @@ -39,7 +31,7 @@ function findStatKeyForRollIndex(state, rollIndex) { * @param {DialogV2} dialog */ function syncDomFromAssignments(state, dialog) { - const root = contentRoot(dialog); + const root = getDialogContentRoot(dialog); if (!root) return; const pool = root.querySelector("[data-roll-stats-pool]"); @@ -81,8 +73,10 @@ function refreshRollStatsUi(state, dialog) { * @param {DialogV2} dialog */ function bindRollStatsListeners(state, dialog) { - const root = contentRoot(dialog); + const root = getDialogContentRoot(dialog); if (!root) return; + if (root.dataset.rollStatsListenersBound === "true") return; + root.dataset.rollStatsListenersBound = "true"; root.querySelectorAll(".roll-stat-token").forEach((token) => { token.addEventListener("dragstart", (event) => { @@ -219,15 +213,15 @@ export async function showRollStatsDialog(actor, { token = null } = {}) { }, ); - return DialogV2.wait({ + return showDgDialog({ + modifier: "roll-stats", content, window: { title: game.i18n.localize("DG.ProfessionSetup.RollStats.Title"), }, position: { width: 520 }, - classes: ["roll-stats-dialog-app"], form: { closeOnSubmit: false }, - render: (_event, dialog) => { + onRender: (dialog) => { bindRollStatsListeners(state, dialog); refreshRollStatsUi(state, dialog); }, @@ -270,5 +264,3 @@ export async function showRollStatsDialog(actor, { token = null } = {}) { ], }); } - -export default { showRollStatsDialog }; diff --git a/module/chat/dg-chat-card.js b/module/chat/dg-chat-card.js index 5f515f0..da015db 100644 --- a/module/chat/dg-chat-card.js +++ b/module/chat/dg-chat-card.js @@ -114,6 +114,29 @@ export async function renderDGChatCard({ speakerName, label = "", content }) { }); } +/** + * Whether HTML already includes an inline dice roll block. + * @param {string} content + * @returns {boolean} + */ +function contentIncludesRollDisplay(content) { + return typeof content === "string" && content.includes("dice-roll"); +} + +/** + * Render roll HTML for embedding in a chat card body. + * @param {Roll[]} rolls + * @returns {Promise} + */ +async function renderRollsHTML(rolls) { + let html = ""; + for (const roll of rolls) { + if (!roll._evaluated) await roll.evaluate(); + html += await roll.render(); + } + return html; +} + /** * Wrap message content in the Delta Green chat card shell. * @param {object} messageData @@ -176,10 +199,18 @@ export async function prepareDGRollChatMessageData({ scene, }); + let bodyContent = content ?? String(roll.total); + + // Foundry only auto-injects roll HTML when content has no child elements. + // DG chat cards always wrap content in a
, so embed rolls in the body. + if (label && !contentIncludesRollDisplay(bodyContent)) { + bodyContent = `${bodyContent}${await renderRollsHTML([roll])}`; + } + let messageData = { author: game.user.id, speaker, - content: content ?? String(roll.total), + content: bodyContent, sound: CONFIG.sounds.dice, rolls: [roll], flags: foundry.utils.mergeObject( diff --git a/module/data/item/profession.js b/module/data/item/profession.js index 1233355..cce5c37 100644 --- a/module/data/item/profession.js +++ b/module/data/item/profession.js @@ -6,10 +6,17 @@ const { NumberField, ObjectField } = foundry.data.fields; export const PROFESSION_OPTION_PICKS_KEY = "optionPicks"; export default class ProfessionItemData extends foundry.abstract.TypeDataModel { + /** @inheritdoc */ + static migrateData(source, options, _state) { + const bonds = Number(source.bonds); + if (!Number.isFinite(bonds) || bonds < 1) source.bonds = 1; + return super.migrateData(source, options, _state); + } + static defineSchema() { return { ...defineBaseItemSystemFields(), - bonds: new NumberField({ initial: 0, min: 0, max: 5, integer: true }), + bonds: new NumberField({ initial: 1, min: 1, max: 5, integer: true }), automaticSkills: new ObjectField({ initial: {} }), automaticSkillMeta: new ObjectField({ initial: {} }), optionSkills: new ObjectField({ initial: { [PROFESSION_OPTION_PICKS_KEY]: 0 } }), diff --git a/module/documents/dg-active-effect.js b/module/documents/dg-active-effect.js index 8af94f1..d9fc4e6 100644 --- a/module/documents/dg-active-effect.js +++ b/module/documents/dg-active-effect.js @@ -17,7 +17,7 @@ function resolveTransferItem(effect) { * @param {Item} item * @returns {{ inactive: boolean, reasonKey: string|null }} */ -export function getTransferSuppressionState(item) { +function getTransferSuppressionState(item) { switch (item.type) { case "weapon": case "armor": @@ -54,7 +54,7 @@ export function getTransferSuppressionState(item) { * @param {Item} item * @returns {boolean} True when a transfer effect on this item should not apply. */ -export function isTransferEffectInactive(item) { +function isTransferEffectInactive(item) { return getTransferSuppressionState(item).inactive; } diff --git a/module/roll/roll-classes.js b/module/roll/roll-classes.js index 4ee80ff..3ecf69f 100644 --- a/module/roll/roll-classes.js +++ b/module/roll/roll-classes.js @@ -13,6 +13,10 @@ import { isDiceSoNiceAvailable, waitForDiceSoNiceMessageAnimation, } from "../utils/dice-so-nice.js"; +import { + clampPercentileRollTarget, + getRollTargetDisplayClassFromModifier, +} from "../utils/active-effect-derived.js"; const { renderTemplate } = foundry.applications.handlebars; @@ -187,9 +191,14 @@ export class DGPercentileRoll extends DGRoll { } } + const rollTargetModifier = this.rollTargetModifier; + const targetDisplayClass = + getRollTargetDisplayClassFromModifier(rollTargetModifier); + return showPercentileRollModifyDialog({ label: this.localizedKey, target: this.target, + targetDisplayClass, hideTarget: hideSanTarget, defaultModifier: customModifierTarget, actor: this.actor, @@ -342,6 +351,7 @@ export class DGPercentileRoll extends DGRoll { */ createLabel() { const startOfLabel = `${this.localizedKey}`; + const rollTargetModifier = this.rollTargetModifier; const endOfLabel = `${game.i18n.localize("DG.Roll.Target")} ${ this.effectiveTarget }`; @@ -353,8 +363,6 @@ export class DGPercentileRoll extends DGRoll { .toUpperCase()}] ${endOfLabel}` : `${startOfLabel}
${endOfLabel}%`; - const rollTargetModifier = this.rollTargetModifier; - if (this.modifier || rollTargetModifier) { label += ` (${this.target}%`; @@ -391,13 +399,19 @@ export class DGPercentileRoll extends DGRoll { } } - /** @deprecated Use rollTargetModifier via Active Effects instead. */ - get exhausted() { - const modifier = this.rollTargetModifier; - return { - isExhausted: modifier !== 0, - exhaustedCheckPenalty: modifier, - }; + /** + * Target after roll-target Active Effects, before dialog modifier. + * @returns {number|null} + */ + get rollTargetEffectiveTarget() { + if (!this.target || Number.isNaN(this.target)) return null; + + const rollTargetModifier = this.rollTargetModifier; + if (!rollTargetModifier) return parseInt(this.target); + + return clampPercentileRollTarget(this.target, rollTargetModifier, { + allowOver99: this.target > 99 && this.type === "stat", + }); } /** @@ -487,7 +501,9 @@ export class DGPercentileRoll extends DGRoll { target = parseInt(this.target); if (rollTargetModifier) { - target += rollTargetModifier; + target = clampPercentileRollTarget(target, rollTargetModifier, { + allowOver99: this.target > 99 && this.type === "stat", + }); } if (this.modifier && !Number.isNaN(this.modifier)) { diff --git a/module/roll/roll-dialogs.js b/module/roll/roll-dialogs.js index d8d7e25..98c36b0 100644 --- a/module/roll/roll-dialogs.js +++ b/module/roll/roll-dialogs.js @@ -1,7 +1,7 @@ +import { showDgDialog } from "../applications/dg-dialog.js"; import DGUtils from "../utils/utility-functions.js"; const { renderTemplate } = foundry.applications.handlebars; -const { DialogV2 } = foundry.applications.api; const PERCENTILE_ROLL_DIALOG_TEMPLATE = "systems/deltagreen/templates/dialog/modify-percentile-roll.html"; @@ -55,6 +55,7 @@ function readMessageMode(dialog) { export async function showPercentileRollModifyDialog({ label, target, + targetDisplayClass = "", hideTarget = false, defaultModifier = 20, actor = null, @@ -68,6 +69,7 @@ export async function showPercentileRollModifyDialog({ data: { label, originalTarget: target, + targetDisplayClass, targetModifier: defaultModifier, hideTarget, }, @@ -101,9 +103,9 @@ export async function showPercentileRollModifyDialog({ }), })); - return DialogV2.wait({ + return showDgDialog({ + modifier: "modify-roll", content, - classes: ["modify-roll-dialog-app"], form: { closeOnSubmit: false }, position: { width: 420 }, window: { @@ -170,7 +172,8 @@ export async function showDamageRollModifyDialog({ itemName, formula }) { }, }); - return DialogV2.wait({ + return showDgDialog({ + modifier: "modify-damage-roll", content, window: { title: game.i18n.localize("DG.ModifySkillRollDialogue.Title"), diff --git a/module/sheets/agent-sheet.js b/module/sheets/agent-sheet.js index 5938d7b..5e211b7 100644 --- a/module/sheets/agent-sheet.js +++ b/module/sheets/agent-sheet.js @@ -16,9 +16,8 @@ import { hasActiveStimulantEffect, } from "../utils/stimulant-effect.js"; import { assignProfessionToAgent } from "../applications/profession-setup-flow.js"; +import { showDgDialog } from "../applications/dg-dialog.js"; import { PROFESSION_OPTION_PICKS_KEY } from "../data/item/profession.js"; - -const { DialogV2 } = foundry.applications.api; const { renderTemplate } = foundry.applications.handlebars; const AgentSheetBase = EffectsTabMixin(DGActorSheet); @@ -362,7 +361,8 @@ export default class DGAgentSheet extends AgentSheetBase { ); let penalty = null; - const confirmed = await DialogV2.wait({ + const confirmed = await showDgDialog({ + modifier: "exhaust-agent", content, window: { title: game.i18n.localize("DG.Physical.ExhaustDialogTitle"), @@ -401,6 +401,7 @@ export default class DGAgentSheet extends AgentSheetBase { await createAgentResourceChatMessage({ actor, token: this.token, + roll: wpRoll, contentKey: "DG.Physical.Chat.Exhausted", labelKey: "DG.Physical.Chat.ExhaustedLabel", i18nData: { @@ -424,6 +425,7 @@ export default class DGAgentSheet extends AgentSheetBase { return; } + const wasExhausted = DGAgentSheet._isActorExhausted(actor); const wpRoll = await new Roll("1d6").evaluate(); const gain = wpRoll.total; const currentWp = Number(actor.system.wp.value) || 0; @@ -441,8 +443,12 @@ export default class DGAgentSheet extends AgentSheetBase { await createAgentResourceChatMessage({ actor, token: this.token, + roll: wpRoll, contentKey: "DG.Physical.Chat.Rested", labelKey: "DG.Physical.Chat.RestedLabel", + extraContentKey: wasExhausted + ? "DG.Physical.Chat.RestedNoLongerExhausted" + : null, i18nData: { name: actor.name, gain, @@ -477,12 +483,12 @@ export default class DGAgentSheet extends AgentSheetBase { {}, ); - const choice = await DialogV2.wait({ + const choice = await showDgDialog({ + modifier: "stimulants", content, window: { title: game.i18n.localize("DG.Physical.StimulantsDialogTitle"), }, - classes: ["stimulants-dialog-app"], buttons: [ { action: "regular", @@ -639,7 +645,8 @@ export default class DGAgentSheet extends AgentSheetBase { }, ); - return foundry.applications.api.DialogV2.wait({ + return showDgDialog({ + modifier: "apply-skill-improvements", content, window: { title: game.i18n.localize("DG.Skills.ApplySkillImprovements.Title"), diff --git a/module/sheets/mixins/profession-item-mixin.js b/module/sheets/mixins/profession-item-mixin.js index 34eb572..4b31ab4 100644 --- a/module/sheets/mixins/profession-item-mixin.js +++ b/module/sheets/mixins/profession-item-mixin.js @@ -8,8 +8,11 @@ import { prepareProfessionSkillRows, splitProfessionSkillMap, } from "../../utils/profession-skills.js"; +import { + getDialogContentRoot, + showDgDialog, +} from "../../applications/dg-dialog.js"; -const { DialogV2 } = foundry.applications.api; const { renderTemplate } = foundry.applications.handlebars; const { ForcedDeletion } = foundry.data.operators; @@ -69,16 +72,16 @@ export default function ProfessionItemMixin(Base) { { catalog, isTyped: false }, ); - const confirmed = await DialogV2.wait({ + const confirmed = await showDgDialog({ + modifier: "add-profession-skill", content, position: { width: 300 }, - classes: ["add-profession-skill-dialog-app"], window: { title: game.i18n.localize("DG.ItemWindow.Profession.AddSkillTitle"), }, form: { closeOnSubmit: false }, - render: (_event, dialog) => { - const root = dialog.element?.querySelector(".dialog-content"); + onRender: (dialog) => { + const root = getDialogContentRoot(dialog); const select = root?.querySelector('[name="skillCatalogId"]'); const typedGroup = root?.querySelector(".profession-typed-label-group"); const typeInput = root?.querySelector('[name="typedSkillLabel"]'); @@ -116,7 +119,7 @@ export default function ProfessionItemMixin(Base) { label: game.i18n.localize("DG.Profession.Dialog.Submit"), default: true, callback: async (_event, _button, dialog) => { - const root = dialog.element.querySelector(".dialog-content"); + const root = getDialogContentRoot(dialog); const select = root?.querySelector('[name="skillCatalogId"]'); const ratingInput = root?.querySelector('[name="skillRating"]'); const typeInput = root?.querySelector('[name="typedSkillLabel"]'); diff --git a/module/sheets/mixins/skill-prep-mixin.js b/module/sheets/mixins/skill-prep-mixin.js index e89f9e4..69d3c78 100644 --- a/module/sheets/mixins/skill-prep-mixin.js +++ b/module/sheets/mixins/skill-prep-mixin.js @@ -1,5 +1,11 @@ import DG from "../../config.js"; import { reorderForColumnSorting } from "../../utils/skill-layout.js"; +import { getRollTargetDisplayClassFromModifier } from "../../utils/active-effect-derived.js"; +import { + applySkillTooltipDisplayMode, + buildAgentSpecialTrainingTooltip, + buildSkillTooltip, +} from "../../utils/skill-tooltip.js"; /** @param {typeof foundry.applications.api.ApplicationV2} Base */ export default function SkillPrepMixin(Base) { @@ -81,6 +87,13 @@ export default function SkillPrepMixin(Base) { skill.sortLabel = skill.key; } + skill.tooltip = buildSkillTooltip( + "sheet", + this.actor, + { kind: "typed", group: skill.group, label: skill.label }, + { proficiency: Number(skill.proficiency) || 0 }, + ); + sortedCustomSkills.push(skill); } @@ -113,13 +126,12 @@ export default function SkillPrepMixin(Base) { /** @returns {void} */ _prepareSkillTooltips() { for (const skill of Object.values(this.actor.system.sortedSkills)) { - skill.tooltip = game.i18n.localize(`DG.Skills.Tooltip.${skill.key}`); - if (!skill.proficiency) { - skill.tooltip = skill.tooltip.concat( - "

", - game.i18n.localize("DG.Tooltip.CannotRollSkillLabel"), - ); - } + skill.tooltip = buildSkillTooltip( + "sheet", + this.actor, + { kind: "fixed", key: skill.key }, + { proficiency: Number(skill.proficiency) || 0 }, + ); } } @@ -166,8 +178,37 @@ export default function SkillPrepMixin(Base) { simplifiedTraining.attributeLabel = attributeLabel; simplifiedTraining.attributeTooltip = attributeTooltip; + + if (this.actor.type === "agent") { + const rollTarget = this.actor.system.rollTarget; + let modifier = 0; + + if (rollTarget) { + if (DG.statistics.includes(training.attribute)) { + modifier = Number(rollTarget.statistics) || 0; + } else { + modifier = Number(rollTarget.allSkills) || 0; + } + } + + simplifiedTraining.rollTargetDisplayClass = + getRollTargetDisplayClassFromModifier(modifier); + } + simplifiedTraining.displayLabel = `${simplifiedTraining.name} (${attributeLabel}, ${simplifiedTraining.targetNumber}%)`; + if (this.actor.type === "agent" && attributeTooltip) { + const rollTargetKey = DG.statistics.includes(training.attribute) + ? "system.rollTarget.statistics" + : "system.rollTarget.allSkills"; + simplifiedTraining.tooltip = buildAgentSpecialTrainingTooltip( + this.actor, + attributeTooltip, + rollTargetKey, + simplifiedTraining.targetNumber, + ); + } + return simplifiedTraining; }); } @@ -201,73 +242,7 @@ export default function SkillPrepMixin(Base) { * @param {HTMLElement} root */ _tooltipsSettings(root) { - const mode = game.settings.get("deltagreen", "skillTooltipDisplay"); - - if (mode !== "hoverShift" && mode !== "never") return; - - const nodes = root.querySelectorAll("[data-tooltip],[title]"); - - if (mode === "never") { - nodes.forEach((el) => { - if (el.dataset.shiftTooltipInstalled === "true") return; - el.removeAttribute("data-tooltip"); - el.removeAttribute("title"); - el.dataset.shiftTooltipInstalled = "true"; - }); - return; - } - - nodes.forEach((el) => { - if (el.dataset.shiftTooltipInstalled === "true") return; - - let html = el.getAttribute("data-tooltip"); - let isHtml = true; - - if (!html) { - const title = el.getAttribute("title"); - if (title) { - html = foundry.utils.escapeHTML(title); - isHtml = false; - } - } - - if (!html) return; - - el.removeAttribute("data-tooltip"); - el.removeAttribute("title"); - el.dataset.shiftTooltipInstalled = "true"; - - const opts = isHtml ? { html } : { text: html }; - - const show = () => game.tooltip.activate(el, opts); - const hide = () => game.tooltip.deactivate(); - - const onKey = (ev) => { - if (ev.key !== "Shift") return; - if (!document.body.contains(el)) { - window.removeEventListener("keydown", onKey); - window.removeEventListener("keyup", onKey); - return; - } - if (ev.type === "keydown") show(); - else hide(); - }; - - const onEnter = (ev) => { - if (ev.shiftKey) show(); - window.addEventListener("keydown", onKey); - window.addEventListener("keyup", onKey); - }; - - const onLeave = () => { - hide(); - window.removeEventListener("keydown", onKey); - window.removeEventListener("keyup", onKey); - }; - - el.addEventListener("pointerenter", onEnter, { passive: true }); - el.addEventListener("pointerleave", onLeave, { passive: true }); - }); + applySkillTooltipDisplayMode(root); } }; } diff --git a/module/utils/active-effect-derived.js b/module/utils/active-effect-derived.js index 60270bf..4b4eaf6 100644 --- a/module/utils/active-effect-derived.js +++ b/module/utils/active-effect-derived.js @@ -14,11 +14,92 @@ import { * @param {number} base * @returns {string} */ -function getActiveEffectModificationClass(current, base) { +export function getActiveEffectModificationClass(current, base) { if (current == null || base == null || current === base) return ""; return current > base ? "ae-mod-increase" : "ae-mod-decrease"; } +/** + * Sheet color for roll-target modifiers (uses modifier sign, not clamped effective vs base). + * @param {number} modifier + * @returns {string} + */ +export function getRollTargetDisplayClassFromModifier(modifier) { + const numericModifier = Number(modifier) || 0; + if (!numericModifier) return ""; + return numericModifier > 0 ? "ae-mod-increase" : "ae-mod-decrease"; +} + +/** + * Apply a roll-target Active Effect modifier with the same clamp rules as percentile rolls. + * @param {number} base + * @param {number} modifier + * @param {{ allowOver99?: boolean }} [options] + * @returns {number} + */ +export function clampPercentileRollTarget( + base, + modifier, + { allowOver99 = false } = {}, +) { + const numericBase = Number(base); + const numericModifier = Number(modifier) || 0; + if (!numericModifier || !Number.isFinite(numericBase)) return numericBase; + + let target = Math.round(numericBase + numericModifier); + target = Math.max(1, target); + if (!allowOver99) { + target = Math.min(target, 99); + } + return target; +} + +/** + * @param {object} entry + * @param {number} modifier + */ +function attachRollTargetDisplay(entry, modifier) { + const numericModifier = Number(modifier) || 0; + if (!numericModifier) { + entry.rollTargetDisplayClass = ""; + return; + } + + entry.rollTargetDisplayClass = + getRollTargetDisplayClassFromModifier(numericModifier); +} + +/** + * Sheet display for agent roll-target Active Effects (skills, stats x5, SAN). + * @param {Actor} actor + * @returns {void} + */ +export function prepareAgentRollTargetDisplay(actor) { + if (actor.type !== "agent") return; + + const system = actor.system; + const rollTarget = system.rollTarget; + if (!rollTarget) return; + + const allSkillsMod = Number(rollTarget.allSkills) || 0; + const sanityMod = Number(rollTarget.sanity) || 0; + const statisticsMod = Number(rollTarget.statistics) || 0; + + for (const skill of Object.values(system.skills ?? {})) { + attachRollTargetDisplay(skill, allSkillsMod); + } + + for (const skill of Object.values(system.typedSkills ?? {})) { + attachRollTargetDisplay(skill, allSkillsMod); + } + + attachRollTargetDisplay(system.sanity, sanityMod); + + for (const stat of Object.values(system.statistics ?? {})) { + attachRollTargetDisplay(stat, statisticsMod); + } +} + /** * Attach sheet display classes comparing prepared data to persisted source (pre–AE). * @param {Actor} actor @@ -82,6 +163,7 @@ export function refreshDerivedAfterActiveEffects(actor) { const sourceStatistics = actor._source?.system?.statistics; prepareStatisticsX5(system.statistics, sourceStatistics); + prepareAgentRollTargetDisplay(actor); applyAgentResourceMaxBonuses(system, sourceStatistics); setSkillTargetProficiencies(system.skills); setSkillTargetProficiencies(system.typedSkills); diff --git a/module/utils/profession-skills.js b/module/utils/profession-skills.js index ee3c946..09e65e8 100644 --- a/module/utils/profession-skills.js +++ b/module/utils/profession-skills.js @@ -75,7 +75,12 @@ const AGENT_SKILL_DEFAULTS = /** @type {Record} */ ({ const BONUS_SKILL_COUNT = 8; const BONUS_SKILL_INCREMENT = 20; + +/** @type {ReadonlySet} */ +const BONUS_SKILL_CATALOG_EXCLUDED_FIXED = new Set(["unnatural"]); const SKILL_CAP = 80; +/** Bonus picks that would waste this many points or more are rejected (e.g. 80 + 20% at cap). */ +const MAX_ALLOWED_BONUS_WASTE = 20; const TYPED_KEY_PATTERN = /^(Art|Craft|Foreign Language|ForeignLanguage|Military Science|MilitaryScience|Pilot|Science|Other)\s*\(([^)]+)\)\s*$/i; @@ -126,16 +131,6 @@ export function isChooseOneProfessionSkillKey( ); } -/** - * @param {string} mapKey - * @param {Record} automaticMeta - * @param {Record} optionMeta - * @returns {ProfessionSkillMeta} - */ -export function getProfessionSkillMeta(mapKey, automaticMeta, optionMeta) { - return automaticMeta[mapKey] ?? optionMeta[mapKey] ?? {}; -} - /** * @param {object} params * @param {string} params.group @@ -393,6 +388,19 @@ export function buildSkillCatalog() { return entries; } +/** + * Skill catalog for character-creation bonus picks (excludes Unnatural). + * + * @returns {SkillCatalogEntry[]} + */ +export function buildBonusSkillCatalog() { + return buildSkillCatalog().filter( + (entry) => + entry.ref.kind !== "fixed" || + !BONUS_SKILL_CATALOG_EXCLUDED_FIXED.has(entry.ref.key), + ); +} + /** * @param {string} catalogId * @param {string} [typedLabel] @@ -420,6 +428,119 @@ export function catalogIdToSkillRef( return null; } +/** + * @param {ProfessionSkillRef} ref + * @returns {string} + */ +function getBonusTrackKey(ref) { + if (ref.kind === "fixed") return `fixed:${ref.key}`; + return formatProfessionSkillKey(ref); +} + +/** + * @param {string} trackKey + * @returns {string} + */ +function getBonusTrackLabel(trackKey) { + if (trackKey.startsWith("fixed:")) { + const key = trackKey.slice(6); + return formatProfessionSkillLabel( + /** @type {FixedSkillRef} */ ({ kind: "fixed", key }), + ); + } + const ref = parseProfessionSkillKey(trackKey); + if (ref) return formatProfessionSkillLabel(ref); + return trackKey; +} + +/** + * @param {string} trackKey + * @param {Record} baseFixed + * @param {Record} baseTyped + * @param {Record} defaults + * @returns {number} + */ +function getBonusTrackBaseValue(trackKey, baseFixed, baseTyped, defaults) { + if (trackKey.startsWith("fixed:")) { + const key = trackKey.slice(6); + return baseFixed[key] ?? defaults[key] ?? 0; + } + return baseTyped[trackKey]?.value ?? 0; +} + +/** + * Bonus skill slots that would waste 20+ points or raise a skill already at the creation cap. + * + * @param {Record} baseFixed + * @param {Record} baseTyped + * @param {string[]} bonusCatalogIds + * @param {string[]} bonusTypedLabels + * @returns {string[]} + */ +export function collectBonusCapValidationErrors( + baseFixed, + baseTyped, + bonusCatalogIds, + bonusTypedLabels, +) { + const defaults = getAgentSkillDefaults(); + /** @type {Record} */ + const counts = {}; + /** @type {(string | null)[]} */ + const slotTrackKeys = []; + + for (let i = 0; i < BONUS_SKILL_COUNT; i++) { + const catalogId = bonusCatalogIds?.[i]; + if (!catalogId) { + slotTrackKeys.push(null); + continue; + } + const ref = catalogIdToSkillRef( + catalogId, + bonusTypedLabels?.[i] ?? "", + ); + if (!ref) { + slotTrackKeys.push(null); + continue; + } + const trackKey = getBonusTrackKey(ref); + slotTrackKeys.push(trackKey); + counts[trackKey] = (counts[trackKey] ?? 0) + 1; + } + + /** @type {Set} */ + const violatingTrackKeys = new Set(); + + for (const [trackKey, count] of Object.entries(counts)) { + const base = getBonusTrackBaseValue(trackKey, baseFixed, baseTyped, defaults); + const final = base + count * BONUS_SKILL_INCREMENT; + const waste = Math.max(0, final - SKILL_CAP); + if (base >= SKILL_CAP || waste >= MAX_ALLOWED_BONUS_WASTE) { + violatingTrackKeys.add(trackKey); + } + } + + /** @type {string[]} */ + const errors = []; + for (let i = 0; i < BONUS_SKILL_COUNT; i++) { + const trackKey = slotTrackKeys[i]; + if (!trackKey || !violatingTrackKeys.has(trackKey)) continue; + + const base = getBonusTrackBaseValue(trackKey, baseFixed, baseTyped, defaults); + const count = counts[trackKey] ?? 0; + const final = base + count * BONUS_SKILL_INCREMENT; + const waste = Math.max(0, final - SKILL_CAP); + + if (base >= SKILL_CAP) { + errors.push(`bonusAtCap:${i}|${trackKey}`); + } else { + errors.push(`bonusWaste:${i}|${waste}|${trackKey}`); + } + } + + return errors; +} + /** * @param {Record} automaticSkills * @param {Record} optionSkills @@ -634,6 +755,15 @@ export function computeSkillValues( bondCount, }); + validationErrors.push( + ...collectBonusCapValidationErrors( + baseFixed, + baseTyped, + bonusIds, + bonusLabels, + ), + ); + return { fixedValues, typedValues, @@ -768,6 +898,8 @@ export function formatProfessionValidationMessages( let needsBonusSkillType = false; /** @type {Set} */ const typedNameRequiredGroups = new Set(); + /** @type {string[]} */ + const bonusAtCapTrackKeys = []; for (const code of errors) { if (code === "optionPicks") { @@ -782,11 +914,29 @@ export function formatProfessionValidationMessages( needsBondRelationship = true; continue; } + const bonusAtCap = code.match(/^bonusAtCap:\d+\|(.+)$/); + if (bonusAtCap) { + bonusAtCapTrackKeys.push(bonusAtCap[1]); + continue; + } + + const bonusWaste = code.match(/^bonusWaste:\d+\|(\d+)\|(.+)$/); + if (bonusWaste) { + push( + game.i18n.format("DG.Profession.Dialog.BonusSkillWasteTooHigh", { + skill: getBonusTrackLabel(bonusWaste[2]), + waste: Number(bonusWaste[1]), + maxWaste: MAX_ALLOWED_BONUS_WASTE - 1, + }), + ); + continue; + } + if (code.startsWith("bonusType")) { needsBonusSkillType = true; continue; } - if (code.startsWith("bonus")) { + if (/^bonus\d+$/.test(code)) { needsBonusSkill = true; continue; } @@ -816,12 +966,6 @@ export function formatProfessionValidationMessages( }), ); } - if (needsBondName) { - push(game.i18n.localize("DG.Profession.Dialog.BondNameRequired")); - } - if (needsBondRelationship) { - push(game.i18n.localize("DG.Profession.Dialog.BondRelationshipRequired")); - } if (needsBonusSkill) { push( game.i18n.format("DG.Profession.Dialog.BonusSkillRequired", { @@ -839,6 +983,20 @@ export function formatProfessionValidationMessages( }), ); } + if (needsBondName) { + push(game.i18n.localize("DG.Profession.Dialog.BondNameRequired")); + } + if (needsBondRelationship) { + push(game.i18n.localize("DG.Profession.Dialog.BondRelationshipRequired")); + } + + for (const trackKey of bonusAtCapTrackKeys) { + push( + game.i18n.format("DG.Profession.Dialog.BonusSkillAlreadyAtCap", { + skill: getBonusTrackLabel(trackKey), + }), + ); + } return messages; } @@ -915,4 +1073,4 @@ export function prepareProfessionSkillRows(skillMap, skillMeta = {}) { return rows; } -export { BONUS_SKILL_COUNT, BONUS_SKILL_INCREMENT, SKILL_CAP }; +export { BONUS_SKILL_COUNT }; diff --git a/module/utils/profession-stat-setup.js b/module/utils/profession-stat-setup.js index 1f39af5..8dff4e5 100644 --- a/module/utils/profession-stat-setup.js +++ b/module/utils/profession-stat-setup.js @@ -94,8 +94,8 @@ export async function rollStatisticScores({ */ export function buildRollStatsChatContent(rolls) { const cells = rolls.map( - (entry) => - `${foundry.utils.escapeHTML(String(entry.total))}`, + (entry, index) => + `${foundry.utils.escapeHTML(String(entry.total))}`, ); const rows = []; for (let i = 0; i < cells.length; i += 3) { diff --git a/module/utils/register-helpers.js b/module/utils/register-helpers.js index c836d47..7e91c7e 100644 --- a/module/utils/register-helpers.js +++ b/module/utils/register-helpers.js @@ -16,14 +16,6 @@ export default function registerHandlebarsHelpers() { return outStr; }); - Handlebars.registerHelper("toLowerCase", (str) => { - try { - return str.toLowerCase(); - } catch (error) { - return ""; - } - }); - Handlebars.registerHelper("toUpperCase", (str) => { try { return str.toUpperCase(); @@ -39,13 +31,6 @@ export default function registerHandlebarsHelpers() { return opts.inverse(this); }); - Handlebars.registerHelper("if_not_eq", (a, b, opts) => { - if (a !== b) { - return opts.fn(this); - } - return opts.inverse(this); - }); - Handlebars.registerHelper("if_gt", (a, b, trueVal, falseVal) => { if (a > b) { return trueVal; diff --git a/module/utils/resource-chat.js b/module/utils/resource-chat.js index e3ac65c..c86ecda 100644 --- a/module/utils/resource-chat.js +++ b/module/utils/resource-chat.js @@ -1,4 +1,7 @@ -import { createDGChatMessage } from "../chat/dg-chat-card.js"; +import { + createDGChatMessage, + createDGRollChatMessage, +} from "../chat/dg-chat-card.js"; /** * @param {object} params @@ -7,6 +10,8 @@ import { createDGChatMessage } from "../chat/dg-chat-card.js"; * @param {string} params.contentKey i18n key for message body. * @param {string} params.labelKey i18n key for card subtitle. * @param {Record} params.i18nData + * @param {string|null} [params.extraContentKey] Optional i18n key appended on a new line after the main body. + * @param {Roll} [params.roll] When set, attaches the roll to the chat message for display and Dice So Nice. * @returns {Promise} */ export async function createAgentResourceChatMessage({ @@ -15,13 +20,32 @@ export async function createAgentResourceChatMessage({ contentKey, labelKey, i18nData, + extraContentKey = null, + roll = null, }) { - const content = game.i18n.format(contentKey, i18nData); + let content = game.i18n.format(contentKey, i18nData); + if (extraContentKey) { + content = `${content}
${game.i18n.format(extraContentKey, i18nData)}`; + } + const label = game.i18n.localize(labelKey); + const messageMode = game.settings.get("core", "messageMode"); + + if (roll) { + return createDGRollChatMessage({ + actor, + token, + roll, + label, + content, + messageMode, + }); + } + return createDGChatMessage({ actor, token, - label: game.i18n.localize(labelKey), + label, content, - messageMode: game.settings.get("core", "messageMode"), + messageMode, }); } diff --git a/module/utils/roll-target-tooltip.js b/module/utils/roll-target-tooltip.js new file mode 100644 index 0000000..9f614f2 --- /dev/null +++ b/module/utils/roll-target-tooltip.js @@ -0,0 +1,209 @@ +import DG from "../config.js"; +import { ROLL_TARGET_FIELD_KEYS } from "../active-effect/effect-fields.js"; +import { clampPercentileRollTarget } from "./active-effect-derived.js"; +import DGUtils from "./utility-functions.js"; + +/** @typedef {"system.rollTarget.allSkills"|"system.rollTarget.sanity"|"system.rollTarget.statistics"} RollTargetFieldKey */ + +/** + * @param {ActiveEffect} effect + * @returns {boolean} + */ +function isRollTargetEffectActive(effect) { + if (effect.disabled) return false; + if (effect.duration?.expired) return false; + if (effect.isSuppressed) return false; + return true; +} + +/** + * @param {ActiveEffect} effect + * @returns {string} + */ +function getRollTargetEffectLabel(effect) { + if (effect.getFlag(DG.ID, "exhaustion")) { + return game.i18n.localize("DG.Physical.ExhaustionEffectName"); + } + + if (effect.name?.trim()) return effect.name; + + const parent = effect.parent; + if (parent?.documentName === "Item") return parent.name ?? ""; + + return effect.name ?? ""; +} + +/** + * @param {Actor} actor + * @param {RollTargetFieldKey} rollTargetFieldKey + * @returns {Array<{ name: string, modifier: number }>} + */ +export function collectRollTargetContributions(actor, rollTargetFieldKey) { + if (actor.type !== "agent" || !ROLL_TARGET_FIELD_KEYS.includes(rollTargetFieldKey)) { + return []; + } + + const contributions = []; + + for (const effect of actor.appliedEffects ?? []) { + if (!isRollTargetEffectActive(effect)) continue; + + const change = (effect.changes ?? []).find( + (entry) => entry.key === rollTargetFieldKey && entry.type === "add", + ); + if (!change) continue; + + const modifier = Number(change.value); + if (!Number.isFinite(modifier) || modifier === 0) continue; + + contributions.push({ + name: getRollTargetEffectLabel(effect), + modifier, + }); + } + + return contributions; +} + +/** + * @param {number} value + * @returns {string} + */ +function formatTargetPercent(value) { + return `${Math.round(value)}%`; +} + +/** + * @param {number} modifier + * @returns {string} + */ +function formatModifierPercent(modifier) { + return `${DGUtils.formatStringWithLeadingPlus(modifier)}%`; +} + +/** + * @param {number} base + * @param {Array<{ name: string, modifier: number }>} contributions + * @param {{ allowOver99?: boolean }} [options] + * @returns {string} + */ +function buildRollTargetBreakdownTable( + base, + contributions, + { allowOver99 = false } = {}, +) { + const rows = [ + [ + game.i18n.localize("DG.Tooltip.RollTarget.BaseTarget"), + formatTargetPercent(base), + ], + ]; + + for (const { name, modifier } of contributions) { + rows.push([name, formatModifierPercent(modifier)]); + } + + const totalModifier = contributions.reduce( + (sum, entry) => sum + entry.modifier, + 0, + ); + const finalTarget = clampPercentileRollTarget(base, totalModifier, { + allowOver99, + }); + + rows.push([ + game.i18n.localize("DG.Tooltip.RollTarget.FinalTarget"), + formatTargetPercent(finalTarget), + ]); + + const body = rows + .map( + ([label, value]) => + `${foundry.utils.escapeHTML(label)}${foundry.utils.escapeHTML(value)}`, + ) + .join(""); + + return `${body}
`; +} + +/** + * @param {number} base + * @returns {string} + */ +function buildRollTargetTargetLine(base) { + return game.i18n.format("DG.Tooltip.RollTarget.Target", { + value: Math.round(base), + }); +} + +/** + * @param {string} existingTooltip + * @param {Actor} actor + * @param {RollTargetFieldKey} rollTargetFieldKey + * @param {number} base + * @param {{ allowOver99?: boolean, showTargetWhenUnmodified?: boolean }} [options] + * @returns {string} + */ +export function appendRollTargetTooltipSection( + existingTooltip, + actor, + rollTargetFieldKey, + base, + options = {}, +) { + if (actor.type !== "agent") return existingTooltip; + + const { showTargetWhenUnmodified = true, ...tableOptions } = options; + + const contributions = collectRollTargetContributions( + actor, + rollTargetFieldKey, + ); + + let appendix = ""; + if (contributions.length > 0) { + appendix = buildRollTargetBreakdownTable(base, contributions, tableOptions); + } else if (showTargetWhenUnmodified) { + appendix = buildRollTargetTargetLine(base); + } + + if (!appendix) return existingTooltip; + if (!existingTooltip) return appendix; + return `${existingTooltip}

${appendix}`; +} + +/** + * Stat and sanity roll-target tooltips for the agent sheet left bar (not skills). + * + * @param {Actor} actor + * @returns {void} + */ +export function prepareAgentStatSanityTooltips(actor) { + if (actor.type !== "agent") return; + + const system = actor.system; + + for (const [key, stat] of Object.entries(system.statistics ?? {})) { + const base = Number(stat.x5) || 0; + const existing = game.i18n.localize(`DG.Attributes.Tooltip.${key}`); + stat.tooltip = appendRollTargetTooltipSection( + existing, + actor, + "system.rollTarget.statistics", + base, + { allowOver99: base > 99 }, + ); + } + + const sanity = system.sanity; + if (sanity) { + const existing = `${game.i18n.localize("DG.Tooltip.CurrentSanityPartOne")}${sanity.currentBreakingPoint}${game.i18n.localize("DG.Tooltip.CurrentSanityPartTwo")}`; + const base = Number(sanity.value) || 0; + sanity.tooltip = appendRollTargetTooltipSection( + existing, + actor, + "system.rollTarget.sanity", + base, + ); + } +} diff --git a/module/utils/skill-layout.js b/module/utils/skill-layout.js index 7f6d669..7aef8ba 100644 --- a/module/utils/skill-layout.js +++ b/module/utils/skill-layout.js @@ -1,3 +1,21 @@ +/** + * Splits a flat list into column arrays for vertical stacking (top-to-bottom per column). + * + * @template T + * @param {T[]} arr + * @param {number} numCols + * @returns {T[][]} + */ +export function splitIntoColumns(arr, numCols) { + if (numCols <= 1) return [arr]; + const columns = Array.from({ length: numCols }, () => /** @type {T[]} */ ([])); + const perCol = Math.ceil(arr.length / numCols); + for (let col = 0; col < numCols; col++) { + columns[col] = arr.slice(col * perCol, (col + 1) * perCol); + } + return columns; +} + /** * Reorders a flat array into column-major order for grid display. * diff --git a/module/utils/skill-tooltip.js b/module/utils/skill-tooltip.js new file mode 100644 index 0000000..3ddccb0 --- /dev/null +++ b/module/utils/skill-tooltip.js @@ -0,0 +1,169 @@ +/** @typedef {import("./profession-skills.js").ProfessionSkillRef} ProfessionSkillRef */ +/** @typedef {"dialog" | "sheet"} SkillTooltipContext */ + +import { appendRollTargetTooltipSection } from "./roll-target-tooltip.js"; + +/** + * Descriptive skill tooltip only (no roll target or 0% appendix). + * + * @param {ProfessionSkillRef} ref + * @returns {string} + */ +export function getSkillDescriptiveTooltip(ref) { + if (ref.kind === "fixed") { + const key = `DG.Skills.Tooltip.${ref.key}`; + const localized = game.i18n.localize(key); + return localized === key ? "" : localized; + } + + const key = `DG.TypeSkills.Tooltip.${ref.group}`; + const localized = game.i18n.localize(key); + return localized === key ? "" : localized; +} + +/** + * Build a skill tooltip for character creation or the agent sheet. + * + * @param {SkillTooltipContext} context + * @param {Actor | null} actor + * @param {ProfessionSkillRef} ref + * @param {{ proficiency?: number }} [options] + * @returns {string} + */ +export function buildSkillTooltip( + context, + actor, + ref, + { proficiency = 0 } = {}, +) { + let tooltip = getSkillDescriptiveTooltip(ref); + + if (ref.kind === "fixed" && !tooltip) { + tooltip = game.i18n.localize(`DG.Skills.Tooltip.${ref.key}`); + } + + if (context === "dialog") return tooltip; + + if (ref.kind === "fixed" && !proficiency) { + tooltip = tooltip.concat( + "

", + game.i18n.localize("DG.Tooltip.CannotRollSkillLabel"), + ); + } + + if (actor?.type === "agent") { + tooltip = appendRollTargetTooltipSection( + tooltip, + actor, + "system.rollTarget.allSkills", + Number(proficiency) || 0, + { showTargetWhenUnmodified: false }, + ); + } + + return tooltip; +} + +/** + * Agent sheet special-training row tooltip (attribute-based, not ProfessionSkillRef). + * + * @param {Actor} actor + * @param {string} attributeTooltipKey i18n key for the underlying attribute/skill + * @param {"system.rollTarget.allSkills"|"system.rollTarget.sanity"|"system.rollTarget.statistics"} rollTargetFieldKey + * @param {number} base + * @returns {string} + */ +export function buildAgentSpecialTrainingTooltip( + actor, + attributeTooltipKey, + rollTargetFieldKey, + base, +) { + return appendRollTargetTooltipSection( + game.i18n.localize(attributeTooltipKey), + actor, + rollTargetFieldKey, + base, + { + allowOver99: + rollTargetFieldKey === "system.rollTarget.statistics" && base > 99, + showTargetWhenUnmodified: false, + }, + ); +} + +/** + * Apply client skillTooltipDisplay setting (hover / hoverShift / never). + * + * @param {HTMLElement} root + * @returns {void} + */ +export function applySkillTooltipDisplayMode(root) { + const mode = game.settings.get("deltagreen", "skillTooltipDisplay"); + + if (mode !== "hoverShift" && mode !== "never") return; + + const nodes = root.querySelectorAll("[data-tooltip],[title]"); + + if (mode === "never") { + nodes.forEach((el) => { + if (el.dataset.shiftTooltipInstalled === "true") return; + el.removeAttribute("data-tooltip"); + el.removeAttribute("title"); + el.dataset.shiftTooltipInstalled = "true"; + }); + return; + } + + nodes.forEach((el) => { + if (el.dataset.shiftTooltipInstalled === "true") return; + + let html = el.getAttribute("data-tooltip"); + let isHtml = true; + + if (!html) { + const title = el.getAttribute("title"); + if (title) { + html = foundry.utils.escapeHTML(title); + isHtml = false; + } + } + + if (!html) return; + + el.removeAttribute("data-tooltip"); + el.removeAttribute("title"); + el.dataset.shiftTooltipInstalled = "true"; + + const opts = isHtml ? { html } : { text: html }; + + const show = () => game.tooltip.activate(el, opts); + const hide = () => game.tooltip.deactivate(); + + const onKey = (ev) => { + if (ev.key !== "Shift") return; + if (!document.body.contains(el)) { + window.removeEventListener("keydown", onKey); + window.removeEventListener("keyup", onKey); + return; + } + if (ev.type === "keydown") show(); + else hide(); + }; + + const onEnter = (ev) => { + if (ev.shiftKey) show(); + window.addEventListener("keydown", onKey); + window.addEventListener("keyup", onKey); + }; + + const onLeave = () => { + hide(); + window.removeEventListener("keydown", onKey); + window.removeEventListener("keyup", onKey); + }; + + el.addEventListener("pointerenter", onEnter, { passive: true }); + el.addEventListener("pointerleave", onLeave, { passive: true }); + }); +} diff --git a/system.json b/system.json index bce0c25..d2622b5 100644 --- a/system.json +++ b/system.json @@ -28,7 +28,7 @@ } }, "esmodules": ["module/deltagreen.js"], - "styles": ["css/deltagreen.css", "css/dialogue.css"], + "styles": ["css/deltagreen.css", "css/dg-dialog.css", "css/dialogue.css"], "languages": [ { "lang": "en", diff --git a/templates/actor/partials/custom-skills-partial-agent.html b/templates/actor/partials/custom-skills-partial-agent.html index beb84f2..7d1f949 100644 --- a/templates/actor/partials/custom-skills-partial-agent.html +++ b/templates/actor/partials/custom-skills-partial-agent.html @@ -53,18 +53,13 @@ data-target="{{block.proficiency}}" data-label="{{block.label}}" {{#unless (eq ../../skillTooltipDisplay "never") }} - {{#if block.proficiency }} - data-tooltip="{{localize 'DG.Tooltip.SkillLabel' }}" - {{/if }} - {{#unless block.proficiency }} - data-tooltip="{{localize 'DG.Tooltip.CannotRollSkillLabel' }}" - {{/unless }} + data-tooltip="{{block.tooltip}}" {{/unless }}> {{block.label}}
-
-
{{else}} - + data-tooltip="{{actor.system.sanity.tooltip}}" /> /
diff --git a/templates/actor/parts/left-bar.html b/templates/actor/parts/left-bar.html index 6b147a1..5e9dd40 100644 --- a/templates/actor/parts/left-bar.html +++ b/templates/actor/parts/left-bar.html @@ -24,18 +24,18 @@ {{#each actor.system.statistics as |statistic key| }}
-
+
{{> "systems/deltagreen/templates/actor/partials/ae-backed-input.html" hostClass="ae-input-host--stat" - inputClass="centered" + inputClass=(concat "centered " statistic.rollTargetDisplayClass) inputId=(concat "system.statistics." key ".value") name=(concat "system.statistics." key ".value") base=statistic.value diff --git a/templates/actor/parts/skills-tab-agent.html b/templates/actor/parts/skills-tab-agent.html index ed8ac17..ab9d98b 100644 --- a/templates/actor/parts/skills-tab-agent.html +++ b/templates/actor/parts/skills-tab-agent.html @@ -27,7 +27,7 @@ {{localizeWithFallback (concat "DG.Skills." skill.key) skill.label }} - -
- +
+
+
-
- +
+ - +
diff --git a/templates/dialog/add-profession.html b/templates/dialog/add-profession.html index 825a3ab..b028c7e 100644 --- a/templates/dialog/add-profession.html +++ b/templates/dialog/add-profession.html @@ -1,136 +1,163 @@ -
-
-
-
- {{localize "DG.Profession.Dialog.Skill"}} - {{localize "DG.Profession.Dialog.Value"}} -
-
- {{localize "DG.Profession.Dialog.Skill"}} - {{localize "DG.Profession.Dialog.Value"}} -
-
- {{localize "DG.Profession.Dialog.Skill"}} - {{localize "DG.Profession.Dialog.Value"}} -
-
- {{localize "DG.Profession.Dialog.Skill"}} - {{localize "DG.Profession.Dialog.Value"}} -
-
-
- {{#each skillRows}} -
- {{label}} - {{value}}% -
- {{/each}} -
-
- - {{#if automaticChooseOneRows.length}} -
-

{{localize "DG.Profession.Dialog.ChooseOneNamesHeader"}}

-
- {{#each automaticChooseOneRows}} - - {{/each}} -
-
- {{/if}} - - {{#if showOptionSkills}} -
-

{{localize "DG.Profession.Dialog.SkillsOptionsHeader" picks=optionPicks}}

-
- {{#each optionRows}} -
- - {{#if isChooseOne}} - - {{/if}} +
+
+
+
+

{{localize "DG.Profession.Dialog.SkillTotalsHeader"}}

+

{{localize "DG.Profession.Dialog.SkillTotalsHint"}}

+
+
+
+
+ {{localize "DG.Profession.Dialog.Skill"}} + {{localize "DG.Profession.Dialog.Value"}}
- {{/each}} -
-
- {{/if}} +
+ {{localize "DG.Profession.Dialog.Skill"}} + {{localize "DG.Profession.Dialog.Value"}} +
+
+
+ {{#each skillRows}} +
+ {{label}} + {{value}}% +
+ {{/each}} +
+
-
-

{{localize "DG.Profession.Dialog.BonusSkillsHeader"}}

-
- {{#each bonusSlots}} -
- + {{/each}} - - -
- {{/each}} -
-
- -
- {{#each validationMessages}} -

{{this}}

- {{/each}} - {{#each capWarnings}} -

{{warning}}

- {{/each}} -
- - {{#if bondCount}} -
-

{{localize "DG.Profession.Dialog.BondsHeader"}}

-
- {{#each bondRows}} -
- {{localize "DG.Profession.Dialog.Bond"}} {{number}} - - -
+
+ {{/if}} +
+ +
+ + +
+ {{#if showSkillOptionsTab}} +
+

{{localize "DG.Profession.Dialog.SkillsOptionsHeader" picks=optionPicks}}

+
+ {{#each optionColumns}} +
+ {{#each this}} +
+ + {{#if isChooseOne}} + + {{/if}} +
+ {{/each}} +
+ {{/each}} +
+
+ {{/if}} + +
+

{{localize "DG.Profession.Dialog.BonusSkillsIntro"}}

+
+ {{#each bonusSlots}} +
+ + +
+ {{/each}} +
+
+ +
+

{{localize "DG.Profession.Dialog.BondsIntro"}}

+
+ {{#each bondRows}} +
+ + + +
+ {{/each}} +
+
+
+ +
+ {{#each validationMessages}} +

{{this}}

+ {{/each}} + {{#each capWarnings}} +

{{warning}}

+ {{/each}} + {{#each atCapMessages}} +

{{this}}

{{/each}}
-
- {{/if}} - + + + diff --git a/templates/dialog/apply-skill-improvements.html b/templates/dialog/apply-skill-improvements.html index 213281f..499009a 100644 --- a/templates/dialog/apply-skill-improvements.html +++ b/templates/dialog/apply-skill-improvements.html @@ -1,7 +1,7 @@ -
- +
+
- + {{{localize "DG.Skills.ApplySkillImprovements.DialogEffectsFollowing" skillList=failedSkillNames}}}
diff --git a/templates/dialog/assign-stats.html b/templates/dialog/assign-stats.html index 266378e..60e7930 100644 --- a/templates/dialog/assign-stats.html +++ b/templates/dialog/assign-stats.html @@ -1,11 +1,11 @@ -
+

{{localize "DG.ProfessionSetup.AssignStats.PointsRemaining" remaining=remaining}}

{{#each stats}} -
- +
diff --git a/templates/dialog/exhaust-agent.html b/templates/dialog/exhaust-agent.html index 8e005ac..ef04722 100644 --- a/templates/dialog/exhaust-agent.html +++ b/templates/dialog/exhaust-agent.html @@ -1,6 +1,6 @@ -
-
- +
+
+
- +
diff --git a/templates/dialog/modify-damage-roll.html b/templates/dialog/modify-damage-roll.html index 91b7f14..2547e55 100644 --- a/templates/dialog/modify-damage-roll.html +++ b/templates/dialog/modify-damage-roll.html @@ -1,4 +1,4 @@ -
+
{{localize 'DG.ModifyDamageRollDialogue.Weapon'}} {{data.label}} @@ -16,4 +16,4 @@ {{selectOptions (getAvailableRollModes) selected=(getDefaultRollMode) localize=true}}
- \ No newline at end of file +
diff --git a/templates/dialog/modify-percentile-roll.html b/templates/dialog/modify-percentile-roll.html index f817562..d971f18 100644 --- a/templates/dialog/modify-percentile-roll.html +++ b/templates/dialog/modify-percentile-roll.html @@ -1,11 +1,11 @@ -
+
{{data.label}}
- + {{#if data.hideTarget}}??{{else}}{{data.originalTarget}}%{{/if}}
@@ -17,7 +17,7 @@
-

{{localize "DG.ModifySkillRollDialogue.QuickModifiersHeader"}}

+

{{localize "DG.ModifySkillRollDialogue.QuickModifiersHeader"}}

{{localize "DG.ModifySkillRollDialogue.QuickModifiersHint"}}

{{#each quickModifiers}} @@ -30,7 +30,7 @@
-

{{localize "DG.ModifySkillRollDialogue.CustomModifierHeader"}}

+

{{localize "DG.ModifySkillRollDialogue.CustomModifierHeader"}}

{{localize "DG.ModifySkillRollDialogue.CustomModifierHint"}}

-
\ No newline at end of file +
diff --git a/templates/dialog/pick-statistics.html b/templates/dialog/pick-statistics.html index 4a05e25..05fa5d3 100644 --- a/templates/dialog/pick-statistics.html +++ b/templates/dialog/pick-statistics.html @@ -1,3 +1,3 @@ -
-

{{localize "DG.ProfessionSetup.PickStatistics.Intro"}}

-
+
+

{{localize "DG.ProfessionSetup.PickStatistics.Intro"}}

+
diff --git a/templates/dialog/roll-stats.html b/templates/dialog/roll-stats.html index 23da887..ee3812a 100644 --- a/templates/dialog/roll-stats.html +++ b/templates/dialog/roll-stats.html @@ -1,4 +1,4 @@ -
+
{{#each stats}} @@ -19,5 +19,5 @@
-

{{localize "DG.ProfessionSetup.RollStats.PoolHeader"}}

-
+

{{localize "DG.ProfessionSetup.RollStats.PoolHeader"}}

+
diff --git a/templates/dialog/take-stimulants.html b/templates/dialog/take-stimulants.html index 7619c7a..4ad9ce6 100644 --- a/templates/dialog/take-stimulants.html +++ b/templates/dialog/take-stimulants.html @@ -1,18 +1,20 @@ -
-
- -

{{localize "DG.Physical.StimulantsRegularExplanation"}}

-
-
- -

{{localize "DG.Physical.StimulantsHardExplanation"}}

+
+
+
+ +

{{localize "DG.Physical.StimulantsRegularExplanation"}}

+
+
+ +

{{localize "DG.Physical.StimulantsHardExplanation"}}

+
diff --git a/templates/item/parts/profession-skills-tab.html b/templates/item/parts/profession-skills-tab.html index ea58146..1f17464 100644 --- a/templates/item/parts/profession-skills-tab.html +++ b/templates/item/parts/profession-skills-tab.html @@ -16,7 +16,7 @@ class="profession-skills-toolbar-control profession-bonds-input" name="system.bonds" value="{{item.system.bonds}}" - min="0" + min="1" max="5" data-dtype="Number" title="{{localize 'DG.ItemWindow.Profession.BondsTooltip'}}" />