Various fixes and add rune support

This commit is contained in:
2026-03-30 23:38:45 +02:00
parent 2bf737a3ef
commit fb04448ab0
18 changed files with 506 additions and 9 deletions

View File

@@ -30,10 +30,19 @@ export default class OathHammerItemSheet extends HandlebarsApplicationMixin(foun
toggleSheet: OathHammerItemSheet.#onToggleSheet,
editImage: OathHammerItemSheet.#onEditImage,
rollRarity: OathHammerItemSheet.#onRollRarity,
removeRune: OathHammerItemSheet.#onRemoveRune,
openRune: OathHammerItemSheet.#onOpenRune,
},
}
_sheetMode = this.constructor.SHEET_MODES.PLAY
_lockedReadOnly = false
/** @override — prevent form submissions when this sheet is opened in read-only mode */
get isEditable() {
if (this._lockedReadOnly) return false
return super.isEditable
}
get isPlayMode() {
return this._sheetMode === this.constructor.SHEET_MODES.PLAY
@@ -85,13 +94,49 @@ export default class OathHammerItemSheet extends HandlebarsApplicationMixin(foun
// Class proficiency choices (for class-sheet checkboxes)
context.armorTypeChoices = ARMOR_TYPE_CHOICES
context.weaponGroupChoices = WEAPON_PROFICIENCY_GROUPS
// Rune context — enrich effect HTML for each attached rune
if (Array.isArray(this.document.system.runes)) {
context.enrichedRunes = await Promise.all(
this.document.system.runes.map(async (rune, idx) => ({
...rune,
idx,
enrichedEffect: await foundry.applications.ux.TextEditor.implementation.enrichHTML(rune.effect ?? "", { async: true }),
}))
)
}
context.acceptsRunes = this.#acceptsRunes()
return context
}
/** Returns true if this item type can have runic spells attached. */
#acceptsRunes() {
const type = this.document.type
if (type === "armor" || type === "weapon") return true
if (type === "magic-item" && this.document.system.itemType === "talisman") return true
return false
}
/** Map runeType expected for each item type. */
static #EXPECTED_RUNE_TYPE = {
armor: "armor",
weapon: "weapon",
"magic-item": "talisman",
}
/** @override */
_onRender(context, options) {
super._onRender(context, options)
this.#dragDrop.forEach((d) => d.bind(this.element))
if (this._lockedReadOnly) {
this.element.querySelector('[data-action="toggleSheet"]')?.remove()
for (const el of this.element.querySelectorAll(
'.window-content input, .window-content select, .window-content textarea, .window-content button'
)) {
el.disabled = true
}
}
for (const pm of this.element.querySelectorAll("prose-mirror[name]")) {
pm.addEventListener("change", async (event) => {
event.stopPropagation()
@@ -129,9 +174,93 @@ export default class OathHammerItemSheet extends HandlebarsApplicationMixin(foun
_onDragOver(event) {}
async _onDrop(event) {}
async _onDrop(event) {
if (!this.#acceptsRunes()) return
let dragData
try { dragData = JSON.parse(event.dataTransfer.getData("text/plain")) } catch { return }
if (dragData?.type !== "Item") return
let spell
try { spell = await fromUuid(dragData.uuid) } catch { return }
if (!spell || spell.type !== "spell") return
const sys = spell.system
if (sys.tradition !== "runic") {
ui.notifications.warn(game.i18n.localize("OATHHAMMER.Rune.NotRunic"))
return
}
const expectedType = OathHammerItemSheet.#EXPECTED_RUNE_TYPE[this.document.type]
if (sys.runeType !== expectedType) {
ui.notifications.warn(game.i18n.format("OATHHAMMER.Rune.WrongType", {
expected: game.i18n.localize(`OATHHAMMER.RuneType.${expectedType.charAt(0).toUpperCase() + expectedType.slice(1)}`),
}))
return
}
const current = this.document.system.runes ?? []
if (current.length >= 2) {
ui.notifications.warn(game.i18n.localize("OATHHAMMER.Rune.MaxRunes"))
return
}
if (current.some(r => r.name === spell.name)) {
ui.notifications.warn(game.i18n.localize("OATHHAMMER.Rune.Duplicate"))
return
}
if (sys.isExalted && current.some(r => r.isExalted)) {
ui.notifications.warn(game.i18n.localize("OATHHAMMER.Rune.MaxExalted"))
return
}
const snapshot = {
sourceUuid: spell.uuid,
name: spell.name,
img: spell.img,
runeType: sys.runeType,
isExalted: sys.isExalted,
difficultyValue: sys.difficultyValue,
effect: sys.effect ?? "",
duration: sys.duration ?? "",
range: sys.range ?? "",
spellSave: sys.spellSave ?? "",
}
await this.document.update({ "system.runes": [...current, snapshot] })
ui.notifications.info(game.i18n.format("OATHHAMMER.Rune.Attached", { name: spell.name }))
}
static async #onRemoveRune(event, target) {
const idx = parseInt(target.dataset.runeIndex, 10)
if (isNaN(idx)) return
const runes = [...(this.document.system.runes ?? [])]
runes.splice(idx, 1)
await this.document.update({ "system.runes": runes })
}
static async #onOpenRune(event, target) {
const uuid = target.dataset.sourceUuid
if (!uuid) return
try {
const spell = await fromUuid(uuid)
if (!spell) {
ui.notifications.warn(game.i18n.localize("OATHHAMMER.Rune.SourceNotFound"))
return
}
// Use a unique ID so this read-only instance never conflicts with the
// document's normal sheet (which uses {ClassName}-{documentId} as its ID).
const SheetClass = spell.sheet.constructor
const sheet = new SheetClass({
document: spell,
id: `${SheetClass.name}-rune-view-${spell.id}`,
})
sheet._lockedReadOnly = true
sheet.render(true)
} catch {
ui.notifications.warn(game.i18n.localize("OATHHAMMER.Rune.SourceNotFound"))
}
}
static #onToggleSheet(event, target) {
if (this._lockedReadOnly) return
const modes = this.constructor.SHEET_MODES
this._sheetMode = this.isEditMode ? modes.PLAY : modes.EDIT
this.render()