Restyled character creation dialog.

Added dialog factory.
This commit is contained in:
Aaron Lutomski
2026-06-21 13:44:54 -07:00
parent 849c4de44d
commit adae5a5d56
43 changed files with 2340 additions and 1112 deletions
+2
View File
@@ -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
+139 -644
View File
@@ -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;
}
+846
View File
@@ -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;
}
+18 -7
View File
@@ -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": "<b>{name}</b> is exhausted. They lose {loss} Willpower, bringing them to {current}/{max}.",
"DG.Physical.Chat.Rested": "<b>{name}</b> gets a full night's rest and regains {gain} Willpower, bringing them to {current}/{max}.",
"DG.Physical.Chat.Exhausted": "<b>{name}</b> is exhausted. They lose <strong>{loss} Willpower</strong>.<br /><br />New total: <strong>{current}/{max}</strong>",
"DG.Physical.Chat.Rested": "<b>{name}</b> gets a full night's rest and regains <strong>{gain} Willpower</strong>.<br /><br />New total: <strong>{current}/{max}</strong>",
"DG.Physical.Chat.RestedNoLongerExhausted": "<br /><strong>{name}</strong> is no longer exhausted.",
"DG.Physical.Chat.StimulantsRegular": "<b>{name}</b> takes regular stimulants, suppressing their exhaustion for {hours} hours. They cannot sleep during this time.",
"DG.Physical.Chat.StimulantsHard": "<b>{name}</b> 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.",
+2
View File
@@ -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 */
+160 -80
View File
@@ -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<computeSkillValues>} 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<computeSkillValues>} 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<computeSkillValues>} computed
* @returns {string}
*/
#buildMessagesHtml(computed) {
const validationHtml = formatProfessionValidationMessages(
computed.validationErrors,
{ optionPicks: this.optionPicks },
)
.map(
(message) =>
`<p class="validation-error">${foundry.utils.escapeHTML(
message,
)}</p>`,
)
.join("");
const { validationMessages, capWarnings, atCapMessages } =
this.#getDisplayMessages(computed);
const capHtml = computed.capWarnings
const toErrorHtml = (messages) =>
messages
.map(
(message) =>
`<p class="dg-dialog__message--error">${foundry.utils.escapeHTML(
message,
)}</p>`,
)
.join("");
const capHtml = capWarnings
.map(
(w) =>
`<p class="cap-warning">${foundry.utils.escapeHTML(
game.i18n.format("DG.Profession.Dialog.CapWarning", {
skill: w.label,
attempted: w.attempted,
waste: w.waste,
}),
`<p class="dg-dialog__message--warning">${foundry.utils.escapeHTML(
w.warning,
)}</p>`,
)
.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 `<div class="add-profession-skill-entry${modifiedClass}" data-skill-row="${foundry.utils.escapeHTML(
return `<div class="dg-dialog__skill-entry${modifiedClass}" data-skill-row="${foundry.utils.escapeHTML(
r.rowId,
)}"><span class="add-profession-skill-label">${foundry.utils.escapeHTML(
)}"><span class="dg-dialog__skill-label"${this.#tooltipAttr(
r.tooltip,
)}>${foundry.utils.escapeHTML(
r.label,
)}</span><span class="add-profession-skill-value" data-skill-value>${
)}</span><span class="dg-dialog__skill-value" data-skill-value>${
r.value
}%</span></div>`;
})
.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 };
+7 -17
View File
@@ -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<string, number>} 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<string, number>} 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 };
+103
View File
@@ -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<void>} [options.onRender]
* @param {string[]} [options.classes] — extra app classes merged after base
* @returns {Promise<unknown>}
*/
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);
},
});
}
@@ -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 };
+2 -4
View File
@@ -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<boolean>} True if Pick Skills was submitted successfully.
* @returns {Promise<boolean>} 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 };
+8 -16
View File
@@ -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 };
+32 -1
View File
@@ -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<string>}
*/
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 <section>, 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(
+8 -1
View File
@@ -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 } }),
+2 -2
View File
@@ -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;
}
+26 -10
View File
@@ -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 = `<b>${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()}]</b> ${endOfLabel}`
: `${startOfLabel}</b><br> ${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)) {
+7 -4
View File
@@ -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"),
+13 -6
View File
@@ -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"),
@@ -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"]');
+49 -74
View File
@@ -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(
"<br><br>",
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);
}
};
}
+83 -1
View File
@@ -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);
+176 -18
View File
@@ -75,7 +75,12 @@ const AGENT_SKILL_DEFAULTS = /** @type {Record<string, number>} */ ({
const BONUS_SKILL_COUNT = 8;
const BONUS_SKILL_INCREMENT = 20;
/** @type {ReadonlySet<string>} */
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<string, ProfessionSkillMeta>} automaticMeta
* @param {Record<string, ProfessionSkillMeta>} 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<string, number>} baseFixed
* @param {Record<string, { group: string, label: string, value: number }>} baseTyped
* @param {Record<string, number>} 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<string, number>} baseFixed
* @param {Record<string, { group: string, label: string, value: number }>} baseTyped
* @param {string[]} bonusCatalogIds
* @param {string[]} bonusTypedLabels
* @returns {string[]}
*/
export function collectBonusCapValidationErrors(
baseFixed,
baseTyped,
bonusCatalogIds,
bonusTypedLabels,
) {
const defaults = getAgentSkillDefaults();
/** @type {Record<string, number>} */
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<string>} */
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<string, number>} automaticSkills
* @param {Record<string, number>} 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<string>} */
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 };
+2 -2
View File
@@ -94,8 +94,8 @@ export async function rollStatisticScores({
*/
export function buildRollStatsChatContent(rolls) {
const cells = rolls.map(
(entry) =>
`<td class="roll-stats-chat-value">${foundry.utils.escapeHTML(String(entry.total))}</td>`,
(entry, index) =>
`<td class="roll-stats-chat-value roll-stats-chat-value--${index % 2 === 0 ? "a" : "b"}">${foundry.utils.escapeHTML(String(entry.total))}</td>`,
);
const rows = [];
for (let i = 0; i < cells.length; i += 3) {
-15
View File
@@ -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;
+28 -4
View File
@@ -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<string, string|number>} 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<ChatMessage>}
*/
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}<br>${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,
});
}
+209
View File
@@ -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]) =>
`<tr><td>${foundry.utils.escapeHTML(label)}</td><td>${foundry.utils.escapeHTML(value)}</td></tr>`,
)
.join("");
return `<table class="dg-roll-target-tooltip"><tbody>${body}</tbody></table>`;
}
/**
* @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}<br><br>${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,
);
}
}
+18
View File
@@ -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.
*
+169
View File
@@ -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(
"<br><br>",
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 });
});
}
+1 -1
View File
@@ -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",
@@ -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}}
</label>
<div class="custom-skill-controls-slot">
<div class="custom-skill-play-controls">
<input class="percentile-skill-input"
<input class="percentile-skill-input {{block.rollTargetDisplayClass}}"
type="text"
name="system.typedSkills.{{block.key}}.proficiency"
value="{{block.proficiency}}"
@@ -108,14 +103,14 @@
{{#if_eq block.type "training" }}
<div class="item flexrow flex-group-left flex-thin-border special-training-box">
<div class="flexrow">
<label class="{{if_gt block.targetNumber 0 'rollable' 'not-rollable'}} skill-label"
<label class="{{if_gt block.targetNumber 0 'rollable' 'not-rollable'}} skill-label {{block.rollTargetDisplayClass}}"
data-action="roll"
data-key="{{block.key}}"
data-name="{{block.name}}"
data-rolltype="special-training"
{{#if block.attributeTooltip }}
{{#if block.tooltip }}
{{#unless (eq ../../skillTooltipDisplay "never") }}
data-tooltip="{{localize block.attributeTooltip}}"
data-tooltip="{{block.tooltip}}"
{{/unless }}
{{/if }}>
{{block.displayLabel}}
+2 -2
View File
@@ -8,14 +8,14 @@
<div>???</div>
</div>
{{else}}
<input class="centered
<input class="centered {{actor.system.sanity.rollTargetDisplayClass}}
{{#if actor.system.sanity.breakingPointHit }}
breaking-point-hit{{/if }}"
type="text"
name="system.sanity.value"
value="{{actor.system.sanity.value }}"
data-dtype="Number"
data-tooltip="{{localize 'DG.Tooltip.CurrentSanityPartOne' }}{{actor.system.sanity.currentBreakingPoint }}{{localize 'DG.Tooltip.CurrentSanityPartTwo' }}" />
data-tooltip="{{actor.system.sanity.tooltip}}" />
<span>/</span>
<div class="max-resource-value {{actor.system.sanity.maxDisplayClass}}"
data-tooltip="{{localize 'DG.Tooltip.MaximumSanity'}}">
+3 -3
View File
@@ -24,18 +24,18 @@
{{#each actor.system.statistics as |statistic key| }}
<div class="flex-group-center flex-mid-border">
<div>
<div class="physical-stats-outer-grid-3col">
<div class="physical-stats-outer-grid-3col agent-left-bar-stat-grid">
<label class="rollable resource-label"
data-action="roll"
data-rolltype="stat"
data-key="{{key}}"
data-tooltip="{{localize (concat 'DG.Attributes.Tooltip.' key)}}"
data-tooltip="{{statistic.tooltip}}"
for="system.statistics.{{key}}.value">
{{localize (concat 'DG.Attributes.' key)}}
</label>
{{> "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
+1 -1
View File
@@ -27,7 +27,7 @@
{{localizeWithFallback (concat "DG.Skills." skill.key) skill.label }}
</label>
<input class="percentile-skill-input"
<input class="percentile-skill-input {{skill.rollTargetDisplayClass}}"
type="text"
name="system.skills.{{skill.key}}.proficiency"
value="{{skill.proficiency}}"
+7 -7
View File
@@ -1,6 +1,6 @@
<form class="add-profession-skill-dialog">
<div class="form-group">
<label for="profession-skill-select">{{localize "DG.ItemWindow.Profession.Skill"}}</label>
<div class="dg-dialog dg-dialog--add-profession-skill">
<div class="dg-dialog__field form-group">
<label class="dg-dialog__field-label" for="profession-skill-select">{{localize "DG.ItemWindow.Profession.Skill"}}</label>
<select id="profession-skill-select" name="skillCatalogId">
<option value="">{{localize "DG.ItemWindow.Profession.SelectSkill"}}</option>
{{#each catalog}}
@@ -8,8 +8,8 @@
{{/each}}
</select>
</div>
<div class="form-group">
<label for="profession-skill-rating">{{localize "DG.ItemWindow.Profession.Rating"}}</label>
<div class="dg-dialog__field form-group">
<label class="dg-dialog__field-label" for="profession-skill-rating">{{localize "DG.ItemWindow.Profession.Rating"}}</label>
<input id="profession-skill-rating"
type="number"
name="skillRating"
@@ -19,7 +19,7 @@
</div>
<div class="form-group stacked profession-typed-label-group hidden">
<div class="profession-typed-name-field">
<label for="profession-typed-label">{{localize "DG.ItemWindow.Profession.SkillType"}}</label>
<label class="dg-dialog__field-label" for="profession-typed-label">{{localize "DG.ItemWindow.Profession.SkillType"}}</label>
<input id="profession-typed-label"
type="text"
name="typedSkillLabel"
@@ -36,4 +36,4 @@
<p class="profession-choose-one-hint notes">{{localize "DG.ItemWindow.Profession.ChooseOneHint"}}</p>
</div>
</div>
</form>
</div>
+157 -130
View File
@@ -1,136 +1,163 @@
<form class="add-profession-dialog">
<section class="add-profession-skills-section">
<div class="add-profession-skills-columns-header">
<div class="add-profession-skill-col-header">
<span>{{localize "DG.Profession.Dialog.Skill"}}</span>
<span>{{localize "DG.Profession.Dialog.Value"}}</span>
</div>
<div class="add-profession-skill-col-header">
<span>{{localize "DG.Profession.Dialog.Skill"}}</span>
<span>{{localize "DG.Profession.Dialog.Value"}}</span>
</div>
<div class="add-profession-skill-col-header">
<span>{{localize "DG.Profession.Dialog.Skill"}}</span>
<span>{{localize "DG.Profession.Dialog.Value"}}</span>
</div>
<div class="add-profession-skill-col-header">
<span>{{localize "DG.Profession.Dialog.Skill"}}</span>
<span>{{localize "DG.Profession.Dialog.Value"}}</span>
</div>
</div>
<div class="add-profession-skills-grid" data-profession-skills-body>
{{#each skillRows}}
<div class="add-profession-skill-entry{{#if isModified}} is-modified{{/if}}"
data-skill-row="{{rowId}}">
<span class="add-profession-skill-label">{{label}}</span>
<span class="add-profession-skill-value" data-skill-value>{{value}}%</span>
</div>
{{/each}}
</div>
</section>
{{#if automaticChooseOneRows.length}}
<section class="add-profession-choose-one-section">
<h3>{{localize "DG.Profession.Dialog.ChooseOneNamesHeader"}}</h3>
<div class="add-profession-choose-one-list">
{{#each automaticChooseOneRows}}
<label class="add-profession-choose-one-row">
<span class="add-profession-choose-one-label">{{localize "DG.Profession.Dialog.ChooseOneNameLabel" group=groupLabel}}</span>
<input type="text"
name="chooseOneLabel-{{key}}"
data-choose-one-key="{{key}}"
value=""
placeholder="{{localize 'DG.Profession.Dialog.SkillTypePlaceholder'}}" />
</label>
{{/each}}
</div>
</section>
{{/if}}
{{#if showOptionSkills}}
<section class="add-profession-options-section">
<h3>{{localize "DG.Profession.Dialog.SkillsOptionsHeader" picks=optionPicks}}</h3>
<div class="add-profession-option-list">
{{#each optionRows}}
<div class="add-profession-option-row" data-option-key="{{key}}">
<label>
<input type="checkbox"
name="optionSkillChecked"
value="{{key}}"
data-option-key="{{key}}"
data-is-choose-one="{{isChooseOne}}" />
<span class="option-skill-label">{{label}}</span>
<span class="option-skill-rating">({{rating}}%)</span>
</label>
{{#if isChooseOne}}
<input type="text"
class="option-choose-one-label"
name="chooseOneLabel-{{key}}"
data-choose-one-key="{{key}}"
placeholder="{{localize 'DG.Profession.Dialog.SkillTypePlaceholder'}}"
disabled />
{{/if}}
<div class="dg-dialog dg-dialog--character-creation">
<div class="dg-dialog__layout dg-dialog__layout--two-column">
<div class="dg-dialog__column dg-dialog__column--left">
<header class="character-creation-skill-totals-header">
<h3 class="dg-dialog__heading">{{localize "DG.Profession.Dialog.SkillTotalsHeader"}}</h3>
<p class="dg-dialog__intro">{{localize "DG.Profession.Dialog.SkillTotalsHint"}}</p>
</header>
<section class="character-creation-skills-section">
<div class="character-creation-skills-columns-header">
<div class="character-creation-skill-col-header">
<span>{{localize "DG.Profession.Dialog.Skill"}}</span>
<span>{{localize "DG.Profession.Dialog.Value"}}</span>
</div>
{{/each}}
</div>
</section>
{{/if}}
<div class="character-creation-skill-col-header">
<span>{{localize "DG.Profession.Dialog.Skill"}}</span>
<span>{{localize "DG.Profession.Dialog.Value"}}</span>
</div>
</div>
<div class="character-creation-skills-grid" data-profession-skills-body>
{{#each skillRows}}
<div class="dg-dialog__skill-entry{{#if isModified}} is-modified{{/if}}"
data-skill-row="{{rowId}}">
<span class="dg-dialog__skill-label"
{{#if tooltip}}data-tooltip="{{tooltip}}"{{/if}}>{{label}}</span>
<span class="dg-dialog__skill-value" data-skill-value>{{value}}%</span>
</div>
{{/each}}
</div>
</section>
<section class="add-profession-bonus-section">
<h3>{{localize "DG.Profession.Dialog.BonusSkillsHeader"}}</h3>
<div class="add-profession-bonus-grid">
{{#each bonusSlots}}
<div class="add-profession-bonus-slot" data-bonus-index="{{index}}">
<select name="bonusSkill-{{index}}" data-bonus-index="{{index}}">
<option value="">{{localize "DG.Profession.Dialog.SelectSkill"}}</option>
{{#each ../catalog}}
<option value="{{id}}">{{label}}</option>
{{#if automaticChooseOneRows.length}}
<section class="character-creation-choose-one-section">
<h3 class="dg-dialog__heading character-creation-section-heading">{{localize "DG.Profession.Dialog.ChooseOneNamesHeader"}}</h3>
<div class="character-creation-choose-one-list">
{{#each automaticChooseOneRows}}
<label class="character-creation-choose-one-row dg-dialog__field">
<span class="dg-dialog__field-label">{{localize "DG.Profession.Dialog.ChooseOneNameLabel" group=groupLabel}}</span>
<input type="text"
name="chooseOneLabel-{{key}}"
data-choose-one-key="{{key}}"
value=""
placeholder="{{localize 'DG.Profession.Dialog.SkillTypePlaceholder'}}" />
</label>
{{/each}}
</select>
<input type="text"
class="bonus-typed-label hidden"
name="bonusTypedLabel-{{index}}"
data-bonus-typed-index="{{index}}"
placeholder="{{localize 'DG.Profession.Dialog.SkillTypePlaceholder'}}" />
</div>
{{/each}}
</div>
</section>
<div class="add-profession-messages" data-profession-messages>
{{#each validationMessages}}
<p class="validation-error">{{this}}</p>
{{/each}}
{{#each capWarnings}}
<p class="cap-warning">{{warning}}</p>
{{/each}}
</div>
{{#if bondCount}}
<section class="add-profession-bonds-section">
<h3>{{localize "DG.Profession.Dialog.BondsHeader"}}</h3>
<div class="add-profession-bonds-grid">
{{#each bondRows}}
<div class="add-profession-bond-row">
<span class="bond-row-label">{{localize "DG.Profession.Dialog.Bond"}} {{number}}</span>
<label class="add-profession-bond-field">
<span class="add-profession-bond-field-label">{{localize "DG.Bonds.Name"}}</span>
<input type="text" name="bondName-{{index}}" />
</label>
<label class="add-profession-bond-field">
<span class="add-profession-bond-field-label">{{localize "DG.Bonds.Relationship"}}</span>
<input type="text" name="bondRelationship-{{index}}" />
</label>
<label class="add-profession-bond-field add-profession-bond-field-score">
<span class="add-profession-bond-field-label">{{localize "DG.Bonds.Score"}}</span>
<input type="number"
name="bondScore-{{index}}"
value="{{chaScore}}"
disabled />
</label>
</div>
</section>
{{/if}}
</div>
<div class="dg-dialog__column dg-dialog__column--right">
<nav class="tabs sheet-tabs character-creation-tabs" data-character-creation-tabs>
{{#if showSkillOptionsTab}}
<a class="item{{#if defaultTabSkillOptions}} active{{/if}}"
data-action="tab"
data-tab="skill-options">{{localize "DG.Profession.Dialog.Tab.SkillOptions"}}</a>
{{/if}}
<a class="item{{#if defaultTabBonus}} active{{/if}}"
data-action="tab"
data-tab="bonus-skills">{{localize "DG.Profession.Dialog.Tab.BonusSkills"}}</a>
<a class="item"
data-action="tab"
data-tab="bonds">{{localize "DG.Profession.Dialog.Tab.Bonds"}}</a>
</nav>
<div class="dg-dialog__tab-panels">
{{#if showSkillOptionsTab}}
<div class="tab{{#if defaultTabSkillOptions}} active{{/if}}"
data-tab="skill-options">
<h3 class="dg-dialog__heading character-creation-section-heading">{{localize "DG.Profession.Dialog.SkillsOptionsHeader" picks=optionPicks}}</h3>
<div class="character-creation-option-list">
{{#each optionColumns}}
<div class="character-creation-option-column">
{{#each this}}
<div class="character-creation-option-row" data-option-key="{{key}}">
<label>
<input type="checkbox"
name="optionSkillChecked"
value="{{key}}"
data-option-key="{{key}}"
data-is-choose-one="{{isChooseOne}}" />
<span class="option-skill-label">{{label}}</span>
<span class="option-skill-rating">({{rating}}%)</span>
</label>
{{#if isChooseOne}}
<input type="text"
class="option-choose-one-label"
name="chooseOneLabel-{{key}}"
data-choose-one-key="{{key}}"
data-option-choose-one="true"
placeholder="{{localize 'DG.Profession.Dialog.SkillTypePlaceholder'}}"
disabled />
{{/if}}
</div>
{{/each}}
</div>
{{/each}}
</div>
</div>
{{/if}}
<div class="tab{{#if defaultTabBonus}} active{{/if}}"
data-tab="bonus-skills">
<p class="dg-dialog__intro character-creation-bonus-intro">{{localize "DG.Profession.Dialog.BonusSkillsIntro"}}</p>
<div class="character-creation-bonus-grid">
{{#each bonusSlots}}
<div class="character-creation-bonus-slot" data-bonus-index="{{index}}">
<select name="bonusSkill-{{index}}" data-bonus-index="{{index}}">
<option value="">{{localize "DG.Profession.Dialog.SelectSkill"}}</option>
{{#each ../catalog}}
<option value="{{id}}">{{label}}</option>
{{/each}}
</select>
<input type="text"
class="bonus-typed-label hidden"
name="bonusTypedLabel-{{index}}"
data-bonus-typed-index="{{index}}"
placeholder="{{localize 'DG.Profession.Dialog.SkillTypePlaceholder'}}" />
</div>
{{/each}}
</div>
</div>
<div class="tab"
data-tab="bonds">
<p class="dg-dialog__intro character-creation-bonds-intro">{{localize "DG.Profession.Dialog.BondsIntro"}}</p>
<div class="character-creation-bonds-list">
{{#each bondRows}}
<div class="character-creation-bond-row">
<label class="character-creation-bond-field">
<span class="character-creation-bond-field-label dg-dialog__field-label">{{localize "DG.Profession.Dialog.BondFieldLabel" number=number}}</span>
<input type="text" name="bondName-{{index}}" />
</label>
<label class="character-creation-bond-field">
<span class="character-creation-bond-field-label dg-dialog__field-label">{{localize "DG.Bonds.Relationship"}}</span>
<input type="text" name="bondRelationship-{{index}}" />
</label>
<label class="character-creation-bond-field character-creation-bond-field-score">
<span class="character-creation-bond-field-label dg-dialog__field-label">{{localize "DG.Bonds.Score"}}</span>
<input type="number"
name="bondScore-{{index}}"
value="{{chaScore}}"
disabled />
</label>
</div>
{{/each}}
</div>
</div>
</div>
<div class="dg-dialog__messages" data-profession-messages>
{{#each validationMessages}}
<p class="dg-dialog__message--error">{{this}}</p>
{{/each}}
{{#each capWarnings}}
<p class="dg-dialog__message--warning">{{warning}}</p>
{{/each}}
{{#each atCapMessages}}
<p class="dg-dialog__message--error">{{this}}</p>
{{/each}}
</div>
</section>
{{/if}}
</form>
</div>
</div>
</div>
@@ -1,7 +1,7 @@
<div>
<label>{{{localize "DG.Skills.ApplySkillImprovements.DialogLabel" formula=baseFormula}}}</label>
<div class="dg-dialog dg-dialog--apply-skill-improvements">
<label class="dg-dialog__intro">{{{localize "DG.Skills.ApplySkillImprovements.DialogLabel" formula=baseFormula}}}</label>
<hr>
<span>
<span class="dg-dialog__intro">
{{{localize "DG.Skills.ApplySkillImprovements.DialogEffectsFollowing" skillList=failedSkillNames}}}
</span>
</div>
+4 -4
View File
@@ -1,11 +1,11 @@
<form class="assign-stats-dialog">
<div class="dg-dialog dg-dialog--assign-stats">
<p class="assign-stats-remaining" data-points-remaining>
{{localize "DG.ProfessionSetup.AssignStats.PointsRemaining" remaining=remaining}}
</p>
<div class="assign-stats-rows">
{{#each stats}}
<label class="assign-stats-row">
<span class="assign-stats-label">{{label}}</span>
<label class="assign-stats-row dg-dialog__field">
<span class="assign-stats-label dg-dialog__field-label">{{label}}</span>
<input type="number"
name="stat-{{key}}"
data-stat-key="{{key}}"
@@ -16,4 +16,4 @@
</label>
{{/each}}
</div>
</form>
</div>
+4 -4
View File
@@ -1,6 +1,6 @@
<form class="exhaust-agent-dialog">
<div class="form-group">
<label for="exhaustion-penalty">{{localize "DG.Physical.ExhaustionPenalty"}}</label>
<div class="dg-dialog dg-dialog--exhaust-agent">
<div class="dg-dialog__field form-group">
<label class="dg-dialog__field-label" for="exhaustion-penalty">{{localize "DG.Physical.ExhaustionPenalty"}}</label>
<input id="exhaustion-penalty"
type="number"
name="exhaustionPenalty"
@@ -9,4 +9,4 @@
max="0"
data-dtype="Number" />
</div>
</form>
</div>
+2 -2
View File
@@ -1,4 +1,4 @@
<form>
<div class="dg-dialog dg-dialog--modify-damage-roll">
<div class="damage-modify-label-grid">
<span>{{localize 'DG.ModifyDamageRollDialogue.Weapon'}}</span>
<span>{{data.label}}</span>
@@ -16,4 +16,4 @@
{{selectOptions (getAvailableRollModes) selected=(getDefaultRollMode) localize=true}}
</select>
</div>
</form>
</div>
+5 -5
View File
@@ -1,11 +1,11 @@
<div class="modify-roll-dialog">
<div class="dg-dialog dg-dialog--modify-roll modify-roll-dialog">
<div class="form-group">
<label>{{localize "DG.ModifySkillRollDialogue.Test"}}</label>
<span class="modify-roll-readonly">{{data.label}}</span>
</div>
<div class="form-group">
<label>{{localize "DG.ModifySkillRollDialogue.OriginalTarget"}}</label>
<span class="modify-roll-readonly">
<span class="modify-roll-readonly {{data.targetDisplayClass}}">
{{#if data.hideTarget}}??{{else}}{{data.originalTarget}}%{{/if}}
</span>
</div>
@@ -17,7 +17,7 @@
</div>
<section class="modify-roll-section modify-roll-quick-section">
<h3>{{localize "DG.ModifySkillRollDialogue.QuickModifiersHeader"}}</h3>
<h3 class="dg-dialog__heading">{{localize "DG.ModifySkillRollDialogue.QuickModifiersHeader"}}</h3>
<p class="modify-roll-hint">{{localize "DG.ModifySkillRollDialogue.QuickModifiersHint"}}</p>
<div class="modify-roll-quick-grid">
{{#each quickModifiers}}
@@ -30,7 +30,7 @@
</section>
<section class="modify-roll-section modify-roll-custom-section">
<h3>{{localize "DG.ModifySkillRollDialogue.CustomModifierHeader"}}</h3>
<h3 class="dg-dialog__heading">{{localize "DG.ModifySkillRollDialogue.CustomModifierHeader"}}</h3>
<p class="modify-roll-hint">{{localize "DG.ModifySkillRollDialogue.CustomModifierHint"}}</p>
<div class="modify-roll-custom-row">
<input type="number"
@@ -45,4 +45,4 @@
</button>
</div>
</section>
</div>
</div>
+3 -3
View File
@@ -1,3 +1,3 @@
<form class="pick-statistics-dialog">
<p class="pick-statistics-intro">{{localize "DG.ProfessionSetup.PickStatistics.Intro"}}</p>
</form>
<div class="dg-dialog dg-dialog--pick-statistics">
<p class="dg-dialog__intro">{{localize "DG.ProfessionSetup.PickStatistics.Intro"}}</p>
</div>
+3 -3
View File
@@ -1,4 +1,4 @@
<form class="roll-stats-dialog">
<div class="dg-dialog dg-dialog--roll-stats">
<div class="roll-stats-layout">
<section class="roll-stats-assignment-section">
{{#each stats}}
@@ -19,5 +19,5 @@
</div>
</section>
</div>
<p class="roll-stats-pool-hint">{{localize "DG.ProfessionSetup.RollStats.PoolHeader"}}</p>
</form>
<p class="roll-stats-pool-hint dg-dialog__intro">{{localize "DG.ProfessionSetup.RollStats.PoolHeader"}}</p>
</div>
+18 -16
View File
@@ -1,18 +1,20 @@
<div class="stimulants-dialog">
<div class="stimulants-option">
<button type="submit"
data-action="regular"
class="stimulants-choice-btn">
{{localize "DG.Physical.StimulantsRegular"}}
</button>
<p class="stimulants-explanation">{{localize "DG.Physical.StimulantsRegularExplanation"}}</p>
</div>
<div class="stimulants-option">
<button type="submit"
data-action="hard"
class="stimulants-choice-btn">
{{localize "DG.Physical.StimulantsHard"}}
</button>
<p class="stimulants-explanation">{{localize "DG.Physical.StimulantsHardExplanation"}}</p>
<div class="dg-dialog dg-dialog--stimulants">
<div class="stimulants-dialog">
<div class="stimulants-option">
<button type="submit"
data-action="regular"
class="stimulants-choice-btn">
{{localize "DG.Physical.StimulantsRegular"}}
</button>
<p class="stimulants-explanation dg-dialog__intro">{{localize "DG.Physical.StimulantsRegularExplanation"}}</p>
</div>
<div class="stimulants-option">
<button type="submit"
data-action="hard"
class="stimulants-choice-btn">
{{localize "DG.Physical.StimulantsHard"}}
</button>
<p class="stimulants-explanation dg-dialog__intro">{{localize "DG.Physical.StimulantsHardExplanation"}}</p>
</div>
</div>
</div>
@@ -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'}}" />