diff --git a/daggerheart.mjs b/daggerheart.mjs index 58600ad4..e223865d 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -13,6 +13,7 @@ import { dualityRollEnricher } from './module/enrichers/DualityRollEnricher.mjs' import { getCommandTarget, rollCommandToJSON, setDiceSoNiceForDualityRoll } from './module/helpers/utils.mjs'; import { abilities } from './module/config/actorConfig.mjs'; import Resources from './module/applications/resources.mjs'; +import DHDualityRoll from './module/data/chat-message/dualityRoll.mjs'; globalThis.SYSTEM = SYSTEM; @@ -40,7 +41,7 @@ Hooks.once('init', () => { CONFIG.Item.dataModels = models.items.config; const { Items, Actors } = foundry.documents.collections; - Items.unregisterSheet('core', foundry.appv1.sheets.ItemSheet); + Items.unregisterSheet('core', foundry.applications.sheets.ItemSheetV2); Items.registerSheet(SYSTEM.id, applications.DhpAncestry, { types: ['ancestry'], makeDefault: true }); Items.registerSheet(SYSTEM.id, applications.DhpCommunity, { types: ['community'], makeDefault: true }); Items.registerSheet(SYSTEM.id, applications.DhpClassSheet, { types: ['class'], makeDefault: true }); @@ -54,12 +55,12 @@ Hooks.once('init', () => { CONFIG.Actor.documentClass = documents.DhpActor; CONFIG.Actor.dataModels = { - pc: models.DhpPC, + character: models.DhCharacter, adversary: models.DhpAdversary, environment: models.DhpEnvironment }; - Actors.unregisterSheet('core', foundry.appv1.sheets.ActorSheet); - Actors.registerSheet(SYSTEM.id, applications.DhpPCSheet, { types: ['pc'], makeDefault: true }); + Actors.unregisterSheet('core', foundry.applications.sheets.ActorSheetV2); + Actors.registerSheet(SYSTEM.id, applications.DhCharacterSheet, { types: ['character'], makeDefault: true }); Actors.registerSheet(SYSTEM.id, applications.DhpAdversarySheet, { types: ['adversary'], makeDefault: true }); Actors.registerSheet(SYSTEM.id, applications.DhpEnvironment, { types: ['environment'], makeDefault: true }); @@ -137,22 +138,28 @@ const renderDualityButton = async event => { title: button.dataset.label, value: rollModifier }); + + const systemData = new DHDualityRoll({ + title: button.dataset.label, + origin: target.id, + roll: roll._formula, + modifiers: modifiers, + hope: hope, + fear: fear, + advantage: advantage, + disadvantage: disadvantage + }); + const cls = getDocumentClass('ChatMessage'); const msgData = { type: 'dualityRoll', sound: CONFIG.sounds.dice, - system: { - title: button.dataset.label, - origin: target.id, - roll: roll._formula, - modifiers: modifiers, - hope: hope, - fear: fear, - advantage: advantage, - disadvantage: disadvantage - }, + system: systemData, user: game.user.id, - content: 'systems/daggerheart/templates/chat/duality-roll.hbs', + content: await foundry.applications.handlebars.renderTemplate( + 'systems/daggerheart/templates/chat/duality-roll.hbs', + systemData + ), rolls: [roll] }; @@ -226,29 +233,34 @@ Hooks.on('chatMessage', (_, message) => { : undefined, title }); - }).then(({ roll, attribute, title }) => { + }).then(async ({ roll, attribute, title }) => { const cls = getDocumentClass('ChatMessage'); + const systemData = new DHDualityRoll({ + title: title, + origin: target?.id, + roll: roll._formula, + modifiers: attribute ? [attribute] : [], + hope: { dice: rollCommand.hope ?? 'd12', value: roll.dice[0].total }, + fear: { dice: rollCommand.fear ?? 'd12', value: roll.dice[1].total }, + advantage: + rollCommand.advantage && !rollCommand.disadvantage + ? { dice: 'd6', value: roll.dice[2].total } + : undefined, + disadvantage: + rollCommand.disadvantage && !rollCommand.advantage + ? { dice: 'd6', value: roll.dice[2].total } + : undefined + }); + const msgData = { type: 'dualityRoll', sound: CONFIG.sounds.dice, - system: { - title: title, - origin: target?.id, - roll: roll._formula, - modifiers: attribute ? [attribute] : [], - hope: { dice: rollCommand.hope ?? 'd12', value: roll.dice[0].total }, - fear: { dice: rollCommand.fear ?? 'd12', value: roll.dice[1].total }, - advantage: - rollCommand.advantage && !rollCommand.disadvantage - ? { dice: 'd6', value: roll.dice[2].total } - : undefined, - disadvantage: - rollCommand.disadvantage && !rollCommand.advantage - ? { dice: 'd6', value: roll.dice[2].total } - : undefined - }, + system: systemData, user: game.user.id, - content: 'systems/daggerheart/templates/chat/duality-roll.hbs', + content: await foundry.applications.handlebars.renderTemplate( + 'systems/daggerheart/templates/chat/duality-roll.hbs', + systemData + ), rolls: [roll] }; @@ -275,10 +287,10 @@ const preloadHandlebarsTemplates = async function () { 'systems/daggerheart/templates/sheets/parts/heritage.hbs', 'systems/daggerheart/templates/sheets/parts/subclassFeature.hbs', 'systems/daggerheart/templates/sheets/parts/effects.hbs', - 'systems/daggerheart/templates/sheets/pc/sections/inventory.hbs', - 'systems/daggerheart/templates/sheets/pc/sections/loadout.hbs', - 'systems/daggerheart/templates/sheets/pc/parts/heritageCard.hbs', - 'systems/daggerheart/templates/sheets/pc/parts/advancementCard.hbs', + 'systems/daggerheart/templates/sheets/character/sections/inventory.hbs', + 'systems/daggerheart/templates/sheets/character/sections/loadout.hbs', + 'systems/daggerheart/templates/sheets/character/parts/heritageCard.hbs', + 'systems/daggerheart/templates/sheets/character/parts/advancementCard.hbs', 'systems/daggerheart/templates/components/card-preview.hbs', 'systems/daggerheart/templates/views/levelup/parts/selectable-card-preview.hbs', 'systems/daggerheart/templates/sheets/global/partials/feature-section-item.hbs', diff --git a/lang/en.json b/lang/en.json index 78532b49..a843e2c6 100755 --- a/lang/en.json +++ b/lang/en.json @@ -13,7 +13,7 @@ "armor": "Armor" }, "Actor": { - "pc": "PC", + "character": "Character", "npc": "NPC", "adversary": "Adversary", "environment": "Environment" @@ -110,8 +110,14 @@ }, "VariantRules": { "ActionTokens": { - "Name": "Action Tokens", - "Hint": "Give each player action tokens to use in combat" + "label": "Action Tokens", + "hint": "Give each player action tokens to use in combat" + }, + "FIELDS": { + "useCoins": { + "label": "Use Coins", + "hint": "test" + } } }, "DualityRollColor": { @@ -132,10 +138,6 @@ "AttackTargetDoesNotExist": "The target token no longer exists" }, "Error": { - "NoClassSelected": "Your character has no class selected!", - "LacksDomain": "Your character doesn't have the domain of the card!", - "MaxLoadoutReached": "You can't have any more domain cards at this level!", - "DuplicateDomainCard": "You already have a domain card with that name!", "ActionRequiresTarget": "The action requires at least one target", "NoAssignedPlayerCharacter": "You have no assigned character.", "NoSelectedToken": "You have no selected token", @@ -1252,7 +1254,11 @@ "MissingClass": "The character is missing a class", "SubclassNotInClass": "The subclass does not belong to the character's class", "ClassAlreadySelected": "The character already has a class", - "SubclassAlreadySelected": "The character already has a subclass for that class." + "SubclassAlreadySelected": "The character already has a subclass for that class.", + "NoClassSelected": "Your character has no class selected!", + "LacksDomain": "Your character doesn't have the domain of the card!", + "MaxLoadoutReached": "You can't have any more domain cards at this level!", + "DuplicateDomainCard": "You already have a domain card with that name!" } }, "Effects": { diff --git a/module/applications/_module.mjs b/module/applications/_module.mjs index 5c031db0..079fe062 100644 --- a/module/applications/_module.mjs +++ b/module/applications/_module.mjs @@ -1,4 +1,4 @@ -export { default as DhpPCSheet } from './sheets/pc.mjs'; +export { default as DhCharacterSheet } from './sheets/character.mjs'; export { default as DhpAdversarySheet } from './sheets/adversary.mjs'; export { default as DhpClassSheet } from './sheets/items/class.mjs'; export { default as DhpSubclass } from './sheets/items/subclass.mjs'; diff --git a/module/applications/levelup.mjs b/module/applications/levelup.mjs index 22c6d33e..61a85677 100644 --- a/module/applications/levelup.mjs +++ b/module/applications/levelup.mjs @@ -149,7 +149,10 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) const experienceIncreaseValues = experienceIncreases .filter(exp => exp.data.length > 0) .flatMap(exp => - exp.data.map(data => this.actor.system.experiences.find(x => x.id === data).description) + exp.data.map(data => { + const experience = Object.keys(this.actor.system.experiences).find(x => x === data); + return this.actor.system.experiences[experience].description; + }) ); context.experienceIncreases = { values: experienceIncreaseValues, @@ -201,7 +204,7 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) const multiclassSubclass = this.actor.system.multiclass?.system?.subclasses?.[0]; const possibleSubclasses = [ - this.actor.system.subclass, + this.actor.system.class.subclass, ...(multiclassSubclass ? [multiclassSubclass] : []) ]; const selectedSubclasses = possibleSubclasses.filter(x => subclassSelections.includes(x.uuid)); @@ -274,8 +277,8 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) context.achievements = { proficiency: { - old: this.actor.system.proficiency.value, - new: this.actor.system.proficiency.value + achivementProficiency, + old: this.actor.system.proficiency, + new: this.actor.system.proficiency + achivementProficiency, shown: achivementProficiency > 0 }, damageThresholds: { @@ -328,10 +331,12 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) break; case 'experience': if (!advancement[choiceKey]) advancement[choiceKey] = []; - const data = checkbox.data.map( - data => - this.actor.system.experiences.find(x => x.id === data)?.description ?? '' - ); + const data = checkbox.data.map(data => { + const experience = Object.keys(this.actor.system.experiences).find( + x => x === data + ); + return this.actor.system.experiences[experience]?.description ?? ''; + }); advancement[choiceKey].push({ data: data, value: checkbox.value }); break; } @@ -354,8 +359,8 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) new: this.actor.system.resources.stress.max + (advancement.stress ?? 0) }, evasion: { - old: this.actor.system.evasion.value, - new: this.actor.system.evasion.value + (advancement.evasion ?? 0) + old: this.actor.system.evasion, + new: this.actor.system.evasion + (advancement.evasion ?? 0) } }, traits: @@ -421,8 +426,9 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) if (experienceIncreaseTagify) { tagifyElement( experienceIncreaseTagify, - this.actor.system.experiences.reduce((acc, experience) => { - acc[experience.id] = { label: experience.description }; + Object.keys(this.actor.system.experiences).reduce((acc, id) => { + const experience = this.actor.system.experiences[id]; + acc[id] = { label: experience.description }; return acc; }, {}), diff --git a/module/applications/multiclassDialog.mjs b/module/applications/multiclassDialog.mjs deleted file mode 100644 index f9e53c17..00000000 --- a/module/applications/multiclassDialog.mjs +++ /dev/null @@ -1,115 +0,0 @@ -const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; - -export default class DhpMulticlassDialog extends HandlebarsApplicationMixin(ApplicationV2) { - constructor(actorName, actorClass, resolve) { - super({}); - - this.actorName = actorName; - this.actorClass = actorClass; - this.resolve = resolve; - - this.classChoices = Array.from( - game.items.reduce((acc, x) => { - if (x.type === 'class' && x.name !== actorClass.name) { - acc.add(x); - } - - return acc; - }, new Set()) - ); - this.subclassChoices = []; - this.domainChoices = []; - - this.data = { - class: null, - subclass: null, - domain: null - }; - } - - get title() { - return `${this.actorName} - Multiclass`; - } - - static DEFAULT_OPTIONS = { - classes: ['daggerheart', 'views', 'multiclass'], - position: { width: 600, height: 'auto' }, - actions: { - selectClass: this.selectClass, - selectSubclass: this.selectSubclass, - selectDomain: this.selectDomain, - finish: this.finish - } - }; - - static PARTS = { - form: { - id: 'levelup', - template: 'systems/daggerheart/templates/views/multiclass.hbs' - } - }; - - async _prepareContext(_options) { - const context = await super._prepareContext(_options); - context.classChoices = this.classChoices; - context.subclassChoices = this.subclassChoices; - context.domainChoices = this.domainChoices; - context.disabledFinish = !this.data.class || !this.data.subclass || !this.data.domain; - context.data = this.data; - - return context; - } - - static async selectClass(_, button) { - const oldClass = this.data.class; - this.data.class = this.data.class?.uuid === button.dataset.class ? null : await fromUuid(button.dataset.class); - if (oldClass !== button.dataset.class) { - this.data.subclass = null; - this.data.domain = null; - this.subclassChoices = this.data.class ? this.data.class.system.subclasses : []; - - //FIXME - this.domainChoices = this.data.class - ? this.data.class.system.domains.map(x => { - const config = SYSTEM.DOMAIN.domains[x]; - return { - name: game.i18n.localize(config.name), - id: config.id, - img: config.src, - disabled: this.actorClass.system.domains.includes(config.id) - }; - }) - : []; - } - - this.render(true); - } - - static async selectSubclass(_, button) { - this.data.subclass = - this.data.subclass?.uuid === button.dataset.subclass - ? null - : this.subclassChoices.find(x => x.uuid === button.dataset.subclass); - this.render(true); - } - - static async selectDomain(_, button) { - const domain = - this.data.domain?.id === button.dataset.domain - ? null - : this.domainChoices.find(x => x.id === button.dataset.domain); - if (domain?.disabled) return; - - this.data.domain = domain; - this.render(true); - } - - static finish() { - this.close({}, this.data); - } - - async close(options = {}, data = null) { - this.resolve(data); - super.close(options); - } -} diff --git a/module/applications/rollSelectionDialog.mjs b/module/applications/rollSelectionDialog.mjs index 3230cf72..e97d7cf1 100644 --- a/module/applications/rollSelectionDialog.mjs +++ b/module/applications/rollSelectionDialog.mjs @@ -1,7 +1,7 @@ const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; export default class RollSelectionDialog extends HandlebarsApplicationMixin(ApplicationV2) { - constructor(experiences, bonusDamage, hopeResource, resolve, isNpc) { + constructor(experiences, hopeResource, resolve) { super({}, {}); this.experiences = experiences; @@ -17,23 +17,13 @@ export default class RollSelectionDialog extends HandlebarsApplicationMixin(Appl fear: ['d12'], advantage: null, disadvantage: null, - bonusDamage: bonusDamage.reduce((acc, x) => { - if (x.appliesOn === SYSTEM.EFFECTS.applyLocations.attackRoll.id) { - acc.push({ - ...x, - hopeUses: 0 - }); - } - - return acc; - }, []), hopeResource: hopeResource }; } static DEFAULT_OPTIONS = { tag: 'form', - id: 'roll-selection', //Having an id causes a new instance to overwrite previous. + id: 'roll-selection', classes: ['daggerheart', 'views', 'roll-selection'], position: { width: 400, @@ -41,8 +31,6 @@ export default class RollSelectionDialog extends HandlebarsApplicationMixin(Appl }, actions: { selectExperience: this.selectExperience, - decreaseHopeUse: this.decreaseHopeUse, - increaseHopeUse: this.increaseHopeUse, setAdvantage: this.setAdvantage, setDisadvantage: this.setDisadvantage, finish: this.finish @@ -74,27 +62,14 @@ export default class RollSelectionDialog extends HandlebarsApplicationMixin(Appl context.fear = this.data.fear; context.advantage = this.data.advantage; context.disadvantage = this.data.disadvantage; - context.experiences = this.experiences.map(x => ({ - ...x, - selected: this.selectedExperiences.find(selected => selected.id === x.id) - })); - context.bonusDamage = this.data.bonusDamage; + context.experiences = Object.keys(this.experiences).map(id => ({ id, ...this.experiences[id] })); context.hopeResource = this.data.hopeResource + 1; - context.hopeUsed = this.getHopeUsed(); return context; } static updateSelection(event, _, formData) { - const { bonusDamage, ...rest } = foundry.utils.expandObject(formData.object); - - for (var index in bonusDamage) { - this.data.bonusDamage[index].initiallySelected = bonusDamage[index].initiallySelected; - if (bonusDamage[index].hopeUses) { - const value = Number.parseInt(bonusDamage[index].hopeUses); - if (!Number.isNaN(value)) this.data.bonusDamage[index].hopeUses = value; - } - } + const { ...rest } = foundry.utils.expandObject(formData.object); this.data = foundry.utils.mergeObject(this.data, rest); this.render(); @@ -104,35 +79,12 @@ export default class RollSelectionDialog extends HandlebarsApplicationMixin(Appl if (this.selectedExperiences.find(x => x.id === button.dataset.key)) { this.selectedExperiences = this.selectedExperiences.filter(x => x.id !== button.dataset.key); } else { - this.selectedExperiences = [ - ...this.selectedExperiences, - this.experiences.find(x => x.id === button.dataset.key) - ]; + this.selectedExperiences = [...this.selectedExperiences, button.dataset.key]; } this.render(); } - getHopeUsed() { - return this.data.bonusDamage.reduce((acc, x) => acc + x.hopeUses, 0); - } - - static decreaseHopeUse(_, button) { - const index = Number.parseInt(button.dataset.index); - if (this.data.bonusDamage[index].hopeUses - 1 >= 0) { - this.data.bonusDamage[index].hopeUses -= 1; - this.render(true); - } - } - - static increaseHopeUse(_, button) { - const index = Number.parseInt(button.dataset.index); - if (this.data.bonusDamage[index].hopeUses <= this.data.hopeResource + 1) { - this.data.bonusDamage[index].hopeUses += 1; - this.render(true); - } - } - static setAdvantage() { this.data.advantage = this.data.advantage ? null : 'd6'; this.data.disadvantage = null; @@ -149,11 +101,10 @@ export default class RollSelectionDialog extends HandlebarsApplicationMixin(Appl static async finish() { const { diceOptions, ...rest } = this.data; + this.resolve({ ...rest, - experiences: this.selectedExperiences, - hopeUsed: this.getHopeUsed(), - bonusDamage: this.data.bonusDamage.reduce((acc, x) => acc.concat(` + ${1 + x.hopeUses}${x.value}`), '') + experiences: this.selectedExperiences.map(x => ({ id: x, ...this.experiences[x] })) }); this.close(); } diff --git a/module/applications/sheets/adversary.mjs b/module/applications/sheets/adversary.mjs index 8345b532..0196a8c8 100644 --- a/module/applications/sheets/adversary.mjs +++ b/module/applications/sheets/adversary.mjs @@ -98,7 +98,7 @@ export default class AdversarySheet extends DaggerheartSheet(ActorSheetV2) { name: x.actor.name, img: x.actor.img, difficulty: x.actor.system.difficulty, - evasion: x.actor.system.evasion.value + evasion: x.actor.system.evasion })); const cls = getDocumentClass('ChatMessage'); diff --git a/module/applications/sheets/character.mjs b/module/applications/sheets/character.mjs new file mode 100644 index 00000000..8eaf67aa --- /dev/null +++ b/module/applications/sheets/character.mjs @@ -0,0 +1,703 @@ +import { capitalize } from '../../helpers/utils.mjs'; +import DhpDeathMove from '../deathMove.mjs'; +import DhpDowntime from '../downtime.mjs'; +import AncestrySelectionDialog from '../ancestrySelectionDialog.mjs'; +import DaggerheartSheet from './daggerheart-sheet.mjs'; +import { abilities } from '../../config/actorConfig.mjs'; +import DhlevelUp from '../levelup.mjs'; +import DHDualityRoll from '../../data/chat-message/dualityRoll.mjs'; + +const { ActorSheetV2 } = foundry.applications.sheets; +const { TextEditor } = foundry.applications.ux; +export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { + constructor(options = {}) { + super(options); + } + + static DEFAULT_OPTIONS = { + tag: 'form', + classes: ['daggerheart', 'sheet', 'pc'], + position: { width: 810, height: 1080 }, + actions: { + attributeRoll: this.rollAttribute, + toggleMarks: this.toggleMarks, + toggleHP: this.toggleHP, + toggleStress: this.toggleStress, + toggleHope: this.toggleHope, + toggleGold: this.toggleGold, + attackRoll: this.attackRoll, + useDomainCard: this.useDomainCard, + removeCard: this.removeDomainCard, + selectClass: this.selectClass, + selectSubclass: this.selectSubclass, + selectAncestry: this.selectAncestry, + selectCommunity: this.selectCommunity, + viewObject: this.viewObject, + useFeature: this.useFeature, + takeShortRest: this.takeShortRest, + takeLongRest: this.takeLongRest, + deleteItem: this.deleteItem, + addScar: this.addScar, + deleteScar: this.deleteScar, + makeDeathMove: this.makeDeathMove, + itemQuantityDecrease: (_, button) => this.setItemQuantity(button, -1), + itemQuantityIncrease: (_, button) => this.setItemQuantity(button, 1), + useAbility: this.useAbility, + useAdvancementCard: this.useAdvancementCard, + useAdvancementAbility: this.useAdvancementAbility, + toggleEquipItem: this.toggleEquipItem, + levelup: this.openLevelUp + }, + window: { + minimizable: false, + resizable: true + }, + form: { + handler: this.updateForm, + submitOnChange: true, + closeOnSubmit: false + }, + dragDrop: [ + { dragSelector: null, dropSelector: '.weapon-section' }, + { dragSelector: null, dropSelector: '.armor-section' }, + { dragSelector: '.item-list .item', dropSelector: null } + ] + }; + + static PARTS = { + form: { + id: 'character', + template: 'systems/daggerheart/templates/sheets/character/character.hbs' + } + }; + + _getTabs() { + const setActive = tabs => { + for (const v of Object.values(tabs)) { + v.active = this.tabGroups[v.group] ? this.tabGroups[v.group] === v.id : v.active; + v.cssClass = v.active ? 'active' : ''; + } + }; + + const primaryTabs = { + features: { + active: true, + cssClass: '', + group: 'primary', + id: 'features', + icon: null, + label: game.i18n.localize('DAGGERHEART.Sheets.PC.Tabs.Features') + }, + loadout: { + active: false, + cssClass: '', + group: 'primary', + id: 'loadout', + icon: null, + label: game.i18n.localize('DAGGERHEART.Sheets.PC.Tabs.Loadout') + }, + inventory: { + active: false, + cssClass: '', + group: 'primary', + id: 'inventory', + icon: null, + label: game.i18n.localize('DAGGERHEART.Sheets.PC.Tabs.Inventory') + }, + story: { + active: false, + cssClass: '', + group: 'primary', + id: 'story', + icon: null, + label: game.i18n.localize('DAGGERHEART.Sheets.PC.Tabs.Story') + } + }; + const secondaryTabs = { + foundation: { + active: true, + cssClass: '', + group: 'secondary', + id: 'foundation', + icon: null, + label: game.i18n.localize('DAGGERHEART.Sheets.PC.Tabs.Foundation') + }, + loadout: { + active: false, + cssClass: '', + group: 'secondary', + id: 'loadout', + icon: null, + label: game.i18n.localize('DAGGERHEART.Sheets.PC.Tabs.Loadout') + }, + vault: { + active: false, + cssClass: '', + group: 'secondary', + id: 'vault', + icon: null, + label: game.i18n.localize('DAGGERHEART.Sheets.PC.Tabs.Vault') + } + }; + + setActive(primaryTabs); + setActive(secondaryTabs); + + return { primary: primaryTabs, secondary: secondaryTabs }; + } + + _attachPartListeners(partId, htmlElement, options) { + super._attachPartListeners(partId, htmlElement, options); + + htmlElement.querySelector('.level-value').addEventListener('change', this.onLevelChange.bind(this)); + } + + async _prepareContext(_options) { + const context = await super._prepareContext(_options); + context.document = this.document; + context.tabs = this._getTabs(); + + context.config = SYSTEM; + + const selectedAttributes = Object.values(this.document.system.traits).map(x => x.base); + context.abilityScoreArray = JSON.parse( + await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.General.AbilityArray) + ).reduce((acc, x) => { + const selectedIndex = selectedAttributes.indexOf(x); + if (selectedIndex !== -1) { + selectedAttributes.splice(selectedIndex, 1); + } else { + acc.push({ name: x, value: x }); + } + + return acc; + }, []); + if (!context.abilityScoreArray.includes(0)) context.abilityScoreArray.push({ name: 0, value: 0 }); + context.abilityScoresFinished = context.abilityScoreArray.every(x => x.value === 0); + + context.attributes = Object.keys(this.document.system.traits).reduce((acc, key) => { + acc[key] = { + ...this.document.system.traits[key], + name: game.i18n.localize(SYSTEM.ACTOR.abilities[key].name), + verbs: SYSTEM.ACTOR.abilities[key].verbs.map(x => game.i18n.localize(x)) + }; + + return acc; + }, {}); + + const ancestry = await this.mapFeatureType( + this.document.system.ancestry ? [this.document.system.ancestry] : [], + SYSTEM.GENERAL.objectTypes + ); + const community = await this.mapFeatureType( + this.document.system.community ? [this.document.system.community] : [], + SYSTEM.GENERAL.objectTypes + ); + const foundation = { + ancestry: ancestry[0], + community: community[0], + advancement: {} + }; + + const nrLoadoutCards = this.document.system.domainCards.loadout.length; + const loadout = await this.mapFeatureType(this.document.system.domainCards.loadout, SYSTEM.DOMAIN.cardTypes); + const vault = await this.mapFeatureType(this.document.system.domainCards.vault, SYSTEM.DOMAIN.cardTypes); + context.abilities = { + foundation: foundation, + loadout: { + top: loadout.slice(0, Math.min(2, nrLoadoutCards)), + bottom: nrLoadoutCards > 2 ? loadout.slice(2, Math.min(5, nrLoadoutCards)) : [], + nrTotal: nrLoadoutCards + }, + vault: vault.map(x => ({ + ...x, + uuid: x.uuid, + sendToLoadoutDisabled: this.document.system.domainCards.loadout.length >= 5 + })) + }; + + context.inventory = { + consumable: { + titles: { + name: game.i18n.localize('DAGGERHEART.Sheets.PC.InventoryTab.ConsumableTitle'), + quantity: game.i18n.localize('DAGGERHEART.Sheets.PC.InventoryTab.QuantityTitle') + }, + items: this.document.items.filter(x => x.type === 'consumable') + }, + miscellaneous: { + titles: { + name: game.i18n.localize('DAGGERHEART.Sheets.PC.InventoryTab.MiscellaneousTitle'), + quantity: game.i18n.localize('DAGGERHEART.Sheets.PC.InventoryTab.QuantityTitle') + }, + items: this.document.items.filter(x => x.type === 'miscellaneous') + }, + weapons: { + titles: { + name: game.i18n.localize('DAGGERHEART.Sheets.PC.InventoryTab.WeaponsTitle'), + quantity: game.i18n.localize('DAGGERHEART.Sheets.PC.InventoryTab.QuantityTitle') + }, + items: this.document.items.filter(x => x.type === 'weapon') + }, + armor: { + titles: { + name: game.i18n.localize('DAGGERHEART.Sheets.PC.InventoryTab.ArmorsTitle'), + quantity: game.i18n.localize('DAGGERHEART.Sheets.PC.InventoryTab.QuantityTitle') + }, + items: this.document.items.filter(x => x.type === 'armor') + } + }; + + if (context.inventory.length === 0) { + context.inventory = Array(1).fill(Array(5).fill([])); + } + + return context; + } + + static async updateForm(event, _, formData) { + await this.document.update(formData.object); + this.render(); + } + + async mapFeatureType(data, configType) { + return await Promise.all( + data.map(async x => { + const abilities = x.system.abilities + ? await Promise.all(x.system.abilities.map(async x => await fromUuid(x.uuid))) + : []; + + return { + ...x, + uuid: x.uuid, + system: { + ...x.system, + abilities: abilities, + type: game.i18n.localize(configType[x.system.type ?? x.type].label) + } + }; + }) + ); + } + + static async rollAttribute(event, button) { + const { roll, hope, fear, advantage, disadvantage, modifiers } = await this.document.dualityRoll( + { title: game.i18n.localize(abilities[button.dataset.attribute].label), value: button.dataset.value }, + event.shiftKey + ); + + const cls = getDocumentClass('ChatMessage'); + + const systemContent = new DHDualityRoll({ + title: game.i18n.format('DAGGERHEART.Chat.DualityRoll.AbilityCheckTitle', { + ability: game.i18n.localize(abilities[button.dataset.attribute].label) + }), + origin: this.document.id, + roll: roll._formula, + modifiers: modifiers, + hope: hope, + fear: fear, + advantage: advantage, + disadvantage: disadvantage + }); + + await cls.create({ + type: 'dualityRoll', + sound: CONFIG.sounds.dice, + system: systemContent, + user: game.user.id, + content: await foundry.applications.handlebars.renderTemplate( + 'systems/daggerheart/templates/chat/duality-roll.hbs', + systemContent + ), + rolls: [roll] + }); + } + + static async toggleMarks(_, button) { + const markValue = Number.parseInt(button.dataset.value); + const newValue = this.document.system.armor.system.marks.value >= markValue ? markValue - 1 : markValue; + await this.document.system.armor.update({ 'system.marks.value': newValue }); + } + + static async toggleHP(_, button) { + const healthValue = Number.parseInt(button.dataset.value); + const newValue = this.document.system.resources.hitPoints.value >= healthValue ? healthValue - 1 : healthValue; + await this.document.update({ 'system.resources.hitPoints.value': newValue }); + } + + static async toggleStress(_, button) { + const healthValue = Number.parseInt(button.dataset.value); + const newValue = this.document.system.resources.stress.value >= healthValue ? healthValue - 1 : healthValue; + await this.document.update({ 'system.resources.stress.value': newValue }); + } + + static async toggleHope(_, button) { + const hopeValue = Number.parseInt(button.dataset.value); + const newValue = this.document.system.resources.hope.value >= hopeValue ? hopeValue - 1 : hopeValue; + await this.document.update({ 'system.resources.hope.value': newValue }); + } + + static async toggleGold(_, button) { + const goldValue = Number.parseInt(button.dataset.value); + const goldType = button.dataset.type; + const newValue = this.document.system.gold[goldType] >= goldValue ? goldValue - 1 : goldValue; + + const update = `system.gold.${goldType}`; + await this.document.update({ [update]: newValue }); + } + + static async attackRoll(event, button) { + const weapon = await fromUuid(button.dataset.weapon); + const damage = { + value: `${this.document.system.proficiency}${weapon.system.damage.value}`, + type: weapon.system.damage.type + }; + const modifier = this.document.system.traits[weapon.system.trait].value; + + const { roll, hope, fear, advantage, disadvantage, modifiers } = await this.document.dualityRoll( + { title: game.i18n.localize(abilities[weapon.system.trait].label), value: modifier }, + event.shiftKey + ); + + const targets = Array.from(game.user.targets).map(x => ({ + id: x.id, + name: x.actor.name, + img: x.actor.img, + difficulty: x.actor.system.difficulty, + evasion: x.actor.system.evasion + })); + + const systemData = new DHDualityRoll({ + title: weapon.name, + origin: this.document.id, + roll: roll._formula, + modifiers: modifiers, + hope: hope, + fear: fear, + advantage: advantage, + disadvantage: disadvantage, + damage: damage, + targets: targets + }); + + const cls = getDocumentClass('ChatMessage'); + const msg = new cls({ + type: 'dualityRoll', + sound: CONFIG.sounds.dice, + system: systemData, + content: await foundry.applications.handlebars.renderTemplate( + 'systems/daggerheart/templates/chat/attack-roll.hbs', + systemData + ), + rolls: [roll] + }); + + await cls.create(msg.toObject()); + } + + static openLevelUp() { + if (!this.document.system.class.value || !this.document.system.class.subclass) { + ui.notifications.error(game.i18n.localize('DAGGERHEART.Sheets.PC.Errors.missingClassOrSubclass')); + return; + } + + new DhlevelUp(this.document).render(true); + } + + static async useDomainCard(_, button) { + const card = this.document.items.find(x => x.uuid === button.dataset.key); + + const cls = getDocumentClass('ChatMessage'); + const systemData = { + title: `${game.i18n.localize('DAGGERHEART.Chat.DomainCard.Title')} - ${capitalize(button.dataset.domain)}`, + origin: this.document.id, + img: card.img, + name: card.name, + description: card.system.effect, + actions: card.system.actions + }; + const msg = new cls({ + type: 'abilityUse', + user: game.user.id, + content: await foundry.applications.handlebars.renderTemplate( + 'systems/daggerheart/templates/chat/ability-use.hbs', + systemData + ), + system: systemData + }); + + cls.create(msg.toObject()); + } + + static async removeDomainCard(_, button) { + if (button.dataset.type === 'domainCard') { + const card = this.document.items.find(x => x.uuid === button.dataset.key); + await card.delete(); + } + } + + static async selectClass() { + (await game.packs.get('daggerheart.classes'))?.render(true); + } + + static async selectSubclass() { + (await game.packs.get('daggerheart.subclasses'))?.render(true); + } + + static async selectAncestry() { + const dialogClosed = new Promise((resolve, _) => { + new AncestrySelectionDialog(resolve).render(true); + }); + const result = await dialogClosed; + + for (var ancestry of this.document.items.filter(x => x => x.type === 'ancestry')) { + await ancestry.delete(); + } + + const createdItems = []; + for (var feature of this.document.items.filter( + x => x.type === 'feature' && x.system.type === SYSTEM.ITEM.featureTypes.ancestry.id + )) { + await feature.delete(); + } + + createdItems.push(result.data); + + await this.document.createEmbeddedDocuments('Item', createdItems); + } + + static async selectCommunity() { + (await game.packs.get('daggerheart.communities'))?.render(true); + } + + static async viewObject(_, button) { + const object = await fromUuid(button.dataset.value); + if (!object) return; + + const tab = button.dataset.tab; + if (tab && object.sheet._tabs) object.sheet._tabs[0].active = tab; + + if (object.sheet.editMode) object.sheet.editMode = false; + + object.sheet.render(true); + } + + static async takeShortRest() { + await new DhpDowntime(this.document, true).render(true); + await this.minimize(); + } + + static async takeLongRest() { + await new DhpDowntime(this.document, false).render(true); + await this.minimize(); + } + + static async addScar() { + if (this.document.system.story.scars.length === 5) return; + + await this.document.update({ + 'system.story.scars': [ + ...this.document.system.story.scars, + { name: game.i18n.localize('DAGGERHEART.Sheets.PC.NewScar'), description: '' } + ] + }); + } + + static async deleteScar(event, button) { + event.stopPropagation(); + await this.document.update({ + 'system.story.scars': this.document.system.story.scars.filter( + (_, index) => index !== Number.parseInt(button.currentTarget.dataset.scar) + ) + }); + } + + static async makeDeathMove() { + if (this.document.system.resources.hitPoints.value === this.document.system.resources.hitPoints.max) { + await new DhpDeathMove(this.document).render(true); + await this.minimize(); + } + } + + async itemUpdate(event) { + const name = event.currentTarget.dataset.item; + const item = await fromUuid($(event.currentTarget).closest('[data-item-id]')[0].dataset.itemId); + await item.update({ [name]: event.currentTarget.value }); + } + + async onLevelChange(event) { + await this.document.updateLevel(Number(event.currentTarget.value)); + this.render(); + } + + static async deleteItem(_, button) { + const item = await fromUuid($(button).closest('[data-item-id]')[0].dataset.itemId); + await item.delete(); + } + + static async setItemQuantity(button, value) { + const item = await fromUuid($(button).closest('[data-item-id]')[0].dataset.itemId); + await item.update({ 'system.quantity': Math.max(item.system.quantity + value, 1) }); + } + + static async useFeature(_, button) { + const item = await fromUuid(button.dataset.id); + + const cls = getDocumentClass('ChatMessage'); + const systemData = { + title: game.i18n.localize('DAGGERHEART.Chat.FeatureTitle'), + origin: this.document.id, + img: item.img, + name: item.name, + description: item.system.description, + actions: item.system.actions + }; + const msg = new cls({ + type: 'abilityUse', + user: game.user.id, + content: await foundry.applications.handlebars.renderTemplate( + 'systems/daggerheart/templates/chat/ability-use.hbs', + systemData + ), + system: systemData + }); + + cls.create(msg.toObject()); + } + + static async useAbility(_, button) { + const item = await fromUuid(button.dataset.feature); + const type = button.dataset.type; + + const cls = getDocumentClass('ChatMessage'); + const systemData = { + title: + type === 'ancestry' + ? game.i18n.localize('DAGGERHEART.Chat.FoundationCard.AncestryTitle') + : type === 'community' + ? game.i18n.localize('DAGGERHEART.Chat.FoundationCard.CommunityTitle') + : game.i18n.localize('DAGGERHEART.Chat.FoundationCard.SubclassFeatureTitle'), + origin: this.document.id, + img: item.img, + name: item.name, + description: item.system.description, + actions: [] + }; + const msg = new cls({ + type: 'abilityUse', + user: game.user.id, + system: systemData, + content: await foundry.applications.handlebars.renderTemplate( + 'systems/daggerheart/templates/chat/ability-use.hbs', + systemData + ) + }); + + cls.create(msg.toObject()); + } + + static async useAdvancementCard(_, button) { + const item = + button.dataset.multiclass === 'true' + ? this.document.system.multiclass.subclass + : this.document.system.class.subclass; + const ability = item.system[`${button.dataset.key}Feature`]; + const title = `${item.name} - ${game.i18n.localize(`DAGGERHEART.Sheets.PC.DomainCard.${capitalize(button.dataset.key)}Title`)}`; + + const cls = getDocumentClass('ChatMessage'); + const systemData = { + title: game.i18n.localize('DAGGERHEART.Chat.FoundationCard.SubclassFeatureTitle'), + origin: this.document.id, + name: title, + img: item.img, + description: ability.description + }; + const msg = new cls({ + type: 'abilityUse', + user: game.user.id, + system: systemData, + content: await foundry.applications.handlebars.renderTemplate( + 'systems/daggerheart/templates/chat/ability-use.hbs', + systemData + ) + }); + + cls.create(msg.toObject()); + } + + static async useAdvancementAbility(_, button) { + const item = this.document.items.find(x => x.uuid === button.dataset.id); + + const cls = getDocumentClass('ChatMessage'); + const systemData = { + title: game.i18n.localize('DAGGERHEART.Chat.FoundationCard.SubclassFeatureTitle'), + origin: this.document.id, + name: item.name, + img: item.img, + description: item.system.description + }; + const msg = new cls({ + user: game.user.id, + system: systemData, + content: await foundry.applications.handlebars.renderTemplate( + 'systems/daggerheart/templates/chat/ability-use.hbs', + systemData + ) + }); + + cls.create(msg.toObject()); + } + + static async toggleEquipItem(_, button) { + const item = this.document.items.get(button.id); + if (item.system.equipped) { + await item.update({ 'system.equipped': false }); + return; + } + + switch (item.type) { + case 'armor': + const currentArmor = this.document.system.armor; + if (currentArmor) { + await currentArmor.update({ 'system.equipped': false }); + } + + await item.update({ 'system.equipped': true }); + break; + case 'weapon': + await this.document.system.constructor.unequipBeforeEquip.bind(this.document.system)(item); + + await item.update({ 'system.equipped': true }); + break; + } + this.render(); + } + + async _onDragStart(_, event) { + super._onDragStart(event); + } + + async _onDrop(event) { + super._onDrop(event); + this._onDropItem(event, TextEditor.getDragEventData(event)); + } + + async _onDropItem(event, data) { + const item = await Item.implementation.fromDropData(data); + const itemData = item.toObject(); + + if (item.type === 'domainCard' && this.document.system.domainCards.loadout.length >= 5) { + itemData.system.inVault = true; + } + + if (this.document.uuid === item.parent?.uuid) return this._onSortItem(event, itemData); + const createdItem = await this._onDropItemCreate(itemData); + + return createdItem; + } + + async _onDropItemCreate(itemData, event) { + itemData = itemData instanceof Array ? itemData : [itemData]; + return this.document.createEmbeddedDocuments('Item', itemData); + } +} diff --git a/module/applications/sheets/pc.mjs b/module/applications/sheets/pc.mjs deleted file mode 100644 index 7433e3f3..00000000 --- a/module/applications/sheets/pc.mjs +++ /dev/null @@ -1,1241 +0,0 @@ -import { capitalize } from '../../helpers/utils.mjs'; -import DhpDeathMove from '../deathMove.mjs'; -import DhpDowntime from '../downtime.mjs'; -import AncestrySelectionDialog from '../ancestrySelectionDialog.mjs'; -import DaggerheartSheet from './daggerheart-sheet.mjs'; -import { abilities } from '../../config/actorConfig.mjs'; -import DhlevelUp from '../levelup.mjs'; - -const { ActorSheetV2 } = foundry.applications.sheets; -const { TextEditor } = foundry.applications.ux; -export default class PCSheet extends DaggerheartSheet(ActorSheetV2) { - constructor(options = {}) { - super(options); - - this.editAttributes = false; - this.onVaultTab = false; - this.currentInventoryPage = 0; - this.selectedScar = null; - this.storyEditor = null; - this.dropItemBlock = false; - this.multiclassFeatureSetSelected = false; - } - - static DEFAULT_OPTIONS = { - tag: 'form', - classes: ['daggerheart', 'sheet', 'pc'], - position: { width: 810, height: 1080 }, - actions: { - toggleEditAttributes: this.toggleEditAttributes, - attributeRoll: this.rollAttribute, - toggleMarks: this.toggleMarks, - toggleAttributeMark: this.toggleAttributeMark, - toggleHP: this.toggleHP, - toggleStress: this.toggleStress, - toggleHope: this.toggleHope, - toggleGold: this.toggleGold, - attackRoll: this.attackRoll, - tabToLoadout: () => this.domainCardsTab(false), - tabToVault: () => this.domainCardsTab(true), - sendToVault: this.moveDomainCard, - sendToLoadout: this.moveDomainCard, - useDomainCard: this.useDomainCard, - removeCard: this.removeDomainCard, - selectClass: this.selectClass, - selectSubclass: this.selectSubclass, - selectAncestry: this.selectAncestry, - selectCommunity: this.selectCommunity, - viewObject: this.viewObject, - useFeature: this.useFeature, - takeShortRest: this.takeShortRest, - takeLongRest: this.takeLongRest, - addMiscItem: this.addMiscItem, - deleteItem: this.deleteItem, - addScar: this.addScar, - selectScar: this.selectScar, - deleteScar: this.deleteScar, - makeDeathMove: this.makeDeathMove, - toggleFeatureDice: this.toggleFeatureDice, - setStoryEditor: this.setStoryEditor, - itemQuantityDecrease: (_, button) => this.setItemQuantity(button, -1), - itemQuantityIncrease: (_, button) => this.setItemQuantity(button, 1), - useAbility: this.useAbility, - useAdvancementCard: this.useAdvancementCard, - useAdvancementAbility: this.useAdvancementAbility, - selectFeatureSet: this.selectFeatureSet, - toggleEquipItem: this.toggleEquipItem - }, - window: { - minimizable: false, - resizable: true - }, - form: { - handler: this.updateForm, - submitOnChange: true, - closeOnSubmit: false - }, - dragDrop: [ - { dragSelector: null, dropSelector: '.weapon-section' }, - { dragSelector: null, dropSelector: '.armor-section' }, - { dragSelector: null, dropSelector: '.inventory-weapon-section-first' }, - { dragSelector: null, dropSelector: '.inventory-weapon-section-second' }, - { dragSelector: '.item-list .item', dropSelector: null } - ] - }; - - static PARTS = { - form: { - id: 'pc', - template: 'systems/daggerheart/templates/sheets/pc/pc.hbs' - } - }; - - _getTabs() { - const setActive = tabs => { - for (const v of Object.values(tabs)) { - v.active = this.tabGroups[v.group] ? this.tabGroups[v.group] === v.id : v.active; - v.cssClass = v.active ? 'active' : ''; - } - }; - - const primaryTabs = { - features: { - active: true, - cssClass: '', - group: 'primary', - id: 'features', - icon: null, - label: game.i18n.localize('DAGGERHEART.Sheets.PC.Tabs.Features') - }, - loadout: { - active: false, - cssClass: '', - group: 'primary', - id: 'loadout', - icon: null, - label: game.i18n.localize('DAGGERHEART.Sheets.PC.Tabs.Loadout') - }, - inventory: { - active: false, - cssClass: '', - group: 'primary', - id: 'inventory', - icon: null, - label: game.i18n.localize('DAGGERHEART.Sheets.PC.Tabs.Inventory') - }, - story: { - active: false, - cssClass: '', - group: 'primary', - id: 'story', - icon: null, - label: game.i18n.localize('DAGGERHEART.Sheets.PC.Tabs.Story') - } - }; - const secondaryTabs = { - foundation: { - active: true, - cssClass: '', - group: 'secondary', - id: 'foundation', - icon: null, - label: game.i18n.localize('DAGGERHEART.Sheets.PC.Tabs.Foundation') - }, - loadout: { - active: false, - cssClass: '', - group: 'secondary', - id: 'loadout', - icon: null, - label: game.i18n.localize('DAGGERHEART.Sheets.PC.Tabs.Loadout') - }, - vault: { - active: false, - cssClass: '', - group: 'secondary', - id: 'vault', - icon: null, - label: game.i18n.localize('DAGGERHEART.Sheets.PC.Tabs.Vault') - } - }; - - setActive(primaryTabs); - setActive(secondaryTabs); - - return { primary: primaryTabs, secondary: secondaryTabs }; - } - - _attachPartListeners(partId, htmlElement, options) { - super._attachPartListeners(partId, htmlElement, options); - htmlElement - .querySelectorAll('.attribute-value') - .forEach(element => element.addEventListener('change', this.attributeChange.bind(this))); - htmlElement - .querySelectorAll('.tab-selector') - .forEach(element => element.addEventListener('click', this.tabSwitch.bind(this))); - htmlElement.querySelector('.level-title.levelup')?.addEventListener('click', this.openLevelUp.bind(this)); - htmlElement - .querySelectorAll('.feature-input') - .forEach(element => element.addEventListener('change', this.onFeatureInputBlur.bind(this))); - htmlElement - .querySelectorAll('.experience-description') - .forEach(element => element.addEventListener('change', this.experienceDescriptionChange.bind(this))); - htmlElement - .querySelectorAll('.experience-value') - .forEach(element => element.addEventListener('change', this.experienceValueChange.bind(this))); - htmlElement.querySelector('.level-value').addEventListener('change', this.onLevelChange.bind(this)); - } - - async _prepareContext(_options) { - const context = await super._prepareContext(_options); - context.document = this.document; - context.tabs = this._getTabs(); - - context.config = SYSTEM; - context.editAttributes = this.editAttributes; - context.onVaultTab = this.onVaultTab; - context.selectedScar = this.selectedScar; - context.storyEditor = this.storyEditor; - context.multiclassFeatureSetSelected = this.multiclassFeatureSetSelected; - - const selectedAttributes = Object.values(this.document.system.traits).map(x => x.base); - context.abilityScoreArray = JSON.parse( - await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.General.AbilityArray) - ).reduce((acc, x) => { - const selectedIndex = selectedAttributes.indexOf(x); - if (selectedIndex !== -1) { - selectedAttributes.splice(selectedIndex, 1); - } else { - acc.push({ name: x, value: x }); - } - - return acc; - }, []); - if (!context.abilityScoreArray.includes(0)) context.abilityScoreArray.push({ name: 0, value: 0 }); - context.abilityScoresFinished = context.abilityScoreArray.every(x => x.value === 0); - - //FIXME: - context.domains = this.document.system.class.value - ? { - first: this.document.system.class.value.system.domains[0] - ? SYSTEM.DOMAIN.domains[this.document.system.class.value.system.domains[0]].src - : null, - second: this.document.system.class.value.system.domains[1] - ? SYSTEM.DOMAIN.domains[this.document.system.class.value.system.domains[1]].src - : null - } - : {}; - - context.attributes = Object.keys(this.document.system.traits).reduce((acc, key) => { - acc[key] = { - ...this.document.system.traits[key], - name: game.i18n.localize(SYSTEM.ACTOR.abilities[key].name), - verbs: SYSTEM.ACTOR.abilities[key].verbs.map(x => game.i18n.localize(x)) - }; - - return acc; - }, {}); - - const ancestry = await this.mapFeatureType( - this.document.system.ancestry ? [this.document.system.ancestry] : [], - SYSTEM.GENERAL.objectTypes - ); - const community = await this.mapFeatureType( - this.document.system.community ? [this.document.system.community] : [], - SYSTEM.GENERAL.objectTypes - ); - const foundation = { - ancestry: ancestry[0], - community: community[0], - advancement: { - ...this.mapAdvancementFeatures(this.document, SYSTEM) - } - }; - - const nrLoadoutCards = this.document.system.domainCards.loadout.length; - const loadout = await this.mapFeatureType(this.document.system.domainCards.loadout, SYSTEM.DOMAIN.cardTypes); - const vault = await this.mapFeatureType(this.document.system.domainCards.vault, SYSTEM.DOMAIN.cardTypes); - context.abilities = { - foundation: foundation, - loadout: { - top: loadout.slice(0, Math.min(2, nrLoadoutCards)), - bottom: nrLoadoutCards > 2 ? loadout.slice(2, Math.min(5, nrLoadoutCards)) : [], - nrTotal: nrLoadoutCards - }, - vault: vault.map(x => ({ - ...x, - uuid: x.uuid, - sendToLoadoutDisabled: - this.document.system.domainCards.loadout.length >= this.document.system.domainData.maxLoadout - })) - }; - - context.inventory = { - consumable: { - titles: { - name: game.i18n.localize('DAGGERHEART.Sheets.PC.InventoryTab.ConsumableTitle'), - quantity: game.i18n.localize('DAGGERHEART.Sheets.PC.InventoryTab.QuantityTitle') - }, - items: this.document.items.filter(x => x.type === 'consumable') - }, - miscellaneous: { - titles: { - name: game.i18n.localize('DAGGERHEART.Sheets.PC.InventoryTab.MiscellaneousTitle'), - quantity: game.i18n.localize('DAGGERHEART.Sheets.PC.InventoryTab.QuantityTitle') - }, - items: this.document.items.filter(x => x.type === 'miscellaneous') - }, - weapons: { - titles: { - name: game.i18n.localize('DAGGERHEART.Sheets.PC.InventoryTab.WeaponsTitle'), - quantity: game.i18n.localize('DAGGERHEART.Sheets.PC.InventoryTab.QuantityTitle') - }, - items: this.document.items.filter(x => x.type === 'weapon') - }, - armor: { - titles: { - name: game.i18n.localize('DAGGERHEART.Sheets.PC.InventoryTab.ArmorsTitle'), - quantity: game.i18n.localize('DAGGERHEART.Sheets.PC.InventoryTab.QuantityTitle') - }, - items: this.document.items.filter(x => x.type === 'armor') - } - }; - - if (context.inventory.length === 0) { - context.inventory = Array(1).fill(Array(5).fill([])); - } - - context.classFeatures = ( - this.multiclassFeatureSetSelected - ? this.document.system.multiclassFeatures - : this.document.system.classFeatures - ).map(x => { - if (x.system.featureType.type !== 'dice') { - return x; - } - - return { - ...x, - uuid: x.uuid, - system: { - ...x.system, - featureType: { - ...x.system.featureType, - data: { - ...x.system.featureType.data, - property: this.document.system.subclass - ? SYSTEM.ACTOR.featureProperties[x.system.featureType.data.property].path(this.document) - : 0 - } - } - } - }; - }); - - return context; - } - - static async updateForm(event, _, formData) { - await this.document.update(formData.object); - this.render(); - } - - async mapFeatureType(data, configType) { - return await Promise.all( - data.map(async x => { - const abilities = x.system.abilities - ? await Promise.all(x.system.abilities.map(async x => await fromUuid(x.uuid))) - : []; - - return { - ...x, - uuid: x.uuid, - system: { - ...x.system, - abilities: abilities, - type: game.i18n.localize(configType[x.system.type ?? x.type].label) - } - }; - }) - ); - } - - mapAdvancementFeatures(actor, config) { - if (!actor.system.class.value || !actor.system.class.subclass) return { foundation: null, advancements: [] }; - - const { subclass, multiclassSubclass } = actor.system.subclassFeatures; - - const foundation = { - type: 'foundation', - multiclass: false, - img: actor.system.subclass.img, - subtitle: game.i18n.localize('DAGGERHEART.Sheets.PC.DomainCard.FoundationTitle'), - domains: actor.system.class.value.system.domains.map(x => config.DOMAIN.domains[x].src), - className: actor.system.class.value.name, - subclassUuid: actor.system.subclass.uuid, - subclassName: actor.system.subclass.name, - spellcast: config.ACTOR.abilities[actor.system.subclass.system.spellcastingTrait]?.name ?? null, - description: actor.system.subclass.system.foundationFeature.description, - abilities: subclass.foundation, - abilityKey: 'foundationFeature' - }; - - const firstKey = - actor.system.subclass.system.specializationFeature.unlocked && - actor.system.subclass.system.specializationFeature.tier === 2 - ? 'sub' - : actor.system.multiclass?.system?.multiclassTier === 2 - ? 'multi' - : null; - const firstType = firstKey === 'sub' ? 'specialization' : 'foundation'; - const firstBase = - firstKey === 'sub' ? actor.system.subclass : firstKey === 'multi' ? actor.system.multiclassSubclass : null; - const first = !firstBase - ? null - : { - type: firstType, - multiclass: firstKey === 'multi', - img: firstBase.img, - subtitle: - firstKey === 'sub' - ? game.i18n.localize('DAGGERHEART.Sheets.PC.DomainCard.SpecializationTitle') - : game.i18n.localize('DAGGERHEART.Sheets.PC.DomainCard.FoundationTitle'), - domains: - firstKey === 'sub' - ? actor.system.class.value.system.domains.map(x => config.DOMAIN.domains[x].src) - : actor.system.multiclass.system.domains.map(x => config.DOMAIN.domains[x].src), - className: firstKey === 'sub' ? actor.system.class.value.name : actor.system.multiclass.name, - subclassUuid: firstBase.uuid, - subclassName: firstBase.name, - spellcast: - firstKey === 'sub' - ? null - : (config.ACTOR.abilities[firstBase.system.spellcastingTrait]?.name ?? null), - description: - firstKey === 'sub' - ? firstBase.system.specializationFeature.description - : firstBase.system.foundationFeature.description, - abilities: firstKey === 'sub' ? subclass.specialization : multiclassSubclass.foundation, - abilityKey: firstKey === 'sub' ? 'specializationFeature' : 'foundationFeature' - }; - - const secondKey = - (actor.system.subclass.system.specializationFeature.unlocked && - actor.system.subclass.system.specializationFeature.tier === 3) || - (actor.system.subclass.system.masteryFeature.unlocked && - actor.system.subclass.system.masteryFeature.tier === 3) - ? 'sub' - : actor.system.multiclass?.system?.multiclassTier === 3 || - actor.system.multiclassSubclass?.system?.specializationFeature?.unlocked - ? 'multi' - : null; - const secondBase = - secondKey === 'sub' - ? actor.system.subclass - : secondKey === 'multi' - ? actor.system.multiclassSubclass - : null; - const secondAbilities = secondKey === 'sub' ? subclass : multiclassSubclass; - const secondType = secondBase - ? secondBase.system.masteryFeature.unlocked - ? 'mastery' - : secondBase.system.specializationFeature.unlocked - ? 'specialization' - : 'foundation' - : null; - const second = !secondBase - ? null - : { - type: secondType, - multiclass: secondKey === 'multi', - img: secondBase.img, - subtitle: secondBase.system.masteryFeature.unlocked - ? game.i18n.localize('DAGGERHEART.Sheets.PC.DomainCard.MasteryTitle') - : secondBase.system.specializationFeature.unlocked - ? game.i18n.localize('DAGGERHEART.Sheets.PC.DomainCard.SpecializationTitle') - : game.i18n.localize('DAGGERHEART.Sheets.PC.DomainCard.FoundationTitle'), - domains: - secondKey === 'sub' - ? actor.system.class.value.system.domains.map(x => config.DOMAIN.domains[x].src) - : actor.system.multiclass.system.domains.map(x => config.DOMAIN.domains[x].src), - className: secondKey === 'sub' ? actor.system.class.value.name : actor.system.multiclass.name, - subclassUuid: secondBase.uuid, - subclassName: secondBase.name, - spellcast: - secondKey === 'sub' || secondBase.system.specializationFeature.unlocked - ? null - : (config.ACTOR.abilities[firstBase.system.spellcastingTrait]?.name ?? null), - description: secondBase.system.masteryFeature.unlocked - ? secondBase.system.masteryFeature.description - : secondBase.system.specializationFeature.unlocked - ? secondBase.system.specializationFeature.description - : firstBase.system.foundationFeature.description, - abilities: secondBase.system.masteryFeature.unlocked - ? secondAbilities.mastery - : secondBase.system.specializationFeature.unlocked - ? secondAbilities.specialization - : secondAbilities.foundation, - abilityKey: secondBase.system.masteryFeature.unlocked - ? 'masteryFeature' - : secondBase.system.specializationFeature.unlocked - ? 'specializationFeature' - : 'foundationFeature' - }; - - return { - foundation: foundation, - first: first, - second: second - }; - } - - async attributeChange(event) { - const path = `system.traits.${event.currentTarget.dataset.attribute}.base`; - await this.document.update({ [path]: event.currentTarget.value }); - } - - static toggleEditAttributes() { - this.editAttributes = !this.editAttributes; - this.render(); - } - - static async rollAttribute(event, button) { - const { roll, hope, fear, advantage, disadvantage, modifiers } = await this.document.dualityRoll( - { title: game.i18n.localize(abilities[button.dataset.attribute].label), value: button.dataset.value }, - event.shiftKey - ); - - const cls = getDocumentClass('ChatMessage'); - - const systemContent = { - title: game.i18n.format('DAGGERHEART.Chat.DualityRoll.AbilityCheckTitle', { - ability: game.i18n.localize(abilities[button.dataset.attribute].label) - }), - origin: this.document.id, - roll: roll._formula, - modifiers: modifiers, - hope: hope, - fear: fear, - advantage: advantage, - disadvantage: disadvantage - }; - - const msg = new cls({ - type: 'dualityRoll', - sound: CONFIG.sounds.dice, - system: systemContent, - user: game.user.id, - content: await foundry.applications.handlebars.renderTemplate( - 'systems/daggerheart/templates/chat/duality-roll.hbs', - systemContent - ), - rolls: [roll] - }); - - await cls.create(msg.toObject()); - } - - static async toggleMarks(_, button) { - const markValue = Number.parseInt(button.dataset.value); - const newValue = this.document.system.armor.system.marks.value >= markValue ? markValue - 1 : markValue; - await this.document.system.armor.update({ 'system.marks.value': newValue }); - } - - static async toggleAttributeMark(_, button) { - const attribute = this.document.system.traits[button.dataset.attribute]; - const newMark = this.document.system.availableAttributeMarks - .filter(x => x > Math.max.apply(null, this.document.system.traits[button.dataset.attribute].levelMarks)) - .sort((a, b) => (a > b ? 1 : -1))[0]; - - if (attribute.levelMark || !newMark) return; - - const path = `system.traits.${button.dataset.attribute}.levelMarks`; - await this.document.update({ [path]: [...attribute.levelMarks, newMark] }); - } - - static async toggleHP(_, button) { - const healthValue = Number.parseInt(button.dataset.value); - const newValue = this.document.system.resources.hitPoints.value >= healthValue ? healthValue - 1 : healthValue; - await this.document.update({ 'system.resources.hitPoints.value': newValue }); - } - - static async toggleStress(_, button) { - const healthValue = Number.parseInt(button.dataset.value); - const newValue = this.document.system.resources.stress.value >= healthValue ? healthValue - 1 : healthValue; - await this.document.update({ 'system.resources.stress.value': newValue }); - } - - static async toggleHope(_, button) { - const hopeValue = Number.parseInt(button.dataset.value); - const newValue = this.document.system.resources.hope.value >= hopeValue ? hopeValue - 1 : hopeValue; - await this.document.update({ 'system.resources.hope.value': newValue }); - } - - static async toggleGold(_, button) { - const goldValue = Number.parseInt(button.dataset.value); - const goldType = button.dataset.type; - const newValue = this.document.system.gold[goldType] >= goldValue ? goldValue - 1 : goldValue; - - const update = `system.gold.${goldType}`; - await this.document.update({ [update]: newValue }); - } - - static async attackRoll(event, button) { - const weapon = await fromUuid(button.dataset.weapon); - const damage = { - value: `${this.document.system.proficiency.value}${weapon.system.damage.value}`, - type: weapon.system.damage.type, - bonusDamage: this.document.system.bonuses.damage - }; - const modifier = this.document.system.traits[weapon.system.trait].value; - - const { roll, hope, fear, advantage, disadvantage, modifiers, bonusDamageString } = - await this.document.dualityRoll( - { title: game.i18n.localize(abilities[weapon.system.trait].label), value: modifier }, - event.shiftKey, - damage.bonusDamage - ); - - damage.value = damage.value.concat(bonusDamageString); - - const targets = Array.from(game.user.targets).map(x => ({ - id: x.id, - name: x.actor.name, - img: x.actor.img, - difficulty: x.actor.system.difficulty, - evasion: x.actor.system.evasion.value - })); - - const systemData = { - title: weapon.name, - origin: this.document.id, - roll: roll._formula, - modifiers: modifiers, - hope: hope, - fear: fear, - advantage: advantage, - disadvantage: disadvantage, - damage: damage, - targets: targets - }; - - const cls = getDocumentClass('ChatMessage'); - const msg = new cls({ - type: 'dualityRoll', - sound: CONFIG.sounds.dice, - system: systemData, - content: await foundry.applications.handlebars.renderTemplate( - 'systems/daggerheart/templates/chat/attack-roll.hbs', - systemData - ), - rolls: [roll] - }); - - await cls.create(msg.toObject()); - } - - tabSwitch(event) { - const tab = event.currentTarget.dataset.tab; - if (tab !== 'loadout') { - this.onVaultTab = false; - } - - this.render(); - } - - openLevelUp() { - if (!this.document.system.class.value || !this.document.system.subclass) { - ui.notifications.error(game.i18n.localize('DAGGERHEART.Sheets.PC.Errors.missingClassOrSubclass')); - return; - } - - new DhlevelUp(this.document).render(true); - } - - static domainCardsTab(toVault) { - this.onVaultTab = toVault; - this.render(); - } - - static async moveDomainCard(_, button) { - const toVault = button.dataset.action === 'sendToVault'; - if (!toVault && this.document.system.domainCards.loadout.length >= this.document.system.domainData.maxLoadout) { - return; - } - - const card = this.document.items.find(x => x.uuid === button.dataset.domain); - await card.update({ 'system.inVault': toVault }); - } - - static async useDomainCard(_, button) { - const card = this.document.items.find(x => x.uuid === button.dataset.key); - - const cls = getDocumentClass('ChatMessage'); - const systemData = { - title: `${game.i18n.localize('DAGGERHEART.Chat.DomainCard.Title')} - ${capitalize(button.dataset.domain)}`, - origin: this.document.id, - img: card.img, - name: card.name, - description: card.system.effect, - actions: card.system.actions - }; - const msg = new cls({ - type: 'abilityUse', - user: game.user.id, - content: await foundry.applications.handlebars.renderTemplate( - 'systems/daggerheart/templates/chat/ability-use.hbs', - systemData - ), - system: systemData - }); - - cls.create(msg.toObject()); - } - - static async removeDomainCard(_, button) { - if (button.dataset.type === 'domainCard') { - const card = this.document.items.find(x => x.uuid === button.dataset.key); - await card.delete(); - } - } - - static async selectClass() { - (await game.packs.get('daggerheart.classes'))?.render(true); - } - - static async selectSubclass() { - (await game.packs.get('daggerheart.subclasses'))?.render(true); - } - - static async selectAncestry() { - const dialogClosed = new Promise((resolve, _) => { - new AncestrySelectionDialog(resolve).render(true); - }); - const result = await dialogClosed; - - // await this.emulateItemDrop({ type: 'item', data: result }); - for (var ancestry of this.document.items.filter(x => x => x.type === 'ancestry')) { - await ancestry.delete(); - } - - const createdItems = []; - for (var feature of this.document.items.filter( - x => x.type === 'feature' && x.system.type === SYSTEM.ITEM.featureTypes.ancestry.id - )) { - await feature.delete(); - } - - // createdItems.push(...result.data.system.abilities); - createdItems.push(result.data); - - await this.document.createEmbeddedDocuments('Item', createdItems); - - // await this.document.createEmbeddedDocuments("Item", [result.toObject()]); - // (await game.packs.get('daggerheart.playtest-ancestries'))?.render(true); - } - - static async selectCommunity() { - (await game.packs.get('daggerheart.communities'))?.render(true); - } - - static async viewObject(_, button) { - const object = await fromUuid(button.dataset.value); - if (!object) return; - - const tab = button.dataset.tab; - if (tab && object.sheet._tabs) object.sheet._tabs[0].active = tab; - - if (object.sheet.editMode) object.sheet.editMode = false; - - object.sheet.render(true); - } - - static async takeShortRest() { - await new DhpDowntime(this.document, true).render(true); - await this.minimize(); - } - - static async takeLongRest() { - await new DhpDowntime(this.document, false).render(true); - await this.minimize(); - } - - static async addMiscItem() { - const result = await this.document.createEmbeddedDocuments('Item', [ - { - name: game.i18n.localize('DAGGERHEART.Sheets.PC.NewItem'), - type: 'miscellaneous' - } - ]); - - await result[0].sheet.render(true); - } - - static async addScar() { - if (this.document.system.story.scars.length === 5) return; - - await this.document.update({ - 'system.story.scars': [ - ...this.document.system.story.scars, - { name: game.i18n.localize('DAGGERHEART.Sheets.PC.NewScar'), description: '' } - ] - }); - } - - static async selectScar(_, button) { - this.selectedScar = Number.parseInt(button.dataset.value); - this.render(); - } - - static async deleteScar(event, button) { - event.stopPropagation(); - await this.document.update({ - 'system.story.scars': this.document.system.story.scars.filter( - (_, index) => index !== Number.parseInt(button.currentTarget.dataset.scar) - ) - }); - } - - static async makeDeathMove() { - if (this.document.system.resources.hitPoints.value === this.document.system.resources.hitPoints.max) { - await new DhpDeathMove(this.document).render(true); - await this.minimize(); - } - } - - static async toggleFeatureDice(_, button) { - const index = Number.parseInt(button.dataset.index); - const feature = this.document.system.classFeatures.find(x => x.uuid === button.dataset.feature); - const path = `system.featureType.data.numbers.${index}`; - if (feature.system.featureType.data.numbers[index]?.used) return; - - if (Object.keys(feature.system.featureType.data.numbers).length <= index) { - const roll = new Roll(feature.system.featureType.data.value); - const rollData = await roll.evaluate(); - const cls = getDocumentClass('ChatMessage'); - const msg = new cls({ - user: game.user.id, - rolls: [roll] - }); - - await cls.create(msg.toObject()); - - await feature.update({ [path]: { value: Number.parseInt(rollData.total), used: false } }); - } else { - await Dialog.confirm({ - title: game.i18n.localize('Confirm feature use'), - content: `Are you sure you want to use ${feature.name}?`, - yes: async () => { - await feature.update({ [path]: { used: true } }); - - const cls = getDocumentClass('ChatMessage'); - const msg = new cls({ - user: game.user.id, - content: await foundry.applications.handlebars.renderTemplate( - 'systems/daggerheart/templates/chat/ability-use.hbs', - { - title: game.i18n.localize('DAGGERHEART.Chat.FeatureTitle'), - card: { - name: `${feature.name} - Roll Of ${feature.system.featureType.data.numbers[index].value}`, - img: feature.img - } - } - ) - }); - - cls.create(msg.toObject()); - }, - no: () => { - return; - }, - defaultYes: false - }); - } - } - - async onFeatureInputBlur(event) { - const feature = this.document.system.classFeatures.find(x => x.uuid === event.currentTarget.dataset.feature); - const value = Number.parseInt(event.currentTarget.value); - if (!Number.isNaN(value)) await feature?.update({ 'system.featureType.data.value': value }); - } - - async experienceDescriptionChange(event) { - const newExperiences = [...this.document.system.experiences]; - newExperiences[event.currentTarget.dataset.index].description = event.currentTarget.value; - await this.document.update({ 'system.experiences': newExperiences }); - } - - async experienceValueChange(event) { - const newExperiences = [...this.document.system.experiences]; - newExperiences[event.currentTarget.dataset.index].value = event.currentTarget.value; - await this.document.update({ 'system.experiences': newExperiences }); - } - - static setStoryEditor(_, button) { - this.storyEditor = this.storyEditor === button.dataset.value ? null : button.dataset.value; - this.render(); - } - - async itemUpdate(event) { - const name = event.currentTarget.dataset.item; - const item = await fromUuid($(event.currentTarget).closest('[data-item-id]')[0].dataset.itemId); - await item.update({ [name]: event.currentTarget.value }); - } - - async onLevelChange(event) { - await this.document.updateLevel(Number(event.currentTarget.value)); - this.render(); - } - - static async deleteItem(_, button) { - const item = await fromUuid($(button).closest('[data-item-id]')[0].dataset.itemId); - await item.delete(); - } - - static async setItemQuantity(button, value) { - const item = await fromUuid($(button).closest('[data-item-id]')[0].dataset.itemId); - await item.update({ 'system.quantity': Math.max(item.system.quantity + value, 1) }); - } - - static async useFeature(_, button) { - const item = await fromUuid(button.dataset.id); - - const cls = getDocumentClass('ChatMessage'); - const systemData = { - title: game.i18n.localize('DAGGERHEART.Chat.FeatureTitle'), - origin: this.document.id, - img: item.img, - name: item.name, - description: item.system.description, - actions: item.system.actions - }; - const msg = new cls({ - type: 'abilityUse', - user: game.user.id, - content: await foundry.applications.handlebars.renderTemplate( - 'systems/daggerheart/templates/chat/ability-use.hbs', - systemData - ), - system: systemData - }); - - cls.create(msg.toObject()); - } - - static async useAbility(_, button) { - const item = await fromUuid(button.dataset.feature); - const type = button.dataset.type; - - const cls = getDocumentClass('ChatMessage'); - const systemData = { - title: - type === 'ancestry' - ? game.i18n.localize('DAGGERHEART.Chat.FoundationCard.AncestryTitle') - : type === 'community' - ? game.i18n.localize('DAGGERHEART.Chat.FoundationCard.CommunityTitle') - : game.i18n.localize('DAGGERHEART.Chat.FoundationCard.SubclassFeatureTitle'), - origin: this.document.id, - img: item.img, - name: item.name, - description: item.system.description, - actions: [] - }; - const msg = new cls({ - type: 'abilityUse', - user: game.user.id, - system: systemData, - content: await foundry.applications.handlebars.renderTemplate( - 'systems/daggerheart/templates/chat/ability-use.hbs', - systemData - ) - }); - - cls.create(msg.toObject()); - } - - static async useAdvancementCard(_, button) { - const item = - button.dataset.multiclass === 'true' - ? this.document.system.multiclassSubclass - : this.document.system.subclass; - const ability = item.system[`${button.dataset.key}Feature`]; - const title = `${item.name} - ${game.i18n.localize(`DAGGERHEART.Sheets.PC.DomainCard.${capitalize(button.dataset.key)}Title`)}`; - - const cls = getDocumentClass('ChatMessage'); - const systemData = { - title: game.i18n.localize('DAGGERHEART.Chat.FoundationCard.SubclassFeatureTitle'), - origin: this.document.id, - name: title, - img: item.img, - description: ability.description - }; - const msg = new cls({ - type: 'abilityUse', - user: game.user.id, - system: systemData, - content: await foundry.applications.handlebars.renderTemplate( - 'systems/daggerheart/templates/chat/ability-use.hbs', - systemData - ) - }); - - cls.create(msg.toObject()); - } - - static async useAdvancementAbility(_, button) { - // const item = await fromUuid(button.dataset.id); - const item = this.document.items.find(x => x.uuid === button.dataset.id); - - const cls = getDocumentClass('ChatMessage'); - const systemData = { - title: game.i18n.localize('DAGGERHEART.Chat.FoundationCard.SubclassFeatureTitle'), - origin: this.document.id, - name: item.name, - img: item.img, - description: item.system.description - }; - const msg = new cls({ - user: game.user.id, - system: systemData, - content: await foundry.applications.handlebars.renderTemplate( - 'systems/daggerheart/templates/chat/ability-use.hbs', - systemData - ) - }); - - cls.create(msg.toObject()); - } - - static async selectFeatureSet(_, button) { - const multiclass = button.dataset.multiclass === 'true'; - this.multiclassFeatureSetSelected = multiclass; - this.render(); - } - - static async toggleEquipItem(_, button) { - const item = this.document.items.get(button.id); - if (item.system.equipped) { - await item.update({ 'system.equipped': false }); - return; - } - - switch (item.type) { - case 'armor': - const currentArmor = this.document.system.armor; - if (currentArmor) { - await currentArmor.update({ 'system.equipped': false }); - } - - await item.update({ 'system.equipped': true }); - break; - case 'weapon': - await this.document.system.constructor.unequipBeforeEquip.bind(this.document.system)(item); - - await item.update({ 'system.equipped': true }); - break; - } - this.render(); - } - - static async close(options) { - this.onVaultTab = false; - super.close(options); - } - - async _onDragStart(_, event) { - if (event.currentTarget.classList.contains('inventory-item')) { - if (!['weapon', 'armor'].includes(event.currentTarget.dataset.type)) { - return; - } - - const targets = { - weapon: ['weapon-section', 'inventory-weapon-section'], - armor: ['armor-section', 'inventory-armor-section'] - }; - - event.dataTransfer.setData( - 'text/plain', - JSON.stringify({ - uuid: event.currentTarget.dataset.item, - internal: true, - targets: targets[event.currentTarget.dataset.type] - }) - ); - } - - super._onDragStart(event); - } - - async _onDrop(event) { - const itemData = event.dataTransfer?.getData('text/plain'); - const item = itemData ? JSON.parse(itemData) : null; - if (item?.internal) { - let target = null; - event.currentTarget.classList.forEach(x => { - if (item.targets.some(target => target === x)) { - target = x; - } - }); - if (target) { - const itemObject = await fromUuid(item.uuid); - switch (target) { - case 'weapon-section': - if ( - itemObject.system.secondary && - this.document.system.equippedWeapons.burden === 'twoHanded' - ) { - ui.notifications.info( - game.i18n.localize('DAGGERHEART.Notification.Info.SecondaryEquipWhileTwohanded') - ); - return; - } else if ( - itemObject.system.burden === 'twoHanded' && - this.document.system.equippedWeapons.secondary - ) { - ui.notifications.info( - game.i18n.localize('DAGGERHEART.Notification.Info.TwohandedEquipWhileSecondary') - ); - return; - } - - const existingWeapon = this.document.items.find( - x => x.system.active && x.system.secondary === itemObject.system.secondary - ); - await existingWeapon?.update({ 'system.active': false }); - await itemObject.update({ 'system.active': true }); - break; - case 'armor-section': - const existingArmor = this.document.items.find(x => x.type === 'armor' && x.system.active); - await existingArmor?.update({ 'system.active': false }); - await itemObject.update({ 'system.active': true }); - break; - case 'inventory-weapon-section': - /* FIXME inventoryWeapon is no longer a field - const existingInventoryWeapon = this.document.items.find(x => x.system.inventoryWeapon); - await existingInventoryWeapon?.update({ 'system.inventoryWeapon': false }); - await itemObject.update({ 'system.inventoryWeapon': true }); - break; - */ - case 'inventory-armor-section': - const existingInventoryArmor = this.document.items.find(x => x.system.inventoryArmor); - await existingInventoryArmor?.update({ 'system.inventoryArmor': false }); - await itemObject.update({ 'system.inventoryArmor': true }); - break; - } - } - } else { - super._onDrop(event); - this._onDropItem(event, TextEditor.getDragEventData(event)); - } - } - - async _onDropItem(event, data) { - if (this.dropItemBlock) { - return; - } else { - this.dropItemBlock = true; - setTimeout(() => (this.dropItemBlock = false), 500); - } - - const element = event.currentTarget; - const item = await Item.implementation.fromDropData(data); - const itemData = item.toObject(); - - const createdItems = []; - - if (item.type === 'domainCard') { - if (!this.document.system.class.value) { - ui.notifications.error(game.i18n.localize('DAGGERHEART.Notification.Error.NoClassSelected')); - return; - } - - if (!this.document.system.domains.find(x => x === item.system.domain)) { - ui.notifications.error(game.i18n.localize('DAGGERHEART.Notification.Error.LacksDomain')); - return; - } - - if (this.document.system.domainCards.total.length === this.document.system.domainData.maxCards) { - ui.notifications.error(game.i18n.localize('DAGGERHEART.Notification.Error.MaxLoadoutReached')); - return; - } - - if (this.document.system.domainCards.total.find(x => x.name === item.name)) { - ui.notifications.error(game.i18n.localize('DAGGERHEART.Notification.Error.DuplicateDomainCard')); - return; - } - - if (this.document.system.domainCards.loadout.length >= this.document.system.domainData.maxLoadout) { - itemData.system.inVault = true; - } - - if (this.document.uuid === item.parent?.uuid) return this._onSortItem(event, itemData); - const createdItem = await this._onDropItemCreate(itemData); - - return createdItem; - } else { - if (item.type === 'ancestry') { - for (var feature of this.document.items.filter( - x => x.type === 'feature' && x.system.type === SYSTEM.ITEM.featureTypes.ancestry.id - )) { - await feature.delete(); - } - - for (var feature of item.system.abilities) { - const data = (await fromUuid(feature.uuid)).toObject(); - const itemData = await this._onDropItemCreate(data); - createdItems.push(itemData); - } - } else if (item.type === 'community') { - for (var feature of this.document.items.filter( - x => x.type === 'feature' && x.system.type === SYSTEM.ITEM.featureTypes.community.id - )) { - await feature.delete(); - } - - for (var feature of item.system.abilities) { - const data = (await fromUuid(feature.uuid)).toObject(); - const itemData = await this._onDropItemCreate(data); - createdItems.push(itemData); - } - } - - if (this.document.uuid === item.parent?.uuid) return this._onSortItem(event, item); - - if (item.type === 'weapon') { - if (!element) return; - - if (element.classList.contains('weapon-section')) { - await this.document.system.constructor.unequipBeforeEquip.bind(this.document.system)(itemData); - itemData.system.equipped = true; - } - } - - if (item.type === 'armor') { - if (!element) return; - - if (element.classList.contains('armor-section')) { - const existing = this.document.system.armor - ? await fromUuid(this.document.system.armor.uuid) - : null; - await existing?.update({ 'system.equipped': false }); - itemData.system.equipped = true; - } - } - - const createdItem = await this._onDropItemCreate(itemData); - createdItems.push(createdItem); - - return createdItems; - } - } - - async _onDropItemCreate(itemData, event) { - itemData = itemData instanceof Array ? itemData : [itemData]; - return this.document.createEmbeddedDocuments('Item', itemData); - } - - async emulateItemDrop(data) { - const event = new DragEvent('drop', { altKey: game.keyboard.isModifierActive('Alt') }); - return this._onDropItem(event, data); - } -} diff --git a/module/config/actorConfig.mjs b/module/config/actorConfig.mjs index dcd85cdb..3698d19e 100644 --- a/module/config/actorConfig.mjs +++ b/module/config/actorConfig.mjs @@ -76,7 +76,7 @@ export const featureProperties = { }, spellcastingTrait: { name: 'DAGGERHEART.FeatureProperty.SpellcastingTrait', - path: actor => actor.system.traits[actor.system.subclass.system.spellcastingTrait].data.value + path: actor => actor.system.traits[actor.system.class.subclass.system.spellcastingTrait].data.value } }; diff --git a/module/config/generalConfig.mjs b/module/config/generalConfig.mjs index fecb939e..6526392f 100644 --- a/module/config/generalConfig.mjs +++ b/module/config/generalConfig.mjs @@ -199,8 +199,8 @@ export const tiers = { }; export const objectTypes = { - pc: { - name: 'TYPES.Actor.pc' + character: { + name: 'TYPES.Actor.character' }, npc: { name: 'TYPES.Actor.npc' diff --git a/module/data/_module.mjs b/module/data/_module.mjs index b70b5a23..85dae6dd 100644 --- a/module/data/_module.mjs +++ b/module/data/_module.mjs @@ -1,4 +1,4 @@ -export { default as DhpPC } from './pc.mjs'; +export { default as DhCharacter } from './character.mjs'; export { default as DhClass } from './item/class.mjs'; export { default as DhSubclass } from './item/subclass.mjs'; export { default as DhCombat } from './combat.mjs'; diff --git a/module/data/character.mjs b/module/data/character.mjs new file mode 100644 index 00000000..a3494ecd --- /dev/null +++ b/module/data/character.mjs @@ -0,0 +1,233 @@ +import { burden } from '../config/generalConfig.mjs'; +import ForeignDocumentUUIDField from './fields/foreignDocumentUUIDField.mjs'; +import { LevelOptionType } from './levelTier.mjs'; + +const attributeField = () => + new foundry.data.fields.SchemaField({ + value: new foundry.data.fields.NumberField({ initial: 0, integer: true }), + tierMarked: new foundry.data.fields.BooleanField({ initial: false }) + }); + +const resourceField = max => + new foundry.data.fields.SchemaField({ + value: new foundry.data.fields.NumberField({ initial: 0, integer: true }), + max: new foundry.data.fields.NumberField({ initial: max, integer: true }) + }); + +export default class DhCharacter extends foundry.abstract.TypeDataModel { + static defineSchema() { + const fields = foundry.data.fields; + + return { + resources: new fields.SchemaField({ + hitPoints: resourceField(6), + stress: resourceField(6), + hope: resourceField(6) + }), + traits: new fields.SchemaField({ + agility: attributeField(), + strength: attributeField(), + finesse: attributeField(), + instinct: attributeField(), + presence: attributeField(), + knowledge: attributeField() + }), + proficiency: new fields.NumberField({ initial: 1, integer: true }), + evasion: new fields.NumberField({ initial: 0, integer: true }), + experiences: new fields.TypedObjectField( + new fields.SchemaField({ + description: new fields.StringField({}), + value: new fields.NumberField({ integer: true, nullable: true, initial: null }) + }), + { + initial: { + [foundry.utils.randomID()]: { description: '', value: 2 }, + [foundry.utils.randomID()]: { description: '', value: 2 } + } + } + ), + gold: new fields.SchemaField({ + coins: new fields.NumberField({ initial: 0, integer: true }), + handfulls: new fields.NumberField({ initial: 0, integer: true }), + bags: new fields.NumberField({ initial: 0, integer: true }), + chests: new fields.NumberField({ initial: 0, integer: true }) + }), + pronouns: new fields.StringField({}), + scars: new fields.TypedObjectField( + new fields.SchemaField({ + name: new fields.StringField({}), + description: new fields.HTMLField() + }) + ), + story: new fields.HTMLField(), + description: new fields.HTMLField(), + class: new fields.SchemaField({ + value: new ForeignDocumentUUIDField({ type: 'Item', nullable: true }), + subclass: new ForeignDocumentUUIDField({ type: 'Item', nullable: true }) + }), + multiclass: new fields.SchemaField({ + value: new ForeignDocumentUUIDField({ type: 'Item', nullable: true }), + subclass: new ForeignDocumentUUIDField({ type: 'Item', nullable: true }) + }), + levelData: new fields.EmbeddedDataField(DhPCLevelData) + }; + } + + get ancestry() { + return this.parent.items.find(x => x.type === 'ancestry') ?? null; + } + + get community() { + return this.parent.items.find(x => x.type === 'community') ?? null; + } + + get domains() { + const classDomains = this.class ? this.class.system.domains : []; + const multiclassDomains = this.multiclass ? this.multiclass.system.domains : []; + return [...classDomains, ...multiclassDomains]; + } + + get domainCards() { + const domainCards = this.parent.items.filter(x => x.type === 'domainCard'); + const loadout = domainCards.filter(x => !x.system.inVault); + const vault = domainCards.filter(x => x.system.inVault); + + return { + loadout: loadout, + vault: vault, + total: [...loadout, ...vault] + }; + } + + get armor() { + return this.parent.items.find(x => x.type === 'armor' && x.system.equipped); + } + + get primaryWeapon() { + return this.parent.items.find(x => x.type === 'weapon' && x.system.equipped && !x.system.secondary); + } + + get secondaryWeapon() { + return this.parent.items.find(x => x.type === 'weapon' && x.system.equipped && x.system.secondary); + } + + get getWeaponBurden() { + return this.primaryWeapon?.system?.burden === burden.twoHanded.value || + (this.primaryWeapon && this.secondaryWeapon) + ? burden.twoHanded.value + : this.primaryWeapon || this.secondaryWeapon + ? burden.oneHanded.value + : null; + } + + get refreshableFeatures() { + return this.parent.items.reduce( + (acc, x) => { + if (x.type === 'feature' && x.system.refreshData?.type === 'feature' && x.system.refreshData?.type) { + acc[x.system.refreshData.type].push(x); + } + + return acc; + }, + { shortRest: [], longRest: [] } + ); + } + + static async unequipBeforeEquip(itemToEquip) { + const primary = this.primaryWeapon, + secondary = this.secondaryWeapon; + if (itemToEquip.system.secondary) { + if (primary && primary.burden === SYSTEM.GENERAL.burden.twoHanded.value) { + await primary.update({ 'system.equipped': false }); + } + + if (secondary) { + await secondary.update({ 'system.equipped': false }); + } + } else { + if (secondary && itemToEquip.system.burden === SYSTEM.GENERAL.burden.twoHanded.value) { + await secondary.update({ 'system.equipped': false }); + } + + if (primary) { + await primary.update({ 'system.equipped': false }); + } + } + } + + prepareBaseData() { + for (var attributeKey in this.traits) { + const attribute = this.traits[attributeKey]; + /* Levleup handling */ + } + + const armor = this.armor; + this.damageThresholds = { + major: armor + ? armor.system.baseThresholds.major + this.levelData.level.current + : this.levelData.level.current, + severe: armor + ? armor.system.baseThresholds.severe + this.levelData.level.current + : this.levelData.level.current * 2 + }; + } + + prepareDerivedData() { + this.resources.hope.max -= Object.keys(this.scars).length; + this.resources.hope.value = Math.min(this.resources.hope.value, this.resources.hope.max); + } +} + +class DhPCLevelData extends foundry.abstract.DataModel { + static defineSchema() { + const fields = foundry.data.fields; + + return { + level: new fields.SchemaField({ + current: new fields.NumberField({ required: true, integer: true, initial: 1 }), + changed: new fields.NumberField({ required: true, integer: true, initial: 1 }) + }), + levelups: new fields.TypedObjectField( + new fields.SchemaField({ + achievements: new fields.SchemaField( + { + experiences: new fields.TypedObjectField( + new fields.SchemaField({ + name: new fields.StringField({ required: true }), + modifier: new fields.NumberField({ required: true, integer: true }) + }) + ), + domainCards: new fields.ArrayField( + new fields.SchemaField({ + uuid: new fields.StringField({ required: true }), + itemUuid: new fields.StringField({ required: true }) + }) + ), + proficiency: new fields.NumberField({ integer: true }) + }, + { nullable: true, initial: null } + ), + selections: new fields.ArrayField( + new fields.SchemaField({ + tier: new fields.NumberField({ required: true, integer: true }), + level: new fields.NumberField({ required: true, integer: true }), + optionKey: new fields.StringField({ required: true }), + type: new fields.StringField({ required: true, choices: LevelOptionType }), + checkboxNr: new fields.NumberField({ required: true, integer: true }), + value: new fields.NumberField({ integer: true }), + minCost: new fields.NumberField({ integer: true }), + amount: new fields.NumberField({ integer: true }), + data: new fields.ArrayField(new fields.StringField({ required: true })), + secondaryData: new fields.StringField(), + itemUuid: new fields.StringField({ required: true }) + }) + ) + }) + ) + }; + } + + get canLevelUp() { + return this.level.current < this.level.changed; + } +} diff --git a/module/data/item/class.mjs b/module/data/item/class.mjs index 423ad8a5..0584f1db 100644 --- a/module/data/item/class.mjs +++ b/module/data/item/class.mjs @@ -56,7 +56,7 @@ export default class DHClass extends BaseDataItem { const allowed = await super._preCreate(data, options, user); if (allowed === false) return; - if (this.actor?.type === 'pc') { + if (this.actor?.type === 'character') { const path = data.system.isMulticlass ? 'system.multiclass.value' : 'system.class.value'; if (foundry.utils.getProperty(this.actor, path)) { ui.notifications.error(game.i18n.localize('DAGGERHEART.Item.Errors.ClassAlreadySelected')); @@ -67,7 +67,7 @@ export default class DHClass extends BaseDataItem { _onCreate(data, options, userId) { super._onCreate(data, options, userId); - if (options.parent?.type === 'pc') { + if (options.parent?.type === 'character') { const path = `system.${data.system.isMulticlass ? 'multiclass.value' : 'class.value'}`; options.parent.update({ [path]: `${options.parent.uuid}.Item.${data._id}` }); } @@ -76,7 +76,7 @@ export default class DHClass extends BaseDataItem { _onDelete(options, userId) { super._onDelete(options, userId); - if (options.parent?.type === 'pc') { + if (options.parent?.type === 'character') { const path = `system.${this.isMulticlass ? 'multiclass' : 'class'}`; options.parent.update({ [`${path}.value`]: null diff --git a/module/data/item/domainCard.mjs b/module/data/item/domainCard.mjs index 6986708f..b5880aad 100644 --- a/module/data/item/domainCard.mjs +++ b/module/data/item/domainCard.mjs @@ -1,13 +1,13 @@ -import DaggerheartAction from "../action.mjs"; -import BaseDataItem from "./base.mjs"; +import DaggerheartAction from '../action.mjs'; +import BaseDataItem from './base.mjs'; export default class DHDomainCard extends BaseDataItem { /** @inheritDoc */ static get metadata() { return foundry.utils.mergeObject(super.metadata, { - label: "TYPES.Item.domainCard", - type: "domainCard", - hasDescription: true, + label: 'TYPES.Item.domainCard', + type: 'domainCard', + hasDescription: true }); } @@ -19,10 +19,37 @@ export default class DHDomainCard extends BaseDataItem { domain: new fields.StringField({ choices: SYSTEM.DOMAIN.domains, required: true, blank: true }), level: new fields.NumberField({ initial: 1, integer: true }), recallCost: new fields.NumberField({ initial: 0, integer: true }), - type: new fields.StringField({ choices: SYSTEM.DOMAIN.cardTypes, required: true, blank: true}), + type: new fields.StringField({ choices: SYSTEM.DOMAIN.cardTypes, required: true, blank: true }), foundation: new fields.BooleanField({ initial: false }), inVault: new fields.BooleanField({ initial: false }), actions: new fields.ArrayField(new fields.EmbeddedDataField(DaggerheartAction)) }; } + + async _preCreate(data, options, user) { + const allowed = await super._preCreate(data, options, user); + if (allowed === false) return; + + if (this.actor?.type === 'character') { + if (!this.actor.system.class.value) { + ui.notifications.error(game.i18n.localize('DAGGERHEART.Item.Errors.NoClassSelected')); + return false; + } + + if (!this.actor.system.domains.find(x => x === item.system.domain)) { + ui.notifications.error(game.i18n.localize('DAGGERHEART.Item.Errors.LacksDomain')); + return false; + } + + if (this.actor.system.domainCards.total.length === 5) { + ui.notifications.error(game.i18n.localize('DAGGERHEART.Item.Errors.MaxLoadoutReached')); + return false; + } + + if (this.actor.system.domainCards.total.find(x => x.name === item.name)) { + ui.notifications.error(game.i18n.localize('DAGGERHEART.Item.Errors.DuplicateDomainCard')); + return false; + } + } + } } diff --git a/module/data/item/subclass.mjs b/module/data/item/subclass.mjs index 883df064..ea506efa 100644 --- a/module/data/item/subclass.mjs +++ b/module/data/item/subclass.mjs @@ -33,7 +33,7 @@ export default class DHSubclass extends BaseDataItem { const allowed = await super._preCreate(data, options, user); if (allowed === false) return; - if (this.actor?.type === 'pc') { + if (this.actor?.type === 'character') { const path = data.system.isMulticlass ? 'system.multiclass' : 'system.class'; const classData = foundry.utils.getProperty(this.actor, path); if (!classData.value) { @@ -52,7 +52,7 @@ export default class DHSubclass extends BaseDataItem { _onCreate(data, options, userId) { super._onCreate(data, options, userId); - if (options.parent?.type === 'pc') { + if (options.parent?.type === 'character') { const path = `system.${data.system.isMulticlass ? 'multiclass.subclass' : 'class.subclass'}`; options.parent.update({ [path]: `${options.parent.uuid}.Item.${data._id}` }); } @@ -61,7 +61,7 @@ export default class DHSubclass extends BaseDataItem { _onDelete(options, userId) { super._onDelete(options, userId); - if (options.parent?.type === 'pc') { + if (options.parent?.type === 'character') { const path = `system.${this.isMulticlass ? 'multiclass.subclass' : 'class.subclass'}`; options.parent.update({ [path]: null }); } diff --git a/module/data/pc.mjs b/module/data/pc.mjs deleted file mode 100644 index 740b51eb..00000000 --- a/module/data/pc.mjs +++ /dev/null @@ -1,413 +0,0 @@ -import { getPathValue } from '../helpers/utils.mjs'; -import ForeignDocumentUUIDField from './fields/foreignDocumentUUIDField.mjs'; -import { LevelOptionType } from './levelTier.mjs'; - -const fields = foundry.data.fields; - -const attributeField = () => - new fields.SchemaField({ - bonus: new fields.NumberField({ initial: 0, integer: true }), - base: new fields.NumberField({ initial: 0, integer: true }), - tierMarked: new fields.BooleanField({ required: true, initial: false }) - }); - -const resourceField = max => - new fields.SchemaField({ - value: new fields.NumberField({ initial: 0, integer: true }), - bonus: new fields.NumberField({ initial: 0, integer: true }), - min: new fields.NumberField({ initial: 0, integer: true }), - baseMax: new fields.NumberField({ initial: max, integer: true }) - }); - -export default class DhpPC extends foundry.abstract.TypeDataModel { - static defineSchema() { - return { - resources: new fields.SchemaField({ - hitPoints: resourceField(6), - stress: resourceField(6), - hope: new fields.SchemaField({ - value: new fields.NumberField({ initial: -1, integer: true }), // FIXME. Logic is gte and needs -1 in PC/Hope. Change to 0 - min: new fields.NumberField({ initial: 0, integer: true }) - }) - }), - bonuses: new fields.SchemaField({ - damage: new fields.ArrayField( - new fields.SchemaField({ - value: new fields.NumberField({ integer: true, initial: 0 }), - type: new fields.StringField({ nullable: true }), - initiallySelected: new fields.BooleanField(), - hopeIncrease: new fields.StringField({ initial: null, nullable: true }), - description: new fields.StringField({}) - }) - ) - }), - traits: new fields.SchemaField({ - agility: attributeField(), - strength: attributeField(), - finesse: attributeField(), - instinct: attributeField(), - presence: attributeField(), - knowledge: attributeField() - }), - proficiency: new fields.SchemaField({ - base: new fields.NumberField({ required: true, initial: 1, integer: true }), - bonus: new fields.NumberField({ required: true, initial: 0, integer: true }) - }), - evasion: new fields.SchemaField({ - bonus: new fields.NumberField({ initial: 0, integer: true }) - }), - experiences: new fields.ArrayField( - new fields.SchemaField({ - id: new fields.StringField({ required: true }), - description: new fields.StringField({}), - value: new fields.NumberField({ integer: true, nullable: true, initial: null }) - }), - { - initial: [ - { id: foundry.utils.randomID(), description: '', value: 2 }, - { id: foundry.utils.randomID(), description: '', value: 2 } - ] - } - ), - gold: new fields.SchemaField({ - coins: new fields.NumberField({ initial: 0, integer: true }), - handfulls: new fields.NumberField({ initial: 0, integer: true }), - bags: new fields.NumberField({ initial: 0, integer: true }), - chests: new fields.NumberField({ initial: 0, integer: true }) - }), - pronouns: new fields.StringField({}), - domainData: new fields.SchemaField({ - maxLoadout: new fields.NumberField({ initial: 2, integer: true }), - maxCards: new fields.NumberField({ initial: 2, integer: true }) - }), - story: new fields.SchemaField({ - background: new fields.HTMLField(), - appearance: new fields.HTMLField(), - connections: new fields.HTMLField(), - scars: new fields.ArrayField( - new fields.SchemaField({ - name: new fields.StringField({}), - description: new fields.HTMLField() - }) - ) - }), - description: new fields.StringField({}), - //Temporary until new FoundryVersion fix --> See Armor.Mjs DataPreparation - armorMarks: new fields.SchemaField({ - max: new fields.NumberField({ initial: 6, integer: true }), - value: new fields.NumberField({ initial: 0, integer: true }) - }), - class: new fields.SchemaField({ - value: new ForeignDocumentUUIDField({ type: 'Item', nullable: true }), - subclass: new ForeignDocumentUUIDField({ type: 'Item', nullable: true }) - }), - multiclass: new fields.SchemaField({ - value: new ForeignDocumentUUIDField({ type: 'Item', nullable: true }), - subclass: new ForeignDocumentUUIDField({ type: 'Item', nullable: true }) - }), - levelData: new fields.EmbeddedDataField(DhPCLevelData) - }; - } - - get tier() { - return this.#getTier(this.levelData.currentLevel); - } - - get ancestry() { - return this.parent.items.find(x => x.type === 'ancestry') ?? null; - } - - get multiclassSubclass() { - return this.parent.items.find(x => x.type === 'subclass' && x.system.multiclass) ?? null; - } - - get subclass() { - return this.parent.items.find(x => x.type === 'subclass' && !x.system.multiclass) ?? null; - } - - get subclassFeatures() { - const subclass = this.subclass; - const multiclass = this.multiclassSubclass; - const subclassItems = this.parent.items.filter(x => x.type === 'feature' && x.system.type === 'subclass'); - return { - subclass: !subclass - ? {} - : { - foundation: subclassItems.filter(x => - subclass.system.foundationFeature.abilities.some(ability => ability.uuid === x.uuid) - ), - specialization: subclassItems.filter(x => - subclass.system.specializationFeature.abilities.some(ability => ability.uuid === x.uuid) - ), - mastery: subclassItems.filter(x => - subclass.system.masteryFeature.abilities.some(ability => ability.uuid === x.uuid) - ) - }, - multiclassSubclass: !multiclass - ? {} - : { - foundation: subclassItems.filter(x => - multiclass.system.foundationFeature.abilities.some(ability => ability.uuid === x.uuid) - ), - specialization: subclassItems.filter(x => - multiclass.system.specializationFeature.abilities.some(ability => ability.uuid === x.uuid) - ), - mastery: subclassItems.filter(x => - multiclass.system.masteryFeature.abilities.some(ability => ability.uuid === x.uuid) - ) - } - }; - } - - get community() { - return this.parent.items.find(x => x.type === 'community') ?? null; - } - - get classFeatures() { - return this.parent.items.filter( - x => x.type === 'feature' && x.system.type === SYSTEM.ITEM.featureTypes.class.id && !x.system.multiclass - ); - } - - get multiclassFeatures() { - return this.parent.items.filter( - x => x.type === 'feature' && x.system.type === SYSTEM.ITEM.featureTypes.class.id && x.system.multiclass - ); - } - - get domains() { - const classDomains = this.class ? this.class.system.domains : []; - const multiclassDomains = this.multiclass ? this.multiclass.system.domains : []; - return [...classDomains, ...multiclassDomains]; - } - - get domainCards() { - const domainCards = this.parent.items.filter(x => x.type === 'domainCard'); - const loadout = domainCards.filter(x => !x.system.inVault); - const vault = domainCards.filter(x => x.system.inVault); - - return { - loadout: loadout, - vault: vault, - total: [...loadout, ...vault] - }; - } - - get armor() { - return this.parent.items.find(x => x.type === 'armor' && x.system.equipped); - } - - get equippedWeapons() { - const primaryWeapon = this.parent.items.find( - x => x.type === 'weapon' && x.system.equipped && !x.system.secondary - ); - const secondaryWeapon = this.parent.items.find( - x => x.type === 'weapon' && x.system.equipped && x.system.secondary - ); - return { - primary: this.#weaponData(primaryWeapon), - secondary: this.#weaponData(secondaryWeapon), - burden: this.getBurden(primaryWeapon, secondaryWeapon) - }; - } - - static async unequipBeforeEquip(itemToEquip) { - const equippedWeapons = this.equippedWeapons; - - if (itemToEquip.system.secondary) { - if (equippedWeapons.primary && equippedWeapons.primary.burden === SYSTEM.GENERAL.burden.twoHanded.value) { - await this.parent.items.get(equippedWeapons.primary.id).update({ 'system.equipped': false }); - } - - if (equippedWeapons.secondary) { - await this.parent.items.get(equippedWeapons.secondary.id).update({ 'system.equipped': false }); - } - } else { - if (equippedWeapons.secondary && itemToEquip.system.burden === SYSTEM.GENERAL.burden.twoHanded.value) { - await this.parent.items.get(equippedWeapons.secondary.id).update({ 'system.equipped': false }); - } - - if (equippedWeapons.primary) { - await this.parent.items.get(equippedWeapons.primary.id).update({ 'system.equipped': false }); - } - } - } - - get effects() { - return this.parent.items.reduce((acc, item) => { - const effects = item.system.effectData; - if (effects && !item.system.disabled) { - for (var key in effects) { - const effect = effects[key]; - for (var effectEntry of effect) { - if (!acc[key]) acc[key] = []; - acc[key].push({ name: item.name, value: effectEntry }); - } - } - } - - return acc; - }, {}); - } - - get refreshableFeatures() { - return this.parent.items.reduce( - (acc, x) => { - if (x.type === 'feature' && x.system.refreshData?.type === 'feature' && x.system.refreshData?.type) { - acc[x.system.refreshData.type].push(x); - } - - return acc; - }, - { shortRest: [], longRest: [] } - ); - } - - //Should not be done in data? - //TODO: REMOVE THIS - #weaponData(weapon) { - return weapon - ? { - id: weapon.id, - name: weapon.name, - trait: game.i18n.localize(CONFIG.daggerheart.ACTOR.abilities[weapon.system.trait].label), - range: CONFIG.daggerheart.GENERAL.range[weapon.system.range], - damage: { - value: weapon.system.damage.value, - type: CONFIG.daggerheart.GENERAL.damageTypes[weapon.system.damage.type] - }, - burden: weapon.system.burden, - feature: CONFIG.daggerheart.ITEM.weaponFeatures[weapon.system.feature], - img: weapon.img, - uuid: weapon.uuid - } - : null; - } - - prepareBaseData() { - this.resources.hitPoints.max = this.resources.hitPoints.baseMax + this.resources.hitPoints.bonus; - this.resources.stress.max = this.resources.stress.baseMax + this.resources.stress.bonus; - this.evasion.value = (this.class?.system?.evasion ?? 0) + this.evasion.bonus; - this.proficiency.value = this.proficiency.base + this.proficiency.bonus; - - for (var attributeKey in this.traits) { - const attribute = this.traits[attributeKey]; - attribute.value = attribute.base + attribute.bonus; - } - } - - prepareDerivedData() { - this.resources.hope.max = 6 - this.story.scars.length; - if (this.resources.hope.value >= this.resources.hope.max) { - this.resources.hope.value = Math.max(this.resources.hope.max - 1, 0); - } - - const armor = this.armor; - this.damageThresholds = { - major: armor - ? armor.system.baseThresholds.major + this.levelData.level.current - : this.levelData.level.current, - severe: armor - ? armor.system.baseThresholds.severe + this.levelData.level.current - : this.levelData.level.current * 2 - }; - - this.applyEffects(); - } - - applyEffects() { - const effects = this.effects; - for (var key in effects) { - const effectType = effects[key]; - for (var effect of effectType) { - switch (key) { - case SYSTEM.EFFECTS.effectTypes.health.id: - this.resources.hitPoints.bonus += effect.value.valueData.value; - break; - case SYSTEM.EFFECTS.effectTypes.stress.id: - this.resources.stress.bonus += effect.value.valueData.value; - break; - case SYSTEM.EFFECTS.effectTypes.damage.id: - this.bonuses.damage.push({ - value: getPathValue(effect.value.valueData.value, this), - type: 'physical', - description: effect.name, - hopeIncrease: effect.value.valueData.hopeIncrease, - initiallySelected: effect.value.initiallySelected, - appliesOn: effect.value.appliesOn - }); - } - } - } - } - - getBurden(primary, secondary) { - const twoHanded = - primary?.system?.burden === 'twoHanded' || - secondary?.system?.burden === 'twoHanded' || - (primary?.system?.burden === 'oneHanded' && secondary?.system?.burden === 'oneHanded'); - const oneHanded = - !twoHanded && (primary?.system?.burden === 'oneHanded' || secondary?.system?.burden === 'oneHanded'); - - return twoHanded ? 'twoHanded' : oneHanded ? 'oneHanded' : null; - } - - #getTier(level) { - if (level >= 8) return 3; - else if (level >= 5) return 2; - else if (level >= 2) return 1; - else return 0; - } -} - -class DhPCLevelData extends foundry.abstract.DataModel { - static defineSchema() { - return { - level: new fields.SchemaField({ - current: new fields.NumberField({ required: true, integer: true, initial: 1 }), - changed: new fields.NumberField({ required: true, integer: true, initial: 1 }) - }), - levelups: new fields.TypedObjectField( - new fields.SchemaField({ - achievements: new fields.SchemaField( - { - experiences: new fields.TypedObjectField( - new fields.SchemaField({ - name: new fields.StringField({ required: true }), - modifier: new fields.NumberField({ required: true, integer: true }) - }) - ), - domainCards: new fields.ArrayField( - new fields.SchemaField({ - uuid: new fields.StringField({ required: true }), - itemUuid: new fields.StringField({ required: true }) - }) - ), - proficiency: new fields.NumberField({ integer: true }) - }, - { nullable: true, initial: null } - ), - selections: new fields.ArrayField( - new fields.SchemaField({ - tier: new fields.NumberField({ required: true, integer: true }), - level: new fields.NumberField({ required: true, integer: true }), - optionKey: new fields.StringField({ required: true }), - type: new fields.StringField({ required: true, choices: LevelOptionType }), - checkboxNr: new fields.NumberField({ required: true, integer: true }), - value: new fields.NumberField({ integer: true }), - minCost: new fields.NumberField({ integer: true }), - amount: new fields.NumberField({ integer: true }), - data: new fields.ArrayField(new fields.StringField({ required: true })), - secondaryData: new fields.StringField(), - itemUuid: new fields.StringField({ required: true }) - }) - ) - }) - ) - }; - } - - get canLevelUp() { - return this.level.current < this.level.changed; - } -} diff --git a/module/data/settings/VariantRules.mjs b/module/data/settings/VariantRules.mjs index 2a1f948d..7d28a1d7 100644 --- a/module/data/settings/VariantRules.mjs +++ b/module/data/settings/VariantRules.mjs @@ -1,11 +1,14 @@ export default class DhVariantRules extends foundry.abstract.DataModel { + static LOCALIZATION_PREFIXES = ['DAGGERHEART.Settings.VariantRules']; + static defineSchema() { const fields = foundry.data.fields; return { actionTokens: new fields.SchemaField({ enabled: new fields.BooleanField({ required: true, initial: false }), tokens: new fields.NumberField({ required: true, integer: true, initial: 3 }) - }) + }), + useCoins: new fields.BooleanField({ initial: false }) }; } diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index bc116550..55b00634 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -10,7 +10,7 @@ export default class DhpActor extends Actor { // Configure prototype token settings const prototypeToken = {}; - if (this.type === 'pc') + if (this.type === 'character') Object.assign(prototypeToken, { sight: { enabled: true }, actorLink: true, @@ -28,7 +28,7 @@ export default class DhpActor extends Actor { } async updateLevel(newLevel) { - if (this.type !== 'pc' || newLevel === this.system.levelData.level.changed) return; + if (this.type !== 'character' || newLevel === this.system.levelData.level.changed) return; if (newLevel > this.system.levelData.level.current) { await this.update({ 'system.levelData.level.changed': newLevel }); @@ -124,7 +124,7 @@ export default class DhpActor extends Actor { } async diceRoll(modifier, shiftKey) { - if (this.type === 'pc') { + if (this.type === 'character') { return await this.dualityRoll(modifier, shiftKey); } else { return await this.npcRoll(modifier, shiftKey); @@ -173,12 +173,11 @@ export default class DhpActor extends Actor { return { roll, dice: dice[0], modifiers, advantageState: advantage === true ? 1 : advantage === false ? 2 : 0 }; } - async dualityRoll(modifier, shiftKey, bonusDamage = []) { + async dualityRoll(modifier, shiftKey) { let hopeDice = 'd12', fearDice = 'd12', advantageDice = null, - disadvantageDice = null, - bonusDamageString = ''; + disadvantageDice = null; const modifiers = modifier.value !== null @@ -195,12 +194,9 @@ export default class DhpActor extends Actor { : []; if (!shiftKey) { const dialogClosed = new Promise((resolve, _) => { - new RollSelectionDialog( - this.system.experiences, - bonusDamage, - this.system.resources.hope.value, - resolve - ).render(true); + new RollSelectionDialog(this.system.experiences, this.system.resources.hope.value, resolve).render( + true + ); }); const result = await dialogClosed; (hopeDice = result.hope), @@ -214,7 +210,6 @@ export default class DhpActor extends Actor { title: x.description }) ); - bonusDamageString = result.bonusDamage; const automateHope = await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Automation.Hope); @@ -268,8 +263,7 @@ export default class DhpActor extends Actor { fear: { dice: fearDice, value: fear }, advantage: { dice: advantageDice, value: advantage }, disadvantage: { dice: disadvantageDice, value: disadvantage }, - modifiers: modifiers, - bonusDamageString + modifiers: modifiers }; } @@ -401,11 +395,6 @@ export default class DhpActor extends Actor { } } - async emulateItemDrop(data) { - const event = new DragEvent('drop', { altKey: game.keyboard.isModifierActive('Alt') }); - return this.sheet._onDropItem(event, { data: data }); - } - //Move to action-scope? async useAction(action) { const userTargets = Array.from(game.user.targets); diff --git a/module/helpers/utils.mjs b/module/helpers/utils.mjs index 3fbe89c3..d764210a 100644 --- a/module/helpers/utils.mjs +++ b/module/helpers/utils.mjs @@ -114,7 +114,7 @@ export const getCommandTarget = () => { ui.notifications.error(game.i18n.localize('DAGGERHEART.Notification.Error.NoSelectedToken')); return null; } - if (target.type !== 'pc') { + if (target.type !== 'character') { ui.notifications.error(game.i18n.localize('DAGGERHEART.Notification.Error.OnlyUseableByPC')); return null; } diff --git a/styles/daggerheart.less b/styles/daggerheart.less index 8ec94054..d3ad28b2 100755 --- a/styles/daggerheart.less +++ b/styles/daggerheart.less @@ -13,6 +13,7 @@ @import './resources.less'; // new styles imports +@import './less/actors/character.less'; @import './less/actors/adversary.less'; @import './less/actors/environment.less'; diff --git a/styles/less/actors/character.less b/styles/less/actors/character.less new file mode 100644 index 00000000..e69de29b diff --git a/system.json b/system.json index c402b837..35f69cde 100644 --- a/system.json +++ b/system.json @@ -203,7 +203,9 @@ }, "documentTypes": { "Actor": { - "pc": {}, + "character": { + "htmlFields": ["story", "description", "scars.*.description"] + }, "adversary": { "htmlFields": ["description", "motivesAndTactics"] }, diff --git a/templates/settings/variant-rules.hbs b/templates/settings/variant-rules.hbs index f39cb2a9..2c4d7d30 100644 --- a/templates/settings/variant-rules.hbs +++ b/templates/settings/variant-rules.hbs @@ -8,6 +8,8 @@ + {{formGroup settingFields.schema.fields.useCoins value=settingFields._source.useCoins localize=true }} +