diff --git a/lang/en.json b/lang/en.json index c62d621..2b6d02c 100644 --- a/lang/en.json +++ b/lang/en.json @@ -168,6 +168,7 @@ "DG.Roll.For": "For", "DG.Roll.Damage": "Damage", "DG.Roll.Roll": "Roll", + "DG.Roll.CannotRollStat": "A statistic of 0 cannot be rolled.", "DG.Physical.Wounds": "Wounds", "DG.Physical.FirstAidLabel": "First Aid Has Already Been Attempted: ", "DG.Physical.ExhaustionPenalty": "Penalty:", @@ -237,7 +238,7 @@ "DG.Gear.Armor": "ARMOR", "DG.Gear.OtherGear": "OTHER GEAR", "DG.Gear.Name": "Name", - "DG.Gear.DamageOrLethality": "Damage/Lethality", + "DG.Gear.DamageOrLethality": "Damage/ Lethality", "DG.Gear.ArmorPiercing": "Armor Piercing", "DG.Gear.ArmorRating": "Armor Rating", "DG.Gear.Equipped": "Equipped", @@ -328,6 +329,13 @@ "DG.ModifyDamageRollDialogue.Weapon": "Weapon:", "DG.ModifyDamageRollDialogue.OuterModifierTooltip": "Use this field to modify the outer part of the formula, such as to double the result.", "DG.ModifyDamageRollDialogue.InnerModifierTooltip": "Use this field to add a flat modifier such as '+1' or another dice term such as '+1d4'.", + "DG.DamageOrLethalityRoll.Title": "Roll Damage or Lethality", + "DG.DamageOrLethalityRoll.Intro": "This weapon has both damage and lethality. Choose which to roll.", + "DG.DamageOrLethalityRoll.RollDamage": "Roll Damage", + "DG.DamageOrLethalityRoll.RollLethality": "Roll Lethality", + "DG.EditStats.Title": "Edit Statistics", + "DG.EditStats.Button": "Edit Statistics", + "DG.EditStats.DistinguishingFeature": "Distinguishing Feature", "DG.Roll.Inhuman": "Inhuman", "DG.Tooltip.BreakPoint": "The current breaking point, reset when current sanity falls to or below this value. The starting breaking point is SAN - POW.", "DG.Skills.ritual": "Ritual", diff --git a/module/MODULES.md b/module/MODULES.md index e111154..69c1e35 100644 --- a/module/MODULES.md +++ b/module/MODULES.md @@ -4,19 +4,19 @@ Entry point: [`deltagreen.js`](deltagreen.js) (registered in `system.json`). ## Folder responsibilities -| Path | Purpose | -| ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [`config/`](config/) | System constants (`DG`, skill list, template paths) | -| [`data/`](data/) | TypeDataModels (`actor/`, `item/`, `derived/`) | -| [`active-effect/`](active-effect/) | Active effect metadata, data model, document class, runtime sync | +| Path | Purpose | +| ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [`config/`](config/) | System constants (`DG`, skill list, template paths) | +| [`data/`](data/) | TypeDataModels (`actor/`, `item/`, `derived/`) | +| [`active-effect/`](active-effect/) | Active effect metadata, data model, document class, runtime sync | | [`profession/`](profession/) | Profession catalog, validation, character-creation math, deferred commit ([`commit-character-creation.js`](profession/commit-character-creation.js)), Damaged Veteran modifiers ([`damaged-veteran-modifiers.js`](profession/damaged-veteran-modifiers.js)), disorder IDs ([`disorders.js`](profession/disorders.js)) | -| [`roll/`](roll/) | Dice roll subclasses, roll pipeline, and roll UX. [`roll/roll.js`](roll/roll.js) is the public API. Internal Roll subclasses live in [`roll/classes/`](roll/classes/). | -| [`sheets/`](sheets/) | ApplicationV2 sheets and mixins | -| [`applications/`](applications/) | Standalone dialogs, chargen orchestrator ([`profession-setup-flow.js`](applications/profession-setup-flow.js)), shared dialog UI ([`character-creation-ui.js`](applications/character-creation-ui.js)), and dialog factory ([`dg-dialog.js`](applications/dg-dialog.js)) | -| [`actor/`](actor/), [`item/`](item/) | Document subclasses | -| [`chat/`](chat/) | Chat cards ([`dg-chat-card.js`](chat/dg-chat-card.js)) and inline actions | -| [`integrations/`](integrations/) | Optional module hooks (Dice So Nice) | -| [`utils/`](utils/) | Generic helpers only (Handlebars, migration, skill layout/tooltips) | +| [`roll/`](roll/) | Dice roll subclasses, roll pipeline, and roll UX. [`roll/roll.js`](roll/roll.js) is the public API. Internal Roll subclasses live in [`roll/classes/`](roll/classes/). | +| [`sheets/`](sheets/) | ApplicationV2 sheets and mixins | +| [`applications/`](applications/) | Standalone dialogs, chargen orchestrator ([`profession-setup-flow.js`](applications/profession-setup-flow.js)), shared dialog UI ([`character-creation-ui.js`](applications/character-creation-ui.js)), and dialog factory ([`dg-dialog.js`](applications/dg-dialog.js)) | +| [`actor/`](actor/), [`item/`](item/) | Document subclasses | +| [`chat/`](chat/) | Chat cards ([`dg-chat-card.js`](chat/dg-chat-card.js)) and inline actions | +| [`integrations/`](integrations/) | Optional module hooks (Dice So Nice) | +| [`utils/`](utils/) | Generic helpers only (Handlebars, migration, skill layout/tooltips) | Root-level modules: [`settings.js`](settings.js), [`templates.js`](templates.js). diff --git a/module/applications/edit-stats.js b/module/applications/edit-stats.js index d4d05b7..cc9e7c1 100644 --- a/module/applications/edit-stats.js +++ b/module/applications/edit-stats.js @@ -1,4 +1,5 @@ import DG, { BASE_TEMPLATE_PATH } from "../config/index.js"; +import { applyDialogTheme } from "./dg-dialog.js"; export default class ActorEditStatForm extends foundry.applications.api.HandlebarsApplicationMixin( foundry.applications.api.ApplicationV2, @@ -11,9 +12,13 @@ export default class ActorEditStatForm extends foundry.applications.api.Handleba /** @override */ static DEFAULT_OPTIONS = { tag: "form", - classes: [DG.ID, "edit-stat-form"], - window: { title: "Edit statistics form", resizable: true }, - position: { width: 400, height: 200 }, + classes: [DG.ID, "edit-stat-form", "dg-dialog-app"], + window: { + title: "DG.EditStats.Title", + resizable: true, + contentClasses: ["standard-form"], + }, + position: { width: 600, height: "auto" }, actions: {}, form: { handler: this.formHandler, @@ -28,11 +33,18 @@ export default class ActorEditStatForm extends foundry.applications.api.Handleba }, }); + /** @override */ + async _onRender(context, options) { + await super._onRender(context, options); + applyDialogTheme(this); + } + /** @override */ async _prepareContext(options) { return { ...(await super._prepareContext(options)), actor: this.actor, + statKeys: DG.statistics, sourceStatistics: this.actor._source?.system?.statistics ?? this.actor.system.statistics, }; diff --git a/module/roll/classes/dg-percentile-roll.js b/module/roll/classes/dg-percentile-roll.js index 57106a6..a247b81 100644 --- a/module/roll/classes/dg-percentile-roll.js +++ b/module/roll/classes/dg-percentile-roll.js @@ -255,6 +255,20 @@ export class DGPercentileRoll extends DGRoll { return { target, localizedKey, skillPath }; } + /** + * i18n key for a user-facing warning when this roll must not be made, or null if allowed. + * @returns {string|null} + */ + get blockedRollMessage() { + if (this.type !== "stat") return null; + + const x5 = Number(this.target); + if (!Number.isFinite(x5) || x5 < 1) { + return "DG.Roll.CannotRollStat"; + } + return null; + } + /** * Create label based on result of roll * @@ -392,7 +406,7 @@ export class DGPercentileRoll extends DGRoll { const { rollTargetModifier } = this; - if (!this.target || Number.isNaN(this.target)) { + if (this.target == null || Number.isNaN(this.target)) { return null; } diff --git a/module/roll/roll-dialogs.js b/module/roll/roll-dialogs.js index 98c36b0..c45c4fa 100644 --- a/module/roll/roll-dialogs.js +++ b/module/roll/roll-dialogs.js @@ -7,6 +7,8 @@ const PERCENTILE_ROLL_DIALOG_TEMPLATE = "systems/deltagreen/templates/dialog/modify-percentile-roll.html"; const DAMAGE_ROLL_DIALOG_TEMPLATE = "systems/deltagreen/templates/dialog/modify-damage-roll.html"; +const DAMAGE_OR_LETHALITY_ROLL_DIALOG_TEMPLATE = + "systems/deltagreen/templates/dialog/damage-or-lethality-roll.html"; const QUICK_MODIFIER_PRESETS = [ { @@ -215,3 +217,41 @@ export async function showDamageRollModifyDialog({ itemName, formula }) { ], }); } + +/** + * @param {object} options + * @param {string} options.itemName + * @returns {Promise<"damage"|"lethality"|void>} + */ +export async function showDamageOrLethalityChoiceDialog({ itemName }) { + const content = await renderTemplate( + DAMAGE_OR_LETHALITY_ROLL_DIALOG_TEMPLATE, + { + itemName, + }, + ); + + return showDgDialog({ + modifier: "damage-or-lethality", + content, + window: { + title: game.i18n.localize("DG.DamageOrLethalityRoll.Title"), + }, + close: () => null, + buttons: [ + { + default: true, + label: game.i18n.localize("DG.DamageOrLethalityRoll.RollDamage"), + action: "damage", + icon: "fas fa-dice", + callback: () => "damage", + }, + { + label: game.i18n.localize("DG.DamageOrLethalityRoll.RollLethality"), + action: "lethality", + icon: "fas fa-skull-crossbones", + callback: () => "lethality", + }, + ], + }); +} diff --git a/module/roll/roll.js b/module/roll/roll.js index d622b66..e1ff5c5 100644 --- a/module/roll/roll.js +++ b/module/roll/roll.js @@ -100,6 +100,11 @@ export async function processDGRoll(event, roll) { const shiftKey = event?.shiftKey ?? false; const which = event?.which ?? 0; + if (roll instanceof DGPercentileRoll && roll.blockedRollMessage) { + ui.notifications.warn(roll.blockedRollMessage, { localize: true }); + return; + } + if (shiftKey || which === 3) { if (!(roll instanceof DGSanityDamageRoll)) { const dialogData = await roll.showDialog(); diff --git a/module/sheets/mixins/roll-sheet-mixin.js b/module/sheets/mixins/roll-sheet-mixin.js index 7367ac4..cb91e39 100644 --- a/module/sheets/mixins/roll-sheet-mixin.js +++ b/module/sheets/mixins/roll-sheet-mixin.js @@ -1,4 +1,5 @@ import { getDGRollToken } from "../../chat/dg-chat-card.js"; +import { showDamageOrLethalityChoiceDialog } from "../../roll/roll-dialogs.js"; import { createDGRollFromDataset, processDGRoll } from "../../roll/roll.js"; /** @param {typeof foundry.applications.api.ApplicationV2} Base */ @@ -36,11 +37,27 @@ export default function RollSheetMixin(Base) { * @private */ static async _onRoll(event, target) { - if (target.classList.contains("not-rollable") || event.which === 2) + if (event.which === 2) return; + + if (target.classList.contains("not-rollable")) { + if (target.dataset.rolltype === "stat") { + ui.notifications.warn("DG.Roll.CannotRollStat", { localize: true }); + } return; + } const item = this.actor.items.get(target.dataset.iid); - const roll = createDGRollFromDataset(target.dataset, { + const rollDataset = { ...target.dataset }; + + if (rollDataset.rolltype === "damage-or-lethality") { + const choice = await showDamageOrLethalityChoiceDialog({ + itemName: item?.name ?? "", + }); + if (!choice) return; + rollDataset.rolltype = choice; + } + + const roll = createDGRollFromDataset(rollDataset, { actor: this.actor, item, element: target, diff --git a/module/utils/register-helpers.js b/module/utils/register-helpers.js index 4448418..6a44602 100644 --- a/module/utils/register-helpers.js +++ b/module/utils/register-helpers.js @@ -122,4 +122,24 @@ export default function registerHandlebarsHelpers() { return result; }); + + Handlebars.registerHelper("hasWeaponDamage", (damage) => { + const trimmed = String(damage ?? "").trim(); + return trimmed !== "" && trimmed !== "0"; + }); + + Handlebars.registerHelper("hasWeaponLethality", (lethality) => { + const value = Number(lethality); + return Number.isFinite(value) && value > 0; + }); + + Handlebars.registerHelper("hasWeaponDamageAndLethality", (item) => { + const damage = item?.system?.damage; + const lethality = item?.system?.lethality; + const trimmed = String(damage ?? "").trim(); + const hasDamage = trimmed !== "" && trimmed !== "0"; + const hasLethality = + Number.isFinite(Number(lethality)) && Number(lethality) > 0; + return hasDamage && hasLethality; + }); } diff --git a/module/utils/roll-target-tooltip.js b/module/utils/roll-target-tooltip.js index 4dfd37e..8fb1206 100644 --- a/module/utils/roll-target-tooltip.js +++ b/module/utils/roll-target-tooltip.js @@ -177,6 +177,22 @@ export function appendRollTargetTooltipSection( return `${existingTooltip}

${appendix}`; } +/** + * @param {string} existingTooltip + * @param {string} [feature] + * @returns {string} + */ +export function appendDistinguishingFeatureSection(existingTooltip, feature) { + const trimmed = String(feature ?? "").trim(); + if (!trimmed) return existingTooltip; + + const label = game.i18n.localize("DG.EditStats.DistinguishingFeature"); + const escaped = foundry.utils.escapeHTML(trimmed); + const section = `

${foundry.utils.escapeHTML(label)}: ${escaped}`; + if (!existingTooltip) return section; + return `${existingTooltip}${section}`; +} + /** * Stat and sanity roll-target tooltips for the agent sheet left bar (not skills). * @@ -191,8 +207,12 @@ export function prepareAgentStatSanityTooltips(actor) { 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( + const withFeature = appendDistinguishingFeatureSection( existing, + stat.distinguishing_feature, + ); + stat.tooltip = appendRollTargetTooltipSection( + withFeature, actor, "system.rollTarget.statistics", base, diff --git a/scss/_utilities.scss b/scss/_utilities.scss index e6528a2..bede06a 100644 --- a/scss/_utilities.scss +++ b/scss/_utilities.scss @@ -69,7 +69,9 @@ } button.rollable.inline-roll { - display: inline; + display: inline-block; + width: max-content; + max-width: 100%; margin: 0; padding: 0; border: none; @@ -78,6 +80,8 @@ button.rollable.inline-roll { color: var(--dg-ink); line-height: inherit; vertical-align: baseline; + text-align: inherit; + box-sizing: border-box; > i { color: inherit; diff --git a/scss/deltagreen/_agent-sheet.scss b/scss/deltagreen/_agent-sheet.scss index 6f7382f..ba2a69f 100644 --- a/scss/deltagreen/_agent-sheet.scss +++ b/scss/deltagreen/_agent-sheet.scss @@ -1,6 +1,7 @@ .agent-sheet { --dg-agent-section-gap: 8px; --dg-agent-list-row-border: 1px solid rgba(33, 33, 33, 0.25); + --weapons-col-damage-trail: 60px; .window-content { display: grid; @@ -794,10 +795,42 @@ .tab.combat .items-list .item:not(.item-header) { display: grid; grid-template-columns: - 24px minmax(0, 1fr) minmax(4rem, auto) minmax(3rem, auto) + 24px minmax(0, 1fr) + minmax(0, calc(4.25rem + var(--weapons-col-damage-trail))) + minmax(0, 3.5rem) 86px; align-items: center; - column-gap: 5px; + column-gap: 10px; + } + + .tab.combat .items-list .weapons-col-damage, + .tab.combat .items-list .weapons-col-armor-piercing { + display: flex; + justify-content: center; + align-items: center; + min-width: 0; + text-align: center; + white-space: normal; + line-height: 1.2; + overflow-wrap: break-word; + box-sizing: border-box; + } + + .tab.combat .items-list .weapons-col-damage { + padding: 0 calc(2px + var(--weapons-col-damage-trail)) 0 2px; + } + + .tab.combat .items-list .weapons-col-armor-piercing { + padding: 0 2px; + } + + .tab.combat .items-list .weapons-col-damage[data-action="roll"], + .tab.gear .items-list .weapons-col-damage[data-action="roll"] { + cursor: pointer; + flex-wrap: nowrap; + white-space: nowrap; + overflow-wrap: normal; + gap: 0.15em; } .tab.combat .items-list .item:not(.item-header) > .flexrow { @@ -840,8 +873,35 @@ margin-top: var(--dg-agent-section-gap); } - .tab.gear .items-list .centered-item-property { - flex: 0 0 auto; + .tab.gear .items-list .weapons-col-damage { + flex: 0 0 calc(4.25rem + var(--weapons-col-damage-trail)); + max-width: calc(4.25rem + var(--weapons-col-damage-trail)); + } + + .tab.gear .items-list .weapons-col-armor-piercing { + flex: 0 0 3.5rem; + max-width: 3.5rem; + } + + .tab.gear .items-list .weapons-col-damage, + .tab.gear .items-list .weapons-col-armor-piercing { + display: flex; + justify-content: center; + align-items: center; + min-width: 0; + text-align: center; + white-space: normal; + line-height: 1.2; + overflow-wrap: break-word; + box-sizing: border-box; + } + + .tab.gear .items-list .weapons-col-damage { + padding: 0 calc(2px + var(--weapons-col-damage-trail)) 0 2px; + } + + .tab.gear .items-list .weapons-col-armor-piercing { + padding: 0 2px; } /* Personal tab */ @@ -945,9 +1005,21 @@ border-radius: 2.5px; background-color: rgba(0, 0, 0, 0.185); font-weight: bold; + color: var(--dg-ink); + + &:hover, + &:focus-visible { + color: var(--color-text-dark-header, #406539); + text-decoration: underline; + } + + &:visited { + color: var(--dg-ink); + } i { flex: 0 0 auto; + color: inherit; } span { diff --git a/scss/deltagreen/_character-sheet-cont.scss b/scss/deltagreen/_character-sheet-cont.scss index b71b850..0b8ab3b 100644 --- a/scss/deltagreen/_character-sheet-cont.scss +++ b/scss/deltagreen/_character-sheet-cont.scss @@ -191,16 +191,42 @@ div .exhausted-section { margin-bottom: 1rem; } -.tab.about .dg-about-links a { +.deltagreen .tab.about .dg-about-links a { display: inline-flex; align-items: center; gap: 0.5rem; width: fit-content; max-width: 100%; + color: var(--dg-ink); + + &:hover, + &:focus-visible { + color: var(--color-text-dark-header, #406539); + text-decoration: underline; + } + + &:visited { + color: var(--dg-ink); + } } -.tab.about .dg-about-links a i { +.deltagreen .tab.about .dg-about-links a i { flex: 0 0 auto; + color: inherit; +} + +.deltagreen .tab.about .dg-about-legal a { + color: var(--dg-ink); + + &:hover, + &:focus-visible { + color: var(--color-text-dark-header, #406539); + text-decoration: underline; + } + + &:visited { + color: var(--dg-ink); + } } .tab.about .dg-about-links a span { diff --git a/scss/deltagreen/_character-sheet.scss b/scss/deltagreen/_character-sheet.scss index 0f4f4f4..e764476 100644 --- a/scss/deltagreen/_character-sheet.scss +++ b/scss/deltagreen/_character-sheet.scss @@ -205,6 +205,23 @@ } } +.deltagreen[id^="dg-settings"] section.window-content { + color: var(--color-text-primary); + + .form-group > label { + color: var(--color-form-label); + } + + .hint { + color: var(--color-form-hint); + } + + input[type="checkbox"]:checked::before, + input[type="radio"]:checked::before { + color: var(--color-text-primary); + } +} + a.btn-tiny { height: 26px; margin-left: 5px; diff --git a/scss/deltagreen/_stats-edit.scss b/scss/deltagreen/_stats-edit.scss index 545154a..86f7542 100644 --- a/scss/deltagreen/_stats-edit.scss +++ b/scss/deltagreen/_stats-edit.scss @@ -5,8 +5,8 @@ /* Edit icon */ .stats-edit-btn { position: absolute; - top: -10px; - right: 0; + top: 50%; + right: -12px; width: 24px; height: 24px; @@ -26,12 +26,13 @@ transition: opacity 0.15s ease, transform 0.15s ease; + transform: translateY(-50%) scale(1); } /* Show on hover of whole stats block */ .stats-wrapper:hover .stats-edit-btn { opacity: 1; - transform: scale(1.05); + transform: translateY(-50%) scale(1.05); } /* Hover feedback */ @@ -39,12 +40,6 @@ background: var(--color-border-highlight, #c9a227); } -.edit-stat-form { - label { - color: var(--color-text-primary); - } -} - .physical-stats-outer-grid-2col { display: grid; grid-template-columns: 1fr 1fr; diff --git a/scss/dg-dialog/_dialogs.scss b/scss/dg-dialog/_dialogs.scss index 966829b..2c4b768 100644 --- a/scss/dg-dialog/_dialogs.scss +++ b/scss/dg-dialog/_dialogs.scss @@ -814,9 +814,15 @@ line-height: 1.4; } -.dg-dialog--damaged-veterans label.checkbox.damaged-veterans-option > input[type="radio"], -.dg-dialog--things-disorder label.checkbox.things-disorder-option > input[type="radio"], -.dg-dialog--hard-experience label.checkbox.hard-experience-bond-option > input[type="radio"] { +.dg-dialog--damaged-veterans + label.checkbox.damaged-veterans-option + > input[type="radio"], +.dg-dialog--things-disorder + label.checkbox.things-disorder-option + > input[type="radio"], +.dg-dialog--hard-experience + label.checkbox.hard-experience-bond-option + > input[type="radio"] { margin-top: 0.15rem; } @@ -847,9 +853,7 @@ overflow: hidden; } -.dg-dialog-app.dg-dialog-app--hard-experience - .dialog-form - > .dialog-content { +.dg-dialog-app.dg-dialog-app--hard-experience .dialog-form > .dialog-content { display: flex; flex-direction: column; flex: 1 1 auto; @@ -881,3 +885,46 @@ padding: 0; border: 0; } + +/* Edit statistics */ +.dg-dialog--edit-stats .edit-stats-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 0.75rem 0.5rem; +} + +.dg-dialog--edit-stats .edit-stats-stat-block { + display: flex; + flex-direction: column; + gap: 0.35rem; + min-width: 0; +} + +.dg-dialog--edit-stats .edit-stats-value-row { + display: flex; + flex-direction: column; + gap: 0.25rem; + align-items: stretch; +} + +.dg-dialog--edit-stats .edit-stats-value-row input { + width: 100%; + text-align: center; +} + +.dg-dialog--edit-stats .edit-stats-feature-row { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.dg-dialog--edit-stats .edit-stats-feature-row textarea { + width: 100%; + min-height: 2.5rem; + resize: vertical; +} + +/* Damage or lethality roll choice */ +.dg-dialog--damage-or-lethality .damage-or-lethality-weapon { + margin: 0.5rem 0 0; +} diff --git a/templates/actor/edit-stats.html b/templates/actor/edit-stats.html index ee4ccef..19757fc 100644 --- a/templates/actor/edit-stats.html +++ b/templates/actor/edit-stats.html @@ -1,26 +1,26 @@ -
- {{#each actor.system.statistics as |statistic key| }} -
-
-
- +
+
+ {{#each statKeys as |key| }} + {{#with (lookup @root.actor.system.statistics key) as |statistic| }} +
+
-
- -
+ data-dtype="Number" + min="0" /> + +
+ {{/with}} + {{/each}}
- {{/each}}
diff --git a/templates/actor/partials/attributes-grid-partial.html b/templates/actor/partials/attributes-grid-partial.html index 6d9a53c..72802f4 100644 --- a/templates/actor/partials/attributes-grid-partial.html +++ b/templates/actor/partials/attributes-grid-partial.html @@ -7,7 +7,7 @@
-