Unify ProseMirror descriptions and fix item sheet layout.

Various ItemV2 fixes.

Weapon sheet reorg to smooth out style inconsistencies.
This commit is contained in:
Aaron Lutomski
2026-06-21 13:32:24 -07:00
parent 7f668b57ab
commit daa99ec81d
43 changed files with 439 additions and 274 deletions
+1
View File
@@ -20,6 +20,7 @@
### **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
## Version 1.6.6 - 2026-04-15
+183 -36
View File
@@ -894,8 +894,10 @@ div.horizontal-bar-section-header {
}
.notes-wrapper {
height: 100%;
margin-block-start: 12px;
display: flex;
flex-direction: column;
min-height: 0;
}
div.motivation-editor-container div.editor {
@@ -905,15 +907,135 @@ div.motivation-editor-container div.editor {
.deltagreen .editor-host {
align-items: stretch;
display: flex;
flex-direction: column;
min-height: 0;
}
/* ProseMirror: compact when toggled off; expand to fill tab space while editing. */
.deltagreen .editor-host prose-mirror {
--min-height: 5rem;
flex: 0 0 auto;
}
.deltagreen:not(.item) .editor-host prose-mirror:not(.active) {
height: auto !important;
}
.deltagreen .editor-host prose-mirror.active {
--min-height: 12rem;
flex: 1 1 auto;
min-height: 12rem;
}
.deltagreen .editor-host prose-mirror.active .editor-container {
flex: 1 1 auto;
min-height: 0;
}
.deltagreen .editor-host prose-mirror .editor-content {
font-family: "SpecialElite", sans-serif !important;
}
/* Actor sheets: compact description when not editing. */
.deltagreen:not(.item) .editor-host prose-mirror:not(.active) .editor-content {
position: relative;
inset: unset;
overflow: visible;
min-height: 2.5rem;
}
/* Let the description block consume remaining tab height while editing. */
.deltagreen:not(.item) .tab:has(.notes-wrapper prose-mirror.active) {
display: flex;
flex-direction: column;
min-height: 0;
}
.deltagreen:not(.item) .tab:has(.notes-wrapper prose-mirror.active) .notes-wrapper,
.deltagreen:not(.item) .tab:has(.notes-wrapper prose-mirror.active) .notes-wrapper .editor-host {
flex: 1 1 auto;
min-height: 0;
}
/* Item sheets: description fills space below header (view and edit). */
.deltagreen.item form {
display: flex;
flex-direction: column;
height: 100%;
}
.deltagreen .editor-host .editor {
height: 100% !important;
.deltagreen.item .window-content {
display: flex;
flex-direction: column;
flex: 1 1 auto;
min-height: 0;
}
.deltagreen .editor .editor-container {
height: 100% !important;
.deltagreen.item [data-application-part="header"],
.deltagreen.item [data-application-part="tabs"] {
flex: 0 0 auto;
}
/* Bond, gear, weapon, armor, motivation (single description part). */
.deltagreen.item section.sheet-body[data-application-part="description"] {
flex: 1 1 auto;
display: flex;
flex-direction: column;
min-height: 0;
overflow: hidden;
}
/* Tome/ritual tab parts — flex only when .active; do not override core display:none on inactive tabs. */
.deltagreen.item .tab[data-tab][data-application-part].active {
flex: 1 1 auto;
display: flex;
flex-direction: column;
min-height: 0;
overflow: hidden;
}
.deltagreen.item section.sheet-body > .tab.top-div-border,
.deltagreen.item .tab[data-application-part="handler"].active > .tab.top-div-border {
flex: 1 1 auto;
display: flex;
flex-direction: column;
min-height: 0;
}
.deltagreen.item .tab.top-div-border > .editor-host,
.deltagreen.item .tab[data-application-part="description"].active > .editor-host {
flex: 1 1 auto;
min-height: 0;
}
.deltagreen.item .tab.top-div-border .editor-host prose-mirror {
flex: 1 1 auto;
min-height: 5rem;
}
.deltagreen.item .tab.top-div-border .editor-host prose-mirror:not(.active) .editor-content {
position: absolute;
inset: 0;
overflow: auto;
min-height: unset;
}
.deltagreen.item .tab.top-div-border .editor-host prose-mirror.active {
min-height: 5rem;
}
/* Core uses display:block on hover for prose-mirror toggles, which breaks flex icon centering. */
.deltagreen prose-mirror:hover button.toggle:enabled {
display: flex;
justify-content: center;
align-items: center;
padding: 0;
}
.deltagreen prose-mirror button.toggle > i {
margin: 0;
line-height: 1;
}
/* I think all the stuff below is now obsolete
@@ -1007,6 +1129,7 @@ input.item-sheet-short-input {
justify-content: center;
text-align: left;
padding-left: 5px;
padding-bottom: 10px;
grid-gap: 5px;
}
@@ -1096,48 +1219,72 @@ input.item-sheet-short-input {
padding-right: 5px;
}
.weapon-sheet-outer-grid-2col {
display: grid;
grid-template-columns: 60% 40%;
.weapon-sheet-horizontal-grid-2col .resource-label {
display: flex;
align-items: center;
justify-content: center;
text-align: center;
padding-left: 5px;
padding-right: 5px;
column-gap: 10px;
row-gap: 5px;
min-height: 1.5rem;
margin: 0;
}
.weapon-sheet-horizontal-grid-left-2col {
.weapon-sheet-horizontal-grid-2col input[type="text"],
.weapon-sheet-horizontal-grid-2col select {
width: 100%;
max-width: 100%;
box-sizing: border-box;
margin: 0;
line-height: 1.5rem;
}
.weapon-sheet-header {
margin-bottom: 12px;
}
/* One grid row per stat pair so left/right labels share the same baseline. */
.weapon-sheet-stats-grid {
display: grid;
grid-template-columns: 50% 50%;
grid-template-columns: minmax(5.5rem, auto) minmax(9rem, 1.35fr) minmax(5.5rem, auto) minmax(9rem, 1.35fr);
align-items: center;
text-align: left;
padding-bottom: 10px;
column-gap: 10px;
column-gap: 12px;
row-gap: 10px;
padding-right: 10px;
padding: 0 2px 10px;
text-align: left;
}
.weapon-sheet-horizontal-grid-right-2col {
display: grid;
grid-template-columns: 60% 40%;
.weapon-sheet-stats-grid .resource-label {
display: flex;
align-items: center;
text-align: left;
padding-bottom: 10px;
column-gap: 10px;
row-gap: 10px;
padding-right: 10px;
min-height: var(--weapon-sheet-control-height, 1.75rem);
margin: 0;
}
.weapon-sheet-secondary-grid-2col {
display: grid;
grid-template-columns: 40% 60%;
align-items: center;
justify-content: left;
text-align: left;
grid-gap: 5px;
padding-right: 5px;
.weapon-sheet-stats-grid input[type="text"],
.weapon-sheet-stats-grid select {
width: 100%;
min-width: 0;
max-width: 100%;
box-sizing: border-box;
margin: 0;
height: var(--weapon-sheet-control-height, 1.75rem);
min-height: var(--weapon-sheet-control-height, 1.75rem);
line-height: 1.5rem;
padding: 0 6px;
}
.weapon-sheet-stats-grid select.weapon-sheet-skill-select {
min-width: 9rem;
}
.weapon-sheet-stats-grid .labeled-resource-checkbox {
justify-self: start;
align-self: center;
margin: 0;
}
.weapon-sheet-stats-pad {
display: block;
min-height: 0;
visibility: hidden;
pointer-events: none;
}
/*
+2 -1
View File
@@ -20,7 +20,8 @@
font-family: "SpecialElite", sans-serif !important;
}
.editor-host .editor {
.editor-host .editor .editor-content,
.editor-host prose-mirror .editor-content {
font-family: "SpecialElite", sans-serif !important;
}
+1 -1
View File
@@ -199,7 +199,7 @@
"DG.ItemWindow.Weapons.LethalityTooltip": "Una arma letal té l'oportunitat de matar instantàniament un objectiu. Tira 1d100 enlloc del dany. Si no és letal, el dany és dalcula tirant 2d10, on el primer d10 és iugal a 10.",
"DG.ItemWindow.Weapons.IsLethalityTooltip": "Marca si l'arma té un % de letalitat associat.",
"DG.ItemWindow.Weapons.ArmorPiercing": "Perforació d'armadures",
"DG.ItemWindow.Weapons.Ammo": "Munició (Quantitat/Tipus)",
"DG.ItemWindow.Weapons.Ammo": "Munició",
"DG.ItemWindow.Weapons.Range": "Abast",
"DG.ItemWindow.Weapons.KillRadius": "Radi de mort",
+1 -1
View File
@@ -215,7 +215,7 @@
"DG.ItemWindow.Weapons.LethalityTooltip": "致死武器有机会立即杀死目标。掷D100而不是伤害骰。如果不致死,则造成伤害,就像结果是2d10掷骰一样,第一个d10上的'00'等于10",
"DG.ItemWindow.Weapons.IsLethalityTooltip": "如果武器有致死等级 % 与之相关,请勾选此项",
"DG.ItemWindow.Weapons.ArmorPiercing": "穿甲",
"DG.ItemWindow.Weapons.Ammo": "弹药 (数量/类别)",
"DG.ItemWindow.Weapons.Ammo": "弹药",
"DG.ItemWindow.Weapons.Range": "射程",
"DG.ItemWindow.Weapons.KillRadius": "击杀半径",
+1 -1
View File
@@ -216,7 +216,7 @@
"DG.ItemWindow.Weapons.LethalityTooltip": "Eine tödliche Waffe hat eine Chance, ein Ziel sofort zu töten. Wirf einen D100 statt eines Schadenswürfels. Wenn sie nicht tödlich ist, wird der Schaden stattdessen wie bei einer 2d10-Probe berechnet, wobei eine '00' auf dem ersten d10 einer 10 entspricht.",
"DG.ItemWindow.Weapons.IsLethalityTooltip": "Prüfe dies, wenn die Waffe eine Tödlichkeit in % hat.",
"DG.ItemWindow.Weapons.ArmorPiercing": "Rüstungsdurchdringung",
"DG.ItemWindow.Weapons.Ammo": "Munition (Menge/Typ)",
"DG.ItemWindow.Weapons.Ammo": "Munition",
"DG.ItemWindow.Weapons.Range": "Reichweite",
"DG.ItemWindow.Weapons.KillRadius": "Tötungsradius",
+1 -1
View File
@@ -282,7 +282,7 @@
"DG.ItemWindow.Weapons.LethalityTooltip": "A lethal weapon has a chance to instantly kill a target. Roll a D100 instead of damage die. If it is not lethal, damage is dealt instead as if the result was a 2d10 roll, with a '00' on the first d10 equaling a 10.",
"DG.ItemWindow.Weapons.IsLethalityTooltip": "Check this if the weapon has a lethality % associated with it.",
"DG.ItemWindow.Weapons.ArmorPiercing": "Armor Piercing",
"DG.ItemWindow.Weapons.Ammo": "Ammo (Amount/Type)",
"DG.ItemWindow.Weapons.Ammo": "Ammo",
"DG.ItemWindow.Weapons.Range": "Range",
"DG.ItemWindow.Weapons.KillRadius": "Kill Radius",
+1 -1
View File
@@ -280,7 +280,7 @@
"DG.ItemWindow.Weapons.LethalityTooltip": "Un arma letal tiene la oportunidad de matar instantáneamente a un objetivo. Tira 1D100 en lugar de un dado de daño. Si no es letal, el daño se calcula en su lugar tirando 2d10, donde un '00' en el primer d10 es igual a 10.",
"DG.ItemWindow.Weapons.IsLethalityTooltip": "Marca si el arma tiene un % de letalidad asociada.",
"DG.ItemWindow.Weapons.ArmorPiercing": "Perforación",
"DG.ItemWindow.Weapons.Ammo": "Munición (cantidad/tipo)",
"DG.ItemWindow.Weapons.Ammo": "Munición",
"DG.ItemWindow.Weapons.Range": "Alcance",
"DG.ItemWindow.Weapons.KillRadius": "Radio muerte",
+1 -1
View File
@@ -280,7 +280,7 @@
"DG.ItemWindow.Weapons.LethalityTooltip": "Une arme létale a une chance de tuer instantanément une cible. Lancer un D100 au lieu d'un jet de dégâts. En cas d'échec, la cible subit des dommages égaux aux 2d10 associés",
"DG.ItemWindow.Weapons.IsLethalityTooltip": "Cochez cette case si l'arme est associée à un pourcentage de Létalité",
"DG.ItemWindow.Weapons.ArmorPiercing": "Perforant",
"DG.ItemWindow.Weapons.Ammo": "Munitions (Nombre/Type)",
"DG.ItemWindow.Weapons.Ammo": "Munitions",
"DG.ItemWindow.Weapons.Range": "Portée",
"DG.ItemWindow.Weapons.KillRadius": "Rayon de mortalité",
+1 -1
View File
@@ -215,7 +215,7 @@
"DG.ItemWindow.Weapons.LethalityTooltip": "致命武器は、対象を即死させる可能性がある。ダメージダイスではなく、D100をロールする。致命でない場合、ダメージは2d10をロールした結果となる。最初のd10の「00」は10として扱う。",
"DG.ItemWindow.Weapons.IsLethalityTooltip": "致命率のある武器なら、ここにチェックする",
"DG.ItemWindow.Weapons.ArmorPiercing": "装甲貫通",
"DG.ItemWindow.Weapons.Ammo": "弾 (数量/種類)",
"DG.ItemWindow.Weapons.Ammo": "弾",
"DG.ItemWindow.Weapons.Range": "射程",
"DG.ItemWindow.Weapons.KillRadius": "致命半径",
+1 -1
View File
@@ -278,7 +278,7 @@
"DG.ItemWindow.Weapons.LethalityTooltip": "Broń śmiertelna ma szansę natychmiastowo zabić cel. Rzuć D100 zamiast kości obrażeń. Jeśli nie jest śmiertelna, obrażenia są traktowane jakby wynik był rzutem 2d10, z '00' na pierwszym d10 traktowanym jak 10.",
"DG.ItemWindow.Weapons.IsLethalityTooltip": "Zaznacz to, jeśli broń ma procentową wartość śmiertelności.",
"DG.ItemWindow.Weapons.ArmorPiercing": "Przebicie Pancerza",
"DG.ItemWindow.Weapons.Ammo": "Amunicja (Ilość/Typ)",
"DG.ItemWindow.Weapons.Ammo": "Amunicja",
"DG.ItemWindow.Weapons.Range": "Zasięg",
"DG.ItemWindow.Weapons.KillRadius": "Zasięg Śmierci",
+1 -1
View File
@@ -280,7 +280,7 @@
"DG.ItemWindow.Weapons.LethalityTooltip": "Uma arma letal tem a chance de matar instantaneamente um alvo. Role um D100 em vez de um dado de dano. Se não for letal, o dano é causado como se o resultado fosse uma rolagem de 2d10, com um '00' no primeiro d10 igual a 10.",
"DG.ItemWindow.Weapons.IsLethalityTooltip": "Marque isso se a arma tiver uma % de letalidade associada a ela.",
"DG.ItemWindow.Weapons.ArmorPiercing": "Perfuração de Armadura",
"DG.ItemWindow.Weapons.Ammo": "Munição (quantidade/tipo)",
"DG.ItemWindow.Weapons.Ammo": "Munição",
"DG.ItemWindow.Weapons.Range": "Alcance",
"DG.ItemWindow.Weapons.KillRadius": "Raio de Morte",
+1 -1
View File
@@ -215,7 +215,7 @@
"DG.ItemWindow.Weapons.LethalityTooltip": "Ett dödligt vapen har en chans att omedelbart döda ett mål. Slå en D100 istället för skadetärningen. Om det inte är dödligt tolkas resultatet som om det var 2 enskilda d10or skada, där en '00' på den första d10an motsvarar 10 skada.",
"DG.ItemWindow.Weapons.IsLethalityTooltip": "Kryssa i detta om vapnet är dödligt.",
"DG.ItemWindow.Weapons.ArmorPiercing": "Genomträngande",
"DG.ItemWindow.Weapons.Ammo": "Ammo (Mängd/Typ)",
"DG.ItemWindow.Weapons.Ammo": "Ammo",
"DG.ItemWindow.Weapons.Range": "Räckvidd",
"DG.ItemWindow.Weapons.KillRadius": "Dödsradie",
+1 -1
View File
@@ -280,7 +280,7 @@
"DG.ItemWindow.Weapons.LethalityTooltip": "Летальна зброя має шанс миттєво вбити ціль. Замість кубів шкоди кидай D100. Якщо результат НЕ летальний, тоді шкода рахується так, ніби випало 2d10, де «00» на першому d10 дорівнює 10.",
"DG.ItemWindow.Weapons.IsLethalityTooltip": "Познач, якщо зброя має відсоток летальності.",
"DG.ItemWindow.Weapons.ArmorPiercing": "Бронепробиття",
"DG.ItemWindow.Weapons.Ammo": "Набої (кількість/тип)",
"DG.ItemWindow.Weapons.Ammo": "Набої",
"DG.ItemWindow.Weapons.Range": "Дальність",
"DG.ItemWindow.Weapons.KillRadius": "Радіус ураження",
+33
View File
@@ -0,0 +1,33 @@
/**
* Rich-text field specs for actor sheets (by actor type).
* @type {Record<string, { path: string, key: string }[]>}
*/
export const ACTOR_RICH_TEXT_BY_TYPE = {
agent: [{ path: "physical.description", key: "description" }],
npc: [{ path: "notes", key: "description" }],
unnatural: [{ path: "notes", key: "description" }],
vehicle: [{ path: "description", key: "description" }],
};
/**
* Rich-text field specs for all item types.
* @type {{ path: string, key: string, types?: string[] }[]}
*/
export const ITEM_RICH_TEXT_FIELDS = [
{ path: "description", key: "description" },
{
path: "handlerNotes",
key: "handlerNotes",
types: ["tome", "ritual"],
},
];
/**
* @param {string} itemType
* @returns {{ path: string, key: string }[]}
*/
export function getItemRichTextFields(itemType) {
return ITEM_RICH_TEXT_FIELDS.filter(
(spec) => !spec.types || spec.types.includes(itemType),
);
}
+2 -5
View File
@@ -1,5 +1,6 @@
import HumanSkillsActorData from "./base/human-skills.js";
import CharacterData from "./base/character.js";
import DGHTMLField from "../fields/html-content-field.js";
const { SchemaField, NumberField, StringField, BooleanField, HTMLField } =
foundry.data.fields;
@@ -29,11 +30,7 @@ export default class AgentData extends CharacterData {
}),
}),
physical: new SchemaField({
description: new HTMLField({
initial: "",
blank: true,
textSearch: true,
}),
description: DGHTMLField(),
wounds: new StringField({ initial: "" }),
firstAidAttempted: new BooleanField({ initial: false }),
exhausted: new BooleanField({ initial: false }),
+2 -1
View File
@@ -1,5 +1,6 @@
import HumanSkillsActorData from "./base/human-skills.js";
import CharacterData from "./base/character.js";
import DGHTMLField from "../fields/html-content-field.js";
const { SchemaField, NumberField, StringField, BooleanField, HTMLField } =
foundry.data.fields;
@@ -19,7 +20,7 @@ export default class NPCData extends CharacterData {
biography: new SchemaField({
profession: new StringField({ initial: "" }),
}),
notes: new HTMLField({ initial: "", blank: true, textSearch: true }),
notes: DGHTMLField(),
shortDescription: new StringField({ initial: "" }),
showUntrainedSkills: new BooleanField({ initial: true }),
};
+2 -6
View File
@@ -1,5 +1,6 @@
import UnnaturalSkillsActorData from "./base/unnatural-skills.js";
import CharacterData from "./base/character.js";
import DGHTMLField from "../fields/html-content-field.js";
const { SchemaField, NumberField, StringField, BooleanField, HTMLField } =
foundry.data.fields;
@@ -17,12 +18,7 @@ export default class UnnaturalData extends CharacterData {
successLoss: new StringField({ initial: "1" }),
}),
schemaVersion: new NumberField({ initial: 1.0 }),
notes: new HTMLField({
initial: "",
blank: true,
textSearch: true,
allowColor: true,
}),
notes: DGHTMLField(),
shortDescription: new StringField({ initial: "" }),
showUntrainedSkills: new BooleanField({ initial: true }),
};
+2 -5
View File
@@ -1,4 +1,5 @@
import SheetSettingsActorData from "./base/sheet-settings.js";
import DGHTMLField from "../fields/html-content-field.js";
const { SchemaField, NumberField, StringField, ArrayField, HTMLField } =
foundry.data.fields;
@@ -8,11 +9,7 @@ export default class VehicleData extends foundry.abstract.TypeDataModel {
return {
...SheetSettingsActorData.defineSchema(),
name: new StringField({ initial: "" }),
description: new HTMLField({
initial: "",
blank: true,
textSearch: true,
}),
description: DGHTMLField(),
shortDescription: new StringField({ initial: "" }),
health: new SchemaField({
value: new NumberField({ initial: 10 }),
+15
View File
@@ -0,0 +1,15 @@
const { HTMLField } = foundry.data.fields;
/**
* Standard Delta Green HTML / ProseMirror field defaults.
* @param {Partial<foundry.data.fields.StringFieldOptions>} [options]
* @returns {foundry.data.fields.HTMLField}
*/
export default function DGHTMLField(options = {}) {
return new HTMLField({
required: false,
blank: true,
initial: "",
...options,
});
}
+2 -2
View File
@@ -1,4 +1,4 @@
const { HTMLField } = foundry.data.fields;
import DGHTMLField from "../fields/html-content-field.js";
/**
* Shared system fields for all Delta Green item types (from legacy template base).
@@ -6,6 +6,6 @@ const { HTMLField } = foundry.data.fields;
*/
export default function defineBaseItemSystemFields() {
return {
description: new HTMLField({ initial: "", blank: true, textSearch: true }),
description: DGHTMLField(),
};
}
+2 -5
View File
@@ -1,4 +1,5 @@
import defineBaseItemSystemFields from "./base-fields.js";
import DGHTMLField from "../fields/html-content-field.js";
const { NumberField, StringField, BooleanField, SchemaField, HTMLField } =
foundry.data.fields;
@@ -22,11 +23,7 @@ export default class RitualItemData extends foundry.abstract.TypeDataModel {
activationCosts: new StringField({ initial: "" }),
activationTime: new StringField({ initial: "" }),
complexity: new StringField({ initial: "Simple" }),
handlerNotes: new HTMLField({
initial: "",
blank: true,
textSearch: true,
}),
handlerNotes: DGHTMLField(),
revealed: new BooleanField({ initial: false }),
};
}
+2 -5
View File
@@ -1,4 +1,5 @@
import defineBaseItemSystemFields from "./base-fields.js";
import DGHTMLField from "../fields/html-content-field.js";
const { NumberField, StringField, BooleanField, SchemaField, HTMLField } =
foundry.data.fields;
@@ -16,11 +17,7 @@ export default class TomeItemData extends foundry.abstract.TypeDataModel {
failedLoss: new StringField({ initial: "1D6" }),
successLoss: new StringField({ initial: "1D4" }),
}),
handlerNotes: new HTMLField({
initial: "",
blank: true,
textSearch: true,
}),
handlerNotes: DGHTMLField(),
revealed: new BooleanField({ initial: false }),
};
}
+5 -53
View File
@@ -1,4 +1,5 @@
import DG from "../config.js";
import { ACTOR_RICH_TEXT_BY_TYPE } from "../config/rich-text-fields.js";
import {
DGPercentileRoll,
createDGRollFromDataset,
@@ -98,8 +99,10 @@ export default class DGActorSheet extends DGSheetMixin(ActorSheetV2) {
// Prepare subname info placeholder.
context.subnameInfoPlaceholder = this._prepareSubnameInfoPlaceholder();
// Prepare descriptions for each sheet.
context.enrichedDescription = await this._prepareDescriptions();
await this._prepareRichTextContext(
context,
ACTOR_RICH_TEXT_BY_TYPE[this.actor.type] ?? [],
);
// Early return if this is a vehicle.
if (this.actor.type === "vehicle") return context;
@@ -258,12 +261,6 @@ export default class DGActorSheet extends DGSheetMixin(ActorSheetV2) {
}
}
/** @override */
activateEditor(target, editorOptions, initialContent) {
editorOptions.content_css = "./systems/deltagreen/css/editor.css";
return super.activateEditor(target, editorOptions, initialContent);
}
/* --------- Context Preparation Functions --------- */
/**
@@ -291,51 +288,6 @@ export default class DGActorSheet extends DGSheetMixin(ActorSheetV2) {
return game.i18n.localize(subnameInfoPlaceholder);
}
/**
* Prepares and enriches the description for an actor based on its type.
*
* @returns {Promise<string>} The outer HTML of the enriched description.
*/
async _prepareDescriptions() {
let descriptionPath;
switch (this.actor.type) {
case "agent":
descriptionPath = "system.physical.description";
break;
case "npc":
case "unnatural":
descriptionPath = "system.notes";
break;
case "vehicle":
descriptionPath = "system.description";
break;
default:
break;
}
const descriptionValue = foundry.utils.getProperty(
this.actor,
descriptionPath,
);
const enrichedDescription =
await foundry.applications.ux.TextEditor.implementation.enrichHTML(
descriptionValue,
{
rollData: this.document.getRollData(),
relativeTo: this.document,
},
);
const { HTMLProseMirrorElement } = foundry.applications.elements;
return HTMLProseMirrorElement.create({
name: descriptionPath,
value: descriptionValue,
enriched: enrichedDescription,
toggled: true,
}).outerHTML;
}
/**
* Sorts the skills on the actor sheet based on the appropriate localized entry.
* If the localized entry is not found, the sort label is the skill key.
+31 -29
View File
@@ -1,10 +1,14 @@
import { BASE_TEMPLATE_PATH } from "../config.js";
import { getItemRichTextFields } from "../config/rich-text-fields.js";
import DGSheetMixin from "./base-sheet.js";
import { createDGRollFromDataset, processDGRoll } from "../roll/roll.js";
const { ItemSheetV2 } = foundry.applications.sheets;
const ITEM_PARTS_PATH = `${BASE_TEMPLATE_PATH}/item/parts`;
/** Tab group id used by item sheet templates (`data-group="primary"`). */
const ITEM_TAB_GROUP = "primary";
/** Tab and part layout per item type. */
const ITEM_SHEET_LAYOUT = /** @type {const} */ ({
weapon: {
@@ -44,22 +48,8 @@ const ITEM_SHEET_LAYOUT = /** @type {const} */ ({
},
},
motivation: {
tabs: {
initial: "attributes",
tabs: [
{
id: "attributes",
label: "DG.ItemWindow.Motivations.Attributes",
},
{
id: "description",
label: "DG.ItemWindow.Motivations.Description",
},
],
},
parts: {
tabs: { template: "templates/generic/tab-navigation.hbs" },
attributes: { template: `${ITEM_PARTS_PATH}/motivation-attributes.html` },
header: { template: `${ITEM_PARTS_PATH}/motivation-header.html` },
description: {
template: `${ITEM_PARTS_PATH}/motivation-description.html`,
scrollable: [""],
@@ -161,31 +151,43 @@ export default class DGItemSheet extends DGSheetMixin(ItemSheetV2) {
/** @inheritdoc */
_getTabsConfig(group) {
const layout = ITEM_SHEET_LAYOUT[this.item.type];
if (!layout?.tabs) return super._getTabsConfig(group);
if (!layout?.tabs || group !== ITEM_TAB_GROUP) {
return super._getTabsConfig(group);
}
const tabs = layout.tabs.tabs.filter((tab) => !tab.gmOnly || game.user.isGM);
return { ...layout.tabs, tabs };
}
/** @inheritdoc */
async _preparePartContext(partId, context, options) {
const partContext = await super._preparePartContext(partId, context, options);
if (partId === "tabs") partContext.tabClasses = "sheet-tabs";
return partContext;
}
/** @inheritdoc */
async _prepareContext(options) {
const context = await super._prepareContext(options);
context.owner = this.document.isOwner;
context.enrichedDescription =
await foundry.applications.ux.TextEditor.implementation.enrichHTML(
this.item.system.description,
{ async: true },
);
if (this.item.type === "tome" || this.item.type === "ritual") {
context.enrichedHandlerNotes =
await foundry.applications.ux.TextEditor.implementation.enrichHTML(
this.item.system.handlerNotes,
{ async: true },
);
const layout = ITEM_SHEET_LAYOUT[this.item.type];
if (layout?.tabs) {
const { initial } = layout.tabs;
const tabIds = layout.tabs.tabs
.filter((tab) => !tab.gmOnly || game.user.isGM)
.map((tab) => tab.id);
if (!tabIds.includes(this.tabGroups[ITEM_TAB_GROUP])) {
this.tabGroups[ITEM_TAB_GROUP] = initial;
}
context.tabs = this._prepareTabs(ITEM_TAB_GROUP);
}
context.owner = this.document.isOwner;
await this._prepareRichTextContext(
context,
getItemRichTextFields(this.item.type),
);
return context;
}
+18
View File
@@ -1,4 +1,5 @@
import DG, { BASE_TEMPLATE_PATH } from "../config.js";
import { prepareRichTextContext } from "./rich-text.js";
const HbsAppMixin = foundry.applications.api.HandlebarsApplicationMixin;
@@ -63,6 +64,23 @@ const DGSheetMixin = (Base) => {
resetPosition() {
this.setPosition(this.options.position);
}
/**
* Prepare `context.richText` from schema-driven ProseMirror field specs.
* @param {ApplicationRenderContext} context
* @param {{ path: string, key: string }[]} fieldSpecs
* @returns {Promise<void>}
*/
async _prepareRichTextContext(context, fieldSpecs) {
if (!fieldSpecs?.length) {
context.richText = {};
return;
}
context.richText = await prepareRichTextContext(
this.document,
fieldSpecs,
);
}
};
};
+50
View File
@@ -0,0 +1,50 @@
const { HTMLField } = foundry.data.fields;
/**
* Build toggled ProseMirror markup for a system schema HTML field (App V2).
* @param {Actor|Item} document
* @param {string} fieldPath Path within `document.system` (e.g. "physical.description")
* @param {object} [options] Additional options passed to DataField#toInput
* @returns {Promise<string>}
*/
export async function prepareProseMirrorInput(document, fieldPath, options = {}) {
const field = document.system.schema.getField(fieldPath);
if (!(field instanceof HTMLField)) {
throw new Error(
`Rich text field "system.${fieldPath}" must be an HTMLField on ${document.documentName} ${document.name}`,
);
}
const value = foundry.utils.getProperty(document.system, fieldPath) ?? "";
const enriched =
await foundry.applications.ux.TextEditor.implementation.enrichHTML(value, {
async: true,
rollData: document.getRollData?.() ?? {},
relativeTo: document,
});
const input = field.toInput({
name: `system.${fieldPath}`,
value,
enriched,
toggled: true,
documentUUID: document.uuid,
...options,
});
return input.outerHTML;
}
/**
* Populate `context.richText` from a list of field specs.
* @param {Actor|Item} document
* @param {{ path: string, key: string }[]} fieldSpecs
* @returns {Promise<Record<string, string>>}
*/
export async function prepareRichTextContext(document, fieldSpecs) {
const richText = {};
for (const { path, key } of fieldSpecs) {
richText[key] = await prepareProseMirrorInput(document, path);
}
return richText;
}
+1 -1
View File
@@ -8,8 +8,8 @@ export default class DGVehicleSheet extends DGActorSheet {
initial: "gear",
labelPrefix: "DG.Navigation.Vehicle",
tabs: [
{ id: "notes" },
{ id: "gear" },
{ id: "notes" },
{ id: "about", icon: "fas fa-question-circle", label: "" },
],
},
+1
View File
@@ -6,5 +6,6 @@
export default async function preloadHandlebarsTemplates() {
return foundry.applications.handlebars.loadTemplates([
"systems/deltagreen/templates/dialog/modify-percentile-roll.html",
"systems/deltagreen/templates/partials/prose-mirror-host.html",
]);
}
+4 -2
View File
@@ -233,10 +233,12 @@
"htmlFields": ["description"]
},
"tome": {
"htmlFields": ["description", "handlerNotes"]
"htmlFields": ["description", "handlerNotes"],
"gmOnlyFields": ["handlerNotes"]
},
"ritual": {
"htmlFields": ["description", "handlerNotes"]
"htmlFields": ["description", "handlerNotes"],
"gmOnlyFields": ["handlerNotes"]
}
}
}
+1 -1
View File
@@ -2,5 +2,5 @@
<div class="horizontal-bar-section-divider">
<span>{{localize "DG.Sheet.BlockHeaders.PhysicalDescriptionNotes"}}</span>
</div>
<div class="editor-host">{{{enrichedDescription}}}</div>
{{> "systems/deltagreen/templates/partials/prose-mirror-host.html" html=richText.description}}
</section>
+1 -3
View File
@@ -1,7 +1,5 @@
<section class="sheet-body">
<div class="tab top-div-border">
<div class="editor-host">
{{editor enrichedDescription target="system.description" engine="prosemirror" button=true owner=owner editable=editable}}
</div>
{{> "systems/deltagreen/templates/partials/prose-mirror-host.html" html=richText.description}}
</div>
</section>
+1 -3
View File
@@ -1,7 +1,5 @@
<section class="sheet-body">
<div class="tab top-div-border">
<div class="editor-host">
{{editor enrichedDescription target="system.description" engine="prosemirror" button=true owner=owner editable=editable}}
</div>
{{> "systems/deltagreen/templates/partials/prose-mirror-host.html" html=richText.description}}
</div>
</section>
+1 -3
View File
@@ -1,7 +1,5 @@
<section class="sheet-body">
<div class="tab top-div-border">
<div class="editor-host">
{{editor enrichedDescription target="system.description" engine="prosemirror" button=true owner=owner editable=editable}}
</div>
{{> "systems/deltagreen/templates/partials/prose-mirror-host.html" html=richText.description}}
</div>
</section>
@@ -1,8 +1,5 @@
<div class="tab description {{tabs.description.cssClass}} top-div-border"
data-group="primary"
data-tab="description">
<div class="editor-host">
{{editor enrichedDescription target="system.description" engine="prosemirror" button=true owner=owner editable=editable}}
<section class="sheet-body">
<div class="tab top-div-border">
{{> "systems/deltagreen/templates/partials/prose-mirror-host.html" html=richText.description}}
</div>
</div>
</section>
@@ -1,15 +1,5 @@
<section class="sheet-body">
<div class="tab attributes {{tabs.attributes.cssClass}} top-div-border"
data-group="primary"
data-tab="attributes">
<header class="">
<div class="motivation-sheet-horizontal-grid-2col">
<span class="resource-label">{{localize 'DG.ItemWindow.Motivations.IsDisorder'}}:</span>
<input type="checkbox"
name="system.crossedOut"
{{checked item.system.crossedOut}}
data-dtype="Boolean"
title="{{localize 'DG.ItemWindow.Motivations.IsDisorderTooltip'}}" />
<span class="resource-label">{{localize 'DG.ItemWindow.Motivations.Motivation'}}:</span>
<input type="text"
name="name"
@@ -21,8 +11,13 @@
style="text-decoration: line-through"
disabled
{{/if }} />
<span class="resource-label">{{localize 'DG.ItemWindow.Motivations.IsDisorder'}}:</span>
<input type="checkbox"
name="system.crossedOut"
{{checked item.system.crossedOut}}
data-dtype="Boolean"
title="{{localize 'DG.ItemWindow.Motivations.IsDisorderTooltip'}}" />
{{#if item.system.crossedOut }}
<span class="resource-label">{{localize 'DG.ItemWindow.Motivations.Disorder'}}:</span>
<input type="text"
name="system.disorder"
@@ -37,6 +32,5 @@
data-dtype="Boolean"
title="{{localize 'DG.ItemWindow.Motivations.DisorderHasBeenCuredTooltip'}}" />
{{/if}}
</div>
</div>
</header>
+1 -5
View File
@@ -1,9 +1,5 @@
<section class="sheet-body">
<div class="tab description {{tabs.description.cssClass}} top-div-border"
data-group="primary"
data-tab="description">
<div class="editor-host">
{{editor enrichedDescription target="system.description" engine="prosemirror" button=true owner=owner editable=editable}}
</div>
{{> "systems/deltagreen/templates/partials/prose-mirror-host.html" html=richText.description}}
</div>
</section>
+1 -3
View File
@@ -25,8 +25,6 @@
<span class="resource-label">{{localize 'DG.ItemWindow.Tome.HandlerNotes'}}:</span>
<div class="tab top-div-border">
<div class="editor-host">
{{editor enrichedHandlerNotes target="system.handlerNotes" engine="prosemirror" button=true owner=owner editable=editable}}
</div>
{{> "systems/deltagreen/templates/partials/prose-mirror-host.html" html=richText.handlerNotes}}
</div>
</div>
+1 -5
View File
@@ -1,9 +1,5 @@
<section class="sheet-body">
<div class="tab description {{tabs.description.cssClass}} top-div-border"
data-group="primary"
data-tab="description">
<div class="editor-host">
{{editor enrichedDescription target="system.description" engine="prosemirror" button=true owner=owner editable=editable}}
</div>
{{> "systems/deltagreen/templates/partials/prose-mirror-host.html" html=richText.description}}
</div>
</section>
+1 -3
View File
@@ -20,8 +20,6 @@
<span class="resource-label">{{localize 'DG.ItemWindow.Tome.HandlerNotes'}}:</span>
<div class="tab top-div-border">
<div class="editor-host">
{{editor enrichedHandlerNotes target="system.handlerNotes" engine="prosemirror" button=true owner=owner editable=editable}}
</div>
{{> "systems/deltagreen/templates/partials/prose-mirror-host.html" html=richText.handlerNotes}}
</div>
</div>
+1 -3
View File
@@ -1,7 +1,5 @@
<section class="sheet-body">
<div class="tab top-div-border">
<div class="editor-host">
{{editor enrichedDescription target="system.description" engine="prosemirror" button=true owner=owner editable=editable}}
</div>
{{> "systems/deltagreen/templates/partials/prose-mirror-host.html" html=richText.description}}
</div>
</section>
+49 -63
View File
@@ -1,4 +1,4 @@
<header class="">
<header class="weapon-sheet-header">
<div class="weapon-sheet-grid-2col">
<img class="item-img" src="{{item.img}}" data-action="editImage" title="{{item.name}}"/>
@@ -24,78 +24,64 @@
</div>
</div>
<div class="weapon-sheet-outer-grid-2col">
<div class="weapon-sheet-stats-grid">
<div class="weapon-sheet-horizontal-grid-left-2col">
<span class="resource-label">{{localize 'DG.ItemWindow.Weapons.Skill'}}:</span>
<select class="weapon-sheet-skill-select" name="system.skill" >
<option {{#if_eq item.system.skill 'firearms'}}selected{{/if_eq}} value="firearms">{{localize 'DG.Skills.firearms'}}</option>
<option {{#if_eq item.system.skill 'melee_weapons'}}selected{{/if_eq}} value="melee_weapons">{{localize 'DG.Skills.melee_weapons'}}</option>
<option {{#if_eq item.system.skill 'unarmed_combat'}}selected{{/if_eq}} value="unarmed_combat">{{localize 'DG.Skills.unarmed_combat'}}</option>
<option {{#if_eq item.system.skill 'heavy_weapons'}}selected{{/if_eq}} value="heavy_weapons">{{localize 'DG.Skills.heavy_weapons'}}</option>
<option {{#if_eq item.system.skill 'athletics'}}selected{{/if_eq}} value="athletics">{{localize 'DG.Skills.athletics'}}</option>
<option {{#if_eq item.system.skill 'demolitions'}}selected{{/if_eq}} value="demolitions">{{localize 'DG.Skills.demolitions'}}</option>
<option {{#if_eq item.system.skill 'artillery'}}selected{{/if_eq}} value="artillery">{{localize 'DG.Skills.artillery'}}</option>
<option {{#if_eq item.system.skill 'str'}}selected{{/if_eq}} value="str">{{localize 'DG.Attributes.str'}}x5</option>
<option {{#if_eq item.system.skill 'con'}}selected{{/if_eq}} value="con">{{localize 'DG.Attributes.con'}}x5</option>
<option {{#if_eq item.system.skill 'dex'}}selected{{/if_eq}} value="dex">{{localize 'DG.Attributes.dex'}}x5</option>
<option {{#if_eq item.system.skill 'int'}}selected{{/if_eq}} value="int">{{localize 'DG.Attributes.int'}}x5</option>
<option {{#if_eq item.system.skill 'pow'}}selected{{/if_eq}} value="pow">{{localize 'DG.Attributes.pow'}}x5</option>
<option {{#if_eq item.system.skill 'cha'}}selected{{/if_eq}} value="cha">{{localize 'DG.Attributes.cha'}}x5</option>
<option {{#if_eq item.system.skill 'custom'}}selected{{/if_eq}} value="custom">{{localize 'DG.ItemWindow.Custom'}}</option>
</select>
<span class="resource-label">{{localize 'DG.ItemWindow.Weapons.Skill'}}:</span>
<select name="system.skill" >
<option {{#if_eq item.system.skill 'firearms'}}selected{{/if_eq}} value="firearms">{{localize 'DG.Skills.firearms'}}</option>
<option {{#if_eq item.system.skill 'melee_weapons'}}selected{{/if_eq}} value="melee_weapons">{{localize 'DG.Skills.melee_weapons'}}</option>
<option {{#if_eq item.system.skill 'unarmed_combat'}}selected{{/if_eq}} value="unarmed_combat">{{localize 'DG.Skills.unarmed_combat'}}</option>
<option {{#if_eq item.system.skill 'heavy_weapons'}}selected{{/if_eq}} value="heavy_weapons">{{localize 'DG.Skills.heavy_weapons'}}</option>
<option {{#if_eq item.system.skill 'athletics'}}selected{{/if_eq}} value="athletics">{{localize 'DG.Skills.athletics'}}</option>
<option {{#if_eq item.system.skill 'demolitions'}}selected{{/if_eq}} value="demolitions">{{localize 'DG.Skills.demolitions'}}</option>
<option {{#if_eq item.system.skill 'artillery'}}selected{{/if_eq}} value="artillery">{{localize 'DG.Skills.artillery'}}</option>
<option {{#if_eq item.system.skill 'str'}}selected{{/if_eq}} value="str">{{localize 'DG.Attributes.str'}}x5</option>
<option {{#if_eq item.system.skill 'con'}}selected{{/if_eq}} value="con">{{localize 'DG.Attributes.con'}}x5</option>
<option {{#if_eq item.system.skill 'dex'}}selected{{/if_eq}} value="dex">{{localize 'DG.Attributes.dex'}}x5</option>
<option {{#if_eq item.system.skill 'int'}}selected{{/if_eq}} value="int">{{localize 'DG.Attributes.int'}}x5</option>
<option {{#if_eq item.system.skill 'pow'}}selected{{/if_eq}} value="pow">{{localize 'DG.Attributes.pow'}}x5</option>
<option {{#if_eq item.system.skill 'cha'}}selected{{/if_eq}} value="cha">{{localize 'DG.Attributes.cha'}}x5</option>
<option {{#if_eq item.system.skill 'custom'}}selected{{/if_eq}} value="custom">{{localize 'DG.ItemWindow.Custom'}}</option>
</select>
</div>
<span class="resource-label">{{localize 'DG.ItemWindow.Weapons.Range'}}:</span>
<input type="text" name="system.range" value="{{item.system.range}}" data-dtype="String"/>
<div class="weapon-sheet-horizontal-grid-right-2col">
{{#if_eq item.system.skill 'custom'}}
<span class="resource-label">{{localize 'DG.ItemWindow.Weapons.CustomSkillTarget'}}:</span>
<input type="text" name="system.customSkillTarget" value="{{item.system.customSkillTarget}}" data-dtype="Number"/>
{{/if_eq}}
</div>
{{#if_eq item.system.skill 'custom'}}
<span class="resource-label">{{localize 'DG.ItemWindow.Weapons.CustomSkillTarget'}}:</span>
<input type="text" name="system.customSkillTarget" value="{{item.system.customSkillTarget}}" data-dtype="Number"/>
<span class="weapon-sheet-stats-pad" aria-hidden="true"></span>
<span class="weapon-sheet-stats-pad" aria-hidden="true"></span>
{{/if_eq}}
<div class="weapon-sheet-horizontal-grid-left-2col">
<span class="resource-label">{{localize 'DG.ItemWindow.Weapons.ArmorPiercing'}}:</span>
<input type="text" name="system.armorPiercing" value="{{item.system.armorPiercing}}" data-dtype="Number"/>
<span class="resource-label">{{localize 'DG.ItemWindow.Weapons.ArmorPiercing'}}:</span>
<input type="text" name="system.armorPiercing" value="{{item.system.armorPiercing}}" data-dtype="Number"/>
<span class="resource-label">{{localize 'DG.ItemWindow.Weapons.KillRadius'}}:</span>
<input type="text" name="system.killRadius" value="{{item.system.killRadius}}" data-dtype="String"/>
<span class="resource-label">{{localize 'DG.ItemWindow.Weapons.SkillModifier'}}:</span>
<input type="text" name="system.skillModifier" value="{{item.system.skillModifier}}" data-dtype="Number"/>
<span class="resource-label">{{localize 'DG.ItemWindow.Weapons.SkillModifier'}}:</span>
<input type="text" name="system.skillModifier" value="{{item.system.skillModifier}}" data-dtype="Number"/>
<span class="resource-label">{{localize 'DG.ItemWindow.Weapons.IsLethality'}}:</span>
<input class="labeled-resource-checkbox" type="checkbox" name="system.isLethal" {{checked item.system.isLethal}} data-dtype="Boolean"
title="{{localize 'DG.ItemWindow.Weapons.IsLethalityTooltip'}}"
{{#unless item.system.isLethal}}
<span class="resource-label">{{localize 'DG.ItemWindow.Weapons.Damage'}}:</span>
<input type="text" name="system.damage" value="{{item.system.damage}}" data-dtype="String"/>
{{/unless}}
{{#if item.system.isLethal}}
<span class="resource-label">{{localize 'DG.ItemWindow.Weapons.Lethality'}} %:</span>
<input type="text" name="system.lethality" value="{{item.system.lethality}}" data-dtype="Number"
title="{{localize 'DG.ItemWindow.Weapons.LethalityTooltip'}}"
/>
{{/if}}
</div>
<div class="weapon-sheet-horizontal-grid-right-2col">
<span class="resource-label">{{localize 'DG.ItemWindow.Weapons.Range'}}:</span>
<input type="text" name="system.range" value="{{item.system.range}}" data-dtype="String"/>
<span class="resource-label">{{localize 'DG.ItemWindow.Weapons.KillRadius'}}:</span>
<input type="text" name="system.killRadius" value="{{item.system.killRadius}}" data-dtype="String"/>
{{#unless item.system.isLethal}}
<span class="resource-label">{{localize 'DG.ItemWindow.Weapons.Damage'}}:</span>
<input type="text" name="system.damage" value="{{item.system.damage}}" data-dtype="String"/>
{{/unless}}
{{#if item.system.isLethal}}
<span class="resource-label">{{localize 'DG.ItemWindow.Weapons.Lethality'}} %:</span>
<input class="" type="text" name="system.lethality" value="{{item.system.lethality}}" data-dtype="Number"
title="{{localize 'DG.ItemWindow.Weapons.LethalityTooltip'}}"
/>
{{/if}}
</div>
</div>
<div class="weapon-sheet-secondary-grid-2col">
<span class="resource-label">{{localize 'DG.ItemWindow.Weapons.Ammo'}}:</span>
<input type="text" name="system.ammo" value="{{item.system.ammo}}" data-dtype="String"/>
<span class="resource-label">{{localize 'DG.ItemWindow.Weapons.IsLethality'}}:</span>
<input class="labeled-resource-checkbox" type="checkbox" name="system.isLethal" {{checked item.system.isLethal}} data-dtype="Boolean"
title="{{localize 'DG.ItemWindow.Weapons.IsLethalityTooltip'}}"
/>
</div>
<br>
</header>
@@ -0,0 +1 @@
<div class="editor-host">{{{html}}}</div>