diff --git a/css/fvtt-oath-hammer.css b/css/fvtt-oath-hammer.css
index 66bd632..8d5b2b1 100644
--- a/css/fvtt-oath-hammer.css
+++ b/css/fvtt-oath-hammer.css
@@ -95,6 +95,98 @@
.oathhammer .tab {
padding: 4px;
}
+.oathhammer prose-mirror {
+ color: #2a1a0a !important;
+ background: transparent;
+}
+.oathhammer prose-mirror.inactive,
+.oathhammer prose-mirror[toggled] {
+ color: #2a1a0a !important;
+}
+.oathhammer .editor-content,
+.oathhammer .prosemirror .editor-content,
+.oathhammer prose-mirror .editor-content {
+ color: #2a1a0a !important;
+ background: transparent;
+}
+.oathhammer .editor-content p,
+.oathhammer .prosemirror .editor-content p,
+.oathhammer prose-mirror .editor-content p,
+.oathhammer .editor-content li,
+.oathhammer .prosemirror .editor-content li,
+.oathhammer prose-mirror .editor-content li,
+.oathhammer .editor-content span,
+.oathhammer .prosemirror .editor-content span,
+.oathhammer prose-mirror .editor-content span,
+.oathhammer .editor-content div,
+.oathhammer .prosemirror .editor-content div,
+.oathhammer prose-mirror .editor-content div,
+.oathhammer .editor-content blockquote,
+.oathhammer .prosemirror .editor-content blockquote,
+.oathhammer prose-mirror .editor-content blockquote,
+.oathhammer .editor-content h1,
+.oathhammer .prosemirror .editor-content h1,
+.oathhammer prose-mirror .editor-content h1,
+.oathhammer .editor-content h2,
+.oathhammer .prosemirror .editor-content h2,
+.oathhammer prose-mirror .editor-content h2,
+.oathhammer .editor-content h3,
+.oathhammer .prosemirror .editor-content h3,
+.oathhammer prose-mirror .editor-content h3,
+.oathhammer .editor-content h4,
+.oathhammer .prosemirror .editor-content h4,
+.oathhammer prose-mirror .editor-content h4,
+.oathhammer .editor-content h5,
+.oathhammer .prosemirror .editor-content h5,
+.oathhammer prose-mirror .editor-content h5,
+.oathhammer .editor-content h6,
+.oathhammer .prosemirror .editor-content h6,
+.oathhammer prose-mirror .editor-content h6 {
+ color: #2a1a0a !important;
+}
+.oathhammer .editor-content a,
+.oathhammer .prosemirror .editor-content a,
+.oathhammer prose-mirror .editor-content a {
+ color: #084a74;
+}
+.oathhammer .ProseMirror,
+.oathhammer .prosemirror .editor-container .ProseMirror {
+ color: #2a1a0a !important;
+ background-color: rgba(245, 234, 208, 0.6);
+ border: 1px solid #084a74;
+ border-radius: 3px;
+ padding: 4px 6px;
+ min-height: 4rem;
+}
+.oathhammer .ProseMirror p,
+.oathhammer .prosemirror .editor-container .ProseMirror p,
+.oathhammer .ProseMirror li,
+.oathhammer .prosemirror .editor-container .ProseMirror li,
+.oathhammer .ProseMirror span,
+.oathhammer .prosemirror .editor-container .ProseMirror span {
+ color: #2a1a0a !important;
+}
+.oathhammer .ProseMirror a,
+.oathhammer .prosemirror .editor-container .ProseMirror a {
+ color: #084a74;
+}
+.oathhammer .ProseMirror.ProseMirror-empty::before,
+.oathhammer .prosemirror .editor-container .ProseMirror.ProseMirror-empty::before {
+ color: rgba(42, 26, 10, 0.45);
+}
+.oathhammer .editor-toolbar {
+ background: rgba(8, 74, 116, 0.1);
+ border-bottom: 1px solid #535128;
+}
+.oathhammer .editor-toolbar .control-icon,
+.oathhammer .editor-toolbar button {
+ color: #084a74;
+}
+.oathhammer .editor-toolbar .control-icon:hover,
+.oathhammer .editor-toolbar button:hover {
+ color: #2a1a0a;
+ background: rgba(200, 168, 75, 0.2);
+}
.oathhammer .skills-container {
display: grid;
grid-template-columns: repeat(2, 1fr);
@@ -183,6 +275,78 @@
font-weight: bold;
color: #084a74;
}
+.application.oathhammer {
+ border: 2px solid #c8a84b;
+ border-radius: 6px;
+ box-shadow: 0 0 0 1px #2a1a0a, 0 4px 24px rgba(0, 0, 0, 0.7), inset 0 0 0 1px rgba(200, 168, 75, 0.3);
+ outline: none;
+}
+.application.oathhammer > header.window-header {
+ background: linear-gradient(to bottom, #0b629a 0%, #084a74 40%, #052c44 100%);
+ border-bottom: 2px solid #c8a84b;
+ border-radius: 4px 4px 0 0;
+ padding: 4px 8px;
+ min-height: 32px;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+.application.oathhammer > header.window-header::before {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 2px;
+ background: linear-gradient(to right, transparent, rgba(200, 168, 75, 0.7) 20%, #c8a84b 50%, rgba(200, 168, 75, 0.7) 80%, transparent);
+ border-radius: 4px 4px 0 0;
+ pointer-events: none;
+}
+.application.oathhammer > header.window-header .window-icon,
+.application.oathhammer > header.window-header img.header-icon {
+ width: 22px;
+ height: 22px;
+ border: 1px solid #c8a84b;
+ border-radius: 3px;
+ -o-object-fit: cover;
+ object-fit: cover;
+ flex-shrink: 0;
+ opacity: 0.9;
+}
+.application.oathhammer > header.window-header .window-title {
+ font-family: "Sherwood", "Palatino Linotype", serif;
+ font-size: 1.05rem;
+ font-weight: normal;
+ color: #c8a84b;
+ text-shadow: 0 1px 3px rgba(0, 0, 0, 0.8), 0 0 8px rgba(200, 168, 75, 0.4);
+ letter-spacing: 0.04em;
+ flex: 1;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+.application.oathhammer > header.window-header .header-control,
+.application.oathhammer > header.window-header .close,
+.application.oathhammer > header.window-header button.header-control {
+ color: rgba(200, 168, 75, 0.7);
+ background: transparent;
+ border: none;
+ padding: 2px 4px;
+ border-radius: 3px;
+ transition: color 0.15s, background 0.15s;
+ font-size: 0.9rem;
+ cursor: pointer;
+ flex-shrink: 0;
+}
+.application.oathhammer > header.window-header .header-control:hover,
+.application.oathhammer > header.window-header .close:hover,
+.application.oathhammer > header.window-header button.header-control:hover {
+ color: #c8a84b;
+ background: rgba(42, 26, 10, 0.4);
+}
+.application.oathhammer > .window-content {
+ border-radius: 0 0 4px 4px;
+}
.oathhammer .actor-img {
border: 2px solid #084a74;
border-radius: 4px;
@@ -728,3 +892,32 @@
font-weight: bold;
color: #084a74;
}
+.oathhammer .item-sheet-common .proficiency-section {
+ display: flex;
+ gap: 8px;
+ margin-top: 8px;
+}
+.oathhammer .item-sheet-common .proficiency-section .proficiency-fieldset {
+ flex: 1;
+}
+.oathhammer .item-sheet-common .proficiency-section .proficiency-checkboxes {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 4px 12px;
+ padding: 4px 2px;
+}
+.oathhammer .item-sheet-common .proficiency-section .proficiency-option {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ font-family: "Calibri", "Segoe UI", sans-serif;
+ font-size: 0.86rem;
+ color: #2a1a0a;
+ cursor: pointer;
+ white-space: nowrap;
+}
+.oathhammer .item-sheet-common .proficiency-section .proficiency-option input[type="checkbox"] {
+ width: auto;
+ height: auto;
+ accent-color: #084a74;
+}
diff --git a/less/base.less b/less/base.less
index f44360e..934f4a2 100644
--- a/less/base.less
+++ b/less/base.less
@@ -101,6 +101,70 @@
padding: 4px;
}
+// ============================================================
+// PROSEMIRROR / HTML EDITOR — force dark text on parchment
+// Foundry v13 uses ProseMirror for HTMLField rendering.
+// Default theme uses light text; we override for readability.
+// ============================================================
+.oathhammer {
+ // The prose-mirror custom element itself (view + edit)
+ prose-mirror {
+ color: @color-dark !important;
+ background: transparent;
+ }
+
+ // View mode (inactive): only force text color, do NOT touch layout/height
+ prose-mirror.inactive,
+ prose-mirror[toggled] {
+ color: @color-dark !important;
+ }
+
+ // The editor-content div (used in both modes)
+ .editor-content,
+ .prosemirror .editor-content,
+ prose-mirror .editor-content {
+ color: @color-dark !important;
+ background: transparent;
+
+ p, li, span, div, blockquote, h1, h2, h3, h4, h5, h6 {
+ color: @color-dark !important;
+ }
+
+ a { color: @color-blue; }
+ }
+
+ // Edit mode: the ProseMirror editable surface
+ .ProseMirror,
+ .prosemirror .editor-container .ProseMirror {
+ color: @color-dark !important;
+ background-color: fade(@color-paper, 60%);
+ border: 1px solid @color-blue;
+ border-radius: 3px;
+ padding: 4px 6px;
+ min-height: 4rem;
+
+ p, li, span { color: @color-dark !important; }
+ a { color: @color-blue; }
+
+ // ProseMirror placeholder ghost text
+ &.ProseMirror-empty::before {
+ color: fade(@color-dark, 45%);
+ }
+ }
+
+ // Editor toolbar
+ .editor-toolbar {
+ background: fade(@color-blue, 10%);
+ border-bottom: 1px solid @color-olive;
+
+ .control-icon,
+ button {
+ color: @color-blue;
+ &:hover { color: @color-dark; background: fade(@color-gold, 20%); }
+ }
+ }
+}
+
// ============================================================
// SKILLS TAB
// ============================================================
diff --git a/less/fvtt-oath-hammer.less b/less/fvtt-oath-hammer.less
index 1d4f2f9..1417909 100644
--- a/less/fvtt-oath-hammer.less
+++ b/less/fvtt-oath-hammer.less
@@ -4,6 +4,7 @@
@import "variables";
@import "base";
+@import "window-chrome";
@import "actor-sheet";
@import "npc-sheet";
@import "item-list";
diff --git a/less/item-sheets.less b/less/item-sheets.less
index 8538a89..595d8c2 100644
--- a/less/item-sheets.less
+++ b/less/item-sheets.less
@@ -114,4 +114,39 @@
font-weight: bold;
color: @color-blue;
}
+
+ // ── Class proficiency checkboxes ────────────────────────────
+ .proficiency-section {
+ display: flex;
+ gap: 8px;
+ margin-top: 8px;
+
+ .proficiency-fieldset {
+ flex: 1;
+ }
+
+ .proficiency-checkboxes {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 4px 12px;
+ padding: 4px 2px;
+ }
+
+ .proficiency-option {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ font-family: @font-body;
+ font-size: @font-size-base;
+ color: @color-dark;
+ cursor: pointer;
+ white-space: nowrap;
+
+ input[type="checkbox"] {
+ width: auto;
+ height: auto;
+ accent-color: @color-blue;
+ }
+ }
+ }
}
diff --git a/less/window-chrome.less b/less/window-chrome.less
new file mode 100644
index 0000000..545c8ee
--- /dev/null
+++ b/less/window-chrome.less
@@ -0,0 +1,107 @@
+// ============================================================
+// WINDOW CHROME — Medieval fantasy window decorations
+// Targets Foundry v13 ApplicationV2 window structure
+// ============================================================
+
+// ── Outer window frame ───────────────────────────────────────
+.application.oathhammer {
+ border: 2px solid @color-gold;
+ border-radius: 6px;
+ box-shadow:
+ 0 0 0 1px @color-dark, // tight dark liner inside the gold border
+ 0 4px 24px rgba(0, 0, 0, 0.7), // deep drop-shadow for depth
+ inset 0 0 0 1px fade(@color-gold, 30%); // soft inner glow
+
+ // Remove Foundry's default border/shadow so ours wins
+ outline: none;
+
+ // ── Title bar ──────────────────────────────────────────────
+ > header.window-header {
+ background: linear-gradient(
+ to bottom,
+ lighten(@color-blue, 8%) 0%,
+ @color-blue 40%,
+ darken(@color-blue, 10%) 100%
+ );
+ border-bottom: 2px solid @color-gold;
+ border-radius: 4px 4px 0 0;
+ padding: 4px 8px;
+ min-height: 32px;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+
+ // Decorative top highlight line
+ &::before {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 2px;
+ background: linear-gradient(
+ to right,
+ transparent,
+ fade(@color-gold, 70%) 20%,
+ @color-gold 50%,
+ fade(@color-gold, 70%) 80%,
+ transparent
+ );
+ border-radius: 4px 4px 0 0;
+ pointer-events: none;
+ }
+
+ // ── Window icon ──────────────────────────────────────────
+ .window-icon,
+ img.header-icon {
+ width: 22px;
+ height: 22px;
+ border: 1px solid @color-gold;
+ border-radius: 3px;
+ object-fit: cover;
+ flex-shrink: 0;
+ opacity: 0.9;
+ }
+
+ // ── Window title text ────────────────────────────────────
+ .window-title {
+ font-family: @font-primary; // Sherwood decorative font
+ font-size: 1.05rem;
+ font-weight: normal;
+ color: @color-gold;
+ text-shadow:
+ 0 1px 3px rgba(0, 0, 0, 0.8),
+ 0 0 8px fade(@color-gold, 40%);
+ letter-spacing: 0.04em;
+ flex: 1;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ // ── Header control buttons (close, minimize, etc.) ───────
+ .header-control,
+ .close,
+ button.header-control {
+ color: fade(@color-gold, 70%);
+ background: transparent;
+ border: none;
+ padding: 2px 4px;
+ border-radius: 3px;
+ transition: color 0.15s, background 0.15s;
+ font-size: 0.9rem;
+ cursor: pointer;
+ flex-shrink: 0;
+
+ &:hover {
+ color: @color-gold;
+ background: fade(@color-dark, 40%);
+ }
+ }
+ }
+
+ // ── Content area — ensure parchment bg fills properly ──────
+ > .window-content {
+ border-radius: 0 0 4px 4px;
+ }
+}
diff --git a/module/applications/sheets/armor-sheet.mjs b/module/applications/sheets/armor-sheet.mjs
index 241a74d..e950a53 100644
--- a/module/applications/sheets/armor-sheet.mjs
+++ b/module/applications/sheets/armor-sheet.mjs
@@ -22,6 +22,8 @@ export default class OathHammerArmorSheet extends OathHammerItemSheet {
/** @override */
async _prepareContext() {
const context = await super._prepareContext()
+ const enrich = (v) => foundry.applications.ux.TextEditor.implementation.enrichHTML(v ?? "", { async: true })
+ context.enrichedMagicEffect = await enrich(this.document.system.magicEffect)
return context
}
}
diff --git a/module/applications/sheets/base-item-sheet.mjs b/module/applications/sheets/base-item-sheet.mjs
index 8d44b3e..603c4e0 100644
--- a/module/applications/sheets/base-item-sheet.mjs
+++ b/module/applications/sheets/base-item-sheet.mjs
@@ -1,4 +1,5 @@
const { HandlebarsApplicationMixin } = foundry.applications.api
+import { ARMOR_TYPE_CHOICES, WEAPON_PROFICIENCY_GROUPS } from "../../config/system.mjs"
export default class OathHammerItemSheet extends HandlebarsApplicationMixin(foundry.applications.sheets.ItemSheetV2) {
static SHEET_MODES = { EDIT: 0, PLAY: 1 }
@@ -68,6 +69,9 @@ export default class OathHammerItemSheet extends HandlebarsApplicationMixin(foun
context.apChoices = Object.fromEntries(
Array.from({ length: 7 }, (_, i) => [i, String(i)])
)
+ // Class proficiency choices (for class-sheet checkboxes)
+ context.armorTypeChoices = ARMOR_TYPE_CHOICES
+ context.weaponGroupChoices = WEAPON_PROFICIENCY_GROUPS
return context
}
diff --git a/module/applications/sheets/class-sheet.mjs b/module/applications/sheets/class-sheet.mjs
index 3f9ffd2..df506e0 100644
--- a/module/applications/sheets/class-sheet.mjs
+++ b/module/applications/sheets/class-sheet.mjs
@@ -27,4 +27,16 @@ export default class OathHammerClassSheet extends OathHammerItemSheet {
)
return context
}
+
+ /** @override — collect checkbox sets explicitly so unchecking all works */
+ _prepareSubmitData(event, form, formData) {
+ const data = super._prepareSubmitData(event, form, formData)
+ data["system.armorProficiency"] = Array.from(
+ form.querySelectorAll('input[name="system.armorProficiency"]:checked')
+ ).map(el => el.value)
+ data["system.weaponProficiency"] = Array.from(
+ form.querySelectorAll('input[name="system.weaponProficiency"]:checked')
+ ).map(el => el.value)
+ return data
+ }
}
diff --git a/module/applications/sheets/magic-item-sheet.mjs b/module/applications/sheets/magic-item-sheet.mjs
index 64440cb..542f995 100644
--- a/module/applications/sheets/magic-item-sheet.mjs
+++ b/module/applications/sheets/magic-item-sheet.mjs
@@ -22,6 +22,8 @@ export default class OathHammerMagicItemSheet extends OathHammerItemSheet {
/** @override */
async _prepareContext() {
const context = await super._prepareContext()
+ const enrich = (v) => foundry.applications.ux.TextEditor.implementation.enrichHTML(v ?? "", { async: true })
+ context.enrichedEffect = await enrich(this.document.system.effect)
return context
}
}
diff --git a/module/applications/sheets/weapon-sheet.mjs b/module/applications/sheets/weapon-sheet.mjs
index 011bf52..3b5594d 100644
--- a/module/applications/sheets/weapon-sheet.mjs
+++ b/module/applications/sheets/weapon-sheet.mjs
@@ -22,6 +22,8 @@ export default class OathHammerWeaponSheet extends OathHammerItemSheet {
/** @override */
async _prepareContext() {
const context = await super._prepareContext()
+ const enrich = (v) => foundry.applications.ux.TextEditor.implementation.enrichHTML(v ?? "", { async: true })
+ context.enrichedMagicEffect = await enrich(this.document.system.magicEffect)
return context
}
}
diff --git a/module/models/class.mjs b/module/models/class.mjs
index e7a16e2..fad05e6 100644
--- a/module/models/class.mjs
+++ b/module/models/class.mjs
@@ -1,3 +1,5 @@
+import { ARMOR_TYPE_CHOICES, WEAPON_PROFICIENCY_GROUPS } from "../config/system.mjs"
+
export default class OathHammerClass extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields
@@ -8,14 +10,27 @@ export default class OathHammerClass extends foundry.abstract.TypeDataModel {
// Class features, starting abilities, advancement options (rich text)
schema.features = new fields.HTMLField({ required: true, textSearch: true })
- // Armor proficiencies (e.g. "Light, Medium, Heavy")
- schema.armorProficiency = new fields.StringField({ required: true, nullable: false, initial: "" })
+ // Armor proficiencies — set of keys from ARMOR_TYPE_CHOICES (light, medium, heavy)
+ schema.armorProficiency = new fields.SetField(
+ new fields.StringField({ choices: ARMOR_TYPE_CHOICES }),
+ { required: true, initial: [] }
+ )
- // Weapon proficiencies (e.g. "Common, Dueling, Heavy, Throwing")
- schema.weaponProficiency = new fields.StringField({ required: true, nullable: false, initial: "" })
+ // Weapon proficiencies — set of keys from WEAPON_PROFICIENCY_GROUPS
+ schema.weaponProficiency = new fields.SetField(
+ new fields.StringField({ choices: WEAPON_PROFICIENCY_GROUPS }),
+ { required: true, initial: [] }
+ )
return schema
}
+ // Migrate old free-text string values to empty sets
+ static migrateData(source) {
+ if (typeof source.armorProficiency === "string") source.armorProficiency = []
+ if (typeof source.weaponProficiency === "string") source.weaponProficiency = []
+ return super.migrateData(source)
+ }
+
static LOCALIZATION_PREFIXES = ["OATHHAMMER.Class"]
}
diff --git a/module/utils.mjs b/module/utils.mjs
index cee73ec..8902ae3 100644
--- a/module/utils.mjs
+++ b/module/utils.mjs
@@ -1,6 +1,11 @@
export default class OathHammerUtils {
static registerHandlebarsHelpers() {
- Handlebars.registerHelper("ifThen", (condition, trueVal, falseVal) => condition ? trueVal : falseVal)
+ Handlebars.registerHelper("includes", (collection, value) => {
+ if (!collection) return false
+ if (collection instanceof Set) return collection.has(value)
+ if (Array.isArray(collection)) return collection.includes(value)
+ return false
+ })
Handlebars.registerHelper("capitalize", (str) => {
if (typeof str !== "string") return str
return str.charAt(0).toUpperCase() + str.slice(1)
diff --git a/templates/actor/character-equipment.hbs b/templates/actor/character-equipment.hbs
index 9ee5174..a8a78f8 100644
--- a/templates/actor/character-equipment.hbs
+++ b/templates/actor/character-equipment.hbs
@@ -1,4 +1,21 @@
+
{{formInput fields.name value=source.name}}
{{formInput fields.name value=source.name}}
-