diff --git a/adventures-with-emmy.mjs b/adventures-with-emmy.mjs index 7f54d61..ec1ec23 100644 --- a/adventures-with-emmy.mjs +++ b/adventures-with-emmy.mjs @@ -24,6 +24,7 @@ Hooks.once("init", function () { CONFIG.Item.dataModels = { ability: models.AwEAbility, field: models.AwEField, + specialization: models.AwESpecialization, archetype: models.AwEArchetype, background: models.AwEBackground, kit: models.AwEKit, @@ -48,6 +49,9 @@ Hooks.once("init", function () { foundry.documents.collections.Items.registerSheet("fvtt-adventures-with-emmy", applications.AwEFieldSheet, { types: ["field"], makeDefault: true }) + foundry.documents.collections.Items.registerSheet("fvtt-adventures-with-emmy", applications.AwESpecializationSheet, { + types: ["specialization"], makeDefault: true + }) foundry.documents.collections.Items.registerSheet("fvtt-adventures-with-emmy", applications.AwEArchetypeSheet, { types: ["archetype"], makeDefault: true }) diff --git a/lang/en.json b/lang/en.json index 202aa8e..de607f5 100644 --- a/lang/en.json +++ b/lang/en.json @@ -36,6 +36,7 @@ "AWEMMY.Archetype.PrerequisiteLevel": "Prerequisite Level", "AWEMMY.Item.Ability": "Ability", "AWEMMY.Item.Field": "Field", + "AWEMMY.Item.Specialization": "Specialization", "AWEMMY.Item.Archetype": "Archetype", "AWEMMY.Item.Background": "Background", "AWEMMY.Item.Kit": "Kit", @@ -81,8 +82,12 @@ "AWEMMY.Character.Pronouns": "Pronouns", "AWEMMY.Character.Specialization": "Specialization", "AWEMMY.Character.DropField": "Drag & drop a Field item here", + "AWEMMY.Character.DropSpecialization": "Drag & drop a Specialization item here", "AWEMMY.Character.DropArchetype": "Drag & drop Archetype items here", "AWEMMY.Character.DropBackground": "Drag & drop a Background item here", + "AWEMMY.Specialization.FieldName": "Parent Field", + "AWEMMY.Specialization.KeyAttributeOverride": "Key Attribute Override", + "AWEMMY.Specialization.Traits": "Traits", "AWEMMY.Creature.EurekaRubric": "Eureka Rubric", "AWEMMY.Creature.Claims": "Claims", "AWEMMY.Creature.Evidence": "Evidence", diff --git a/module/applications/_module.mjs b/module/applications/_module.mjs index aabe415..4511915 100644 --- a/module/applications/_module.mjs +++ b/module/applications/_module.mjs @@ -2,6 +2,7 @@ export { default as AwECharacterSheet } from "./sheets/character-sheet.mjs" export { default as AwECreatureSheet } from "./sheets/creature-sheet.mjs" export { default as AwEAbilitySheet } from "./sheets/ability-sheet.mjs" export { default as AwEFieldSheet } from "./sheets/field-sheet.mjs" +export { default as AwESpecializationSheet } from "./sheets/specialization-sheet.mjs" export { default as AwEArchetypeSheet } from "./sheets/archetype-sheet.mjs" export { default as AwEBackgroundSheet } from "./sheets/background-sheet.mjs" export { default as AwEKitSheet } from "./sheets/kit-sheet.mjs" diff --git a/module/applications/sheets/character-sheet.mjs b/module/applications/sheets/character-sheet.mjs index 7fe3f72..911fec3 100644 --- a/module/applications/sheets/character-sheet.mjs +++ b/module/applications/sheets/character-sheet.mjs @@ -78,19 +78,39 @@ export default class AwECharacterSheet extends AwEActorSheet { case "main": context.tab = context.tabs.main context.abilities = doc.itemTypes.ability.map(item => ({ - ...item, + id: item.id, + uuid: item.uuid, + name: item.name, + img: item.img, + system: item.system, costLabel: game.i18n.localize(SYSTEM.ABILITY_COST[item.system.cost]?.label ?? item.system.cost) })) break case "biography": context.tab = context.tabs.biography context.fields = doc.itemTypes.field.map(item => ({ - ...item, + id: item.id, + uuid: item.uuid, + name: item.name, + img: item.img, + system: item.system, keyAttrLabel: game.i18n.localize(SYSTEM.ATTRIBUTES[item.system.keyAttribute]?.label ?? item.system.keyAttribute), keyAttr2Label: item.system.keyAttribute2 ? game.i18n.localize(SYSTEM.ATTRIBUTES[item.system.keyAttribute2]?.label ?? item.system.keyAttribute2) : null })) + context.specializations = (doc.itemTypes.specialization ?? []).map(item => { + const fieldMatch = doc.itemTypes.field.some(f => + AwECharacterSheet.#slugify(f.name) === AwECharacterSheet.#slugify(item.system.fieldName) + ) + const attrOverrideLabel = item.system.keyAttributeOverride + ? game.i18n.localize(SYSTEM.ATTRIBUTES[item.system.keyAttributeOverride]?.label ?? item.system.keyAttributeOverride) + : null + return { + id: item.id, uuid: item.uuid, name: item.name, img: item.img, system: item.system, + fieldMatch, attrOverrideLabel + } + }) context.archetypes = doc.itemTypes.archetype context.backgrounds = doc.itemTypes.background context.enrichedDescription = await foundry.applications.ux.TextEditor.implementation.enrichHTML( @@ -123,8 +143,8 @@ export default class AwECharacterSheet extends AwEActorSheet { /** @override */ async _onDropItem(item) { if (!item) return - // field/background: max 1 (replace existing); archetype: multiple allowed - if (item.type === "field" || item.type === "background") { + // field/background/specialization: max 1 (replace existing); archetype: multiple allowed + if (item.type === "field" || item.type === "background" || item.type === "specialization") { const existing = this.document.itemTypes[item.type] if (existing.length > 0) await existing[0].delete() return this.document.createEmbeddedDocuments("Item", [item.toObject()]) @@ -197,6 +217,7 @@ export default class AwECharacterSheet extends AwEActorSheet { /** * Roll the key attribute check from a Field item. + * If a matching Specialization has a keyAttributeOverride, it takes priority. * @param {PointerEvent} event The triggering event. * @param {HTMLElement} target The target element. */ @@ -204,7 +225,23 @@ export default class AwECharacterSheet extends AwEActorSheet { const itemId = target.closest("[data-item-id]")?.dataset.itemId const item = this.document.items.get(itemId) if (!item) return - const attrId = target.dataset.attributeId ?? item.system.keyAttribute - await this.document.rollAttribute(attrId) + + // Check for a specialization that matches this field and overrides the key attribute + const spec = this.document.itemTypes.specialization?.find(s => + AwECharacterSheet.#slugify(s.system.fieldName) === AwECharacterSheet.#slugify(item.name) + ) + const attrId = spec?.system.keyAttributeOverride + || target.dataset.attributeId + || item.system.keyAttribute + + await this.document.rollAttribute(attrId, { + sourceItemName: item.name, + sourceItemImg: item.img + }) + } + + /** Slugify a string for loose name matching (lowercase, trim, spaces→dash, strip non-alphanum). */ + static #slugify(str) { + return (str ?? "").toLowerCase().trim().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "") } } diff --git a/module/applications/sheets/field-sheet.mjs b/module/applications/sheets/field-sheet.mjs index 9d21bc2..9321748 100644 --- a/module/applications/sheets/field-sheet.mjs +++ b/module/applications/sheets/field-sheet.mjs @@ -5,11 +5,7 @@ export default class AwEFieldSheet extends AwEItemSheet { static DEFAULT_OPTIONS = { classes: ["field"], position: { width: 620 }, - window: { contentClasses: ["field-content"] }, - actions: { - addSpecialization: AwEFieldSheet.#onAddSpecialization, - removeSpecialization: AwEFieldSheet.#onRemoveSpecialization - } + window: { contentClasses: ["field-content"] } } /** @override */ @@ -18,29 +14,4 @@ export default class AwEFieldSheet extends AwEItemSheet { template: "systems/fvtt-adventures-with-emmy/templates/field.hbs" } } - - /** - * Handle adding a specialization. - * @param {PointerEvent} event - The initiating event. - * @param {HTMLElement} target - The input element. - */ - static async #onAddSpecialization(event, target) { - const value = target.value.trim() - if (!value) return - const current = this.document.system.specializations ?? [] - await this.document.update({ "system.specializations": [...current, value] }) - target.value = "" - } - - /** - * Handle removing a specialization. - * @param {PointerEvent} event - The initiating click event. - * @param {HTMLElement} target - The remove button. - */ - static async #onRemoveSpecialization(event, target) { - const index = Number(target.dataset.index) - const current = [...(this.document.system.specializations ?? [])] - current.splice(index, 1) - await this.document.update({ "system.specializations": current }) - } } diff --git a/module/applications/sheets/specialization-sheet.mjs b/module/applications/sheets/specialization-sheet.mjs new file mode 100644 index 0000000..b868098 --- /dev/null +++ b/module/applications/sheets/specialization-sheet.mjs @@ -0,0 +1,28 @@ +import AwEItemSheet from "./base-item-sheet.mjs" +import { SYSTEM } from "../../config/system.mjs" + +export default class AwESpecializationSheet extends AwEItemSheet { + /** @override */ + static DEFAULT_OPTIONS = { + classes: ["specialization"], + position: { width: 620 }, + window: { contentClasses: ["specialization-content"] } + } + + /** @override */ + static PARTS = { + main: { + template: "systems/fvtt-adventures-with-emmy/templates/specialization.hbs" + } + } + + /** @override */ + async _prepareContext(options) { + const context = await super._prepareContext(options) + context.attributeChoices = { + "": "—", + ...Object.fromEntries(Object.values(SYSTEM.ATTRIBUTES).map(a => [a.id, game.i18n.localize(a.label)])) + } + return context + } +} diff --git a/module/documents/roll.mjs b/module/documents/roll.mjs index 409c1f3..5aee37a 100644 --- a/module/documents/roll.mjs +++ b/module/documents/roll.mjs @@ -14,9 +14,11 @@ export default class AwERoll extends Roll { get knowledgeBonus() { return this.options.knowledgeBonus ?? 0 } get dc() { return this.options.dc } get outcome() { return this.options.outcome } - get actorId() { return this.options.actorId } - get actorName() { return this.options.actorName } - get actorImage() { return this.options.actorImage } + get actorId() { return this.options.actorId } + get actorName() { return this.options.actorName } + get actorImage() { return this.options.actorImage } + get sourceItemName() { return this.options.sourceItemName } + get sourceItemImg() { return this.options.sourceItemImg } // --- Outcome calculation --- @@ -153,9 +155,11 @@ export default class AwERoll extends Roll { bonus, knowledgeBonus, dc, - actorId: options.actorId, - actorName: options.actorName, - actorImage: options.actorImage + actorId: options.actorId, + actorName: options.actorName, + actorImage: options.actorImage, + sourceItemName: options.sourceItemName, + sourceItemImg: options.sourceItemImg }) await roll.evaluate() @@ -189,8 +193,10 @@ export default class AwERoll extends Roll { dice: this.dice, outcome: isPrivate ? null : this.outcome, dc: this.dc, - actorName: this.actorName, - actorImage: this.actorImage, + actorName: this.actorName, + actorImage: this.actorImage, + sourceItemName: this.sourceItemName, + sourceItemImg: this.sourceItemImg, isPrivate } ) diff --git a/module/models/_module.mjs b/module/models/_module.mjs index e5d1fbf..1f14d69 100644 --- a/module/models/_module.mjs +++ b/module/models/_module.mjs @@ -2,6 +2,7 @@ export { default as AwECharacter } from "./character.mjs" export { default as AwECreature } from "./creature.mjs" export { default as AwEAbility } from "./ability.mjs" export { default as AwEField } from "./field.mjs" +export { default as AwESpecialization } from "./specialization.mjs" export { default as AwEArchetype } from "./archetype.mjs" export { default as AwEBackground } from "./background.mjs" export { default as AwEKit } from "./kit.mjs" diff --git a/module/models/field.mjs b/module/models/field.mjs index 19cae9b..d96d608 100644 --- a/module/models/field.mjs +++ b/module/models/field.mjs @@ -19,7 +19,6 @@ export default class AwEField extends foundry.abstract.TypeDataModel { blank: true, choices: { "": "—", ...Object.values(SYSTEM.ATTRIBUTES).reduce((o, a) => { o[a.id] = a.label; return o }, {}) } }) - schema.specializations = new fields.ArrayField(new fields.StringField()) schema.knowledgeBonus = new fields.StringField({ initial: "", required: false, nullable: true }) return schema diff --git a/module/models/specialization.mjs b/module/models/specialization.mjs new file mode 100644 index 0000000..53ad326 --- /dev/null +++ b/module/models/specialization.mjs @@ -0,0 +1,31 @@ +import { SYSTEM } from "../config/system.mjs" + +export default class AwESpecialization extends foundry.abstract.TypeDataModel { + static defineSchema() { + const fields = foundry.data.fields + const schema = {} + + schema.fieldName = new fields.StringField({ + required: true, + nullable: false, + initial: "", + blank: true + }) + + schema.keyAttributeOverride = new fields.StringField({ + required: false, + nullable: true, + initial: "", + blank: true, + choices: { + "": "—", + ...Object.fromEntries(Object.values(SYSTEM.ATTRIBUTES).map(a => [a.id, a.label])) + } + }) + + schema.description = new fields.HTMLField({ required: true, textSearch: true }) + schema.traits = new fields.ArrayField(new fields.StringField()) + + return schema + } +} diff --git a/system.json b/system.json index 2e34548..4b04cc5 100644 --- a/system.json +++ b/system.json @@ -18,6 +18,7 @@ "creature": { "htmlFields": ["description"] } }, "Item": { + "specialization": { "htmlFields": ["description"] }, "ability": { "htmlFields": ["description"] }, "field": { "htmlFields": ["description"] }, "archetype": { "htmlFields": ["description"] }, diff --git a/templates/character-biography.hbs b/templates/character-biography.hbs index 772507b..253be4a 100644 --- a/templates/character-biography.hbs +++ b/templates/character-biography.hbs @@ -38,6 +38,33 @@ {{#unless fields.length}}