diff --git a/README.md b/README.md index eaff78f9..760194f4 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,24 @@ # Daggerheart + ## Table of Contents + - [Overview](#overview) - [User Install Guide](#user-install) - [Developer Setup](#developer-setup) - [Contribution Info](#contributing) ## Overview + This is a community repo for a Foundry VTT implementation of Daggerheart. It is not associated with Critical Role or Darrington Press. ## User Install + 1. **(Not Yet Supported - No Releases Yet)** Pasting `https://raw.githubusercontent.com/Foundryborne/daggerheart/refs/heads/main/system.json` into the Install System dialog on the Setup menu of the application. 2. **(Not Yet Supported - No Releases Yet)** Browsing the repository's Releases page, where you can copy any system.json link for use in the Install System dialog. 3. **(Not Yet Supported - No Releases Yet)** Downloading one of the .zip archives from the Releases page and extracting it into your foundry Data folder, under Data/systems/daggerheart. ## Development Setup + - Open a terminal in the directory with the repo `cd //` - NOTE: The repo should be placed in the system files are or somewhere else and a link (if on linux) is placed in the system directory - NOTE: Linux link can be made using `ln -snf daggerheart` inside the system folder @@ -33,8 +38,8 @@ This is a community repo for a Foundry VTT implementation of Daggerheart. It is Now you should be able to build the app using `npm start` [Foundry VTT Website][1] -[1]: https://foundryvtt.com/ +[1]: https://foundryvtt.com/ -## Contributing +## Contributing Looking to contribute to the project? Look no further, check out our [contributing guide](contributing.md), and keep the [Code of Conduct](coc.md) in mind when working on things. diff --git a/daggerheart.mjs b/daggerheart.mjs index 909324b4..27c01dd6 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -85,6 +85,7 @@ Hooks.once('init', () => { Actors.unregisterSheet('core', foundry.applications.sheets.ActorSheetV2); Actors.registerSheet(SYSTEM.id, applications.DhCharacterSheet, { types: ['character'], makeDefault: true }); + Actors.registerSheet(SYSTEM.id, applications.DhCompanionSheet, { types: ['companion'], makeDefault: true }); Actors.registerSheet(SYSTEM.id, applications.DhpAdversarySheet, { types: ['adversary'], makeDefault: true }); Actors.registerSheet(SYSTEM.id, applications.DhpEnvironment, { types: ['environment'], makeDefault: true }); @@ -285,6 +286,7 @@ const preloadHandlebarsTemplates = async function () { return foundry.applications.handlebars.loadTemplates([ 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs', 'systems/daggerheart/templates/sheets/global/partials/inventory-item.hbs', + 'systems/daggerheart/templates/sheets/global/partials/action-item.hbs', 'systems/daggerheart/templates/sheets/global/partials/domain-card-item.hbs', 'systems/daggerheart/templates/sheets/global/partials/inventory-fieldset-items.hbs', @@ -322,7 +324,7 @@ const preloadHandlebarsTemplates = async function () { 'systems/daggerheart/templates/views/actionTypes/range-target.hbs', 'systems/daggerheart/templates/views/actionTypes/effect.hbs', 'systems/daggerheart/templates/settings/components/settings-item-line.hbs', - + 'systems/daggerheart/templates/chat/parts/target-chat.hbs' ]); }; diff --git a/lang/en.json b/lang/en.json index dae24038..2f9790da 100755 --- a/lang/en.json +++ b/lang/en.json @@ -14,6 +14,7 @@ }, "Actor": { "character": "Character", + "companion": "Companion", "adversary": "Adversary", "environment": "Environment" } @@ -490,7 +491,29 @@ "evasion": "Permanently gain a +1 bonus to your Evasion.", "subclass": "Take an upgraded subclass card. Then cross out the multiclass option for this tier.", "proficiency": "Increase your Proficiency by +1.", - "multiclass": "Multiclass: Choose an additional class for your character, then cross out an unused “Take an upgraded subclass card” and the other multiclass option on this sheet." + "multiclass": "Multiclass: Choose an additional class for your character, then cross out an unused “Take an upgraded subclass card” and the other multiclass option on this sheet.", + "intelligent": "Your companion gains a permanent +1 bonus to a Companion Experience of your choice.", + "lightInTheDark": "Gain an additional Hope slot for your character.", + "creatureComfort": "Once per rest, when you take time during a quiet moment to give your companion love and attention, you can gain a Hope or you can both clear a Stress.", + "armored": "When your companion takes damage, you can mark one of your Armor Slots instead of marking one of their Stress.", + "vicious": "Increase your companion's damage dice or range by one step (d6 to d8, Close to Far, etc.)", + "resilient": "Your companion gains an additional Stress slot.", + "bonded": "When you mark your last Hit Point, your companion rushes to your side to comfort you. Roll a number of d6s equal to the unmarked Stress slots they have and mark them. If any roll a 6, your companion helps you up. Clear your last Hit Point and return to the scene.", + "aware": "Your companion gains a permanent +2 bonus to their Evasion." + }, + "Actions": { + "CreatureComfort": { + "Name": "Creature Comfort", + "Description": "Once per rest, when you take time during a quiet moment to give your companion love and attention, you can gain a Hope or you can both clear a Stress." + }, + "Armored": { + "Name": "Armored", + "Description": "When your companion takes damage, you can mark one of your Armor Slots instead of marking one of their Stress." + }, + "Bonded": { + "Name": "Bonded", + "Description": "When you mark your last Hit Point, your companion rushes to your side to comfort you. Roll a number of d6s equal to the unmarked Stress slots they have and mark them. If any roll a 6, your companion helps you up. Clear your last Hit Point and return to the scene." + } }, "Tier2": { "Label": "Levels 2-4", @@ -961,7 +984,9 @@ "content": "Returning to the previous level selection will remove all selections made for this level. Do you want to proceed?" }, "Selections": { - "emptyDomainCardHint": "{domain} level {level} or below" + "emptyDomainCardHint": "{domain} level {level} or below", + "viciousDamage": "Damage", + "viciousRange": "Range" }, "summary": { "levelAchievements": "Level Achievements", @@ -979,7 +1004,11 @@ "multiclass": "Multiclass", "traits": "Increased Traits", "experienceIncreases": "Experience Increases", - "damageThresholds": "Damage Thresholds" + "damageThresholds": "Damage Thresholds", + "vicious": "Vicious", + "damageIncreased": "Damage Increased: {damage}", + "rangeIncreased": "Range Increased: {range}", + "simpleFeature": "Feature: {feature}" }, "notifications": { "info": { @@ -1132,6 +1161,8 @@ "CharacterSetup": "Character setup isn't done yet", "Level": "Level", "LevelUp": "You can level up", + "Features": "Features", + "CompanionFeatures": "Companion Features", "Tabs": { "Features": "Features", "Inventory": "Inventory", @@ -1176,9 +1207,6 @@ "Experience": { "Title": "Experience" }, - "Features": { - "Title": "Class Features" - }, "Gold": { "Title": "Gold", "Coins": "Coins", @@ -1245,6 +1273,24 @@ "tooLowLevel": "You cannot lower the character level below starting level" } }, + "Companion": { + "FIELDS": { + "partner": { "label": "Partner" }, + "evasion": { + "value": { "label": "Evasion" } + }, + "resources": { + "stress": { + "value": { "label": "Stress" } + } + }, + "attack": { + "name": { "label": "Attack Name" } + } + }, + "Experiences": "Experiences", + "Level": "Level" + }, "Adversary": { "FIELDS": { "tier": { "label": "Tier" }, diff --git a/module/applications/_module.mjs b/module/applications/_module.mjs index c9f5ddc6..25d48846 100644 --- a/module/applications/_module.mjs +++ b/module/applications/_module.mjs @@ -1,4 +1,5 @@ export { default as DhCharacterSheet } from './sheets/character.mjs'; +export { default as DhCompanionSheet } from './sheets/companion.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/ancestrySelectionDialog.mjs b/module/applications/ancestrySelectionDialog.mjs index 0cdb0dd9..bc2f6b5e 100644 --- a/module/applications/ancestrySelectionDialog.mjs +++ b/module/applications/ancestrySelectionDialog.mjs @@ -143,7 +143,7 @@ export default class AncestrySelectionDialog extends HandlebarsApplicationMixin( } static _onEditImage() { - const fp = new FilePicker({ + const fp = new foundry.applications.apps.FilePicker.implementation({ current: this.data.ancestryInfo.img, type: 'image', redirectToRoot: ['icons/svg/mystery-man.svg'], diff --git a/module/applications/contextMenu.mjs b/module/applications/contextMenu.mjs index ff171bfe..f7658d42 100644 --- a/module/applications/contextMenu.mjs +++ b/module/applications/contextMenu.mjs @@ -1,4 +1,4 @@ -export default class DhContextMenu extends ContextMenu { +export default class DhContextMenu extends foundry.applications.ux.ContextMenu.implementation { constructor(container, selector, menuItems, options) { super(container, selector, menuItems, options); @@ -26,10 +26,16 @@ export default class DhContextMenu extends ContextMenu { event.preventDefault(); event.stopPropagation(); const { clientX, clientY } = event; - const selector = "[data-item-id]"; + const selector = '[data-item-id]'; const target = event.target.closest(selector) ?? event.currentTarget.closest(selector); - target?.dispatchEvent(new PointerEvent("contextmenu", { - view: window, bubbles: true, cancelable: true, clientX, clientY - })); + target?.dispatchEvent( + new PointerEvent('contextmenu', { + view: window, + bubbles: true, + cancelable: true, + clientX, + clientY + }) + ); } } diff --git a/module/applications/countdowns.mjs b/module/applications/countdowns.mjs index 0eac145f..9fcb0a2b 100644 --- a/module/applications/countdowns.mjs +++ b/module/applications/countdowns.mjs @@ -160,7 +160,7 @@ class Countdowns extends HandlebarsApplicationMixin(ApplicationV2) { static onEditImage(_, target) { const setting = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Countdowns)[this.basePath]; const current = setting.countdowns[target.dataset.countdown].img; - const fp = new FilePicker({ + const fp = new foundry.applications.apps.FilePicker.implementation({ current, type: 'image', callback: async path => this.updateImage.bind(this)(path, target.dataset.countdown), diff --git a/module/applications/levelup/characterLevelup.mjs b/module/applications/levelup/characterLevelup.mjs new file mode 100644 index 00000000..26a425e1 --- /dev/null +++ b/module/applications/levelup/characterLevelup.mjs @@ -0,0 +1,383 @@ +import LevelUpBase from './levelup.mjs'; +import { DhLevelup } from '../../data/levelup.mjs'; +import { domains } from '../../config/domainConfig.mjs'; +import { abilities } from '../../config/actorConfig.mjs'; + +export default class DhCharacterLevelUp extends LevelUpBase { + constructor(actor) { + super(actor); + + this.levelTiers = this.addBonusChoices(game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.LevelTiers)); + const playerLevelupData = actor.system.levelData; + this.levelup = new DhLevelup(DhLevelup.initializeData(this.levelTiers, playerLevelupData)); + } + + async _preparePartContext(partId, context) { + await super._preparePartContext(partId, context); + + const currentLevel = this.levelup.levels[this.levelup.currentLevel]; + switch (partId) { + case 'selections': + const advancementChoices = Object.keys(currentLevel.choices).reduce((acc, choiceKey) => { + Object.keys(currentLevel.choices[choiceKey]).forEach(checkboxNr => { + const checkbox = currentLevel.choices[choiceKey][checkboxNr]; + const data = { + ...checkbox, + path: `levels.${this.levelup.currentLevel}.choices.${choiceKey}.${checkboxNr}`, + level: this.levelup.currentLevel + }; + + if (!acc[choiceKey]) acc[choiceKey] = []; + acc[choiceKey].push(data); + }); + + return acc; + }, {}); + + const traits = Object.values(advancementChoices.trait ?? {}); + const traitValues = traits.filter(trait => trait.data.length > 0).flatMap(trait => trait.data); + context.traits = { + values: traitValues, + active: traits.length > 0, + progress: { + selected: traitValues.length, + max: traits.reduce((acc, exp) => acc + exp.amount, 0) + } + }; + + const experienceIncreases = Object.values(advancementChoices.experience ?? {}); + const experienceIncreaseValues = experienceIncreases + .filter(exp => exp.data.length > 0) + .flatMap(exp => + exp.data.map(data => { + const experience = Object.keys(this.actor.system.experiences).find(x => x === data); + return this.actor.system.experiences[experience].name; + }) + ); + context.experienceIncreases = { + values: experienceIncreaseValues, + active: experienceIncreases.length > 0, + progress: { + selected: experienceIncreaseValues.length, + max: experienceIncreases.reduce((acc, exp) => acc + exp.amount, 0) + } + }; + + context.newExperiences = Object.keys(currentLevel.achievements.experiences).map(key => { + const experience = currentLevel.achievements.experiences[key]; + return { + ...experience, + level: this.levelup.currentLevel, + key: key + }; + }); + + const allDomainCards = { + ...advancementChoices.domainCard, + ...currentLevel.achievements.domainCards + }; + const allDomainCardKeys = Object.keys(allDomainCards); + + const classDomainsData = this.actor.system.class.value.system.domains.map(domain => ({ + domain, + multiclass: false + })); + const multiclassDomainsData = (this.actor.system.multiclass?.value?.system?.domains ?? []).map( + domain => ({ domain, multiclass: true }) + ); + const domainsData = [...classDomainsData, ...multiclassDomainsData]; + const multiclassDomain = this.levelup.classUpgradeChoices?.multiclass?.domain; + if (multiclassDomain) { + if (!domainsData.some(x => x.domain === multiclassDomain)) + domainsData.push({ domain: multiclassDomain, multiclass: true }); + } + + context.domainCards = []; + for (var key of allDomainCardKeys) { + const domainCard = allDomainCards[key]; + if (domainCard.level > this.levelup.endLevel) continue; + + const uuid = domainCard.data?.length > 0 ? domainCard.data[0] : domainCard.uuid; + const card = uuid ? await foundry.utils.fromUuid(uuid) : {}; + + context.domainCards.push({ + ...(card.toObject?.() ?? card), + emptySubtexts: domainsData.map(domain => { + const levelBase = domain.multiclass + ? Math.ceil(this.levelup.currentLevel / 2) + : this.levelup.currentLevel; + const levelMax = domainCard.secondaryData?.limit + ? Math.min(domainCard.secondaryData.limit, levelBase) + : levelBase; + + return game.i18n.format('DAGGERHEART.Application.LevelUp.Selections.emptyDomainCardHint', { + domain: game.i18n.localize(domains[domain.domain].label), + level: levelMax + }); + }), + path: domainCard.data + ? `${domainCard.path}.data` + : `levels.${domainCard.level}.achievements.domainCards.${key}.uuid`, + limit: domainCard.secondaryData?.limit ?? null, + compendium: 'domains' + }); + } + + const subclassSelections = advancementChoices.subclass?.flatMap(x => x.data) ?? []; + const possibleSubclasses = [this.actor.system.class.subclass]; + if (this.actor.system.multiclass?.subclass) { + possibleSubclasses.push(this.actor.system.multiclass.subclass); + } + + context.subclassCards = []; + if (advancementChoices.subclass?.length > 0) { + const featureStateIncrease = Object.values(this.levelup.levels).reduce((acc, level) => { + acc += Object.values(level.choices).filter(choice => { + return Object.values(choice).every(checkbox => checkbox.type === 'subclass'); + }).length; + return acc; + }, 0); + + for (var subclass of possibleSubclasses) { + const choice = + advancementChoices.subclass.find(x => x.data[0] === subclass.uuid) ?? + advancementChoices.subclass.find(x => x.data.length === 0); + const featureState = subclass.system.featureState + featureStateIncrease; + const data = await foundry.utils.fromUuid(subclass.uuid); + context.subclassCards.push({ + ...data.toObject(), + path: choice?.path, + uuid: data.uuid, + selected: subclassSelections.includes(subclass.uuid), + featureState: featureState, + featureLabel: game.i18n.localize(subclassFeatureLabels[featureState]), + isMulticlass: subclass.system.isMulticlass ? 'true' : 'false' + }); + } + } + + const multiclasses = Object.values(advancementChoices.multiclass ?? {}); + if (multiclasses?.[0]) { + const data = multiclasses[0]; + const multiclass = data.data.length > 0 ? await foundry.utils.fromUuid(data.data[0]) : {}; + + context.multiclass = { + ...data, + ...(multiclass.toObject?.() ?? multiclass), + uuid: multiclass.uuid, + domains: + multiclass?.system?.domains.map(key => { + const domain = domains[key]; + const alreadySelected = this.actor.system.class.value.system.domains.includes(key); + + return { + ...domain, + selected: key === data.secondaryData.domain, + disabled: + (data.secondaryData.domain && key !== data.secondaryData.domain) || + alreadySelected + }; + }) ?? [], + subclasses: + multiclass?.system?.subclasses.map(subclass => ({ + ...subclass, + uuid: subclass.uuid, + selected: data.secondaryData.subclass === subclass.uuid, + disabled: data.secondaryData.subclass && data.secondaryData.subclass !== subclass.uuid + })) ?? [], + compendium: 'classes', + limit: 1 + }; + } + + break; + case 'summary': + const { current: currentActorLevel, changed: changedActorLevel } = this.actor.system.levelData.level; + const actorArmor = this.actor.system.armor; + const levelKeys = Object.keys(this.levelup.levels); + let achivementProficiency = 0; + const achievementCards = []; + let achievementExperiences = []; + for (var levelKey of levelKeys) { + const level = this.levelup.levels[levelKey]; + if (Number(levelKey) < this.levelup.startLevel) continue; + + achivementProficiency += level.achievements.proficiency ?? 0; + const cards = level.achievements.domainCards ? Object.values(level.achievements.domainCards) : null; + if (cards) { + for (var card of cards) { + const itemCard = await foundry.utils.fromUuid(card.uuid); + achievementCards.push(itemCard); + } + } + + achievementExperiences = level.achievements.experiences + ? Object.values(level.achievements.experiences).reduce((acc, experience) => { + if (experience.name) acc.push(experience); + return acc; + }, []) + : []; + } + + context.achievements = { + proficiency: { + old: this.actor.system.proficiency.total, + new: this.actor.system.proficiency.total + achivementProficiency, + shown: achivementProficiency > 0 + }, + damageThresholds: { + major: { + old: this.actor.system.damageThresholds.major, + new: this.actor.system.damageThresholds.major + changedActorLevel - currentActorLevel + }, + severe: { + old: this.actor.system.damageThresholds.severe, + new: + this.actor.system.damageThresholds.severe + + (actorArmor + ? changedActorLevel - currentActorLevel + : (changedActorLevel - currentActorLevel) * 2) + }, + unarmored: !actorArmor + }, + domainCards: { + values: achievementCards, + shown: achievementCards.length > 0 + }, + experiences: { + values: achievementExperiences, + shown: achievementExperiences.length > 0 + } + }; + + const advancement = {}; + for (var levelKey of levelKeys) { + const level = this.levelup.levels[levelKey]; + if (Number(levelKey) < this.levelup.startLevel) continue; + + for (var choiceKey of Object.keys(level.choices)) { + const choice = level.choices[choiceKey]; + for (var checkbox of Object.values(choice)) { + switch (choiceKey) { + case 'proficiency': + case 'hitPoint': + case 'stress': + case 'evasion': + advancement[choiceKey] = advancement[choiceKey] + ? advancement[choiceKey] + Number(checkbox.value) + : Number(checkbox.value); + break; + case 'trait': + if (!advancement[choiceKey]) advancement[choiceKey] = {}; + for (var traitKey of checkbox.data) { + if (!advancement[choiceKey][traitKey]) advancement[choiceKey][traitKey] = 0; + advancement[choiceKey][traitKey] += 1; + } + break; + case 'domainCard': + if (!advancement[choiceKey]) advancement[choiceKey] = []; + if (checkbox.data.length === 1) { + const choiceItem = await foundry.utils.fromUuid(checkbox.data[0]); + advancement[choiceKey].push(choiceItem.toObject()); + } + break; + case 'experience': + if (!advancement[choiceKey]) advancement[choiceKey] = []; + 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; + case 'subclass': + if (checkbox.data[0]) { + const subclassItem = await foundry.utils.fromUuid(checkbox.data[0]); + if (!advancement[choiceKey]) advancement[choiceKey] = []; + advancement[choiceKey].push({ + ...subclassItem.toObject(), + featureLabel: game.i18n.localize( + subclassFeatureLabels[Number(checkbox.secondaryData.featureState)] + ) + }); + } + break; + case 'multiclass': + const multiclassItem = await foundry.utils.fromUuid(checkbox.data[0]); + const subclass = multiclassItem + ? await foundry.utils.fromUuid(checkbox.secondaryData.subclass) + : null; + advancement[choiceKey] = multiclassItem + ? { + ...multiclassItem.toObject(), + domain: checkbox.secondaryData.domain + ? game.i18n.localize(domains[checkbox.secondaryData.domain].label) + : null, + subclass: subclass ? subclass.name : null + } + : {}; + break; + } + } + } + } + + context.advancements = { + statistics: { + proficiency: { + old: context.achievements.proficiency.new, + new: context.achievements.proficiency.new + (advancement.proficiency ?? 0) + }, + hitPoints: { + old: this.actor.system.resources.hitPoints.maxTotal, + new: this.actor.system.resources.hitPoints.maxTotal + (advancement.hitPoint ?? 0) + }, + stress: { + old: this.actor.system.resources.stress.maxTotal, + new: this.actor.system.resources.stress.maxTotal + (advancement.stress ?? 0) + }, + evasion: { + old: this.actor.system.evasion.total, + new: this.actor.system.evasion.total + (advancement.evasion ?? 0) + } + }, + traits: Object.keys(this.actor.system.traits).reduce((acc, traitKey) => { + if (advancement.trait?.[traitKey]) { + if (!acc) acc = {}; + acc[traitKey] = { + label: game.i18n.localize(abilities[traitKey].label), + old: this.actor.system.traits[traitKey].total, + new: this.actor.system.traits[traitKey].total + advancement.trait[traitKey] + }; + } + return acc; + }, null), + domainCards: advancement.domainCard ?? [], + experiences: + advancement.experience?.flatMap(x => x.data.map(data => ({ name: data, modifier: x.value }))) ?? + [], + multiclass: advancement.multiclass, + subclass: advancement.subclass + }; + + context.advancements.statistics.proficiency.shown = + context.advancements.statistics.proficiency.new > context.advancements.statistics.proficiency.old; + context.advancements.statistics.hitPoints.shown = + context.advancements.statistics.hitPoints.new > context.advancements.statistics.hitPoints.old; + context.advancements.statistics.stress.shown = + context.advancements.statistics.stress.new > context.advancements.statistics.stress.old; + context.advancements.statistics.evasion.shown = + context.advancements.statistics.evasion.new > context.advancements.statistics.evasion.old; + context.advancements.statistics.shown = + context.advancements.statistics.proficiency.shown || + context.advancements.statistics.hitPoints.shown || + context.advancements.statistics.stress.shown || + context.advancements.statistics.evasion.shown; + + break; + } + + return context; + } +} diff --git a/module/applications/levelup/companionLevelup.mjs b/module/applications/levelup/companionLevelup.mjs new file mode 100644 index 00000000..64cbef82 --- /dev/null +++ b/module/applications/levelup/companionLevelup.mjs @@ -0,0 +1,163 @@ +import BaseLevelUp from './levelup.mjs'; +import { defaultCompanionTier, LevelOptionType } from '../../data/levelTier.mjs'; +import { DhLevelup } from '../../data/levelup.mjs'; +import { diceTypes, range } from '../../config/generalConfig.mjs'; + +export default class DhCompanionLevelUp extends BaseLevelUp { + constructor(actor) { + super(actor); + + this.levelTiers = this.addBonusChoices(defaultCompanionTier); + const playerLevelupData = actor.system.levelData; + this.levelup = new DhLevelup(DhLevelup.initializeData(this.levelTiers, playerLevelupData)); + } + + async _preparePartContext(partId, context) { + await super._preparePartContext(partId, context); + + const currentLevel = this.levelup.levels[this.levelup.currentLevel]; + switch (partId) { + case 'selections': + const advancementChoices = Object.keys(currentLevel.choices).reduce((acc, choiceKey) => { + Object.keys(currentLevel.choices[choiceKey]).forEach(checkboxNr => { + const checkbox = currentLevel.choices[choiceKey][checkboxNr]; + const data = { + ...checkbox, + path: `levels.${this.levelup.currentLevel}.choices.${choiceKey}.${checkboxNr}`, + level: this.levelup.currentLevel + }; + + if (!acc[choiceKey]) acc[choiceKey] = []; + acc[choiceKey].push(data); + }); + + return acc; + }, {}); + + const experienceIncreases = Object.values(advancementChoices.experience ?? {}); + const experienceIncreaseValues = experienceIncreases + .filter(exp => exp.data.length > 0) + .flatMap(exp => + exp.data.map(data => { + const experience = Object.keys(this.actor.system.experiences).find(x => x === data); + return this.actor.system.experiences[experience].name; + }) + ); + context.experienceIncreases = { + values: experienceIncreaseValues, + active: experienceIncreases.length > 0, + progress: { + selected: experienceIncreaseValues.length, + max: experienceIncreases.reduce((acc, exp) => acc + exp.amount, 0) + } + }; + + context.newExperiences = Object.keys(currentLevel.achievements.experiences).map(key => { + const experience = currentLevel.achievements.experiences[key]; + return { + ...experience, + level: this.levelup.currentLevel, + key: key + }; + }); + + context.vicious = advancementChoices.vicious ? Object.values(advancementChoices.vicious) : null; + context.viciousChoices = { + damage: game.i18n.localize('DAGGERHEART.Application.LevelUp.Selections.viciousDamage'), + range: game.i18n.localize('DAGGERHEART.Application.LevelUp.Selections.viciousRange') + }; + + break; + case 'summary': + const levelKeys = Object.keys(this.levelup.levels); + const actorDamageDice = this.actor.system.attack.damage.parts[0].value.dice; + const actorRange = this.actor.system.attack.range; + const advancement = {}; + for (var levelKey of levelKeys) { + const level = this.levelup.levels[levelKey]; + if (Number(levelKey) < this.levelup.startLevel) continue; + + for (var choiceKey of Object.keys(level.choices)) { + const choice = level.choices[choiceKey]; + for (var checkbox of Object.values(choice)) { + switch (choiceKey) { + case 'stress': + case 'evasion': + advancement[choiceKey] = advancement[choiceKey] + ? advancement[choiceKey] + Number(checkbox.value) + : Number(checkbox.value); + break; + case 'experience': + if (!advancement[choiceKey]) advancement[choiceKey] = []; + const data = checkbox.data.map(data => { + const experience = Object.keys(this.actor.system.experiences).find( + x => x === data + ); + return this.actor.system.experiences[experience]?.name ?? ''; + }); + advancement[choiceKey].push({ data: data, value: checkbox.value }); + break; + case 'vicious': + if (!advancement[choiceKey]) advancement[choiceKey] = { damage: null, range: null }; + const isDamage = checkbox.data[0] === 'damage'; + const options = isDamage ? diceTypes : range; + const keys = Object.keys(options); + const actorKey = keys.indexOf(isDamage ? actorDamageDice : actorRange); + const currentIndex = advancement[choiceKey][checkbox.data[0]] + ? keys.indexOf(advancement[choiceKey][checkbox.data[0]]) + : actorKey; + advancement[choiceKey][checkbox.data[0]] = + options[keys[Math.min(currentIndex + 1, keys.length - 1)]]; + default: + if (!advancement.simple) advancement.simple = {}; + advancement.simple[choiceKey] = game.i18n.localize( + LevelOptionType[checkbox.type].label + ); + break; + } + } + } + } + + context.advancements = { + statistics: { + stress: { + old: this.actor.system.resources.stress.maxTotal, + new: this.actor.system.resources.stress.maxTotal + (advancement.stress ?? 0) + }, + evasion: { + old: this.actor.system.evasion.total, + new: this.actor.system.evasion.total + (advancement.evasion ?? 0) + } + }, + experiences: + advancement.experience?.flatMap(x => x.data.map(data => ({ name: data, modifier: x.value }))) ?? + [], + vicious: { + damage: advancement.vicious?.damage + ? { + old: actorDamageDice, + new: advancement.vicious.damage + } + : null, + range: advancement.vicious?.range + ? { + old: game.i18n.localize(`DAGGERHEART.Range.${actorRange}.name`), + new: game.i18n.localize(advancement.vicious.range.label) + } + : null + }, + simple: advancement.simple ?? {} + }; + + context.advancements.statistics.stress.shown = + context.advancements.statistics.stress.new > context.advancements.statistics.stress.old; + context.advancements.statistics.evasion.shown = + context.advancements.statistics.evasion.new > context.advancements.statistics.evasion.old; + context.advancements.statistics.shown = + context.advancements.statistics.stress.shown || context.advancements.statistics.evasion.shown; + } + + return context; + } +} diff --git a/module/applications/levelup.mjs b/module/applications/levelup/levelup.mjs similarity index 71% rename from module/applications/levelup.mjs rename to module/applications/levelup/levelup.mjs index 3094c5bb..02291514 100644 --- a/module/applications/levelup.mjs +++ b/module/applications/levelup/levelup.mjs @@ -1,7 +1,6 @@ -import { abilities, subclassFeatureLabels } from '../config/actorConfig.mjs'; -import { domains } from '../config/domainConfig.mjs'; -import { DhLevelup } from '../data/levelup.mjs'; -import { getDeleteKeys, tagifyElement } from '../helpers/utils.mjs'; +import { abilities, subclassFeatureLabels } from '../../config/actorConfig.mjs'; +import { domains } from '../../config/domainConfig.mjs'; +import { getDeleteKeys, tagifyElement } from '../../helpers/utils.mjs'; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; @@ -10,10 +9,6 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) super({}); this.actor = actor; - this.levelTiers = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.LevelTiers); - - const playerLevelupData = actor.system.levelData; - this.levelup = new DhLevelup(DhLevelup.initializeData(this.levelTiers, playerLevelupData, actor.system.level)); this._dragDrop = this._createDragDropHandlers(); this.tabGroups.primary = 'advancements'; @@ -81,6 +76,21 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) } }; + addBonusChoices(levelTiers) { + for (var tierKey in levelTiers.tiers) { + const tier = levelTiers.tiers[tierKey]; + tier.maxSelections = [...Array(tier.levels.end - tier.levels.start + 1).keys()].reduce((acc, index) => { + const level = tier.levels.start + index; + const bonus = this.actor.system.levelData.level.bonuses[level]; + acc[level] = tier.availableOptions + (bonus ?? 0); + + return acc; + }, {}); + } + + return levelTiers; + } + async _prepareContext(_options) { const context = await super._prepareContext(_options); context.levelup = this.levelup; @@ -118,181 +128,6 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) context.tabs.advancements.progress = { selected: selections, max: currentLevel.maxSelections }; context.showTabs = this.tabGroups.primary !== 'summary'; break; - case 'selections': - const advancementChoices = Object.keys(currentLevel.choices).reduce((acc, choiceKey) => { - Object.keys(currentLevel.choices[choiceKey]).forEach(checkboxNr => { - const checkbox = currentLevel.choices[choiceKey][checkboxNr]; - const data = { - ...checkbox, - path: `levels.${this.levelup.currentLevel}.choices.${choiceKey}.${checkboxNr}`, - level: this.levelup.currentLevel - }; - - if (!acc[choiceKey]) acc[choiceKey] = []; - acc[choiceKey].push(data); - }); - - return acc; - }, {}); - - const traits = Object.values(advancementChoices.trait ?? {}); - const traitValues = traits.filter(trait => trait.data.length > 0).flatMap(trait => trait.data); - context.traits = { - values: traitValues, - active: traits.length > 0, - progress: { - selected: traitValues.length, - max: traits.reduce((acc, exp) => acc + exp.amount, 0) - } - }; - - const experienceIncreases = Object.values(advancementChoices.experience ?? {}); - const experienceIncreaseValues = experienceIncreases - .filter(exp => exp.data.length > 0) - .flatMap(exp => - 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, - active: experienceIncreases.length > 0, - progress: { - selected: experienceIncreaseValues.length, - max: experienceIncreases.reduce((acc, exp) => acc + exp.amount, 0) - } - }; - - context.newExperiences = Object.keys(currentLevel.achievements.experiences).map(key => { - const experience = currentLevel.achievements.experiences[key]; - return { - ...experience, - level: this.levelup.currentLevel, - key: key - }; - }); - - const allDomainCards = { - ...advancementChoices.domainCard, - ...currentLevel.achievements.domainCards - }; - const allDomainCardKeys = Object.keys(allDomainCards); - - const classDomainsData = this.actor.system.class.value.system.domains.map(domain => ({ - domain, - multiclass: false - })); - const multiclassDomainsData = (this.actor.system.multiclass?.value?.system?.domains ?? []).map( - domain => ({ domain, multiclass: true }) - ); - const domainsData = [...classDomainsData, ...multiclassDomainsData]; - const multiclassDomain = this.levelup.classUpgradeChoices?.multiclass?.domain; - if (multiclassDomain) { - if (!domainsData.some(x => x.domain === multiclassDomain)) - domainsData.push({ domain: multiclassDomain, multiclass: true }); - } - - context.domainCards = []; - for (var key of allDomainCardKeys) { - const domainCard = allDomainCards[key]; - if (domainCard.level > this.levelup.endLevel) continue; - - const uuid = domainCard.data?.length > 0 ? domainCard.data[0] : domainCard.uuid; - const card = uuid ? await foundry.utils.fromUuid(uuid) : {}; - - context.domainCards.push({ - ...(card.toObject?.() ?? card), - emptySubtexts: domainsData.map(domain => { - const levelBase = domain.multiclass - ? Math.ceil(this.levelup.currentLevel / 2) - : this.levelup.currentLevel; - const levelMax = domainCard.secondaryData?.limit - ? Math.min(domainCard.secondaryData.limit, levelBase) - : levelBase; - - return game.i18n.format('DAGGERHEART.Application.LevelUp.Selections.emptyDomainCardHint', { - domain: game.i18n.localize(domains[domain.domain].label), - level: levelMax - }); - }), - path: domainCard.data - ? `${domainCard.path}.data` - : `levels.${domainCard.level}.achievements.domainCards.${key}.uuid`, - limit: domainCard.secondaryData?.limit ?? null, - compendium: 'domains' - }); - } - - const subclassSelections = advancementChoices.subclass?.flatMap(x => x.data) ?? []; - const possibleSubclasses = [this.actor.system.class.subclass]; - if (this.actor.system.multiclass?.subclass) { - possibleSubclasses.push(this.actor.system.multiclass.subclass); - } - - context.subclassCards = []; - if (advancementChoices.subclass?.length > 0) { - const featureStateIncrease = Object.values(this.levelup.levels).reduce((acc, level) => { - acc += Object.values(level.choices).filter(choice => { - return Object.values(choice).every(checkbox => checkbox.type === 'subclass'); - }).length; - return acc; - }, 0); - - for (var subclass of possibleSubclasses) { - const choice = - advancementChoices.subclass.find(x => x.data[0] === subclass.uuid) ?? - advancementChoices.subclass.find(x => x.data.length === 0); - const featureState = subclass.system.featureState + featureStateIncrease; - const data = await foundry.utils.fromUuid(subclass.uuid); - context.subclassCards.push({ - ...data.toObject(), - path: choice?.path, - uuid: data.uuid, - selected: subclassSelections.includes(subclass.uuid), - featureState: featureState, - featureLabel: game.i18n.localize(subclassFeatureLabels[featureState]), - isMulticlass: subclass.system.isMulticlass ? 'true' : 'false' - }); - } - } - - const multiclasses = Object.values(advancementChoices.multiclass ?? {}); - if (multiclasses?.[0]) { - const data = multiclasses[0]; - const multiclass = data.data.length > 0 ? await foundry.utils.fromUuid(data.data[0]) : {}; - - context.multiclass = { - ...data, - ...(multiclass.toObject?.() ?? multiclass), - uuid: multiclass.uuid, - domains: - multiclass?.system?.domains.map(key => { - const domain = domains[key]; - const alreadySelected = this.actor.system.class.value.system.domains.includes(key); - - return { - ...domain, - selected: key === data.secondaryData.domain, - disabled: - (data.secondaryData.domain && key !== data.secondaryData.domain) || - alreadySelected - }; - }) ?? [], - subclasses: - multiclass?.system?.subclasses.map(subclass => ({ - ...subclass, - uuid: subclass.uuid, - selected: data.secondaryData.subclass === subclass.uuid, - disabled: data.secondaryData.subclass && data.secondaryData.subclass !== subclass.uuid - })) ?? [], - compendium: 'classes', - limit: 1 - }; - } - - break; - case 'summary': const { current: currentActorLevel, changed: changedActorLevel } = this.actor.system.levelData.level; const actorArmor = this.actor.system.armor; const levelKeys = Object.keys(this.levelup.levels); @@ -516,7 +351,7 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) experienceIncreaseTagify, Object.keys(this.actor.system.experiences).reduce((acc, id) => { const experience = this.actor.system.experiences[id]; - acc[id] = { label: experience.description }; + acc[id] = { label: experience.name }; return acc; }, {}), @@ -594,20 +429,20 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) return; } - if ( - Object.values(this.levelup.levels).some(level => { - const achievementExists = Object.values(level.achievements.domainCards).some( - card => card.uuid === item.uuid - ); - const advancementExists = Object.keys(level.choices).some(choiceKey => { - if (choiceKey !== 'domainCard') return false; - const choice = level.choices[choiceKey]; - return Object.values(choice).some(checkbox => checkbox.data.includes(item.uuid)); - }); + const cardExistsInCharacter = this.actor.items.find(x => x.name === item.name); // Any other way to check? The item is a copy so different ids + const cardExistsInLevelup = Object.values(this.levelup.levels).some(level => { + const achievementExists = Object.values(level.achievements.domainCards).some( + card => card.uuid === item.uuid + ); + const advancementExists = Object.keys(level.choices).some(choiceKey => { + if (choiceKey !== 'domainCard') return false; + const choice = level.choices[choiceKey]; + return Object.values(choice).some(checkbox => checkbox.data.includes(item.uuid)); + }); - return achievementExists || advancementExists; - }) - ) { + return achievementExists || advancementExists; + }); + if (cardExistsInCharacter || cardExistsInLevelup) { ui.notifications.error( game.i18n.localize('DAGGERHEART.Application.LevelUp.notifications.error.domainCardDuplicate') ); diff --git a/module/applications/roll.mjs b/module/applications/roll.mjs index 51a02dc3..eaf4747b 100644 --- a/module/applications/roll.mjs +++ b/module/applications/roll.mjs @@ -29,7 +29,7 @@ export class DHRoll extends Roll { for (const hook of config.hooks) { if (Hooks.call(`${SYSTEM.id}.preRoll${hook.capitalize()}`, config, message) === false) return null; } - + this.applyKeybindings(config); let roll = new this(config.roll.formula, config.data, config); @@ -39,7 +39,7 @@ export class DHRoll extends Roll { const configDialog = await DialogClass.configure(roll, config, message); if (!configDialog) return; } - + for (const hook of config.hooks) { if (Hooks.call(`${SYSTEM.id}.post${hook.capitalize()}RollConfiguration`, roll, config, message) === false) return []; @@ -66,7 +66,7 @@ export class DHRoll extends Roll { } static postEvaluate(roll, config = {}) { - if(!config.roll) config.roll = {}; + if (!config.roll) config.roll = {}; config.roll.total = roll.total; config.roll.formula = roll.formula; config.roll.dice = []; @@ -98,7 +98,7 @@ export class DHRoll extends Roll { constructFormula(config) { // const formula = Roll.replaceFormulaData(this.options.roll.formula, config.data); - this.terms = Roll.parse(this.options.roll.formula, config.data) + this.terms = Roll.parse(this.options.roll.formula, config.data); return (this._formula = this.constructor.getFormula(this.terms)); } } @@ -195,7 +195,7 @@ export class D20Roll extends DHRoll { this.applyAdvantage(); // this.options.roll.modifiers = []; this.applyBaseBonus(); - + this.options.experiences?.forEach(m => { if (this.options.data.experiences?.[m]) this.options.roll.modifiers.push({ @@ -225,10 +225,12 @@ export class D20Roll extends DHRoll { } applyBaseBonus() { - this.options.roll.modifiers = [{ - label : 'Bonus to Hit', - value: Roll.replaceFormulaData('@attackBonus', this.data) - }]; + this.options.roll.modifiers = [ + { + label: 'Bonus to Hit', + value: Roll.replaceFormulaData('@attackBonus', this.data) + } + ]; } static postEvaluate(roll, config = {}) { @@ -238,7 +240,8 @@ export class D20Roll extends DHRoll { const difficulty = config.roll.difficulty ?? target.difficulty ?? target.evasion; target.hit = this.isCritical || roll.total >= difficulty; }); - } else if (config.roll.difficulty) config.roll.success = roll.isCritical || roll.total >= config.roll.difficulty; + } else if (config.roll.difficulty) + config.roll.success = roll.isCritical || roll.total >= config.roll.difficulty; config.roll.advantage = { type: config.advantage, dice: roll.dAdvantage?.denomination, @@ -350,7 +353,7 @@ export class DualityRoll extends D20Roll { bardRallyFaces = this.hasBarRally, advDie = new foundry.dice.terms.Die({ faces: dieFaces }); if (this.hasAdvantage || this.hasDisadvantage || bardRallyFaces) - this.terms.push(new foundry.dice.terms.OperatorTerm({ operator: (this.hasDisadvantage ? '-' : '+') })); + this.terms.push(new foundry.dice.terms.OperatorTerm({ operator: this.hasDisadvantage ? '-' : '+' })); if (bardRallyFaces) { const rallyDie = new foundry.dice.terms.Die({ faces: bardRallyFaces }); if (this.hasAdvantage) { @@ -367,10 +370,12 @@ export class DualityRoll extends D20Roll { } applyBaseBonus() { - this.options.roll.modifiers = [{ - label : `DAGGERHEART.Abilities.${this.options.roll.trait}.name`, - value: Roll.replaceFormulaData(`@traits.${this.options.roll.trait}.total`, this.data) - }]; + this.options.roll.modifiers = [ + { + label: `DAGGERHEART.Abilities.${this.options.roll.trait}.name`, + value: Roll.replaceFormulaData(`@traits.${this.options.roll.trait}.total`, this.data) + } + ]; } static postEvaluate(roll, config = {}) { @@ -388,7 +393,7 @@ export class DualityRoll extends D20Roll { total: roll.dHope.total + roll.dFear.total, label: roll.totalLabel }; - console.log(roll, config) + console.log(roll, config); } } diff --git a/module/applications/settings/components/settingsActionsView.mjs b/module/applications/settings/components/settingsActionsView.mjs index 9b223ec5..ff0f0286 100644 --- a/module/applications/settings/components/settingsActionsView.mjs +++ b/module/applications/settings/components/settingsActionsView.mjs @@ -81,7 +81,7 @@ export default class DhSettingsActionView extends HandlebarsApplicationMixin(App } static onEditImage() { - const fp = new FilePicker({ + const fp = new foundry.applications.apps.FilePicker.implementation({ current: this.img, type: 'image', callback: async path => { diff --git a/module/applications/sheets/api/base-item.mjs b/module/applications/sheets/api/base-item.mjs index 1af01b61..1299b208 100644 --- a/module/applications/sheets/api/base-item.mjs +++ b/module/applications/sheets/api/base-item.mjs @@ -46,19 +46,18 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) { switch (partId) { case 'description': - const value = foundry.utils.getProperty(this.document, "system.description") ?? ""; + const value = foundry.utils.getProperty(this.document, 'system.description') ?? ''; context.enrichedDescription = await TextEditor.enrichHTML(value, { relativeTo: this.item, rollData: this.item.getRollData(), secrets: this.item.isOwner - }) + }); break; } return context; } - /* -------------------------------------------- */ /* Application Clicks Actions */ /* -------------------------------------------- */ @@ -70,11 +69,11 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) { */ static async selectActionType() { const content = await foundry.applications.handlebars.renderTemplate( - 'systems/daggerheart/templates/views/actionType.hbs', - { types: SYSTEM.ACTIONS.actionTypes } - ), - title = 'Select Action Type' - + 'systems/daggerheart/templates/views/actionType.hbs', + { types: SYSTEM.ACTIONS.actionTypes } + ), + title = 'Select Action Type'; + return foundry.applications.api.DialogV2.prompt({ window: { title }, content, @@ -92,7 +91,7 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) { */ static async #addAction(_event, _button) { const actionType = await DHBaseItemSheet.selectActionType(); - if(!actionType) return; + if (!actionType) return; try { const cls = actionsTypes[actionType] ?? actionsTypes.attack, action = new cls( @@ -134,9 +133,7 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) { event.stopPropagation(); const actionIndex = button.closest('[data-index]').dataset.index; await this.document.update({ - 'system.actions': this.document.system.actions.filter( - (_, index) => index !== Number.parseInt(actionIndex) - ) + 'system.actions': this.document.system.actions.filter((_, index) => index !== Number.parseInt(actionIndex)) }); } } diff --git a/module/applications/sheets/character.mjs b/module/applications/sheets/character.mjs index ad294aff..a8ed09c1 100644 --- a/module/applications/sheets/character.mjs +++ b/module/applications/sheets/character.mjs @@ -4,7 +4,7 @@ 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 DhCharacterlevelUp from '../levelup/characterLevelup.mjs'; import DhCharacterCreation from '../characterCreation.mjs'; const { ActorSheetV2 } = foundry.applications.sheets; @@ -303,8 +303,11 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { } getItem(element) { - const itemId = (element.target ?? element).closest('[data-item-id]').dataset.itemId, - item = this.document.items.get(itemId); + const listElement = (element.target ?? element).closest('[data-item-id]'); + const document = listElement.dataset.companion ? this.document.system.companion : this.document; + + const itemId = listElement.dataset.itemId, + item = document.items.get(itemId); return item; } @@ -313,7 +316,7 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { } static _onEditImage() { - const fp = new FilePicker({ + const fp = new foundry.applications.apps.FilePicker.implementation({ current: this.document.img, type: 'image', redirectToRoot: ['icons/svg/mystery-man.svg'], @@ -328,25 +331,8 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { const context = await super._prepareContext(_options); context.document = this.document; context.tabs = super._getTabs(this.constructor.TABS); - context.config = SYSTEM; - const selectedAttributes = Object.values(this.document.system.traits).map(x => x.base); - context.abilityScoreArray = await game.settings - .get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Homebrew) - .traitArray.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], @@ -357,67 +343,7 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { 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, - listView: game.user.getFlag(SYSTEM.id, SYSTEM.FLAGS.displayDomainCardsAsList) - }, - 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') - }, currency: { title: game.i18n.localize('DAGGERHEART.Sheets.PC.Gold.Title'), coins: game.i18n.localize('DAGGERHEART.Sheets.PC.Gold.Coins'), @@ -536,28 +462,6 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { } } - /* -------------------------------------------- */ - - 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 abilityLabel = game.i18n.localize(abilities[button.dataset.attribute].label); const config = { @@ -643,7 +547,7 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { return; } - new DhlevelUp(this.document).render(true); + new DhCharacterlevelUp(this.document).render(true); } static async useDomainCard(event, button) { @@ -709,15 +613,22 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { static async useItem(event, button) { const item = this.getItem(button); if (!item) return; - const wasUsed = await item.use(event); - if (wasUsed && item.type === 'weapon') { - Hooks.callAll(SYSTEM.HOOKS.characterAttack, {}); + + // Should dandle its actions. Or maybe they'll be separate buttons as per an Issue on the board + if (item.type === 'feature') { + item.toChat(); + } else { + const wasUsed = await item.use(event); + if (wasUsed && item.type === 'weapon') { + Hooks.callAll(SYSTEM.HOOKS.characterAttack, {}); + } } } - static async viewObject(event, button) { + static async viewObject(event) { const item = this.getItem(event); if (!item) return; + item.sheet.render(true); } @@ -771,9 +682,10 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { this.render(); } - static async deleteItem(event, button) { + static async deleteItem(event) { const item = this.getItem(event); if (!item) return; + await item.delete(); } diff --git a/module/applications/sheets/companion.mjs b/module/applications/sheets/companion.mjs new file mode 100644 index 00000000..46080814 --- /dev/null +++ b/module/applications/sheets/companion.mjs @@ -0,0 +1,86 @@ +import { GMUpdateEvent, socketEvent } from '../../helpers/socket.mjs'; +import DhCompanionlevelUp from '../levelup/companionLevelup.mjs'; +import DaggerheartSheet from './daggerheart-sheet.mjs'; + +const { ActorSheetV2 } = foundry.applications.sheets; +export default class DhCompanionSheet extends DaggerheartSheet(ActorSheetV2) { + static DEFAULT_OPTIONS = { + tag: 'form', + classes: ['daggerheart', 'sheet', 'actor', 'dh-style', 'companion'], + position: { width: 700, height: 1000 }, + actions: { + attackRoll: this.attackRoll, + levelUp: this.levelUp + }, + form: { + handler: this.updateForm, + submitOnChange: true, + closeOnSubmit: false + } + }; + + static PARTS = { + sidebar: { template: 'systems/daggerheart/templates/sheets/actors/companion/tempMain.hbs' } + }; + + _attachPartListeners(partId, htmlElement, options) { + super._attachPartListeners(partId, htmlElement, options); + + htmlElement.querySelector('.partner-value')?.addEventListener('change', this.onPartnerChange.bind(this)); + } + + async _prepareContext(_options) { + const context = await super._prepareContext(_options); + context.document = this.document; + context.playerCharacters = game.actors + .filter( + x => + x.type === 'character' && + (x.ownership.default === 3 || + x.ownership[game.user.id] === 3 || + this.document.system.partner?.uuid === x.uuid) + ) + .map(x => ({ key: x.uuid, name: x.name })); + + return context; + } + + static async updateForm(event, _, formData) { + await this.document.update(formData.object); + this.render(); + } + + async onPartnerChange(event) { + const partnerDocument = event.target.value + ? await foundry.utils.fromUuid(event.target.value) + : this.document.system.partner; + const partnerUpdate = { 'system.companion': event.target.value ? this.document.uuid : null }; + + if (!partnerDocument.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER)) { + await game.socket.emit(`system.${SYSTEM.id}`, { + action: socketEvent.GMUpdate, + data: { + action: GMUpdateEvent.UpdateDocument, + uuid: partnerDocument.uuid, + update: update + } + }); + } else { + await partnerDocument.update(partnerUpdate); + } + + await this.document.update({ 'system.partner': event.target.value }); + + if (!event.target.value) { + await this.document.updateLevel(1); + } + } + + static async attackRoll(event) { + this.actor.system.attack.use(event); + } + + static async levelUp() { + new DhCompanionlevelUp(this.document).render(true); + } +} diff --git a/module/applications/sheets/daggerheart-sheet.mjs b/module/applications/sheets/daggerheart-sheet.mjs index 4810b0a7..821972d9 100644 --- a/module/applications/sheets/daggerheart-sheet.mjs +++ b/module/applications/sheets/daggerheart-sheet.mjs @@ -38,7 +38,7 @@ export default function DhpApplicationMixin(Base) { const attr = target.dataset.edit; const current = foundry.utils.getProperty(this.document, attr); const { img } = this.document.constructor.getDefaultArtwork?.(this.document.toObject()) ?? {}; - const fp = new FilePicker({ + const fp = new foundry.applications.apps.FilePicker.implementation({ current, type: 'image', redirectToRoot: img ? [img] : [], diff --git a/module/applications/sheets/items/feature.mjs b/module/applications/sheets/items/feature.mjs index 655e4542..a5b8bd8f 100644 --- a/module/applications/sheets/items/feature.mjs +++ b/module/applications/sheets/items/feature.mjs @@ -6,11 +6,7 @@ export default class FeatureSheet extends DHBaseItemSheet { id: 'daggerheart-feature', classes: ['feature'], position: { height: 600 }, - window: { resizable: true }, - actions: { - addEffect: this.addEffect, - removeEffect: this.removeEffect - } + window: { resizable: true } }; /**@override */ @@ -22,27 +18,16 @@ export default class FeatureSheet extends DHBaseItemSheet { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-actions.hbs', scrollable: ['.actions'] }, - settings: { - template: 'systems/daggerheart/templates/sheets/items/feature/settings.hbs', - scrollable: ['.settings'] - }, effects: { - template: 'systems/daggerheart/templates/sheets/items/feature/effects.hbs', + template: 'systems/daggerheart/templates/sheets/global/tabs/tab-effects.hbs', scrollable: ['.effects'] } }; - /** - * Internally tracks the selected effect type from the select. - * @type {String} - * @private - */ - _selectedEffectType; - /**@override */ static TABS = { primary: { - tabs: [{ id: 'description' }, { id: 'actions' }, { id: 'settings' }, { id: 'effects' }], + tabs: [{ id: 'description' }, { id: 'actions' }, { id: 'effects' }], initial: 'description', labelPrefix: 'DAGGERHEART.Sheets.TABS' } @@ -50,68 +35,10 @@ export default class FeatureSheet extends DHBaseItemSheet { /* -------------------------------------------- */ - /**@inheritdoc*/ - _attachPartListeners(partId, htmlElement, options) { - super._attachPartListeners(partId, htmlElement, options); - if (partId === 'effects') - htmlElement.querySelector('.effect-select')?.addEventListener('change', this._effectSelect.bind(this)); - } - - /** - * Handles selection of a new effect type. - * @param {Event} event - Change Event - */ - _effectSelect(event) { - const value = event.currentTarget.value; - this._selectedEffectType = value; - this.render({ parts: ['effects'] }); - } - - /* -------------------------------------------- */ - /**@inheritdoc */ async _prepareContext(_options) { const context = await super._prepareContext(_options); - context.properties = CONFIG.daggerheart.ACTOR.featureProperties; - context.dice = CONFIG.daggerheart.GENERAL.diceTypes; - context.effectConfig = CONFIG.daggerheart.EFFECTS; - - context.selectedEffectType = this._selectedEffectType; return context; } - - /* -------------------------------------------- */ - /* Application Clicks Actions */ - /* -------------------------------------------- */ - - /** - * Adds a new effect to the item, based on the selected effect type. - * @param {PointerEvent} _event - The originating click event - * @param {HTMLElement} _target - The capturing HTML element which defines the [data-action] - * @returns - */ - static async addEffect(_event, _target) { - const type = this._selectedEffectType; - if (!type) return; - const { id, name, ...rest } = CONFIG.daggerheart.EFFECTS.effectTypes[type]; - await this.item.update({ - [`system.effects.${foundry.utils.randomID()}`]: { - type, - value: '', - ...rest - } - }); - } - - /** - * Removes an effect from the item. - * @param {PointerEvent} _event - The originating click event - * @param {HTMLElement} target - The capturing HTML element which defines the [data-action] - * @returns - */ - static async removeEffect(_event, target) { - const path = `system.effects.-=${target.dataset.effect}`; - await this.item.update({ [path]: null }); - } } diff --git a/module/config/actionConfig.mjs b/module/config/actionConfig.mjs index 91004bdd..5dca28c2 100644 --- a/module/config/actionConfig.mjs +++ b/module/config/actionConfig.mjs @@ -76,7 +76,7 @@ export const damageOnSave = { label: 'Full damage', mod: 1 } -} +}; export const diceCompare = { below: { @@ -104,4 +104,4 @@ export const diceCompare = { label: 'Above', operator: '>' } -} +}; diff --git a/module/config/generalConfig.mjs b/module/config/generalConfig.mjs index a6484b61..28cfc576 100644 --- a/module/config/generalConfig.mjs +++ b/module/config/generalConfig.mjs @@ -261,48 +261,6 @@ export const tiers = { } }; -export const objectTypes = { - character: { - name: 'TYPES.Actor.character' - }, - npc: { - name: 'TYPES.Actor.npc' - }, - adversary: { - name: 'TYPES.Actor.adversary' - }, - ancestry: { - name: 'TYPES.Item.ancestry' - }, - community: { - name: 'TYPES.Item.community' - }, - class: { - name: 'TYPES.Item.class' - }, - subclass: { - name: 'TYPES.Item.subclass' - }, - feature: { - name: 'TYPES.Item.feature' - }, - domainCard: { - name: 'TYPES.Item.domainCard' - }, - consumable: { - name: 'TYPES.Item.consumable' - }, - miscellaneous: { - name: 'TYPES.Item.miscellaneous' - }, - weapon: { - name: 'TYPES.Item.weapon' - }, - armor: { - name: 'TYPES.Item.armor' - } -}; - export const diceTypes = { d4: 'd4', d6: 'd6', @@ -325,7 +283,7 @@ export const diceSetNumbers = { cast: 'Spellcast', scale: 'Cost Scaling', flat: 'Flat' -} +}; export const getDiceSoNicePresets = () => { const { diceSoNice } = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.appearance); diff --git a/module/data/action/action.mjs b/module/data/action/action.mjs index ee9bbabe..da776df8 100644 --- a/module/data/action/action.mjs +++ b/module/data/action/action.mjs @@ -73,7 +73,10 @@ export class DHBaseAction extends foundry.abstract.DataModel { save: new fields.SchemaField({ trait: new fields.StringField({ nullable: true, initial: null, choices: SYSTEM.ACTOR.abilities }), difficulty: new fields.NumberField({ nullable: true, initial: 10, integer: true, min: 0 }), - damageMod: new fields.StringField({ initial: SYSTEM.ACTIONS.damageOnSave.none.id, choices: SYSTEM.ACTIONS.damageOnSave }) + damageMod: new fields.StringField({ + initial: SYSTEM.ACTIONS.damageOnSave.none.id, + choices: SYSTEM.ACTIONS.damageOnSave + }) }), target: new fields.SchemaField({ type: new fields.StringField({ @@ -98,9 +101,12 @@ export class DHBaseAction extends foundry.abstract.DataModel { initial: SYSTEM.GENERAL.healingTypes.hitPoints.id, label: 'Healing' }), - resultBased: new fields.BooleanField({ initial: false, label: "DAGGERHEART.Actions.Settings.ResultBased.label" }), + resultBased: new fields.BooleanField({ + initial: false, + label: 'DAGGERHEART.Actions.Settings.ResultBased.label' + }), value: new fields.EmbeddedDataField(DHActionDiceData), - valueAlt: new fields.EmbeddedDataField(DHActionDiceData), + valueAlt: new fields.EmbeddedDataField(DHActionDiceData) }) }, extraSchemas = {}; @@ -124,7 +130,11 @@ export class DHBaseAction extends foundry.abstract.DataModel { } get actor() { - return this.item instanceof DhpActor ? this.item : this.item?.actor; + return this.item instanceof DhpActor + ? this.item + : this.item?.parent instanceof DhpActor + ? this.item.parent + : this.item?.actor; } get chatTemplate() { @@ -153,7 +163,7 @@ export class DHBaseAction extends foundry.abstract.DataModel { return updateSource; } - getRollData(data={}) { + getRollData(data = {}) { const actorData = this.actor.getRollData(false); // Remove when included directly in Actor getRollData @@ -166,11 +176,11 @@ export class DHBaseAction extends foundry.abstract.DataModel { return a; }, {}) : 1; */ - actorData.scale = data.costs?.length // Right now only return the first scalable cost. - ? (data.costs.find(c => c.scalable)?.total ?? 1) - : 1; + actorData.scale = data.costs?.length // Right now only return the first scalable cost. + ? (data.costs.find(c => c.scalable)?.total ?? 1) + : 1; actorData.roll = {}; - + return actorData; } @@ -191,12 +201,14 @@ export class DHBaseAction extends foundry.abstract.DataModel { // Prepare Costs const costsConfig = this.prepareCost(); - if(isFastForward && !this.hasCost(costsConfig)) return ui.notifications.warn("You don't have the resources to use that action."); + if (isFastForward && !this.hasCost(costsConfig)) + return ui.notifications.warn("You don't have the resources to use that action."); // config = this.prepareUseCost(config) // Prepare Uses const usesConfig = this.prepareUse(); - if(isFastForward && !this.hasUses(usesConfig)) return ui.notifications.warn("That action doesn't have remaining uses."); + if (isFastForward && !this.hasUses(usesConfig)) + return ui.notifications.warn("That action doesn't have remaining uses."); // config = this.prepareUseCost(config) // Prepare Roll Data @@ -209,24 +221,24 @@ export class DHBaseAction extends foundry.abstract.DataModel { costs: costsConfig, uses: usesConfig, data: actorData - } - - if ( Hooks.call(`${SYSTEM.id}.preUseAction`, this, config) === false ) return; + }; + + if (Hooks.call(`${SYSTEM.id}.preUseAction`, this, config) === false) return; // Display configuration window if necessary - if ( config.dialog?.configure && this.requireConfigurationDialog(config) ) { + if (config.dialog?.configure && this.requireConfigurationDialog(config)) { config = await D20RollDialog.configure(config); if (!config) return; } - if ( this.hasRoll ) { + if (this.hasRoll) { const rollConfig = this.prepareRoll(config); config.roll = rollConfig; config = await this.actor.diceRoll(config); if (!config) return; } - if( this.hasSave ) { + if (this.hasSave) { /* config.targets.forEach((t) => { if(t.hit) { const target = game.canvas.tokens.get(t.id), @@ -258,16 +270,16 @@ export class DHBaseAction extends foundry.abstract.DataModel { }) */ } - if ( this.doFollowUp() ) { - if(this.rollDamage) await this.rollDamage(event, config); - if(this.rollHealing) await this.rollHealing(event, config); - if(this.trigger) await this.trigger(event, config); + if (this.doFollowUp()) { + if (this.rollDamage) await this.rollDamage(event, config); + if (this.rollHealing) await this.rollHealing(event, config); + if (this.trigger) await this.trigger(event, config); } // Consume resources await this.consume(config); - - if ( Hooks.call(`${SYSTEM.id}.postUseAction`, this, config) === false ) return; + + if (Hooks.call(`${SYSTEM.id}.postUseAction`, this, config) === false) return; return config; } @@ -287,7 +299,7 @@ export class DHBaseAction extends foundry.abstract.DataModel { hasHealing: !!this.healing, hasEffect: !!this.effects?.length, hasSave: this.hasSave - } + }; } requireConfigurationDialog(config) { @@ -317,7 +329,6 @@ export class DHBaseAction extends foundry.abstract.DataModel { } targets = targets.map(t => this.formatTarget(t)); return targets; - } prepareRange() { @@ -326,7 +337,7 @@ export class DHBaseAction extends foundry.abstract.DataModel { } prepareRoll() { - const roll = { + const roll = { modifiers: [], trait: this.roll?.trait, label: 'Attack', @@ -334,8 +345,8 @@ export class DHBaseAction extends foundry.abstract.DataModel { difficulty: this.roll?.difficulty, formula: this.roll.getFormula() }; - if(this.roll?.type === 'diceSet') roll.lite = true; - + if (this.roll?.type === 'diceSet') roll.lite = true; + return roll; } @@ -344,12 +355,14 @@ export class DHBaseAction extends foundry.abstract.DataModel { } async consume(config) { - const resources = config.costs.filter(c => c.enabled !== false).map(c => { - return { type: c.type, value: (c.total ?? c.value) * -1 }; - }); - + const resources = config.costs + .filter(c => c.enabled !== false) + .map(c => { + return { type: c.type, value: (c.total ?? c.value) * -1 }; + }); + await this.actor.modifyResource(resources); - if(config.uses?.enabled) { + if (config.uses?.enabled) { const newActions = foundry.utils.getProperty(this.item.system, this.systemPath).map(x => x.toObject()); newActions[this.index].uses.value++; await this.item.update({ [`system.${this.systemPath}`]: newActions }); @@ -388,13 +401,16 @@ export class DHBaseAction extends foundry.abstract.DataModel { hasCost(costs) { const realCosts = this.getRealCosts(costs); - return realCosts.reduce((a, c) => a && this.actor.system.resources[c.type]?.value >= (c.total ?? c.value), true); + return realCosts.reduce( + (a, c) => a && this.actor.system.resources[c.type]?.value >= (c.total ?? c.value), + true + ); } /* COST */ /* USES */ calcUses(uses) { - if(!uses) return null; + if (!uses) return null; return { ...uses, enabled: uses.hasOwnProperty('enabled') ? uses.enabled : true @@ -402,7 +418,7 @@ export class DHBaseAction extends foundry.abstract.DataModel { } hasUses(uses) { - if(!uses) return true; + if (!uses) return true; return (uses.hasOwnProperty('enabled') && !uses.enabled) || uses.value + 1 <= uses.max; } /* USES */ @@ -432,7 +448,7 @@ export class DHBaseAction extends foundry.abstract.DataModel { /* TARGET */ /* RANGE */ - + /* RANGE */ /* EFFECTS */ @@ -441,10 +457,10 @@ export class DHBaseAction extends foundry.abstract.DataModel { let effects = this.effects; data.system.targets.forEach(async token => { if (!token.hit && !force) return; - if(this.hasSave && token.saved.success === true) { - effects = this.effects.filter(e => e.onSave === true) + if (this.hasSave && token.saved.success === true) { + effects = this.effects.filter(e => e.onSave === true); } - if(!effects.length) return; + if (!effects.length) return; effects.forEach(async e => { const actor = canvas.tokens.get(token.id)?.actor, effect = this.item.effects.get(e._id); @@ -479,35 +495,42 @@ export class DHBaseAction extends foundry.abstract.DataModel { /* SAVE */ async rollSave(target, event, message) { - if(!target?.actor) return; - target.actor.diceRoll({ - event, - title: 'Roll Save', - roll: { - trait: this.save.trait, - difficulty: this.save.difficulty, - type: "reaction" - }, - data: target.actor.getRollData() - }).then(async (result) => { - if(result) this.updateChatMessage(message, target.id, {result: result.roll.total, success: result.roll.success}); - }) + if (!target?.actor) return; + target.actor + .diceRoll({ + event, + title: 'Roll Save', + roll: { + trait: this.save.trait, + difficulty: this.save.difficulty, + type: 'reaction' + }, + data: target.actor.getRollData() + }) + .then(async result => { + if (result) + this.updateChatMessage(message, target.id, { + result: result.roll.total, + success: result.roll.success + }); + }); } - async updateChatMessage(message, targetId, changes, chain=true) { + async updateChatMessage(message, targetId, changes, chain = true) { setTimeout(async () => { const chatMessage = ui.chat.collection.get(message._id), msgTargets = chatMessage.system.targets, msgTarget = msgTargets.find(mt => mt.id === targetId); msgTarget.saved = changes; - await chatMessage.update({'system.targets': msgTargets}); - },100); - if(chain) { - if(message.system.source.message) this.updateChatMessage(ui.chat.collection.get(message.system.source.message), targetId, changes, false); + await chatMessage.update({ 'system.targets': msgTargets }); + }, 100); + if (chain) { + if (message.system.source.message) + this.updateChatMessage(ui.chat.collection.get(message.system.source.message), targetId, changes, false); const relatedChatMessages = ui.chat.collection.filter(c => c.system.source.message === message._id); relatedChatMessages.forEach(c => { this.updateChatMessage(c, targetId, changes, false); - }) + }); } } /* SAVE */ @@ -525,7 +548,7 @@ export class DHDamageAction extends DHBaseAction { getFormulaValue(part, data) { let formulaValue = part.value; - if(this.hasRoll && part.resultBased && data.system.roll.result.duality === -1) return part.valueAlt; + if (this.hasRoll && part.resultBased && data.system.roll.result.duality === -1) return part.valueAlt; return formulaValue; } @@ -535,19 +558,19 @@ export class DHDamageAction extends DHBaseAction { if (!formula || formula == '') return; let roll = { formula: formula, total: formula }, bonusDamage = []; - - if(isNaN(formula)) formula = Roll.replaceFormulaData(formula, this.getRollData(data.system ?? data)); + + if (isNaN(formula)) formula = Roll.replaceFormulaData(formula, this.getRollData(data.system ?? data)); const config = { title: game.i18n.format('DAGGERHEART.Chat.DamageRoll.Title', { damage: this.name }), - roll: {formula}, - targets: (data.system?.targets.filter(t => t.hit) ?? data.targets), + roll: { formula }, + targets: data.system?.targets.filter(t => t.hit) ?? data.targets, hasSave: this.hasSave, source: data.system?.source, event }; - if(this.hasSave) config.onSave = this.save.damageMod; - if(data.system) { + if (this.hasSave) config.onSave = this.save.damageMod; + if (data.system) { config.source.message = data._id; config.directDamage = false; } @@ -597,7 +620,8 @@ export class DHHealingAction extends DHBaseAction { getFormulaValue(data) { let formulaValue = this.healing.value; - if(this.hasRoll && this.healing.resultBased && data.system.roll.result.duality === -1) return this.healing.valueAlt; + if (this.hasRoll && this.healing.resultBased && data.system.roll.result.duality === -1) + return this.healing.valueAlt; return formulaValue; } @@ -608,12 +632,12 @@ export class DHHealingAction extends DHBaseAction { if (!formula || formula == '') return; let roll = { formula: formula, total: formula }, bonusDamage = []; - + const config = { title: game.i18n.format('DAGGERHEART.Chat.HealingRoll.Title', { healing: game.i18n.localize(SYSTEM.GENERAL.healingTypes[this.healing.type].label) }), - roll: {formula}, + roll: { formula }, targets: (data.system?.targets ?? data.targets).filter(t => t.hit), messageType: 'healing', type: this.healing.type, diff --git a/module/data/action/actionDice.mjs b/module/data/action/actionDice.mjs index e422b856..8a6aa12a 100644 --- a/module/data/action/actionDice.mjs +++ b/module/data/action/actionDice.mjs @@ -25,17 +25,20 @@ export class DHActionRollData extends foundry.abstract.DataModel { initial: 'above', label: 'Should be' }), - treshold: new fields.NumberField({ initial: 1, integer: true, min: 1, label: 'Treshold' }), + treshold: new fields.NumberField({ initial: 1, integer: true, min: 1, label: 'Treshold' }) }) }; } getFormula() { - if(!this.type) return; + if (!this.type) return; let formula = ''; switch (this.type) { case 'diceSet': - const multiplier = this.diceRolling.multiplier === 'flat' ? this.diceRolling.flatMultiplier : `@${this.diceRolling.multiplier}`; + const multiplier = + this.diceRolling.multiplier === 'flat' + ? this.diceRolling.flatMultiplier + : `@${this.diceRolling.multiplier}`; formula = `${multiplier}${this.diceRolling.dice}cs${SYSTEM.ACTIONS.diceCompare[this.diceRolling.compare].operator}${this.diceRolling.treshold}`; break; default: @@ -75,9 +78,7 @@ export class DHActionDiceData extends foundry.abstract.DataModel { : `${multiplier ?? 1}${this.dice}${this.bonus ? (this.bonus < 0 ? ` - ${Math.abs(this.bonus)}` : ` + ${this.bonus}`) : ''}`; */ const multiplier = this.multiplier === 'flat' ? this.flatMultiplier : `@${this.multiplier}`, bonus = this.bonus ? (this.bonus < 0 ? ` - ${Math.abs(this.bonus)}` : ` + ${this.bonus}`) : ''; - return this.custom.enabled - ? this.custom.formula - : `${multiplier ?? 1}${this.dice}${bonus}`; + return this.custom.enabled ? this.custom.formula : `${multiplier ?? 1}${this.dice}${bonus}`; } } @@ -105,9 +106,12 @@ export class DHDamageData extends foundry.abstract.DataModel { nullable: false, required: true }), - resultBased: new fields.BooleanField({ initial: false, label: "DAGGERHEART.Actions.Settings.ResultBased.label" }), + resultBased: new fields.BooleanField({ + initial: false, + label: 'DAGGERHEART.Actions.Settings.ResultBased.label' + }), value: new fields.EmbeddedDataField(DHActionDiceData), - valueAlt: new fields.EmbeddedDataField(DHActionDiceData), + valueAlt: new fields.EmbeddedDataField(DHActionDiceData) }; } } diff --git a/module/data/actor/_module.mjs b/module/data/actor/_module.mjs index cdbf7178..c19036eb 100644 --- a/module/data/actor/_module.mjs +++ b/module/data/actor/_module.mjs @@ -1,11 +1,13 @@ import DhCharacter from './character.mjs'; +import DhCompanion from './companion.mjs'; import DhAdversary from './adversary.mjs'; import DhEnvironment from './environment.mjs'; -export { DhCharacter, DhAdversary, DhEnvironment }; +export { DhCharacter, DhCompanion, DhAdversary, DhEnvironment }; export const config = { character: DhCharacter, + companion: DhCompanion, adversary: DhAdversary, environment: DhEnvironment }; diff --git a/module/data/actor/character.mjs b/module/data/actor/character.mjs index fb3466bb..7b1ec939 100644 --- a/module/data/actor/character.mjs +++ b/module/data/actor/character.mjs @@ -1,6 +1,7 @@ import { burden } from '../../config/generalConfig.mjs'; +import ActionField from '../fields/actionField.mjs'; import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs'; -import { LevelOptionType } from '../levelTier.mjs'; +import DhLevelData from '../levelData.mjs'; import BaseDataActor from './base.mjs'; const attributeField = () => @@ -96,7 +97,8 @@ export default class DhCharacter extends BaseDataActor { value: new ForeignDocumentUUIDField({ type: 'Item', nullable: true }), subclass: new ForeignDocumentUUIDField({ type: 'Item', nullable: true }) }), - levelData: new fields.EmbeddedDataField(DhPCLevelData), + actions: new fields.ArrayField(new ActionField()), + levelData: new fields.EmbeddedDataField(DhLevelData), bonuses: new fields.SchemaField({ armorScore: new fields.NumberField({ integer: true, initial: 0 }), damageThresholds: new fields.SchemaField({ @@ -115,6 +117,7 @@ export default class DhCharacter extends BaseDataActor { magic: new fields.NumberField({ integer: true, initial: 0 }) }) }), + companion: new ForeignDocumentUUIDField({ type: 'Actor', nullable: true, initial: null }), rules: new fields.SchemaField({ maxArmorMarked: new fields.SchemaField({ value: new fields.NumberField({ required: true, integer: true, initial: 1 }), @@ -154,10 +157,25 @@ export default class DhCharacter extends BaseDataActor { return this.parent.items.find(x => x.type === 'community') ?? null; } + get features() { + return this.parent.items.filter(x => x.type === 'feature') ?? []; + } + + get companionFeatures() { + return this.companion ? this.companion.items.filter(x => x.type === 'feature') : []; + } + get needsCharacterSetup() { return !this.class.value || !this.class.subclass; } + get spellcastingModifiers() { + return { + main: this.class.subclass?.system?.spellcastingTrait, + multiclass: this.multiclass.subclass?.system?.spellcastingTrait + }; + } + get domains() { const classDomains = this.class.value ? this.class.value.system.domains : []; const multiclassDomains = this.multiclass.value ? this.multiclass.value.system.domains : []; @@ -197,6 +215,12 @@ export default class DhCharacter extends BaseDataActor { : null; } + get deathMoveViable() { + return ( + this.resources.hitPoints.maxTotal > 0 && this.resources.hitPoints.value >= this.resources.hitPoints.maxTotal + ); + } + static async unequipBeforeEquip(itemToEquip) { const primary = this.primaryWeapon, secondary = this.secondaryWeapon; @@ -307,58 +331,10 @@ export default class DhCharacter extends BaseDataActor { level: this.levelData.level.current }; } -} -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.TypedObjectField(new fields.StringField({ required: true })), - itemUuid: new fields.StringField({ required: true }) - }) - ) - }) - ) - }; - } - - get canLevelUp() { - return this.level.current < this.level.changed; + async _preDelete() { + if (this.companion) { + this.companion.updateLevel(1); + } } } diff --git a/module/data/actor/companion.mjs b/module/data/actor/companion.mjs new file mode 100644 index 00000000..abc38e93 --- /dev/null +++ b/module/data/actor/companion.mjs @@ -0,0 +1,139 @@ +import BaseDataActor from './base.mjs'; +import DhLevelData from '../levelData.mjs'; +import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs'; +import ActionField from '../fields/actionField.mjs'; +import { adjustDice, adjustRange } from '../../helpers/utils.mjs'; + +export default class DhCompanion extends BaseDataActor { + static LOCALIZATION_PREFIXES = ['DAGGERHEART.Sheets.Companion']; + + static get metadata() { + return foundry.utils.mergeObject(super.metadata, { + label: 'TYPES.Actor.companion', + type: 'companion' + }); + } + + static defineSchema() { + const fields = foundry.data.fields; + + return { + partner: new ForeignDocumentUUIDField({ type: 'Actor' }), + resources: new fields.SchemaField({ + stress: new fields.SchemaField({ + value: new fields.NumberField({ initial: 0, integer: true }), + bonus: new fields.NumberField({ initial: 0, integer: true }), + max: new fields.NumberField({ initial: 3, integer: true }) + }), + hope: new fields.NumberField({ initial: 0, integer: true }) + }), + evasion: new fields.SchemaField({ + value: new fields.NumberField({ required: true, min: 1, initial: 10, integer: true }), + bonus: new fields.NumberField({ initial: 0, integer: true }) + }), + experiences: new fields.TypedObjectField( + new fields.SchemaField({ + name: new fields.StringField({}), + value: new fields.NumberField({ integer: true, initial: 0 }), + bonus: new fields.NumberField({ integer: true, initial: 0 }) + }), + { + initial: { + experience1: { value: 2 }, + experience2: { value: 2 } + } + } + ), + attack: new ActionField({ + initial: { + name: 'Attack', + _id: foundry.utils.randomID(), + systemPath: 'attack', + type: 'attack', + range: 'melee', + target: { + type: 'any', + amount: 1 + }, + roll: { + type: 'weapon', + bonus: 0 + }, + damage: { + parts: [ + { + multiplier: 'flat', + value: { + dice: 'd6', + multiplier: 'flat' + } + } + ] + } + } + }), + actions: new fields.ArrayField(new ActionField()), + levelData: new fields.EmbeddedDataField(DhLevelData) + }; + } + + get attackBonus() { + return this.attack.roll.bonus ?? 0; + } + + prepareBaseData() { + const partnerSpellcastingModifier = this.partner?.system?.spellcastingModifiers?.main; + const spellcastingModifier = this.partner?.system?.traits?.[partnerSpellcastingModifier]?.total; + this.attack.roll.bonus = spellcastingModifier ?? 0; // Needs to expand on which modifier it is that should be used because of multiclassing; + + for (let levelKey in this.levelData.levelups) { + const level = this.levelData.levelups[levelKey]; + for (let selection of level.selections) { + switch (selection.type) { + case 'hope': + this.resources.hope += selection.value; + break; + case 'vicious': + if (selection.data[0] === 'damage') { + this.attack.damage.parts[0].value.dice = adjustDice(this.attack.damage.parts[0].value.dice); + } else { + this.attack.range = adjustRange(this.attack.range); + } + break; + case 'stress': + this.resources.stress.bonus += selection.value; + break; + case 'evasion': + this.evasion.bonus += selection.value; + break; + case 'experience': + Object.keys(this.experiences).forEach(key => { + const experience = this.experiences[key]; + experience.bonus += selection.value; + }); + break; + } + } + } + } + + prepareDerivedData() { + for (var experienceKey in this.experiences) { + var experience = this.experiences[experienceKey]; + experience.total = experience.value + experience.bonus; + } + + if (this.partner) { + this.partner.system.resources.hope.max += this.resources.hope; + } + + this.resources.stress.maxTotal = this.resources.stress.max + this.resources.stress.bonus; + this.evasion.total = this.evasion.value + this.evasion.bonus; + } + + async _preDelete() { + if (this.partner) { + await this.partner.update({ 'system.companion': null }); + } + } +} diff --git a/module/data/item/armor.mjs b/module/data/item/armor.mjs index ffd00a23..9c19fb80 100644 --- a/module/data/item/armor.mjs +++ b/module/data/item/armor.mjs @@ -10,7 +10,7 @@ export default class DHArmor extends BaseDataItem { type: 'armor', hasDescription: true, isQuantifiable: true, - isInventoryItem: true, + isInventoryItem: true }); } diff --git a/module/data/item/base.mjs b/module/data/item/base.mjs index 0df64a3e..735c6588 100644 --- a/module/data/item/base.mjs +++ b/module/data/item/base.mjs @@ -20,7 +20,7 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel { type: 'base', hasDescription: false, isQuantifiable: false, - isInventoryItem: false, + isInventoryItem: false }; } @@ -54,9 +54,9 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel { const data = { ...actorRollData, item: { ...this } }; return data; } - + async _preCreate(data, options, user) { - if(!this.constructor.metadata.hasInitialAction || !foundry.utils.isEmpty(this.actions)) return; + if (!this.constructor.metadata.hasInitialAction || !foundry.utils.isEmpty(this.actions)) return; const actionType = { weapon: 'attack' }[this.constructor.metadata.type], @@ -72,6 +72,6 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel { parent: this.parent } ); - this.updateSource({actions: [action]}); + this.updateSource({ actions: [action] }); } } diff --git a/module/data/item/consumable.mjs b/module/data/item/consumable.mjs index cb8a13b5..3e70f97a 100644 --- a/module/data/item/consumable.mjs +++ b/module/data/item/consumable.mjs @@ -9,7 +9,7 @@ export default class DHConsumable extends BaseDataItem { type: 'consumable', hasDescription: true, isQuantifiable: true, - isInventoryItem: true, + isInventoryItem: true }); } diff --git a/module/data/item/feature.mjs b/module/data/item/feature.mjs index 0e502eb6..85a9c42f 100644 --- a/module/data/item/feature.mjs +++ b/module/data/item/feature.mjs @@ -1,4 +1,3 @@ -import { getTier } from '../../helpers/utils.mjs'; import BaseDataItem from './base.mjs'; import ActionField from '../fields/actionField.mjs'; @@ -17,135 +16,7 @@ export default class DHFeature extends BaseDataItem { const fields = foundry.data.fields; return { ...super.defineSchema(), - - //A type of feature seems unnecessary - type: new fields.StringField({ choices: SYSTEM.ITEM.featureTypes }), - - //TODO: remove actionType field - actionType: new fields.StringField({ - choices: SYSTEM.ITEM.actionTypes, - initial: SYSTEM.ITEM.actionTypes.passive.id - }), - //TODO: remove featureType field - featureType: new fields.SchemaField({ - type: new fields.StringField({ - choices: SYSTEM.ITEM.valueTypes, - initial: Object.keys(SYSTEM.ITEM.valueTypes).find(x => x === 'normal') - }), - data: new fields.SchemaField({ - value: new fields.StringField({}), - property: new fields.StringField({ - choices: SYSTEM.ACTOR.featureProperties, - initial: Object.keys(SYSTEM.ACTOR.featureProperties).find(x => x === 'spellcastingTrait') - }), - max: new fields.NumberField({ initial: 1, integer: true }), - numbers: new fields.TypedObjectField( - new fields.SchemaField({ - value: new fields.NumberField({ integer: true }), - used: new fields.BooleanField({ initial: false }) - }) - ) - }) - }), - refreshData: new fields.SchemaField( - { - type: new fields.StringField({ choices: SYSTEM.GENERAL.refreshTypes }), - uses: new fields.NumberField({ initial: 1, integer: true }), - //TODO: remove refreshed field - refreshed: new fields.BooleanField({ initial: true }) - }, - { nullable: true, initial: null } - ), - //TODO: remove refreshed field - multiclass: new fields.NumberField({ initial: null, nullable: true, integer: true }), - disabled: new fields.BooleanField({ initial: false }), - - //TODO: re do it completely or just remove it - effects: new fields.TypedObjectField( - new fields.SchemaField({ - type: new fields.StringField({ choices: SYSTEM.EFFECTS.effectTypes }), - valueType: new fields.StringField({ choices: SYSTEM.EFFECTS.valueTypes }), - parseType: new fields.StringField({ choices: SYSTEM.EFFECTS.parseTypes }), - initiallySelected: new fields.BooleanField({ initial: true }), - options: new fields.ArrayField( - new fields.SchemaField({ - name: new fields.StringField({}), - value: new fields.StringField({}) - }), - { nullable: true, initial: null } - ), - dataField: new fields.StringField({}), - appliesOn: new fields.StringField( - { - choices: SYSTEM.EFFECTS.applyLocations - }, - { nullable: true, initial: null } - ), - applyLocationChoices: new fields.TypedObjectField(new fields.StringField({}), { - nullable: true, - initial: null - }), - valueData: new fields.SchemaField({ - value: new fields.StringField({}), - fromValue: new fields.StringField({ initial: null, nullable: true }), - type: new fields.StringField({ initial: null, nullable: true }), - hopeIncrease: new fields.StringField({ initial: null, nullable: true }) - }) - }) - ), actions: new fields.ArrayField(new ActionField()) }; } - - get multiclassTier() { - return getTier(this.multiclass); - } - - async refresh() { - if (this.refreshData) { - if (this.featureType.type === SYSTEM.ITEM.valueTypes.dice.id) { - const update = { 'system.refreshData.refreshed': true }; - Object.keys(this.featureType.data.numbers).forEach( - x => (update[`system.featureType.data.numbers.-=${x}`] = null) - ); - await this.parent.update(update); - } else { - await this.parent.update({ 'system.refreshData.refreshed': true }); - } - } - } - - get effectData() { - const effectValues = Object.values(this.effects); - const effectCategories = Object.keys(SYSTEM.EFFECTS.effectTypes).reduce((acc, effectType) => { - acc[effectType] = effectValues.reduce((acc, effect) => { - if (effect.type === effectType) { - acc.push({ ...effect, valueData: this.#parseValues(effect.parseType, effect.valueData) }); - } - - return acc; - }, []); - - return acc; - }, {}); - - return effectCategories; - } - - #parseValues(parseType, values) { - return Object.keys(values).reduce((acc, prop) => { - acc[prop] = this.#parseValue(parseType, values[prop]); - - return acc; - }, {}); - } - - #parseValue(parseType, value) { - switch (parseType) { - case SYSTEM.EFFECTS.parseTypes.number.id: - return Number.parseInt(value); - default: - return value; - } - } } diff --git a/module/data/item/miscellaneous.mjs b/module/data/item/miscellaneous.mjs index 529cf9a9..cad07f48 100644 --- a/module/data/item/miscellaneous.mjs +++ b/module/data/item/miscellaneous.mjs @@ -9,7 +9,7 @@ export default class DHMiscellaneous extends BaseDataItem { type: 'miscellaneous', hasDescription: true, isQuantifiable: true, - isInventoryItem: true, + isInventoryItem: true }); } diff --git a/module/data/item/weapon.mjs b/module/data/item/weapon.mjs index 005f08af..9154eb31 100644 --- a/module/data/item/weapon.mjs +++ b/module/data/item/weapon.mjs @@ -2,7 +2,7 @@ import BaseDataItem from './base.mjs'; import FormulaField from '../fields/formulaField.mjs'; import ActionField from '../fields/actionField.mjs'; import { weaponFeatures } from '../../config/itemConfig.mjs'; -import { actionsTypes } from '../action/_module.mjs'; +import { actionsTypes } from '../action/_module.mjs'; export default class DHWeapon extends BaseDataItem { /** @inheritDoc */ @@ -13,7 +13,7 @@ export default class DHWeapon extends BaseDataItem { hasDescription: true, isQuantifiable: true, isInventoryItem: true, - hasInitialAction: true, + hasInitialAction: true }); } diff --git a/module/data/levelData.mjs b/module/data/levelData.mjs new file mode 100644 index 00000000..2432a313 --- /dev/null +++ b/module/data/levelData.mjs @@ -0,0 +1,61 @@ +import { LevelOptionType } from './levelTier.mjs'; + +export default class DhLevelData 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 }), + bonuses: new fields.TypedObjectField(new fields.NumberField({ integer: true, nullable: false })) + }), + 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.TypedObjectField(new fields.StringField({ required: true })), + itemUuid: new fields.DocumentUUIDField({ required: true }), + featureIds: new fields.ArrayField(new fields.StringField()) + }) + ) + }) + ) + }; + } + + get actions() { + return Object.values(this.levelups).flatMap(level => level.selections.flatMap(s => s.actions)); + } + + get canLevelUp() { + return this.level.current < this.level.changed; + } +} diff --git a/module/data/levelTier.mjs b/module/data/levelTier.mjs index 6cf11252..d29ce09e 100644 --- a/module/data/levelTier.mjs +++ b/module/data/levelTier.mjs @@ -58,6 +58,58 @@ class DhLevelOption extends foundry.abstract.DataModel { } } +export const CompanionLevelOptionType = { + hope: { + id: 'hope', + label: 'Light In The Dark' + }, + creatureComfort: { + id: 'creatureComfort', + label: 'Creature Comfort', + features: [ + { + name: 'DAGGERHEART.LevelUp.Actions.CreatureComfort.Name', + img: 'icons/magic/life/heart-cross-purple-orange.webp', + description: 'DAGGERHEART.LevelUp.Actions.CreatureComfort.Description' + } + ] + }, + armored: { + id: 'armored', + label: 'Armored', + features: [ + { + name: 'DAGGERHEART.LevelUp.Actions.Armored.Name', + img: 'icons/equipment/shield/kite-wooden-oak-glow.webp', + description: 'DAGGERHEART.LevelUp.Actions.Armored.Description' + } + ] + }, + vicious: { + id: 'vicious', + label: 'Viscious' + }, + resilient: { + id: 'resilient', + label: 'Resilient' + }, + bonded: { + id: 'bonded', + label: 'Bonded', + features: [ + { + name: 'DAGGERHEART.LevelUp.Actions.Bonded.Name', + img: 'icons/magic/life/heart-red-blue.webp', + description: 'DAGGERHEART.LevelUp.Actions.Bonded.Description' + } + ] + }, + aware: { + id: 'aware', + label: 'Aware' + } +}; + export const LevelOptionType = { trait: { id: 'trait', @@ -106,7 +158,8 @@ export const LevelOptionType = { multiclass: { id: 'multiclass', label: 'Multiclass' - } + }, + ...CompanionLevelOptionType }; export const defaultLevelTiers = { @@ -338,3 +391,80 @@ export const defaultLevelTiers = { } } }; + +export const defaultCompanionTier = { + tiers: { + 2: { + tier: 2, + name: 'Companion Choices', + levels: { + start: 2, + end: 10 + }, + initialAchievements: {}, + availableOptions: 1, + domainCardByLevel: 0, + options: { + experience: { + label: 'DAGGERHEART.LevelUp.Options.intelligent', + checkboxSelections: 3, + minCost: 1, + type: LevelOptionType.experience.id, + value: 1, + amount: 1 + }, + hope: { + label: 'DAGGERHEART.LevelUp.Options.lightInTheDark', + checkboxSelections: 1, + minCost: 1, + type: CompanionLevelOptionType.hope.id, + value: 1 + }, + creatureComfort: { + label: 'DAGGERHEART.LevelUp.Options.creatureComfort', + checkboxSelections: 1, + minCost: 1, + type: CompanionLevelOptionType.creatureComfort.id, + value: 1 + }, + armored: { + label: 'DAGGERHEART.LevelUp.Options.armored', + checkboxSelections: 1, + minCost: 1, + type: CompanionLevelOptionType.armored.id, + value: 1 + }, + vicious: { + label: 'DAGGERHEART.LevelUp.Options.vicious', + checkboxSelections: 3, + minCost: 1, + type: CompanionLevelOptionType.vicious.id, + value: 1, + amount: 1 + }, + stress: { + label: 'DAGGERHEART.LevelUp.Options.resilient', + checkboxSelections: 3, + minCost: 1, + type: LevelOptionType.stress.id, + value: 1 + }, + bonded: { + label: 'DAGGERHEART.LevelUp.Options.bonded', + checkboxSelections: 1, + minCost: 1, + type: CompanionLevelOptionType.bonded.id, + value: 1 + }, + evasion: { + label: 'DAGGERHEART.LevelUp.Options.aware', + checkboxSelections: 3, + minCost: 1, + type: LevelOptionType.evasion.id, + value: 2, + amount: 1 + } + } + } + } +}; diff --git a/module/data/levelup.mjs b/module/data/levelup.mjs index a964d716..0f248f45 100644 --- a/module/data/levelup.mjs +++ b/module/data/levelup.mjs @@ -32,7 +32,7 @@ export class DhLevelup extends foundry.abstract.DataModel { return acc; }, {}); - levels[i] = DhLevelupLevel.initializeData(pcLevelData.levelups[i], tier.availableOptions, { + levels[i] = DhLevelupLevel.initializeData(pcLevelData.levelups[i], tier.maxSelections[i], { ...initialAchievements, experiences, domainCards @@ -46,7 +46,7 @@ export class DhLevelup extends foundry.abstract.DataModel { name: tier.name, belongingLevels: belongingLevels, options: Object.keys(tier.options).reduce((acc, key) => { - acc[key] = tier.options[key].toObject(); + acc[key] = tier.options[key].toObject?.() ?? tier.options[key]; return acc; }, {}) }; @@ -98,6 +98,7 @@ export class DhLevelup extends foundry.abstract.DataModel { case 'experience': case 'domainCard': case 'subclass': + case 'vicious': return checkbox.data.length === (checkbox.amount ?? 1); case 'multiclass': const classSelected = checkbox.data.length === 1; diff --git a/module/dialogs/d20RollDialog.mjs b/module/dialogs/d20RollDialog.mjs index 30c0c40e..7c4fd06b 100644 --- a/module/dialogs/d20RollDialog.mjs +++ b/module/dialogs/d20RollDialog.mjs @@ -96,7 +96,10 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio } else { this.config.experiences = [...this.config.experiences, button.dataset.key]; } */ - this.config.experiences = this.config.experiences.indexOf(button.dataset.key) > -1 ? this.config.experiences.filter(x => x !== button.dataset.key) : [...this.config.experiences, button.dataset.key]; + this.config.experiences = + this.config.experiences.indexOf(button.dataset.key) > -1 + ? this.config.experiences.filter(x => x !== button.dataset.key) + : [...this.config.experiences, button.dataset.key]; this.render(); } @@ -109,7 +112,7 @@ export default class D20RollDialog extends HandlebarsApplicationMixin(Applicatio if (!options.submitted) this.config = false; } - static async configure(roll, config = {}, options={}) { + static async configure(roll, config = {}, options = {}) { return new Promise(resolve => { const app = new this(roll, config, options); app.addEventListener('close', () => resolve(app.config), { once: true }); diff --git a/module/dialogs/damageDialog.mjs b/module/dialogs/damageDialog.mjs index d3c571a9..b94c2aab 100644 --- a/module/dialogs/damageDialog.mjs +++ b/module/dialogs/damageDialog.mjs @@ -1,7 +1,7 @@ const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api; export default class DamageDialog extends HandlebarsApplicationMixin(ApplicationV2) { - constructor(roll, config={}, options={}) { + constructor(roll, config = {}, options = {}) { super(options); this.roll = roll; @@ -42,19 +42,19 @@ export default class DamageDialog extends HandlebarsApplicationMixin(Application } static async submitRoll() { - await this.close({ submitted: true }); + await this.close({ submitted: true }); } /** @override */ - _onClose(options={}) { - if ( !options.submitted ) this.config = false; + _onClose(options = {}) { + if (!options.submitted) this.config = false; } - static async configure(roll, config={}) { + static async configure(roll, config = {}) { return new Promise(resolve => { const app = new this(roll, config); - app.addEventListener("close", () => resolve(app.config), { once: true }); + app.addEventListener('close', () => resolve(app.config), { once: true }); app.render({ force: true }); }); } -} \ No newline at end of file +} diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index 6e475d7e..49282385 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -1,6 +1,8 @@ import DamageSelectionDialog from '../applications/damageSelectionDialog.mjs'; import { GMUpdateEvent, socketEvent } from '../helpers/socket.mjs'; import DamageReductionDialog from '../applications/damageReductionDialog.mjs'; +import { LevelOptionType } from '../data/levelTier.mjs'; +import DHFeature from '../data/item/feature.mjs'; export default class DhpActor extends Actor { async _preCreate(data, options, user) { @@ -8,7 +10,7 @@ export default class DhpActor extends Actor { // Configure prototype token settings const prototypeToken = {}; - if (this.type === 'character') + if (['character', 'companion'].includes(this.type)) Object.assign(prototypeToken, { sight: { enabled: true }, actorLink: true, @@ -18,7 +20,7 @@ export default class DhpActor extends Actor { } async updateLevel(newLevel) { - if (this.type !== 'character' || newLevel === this.system.levelData.level.changed) return; + if (!['character', 'companion'].includes(this.type) || newLevel === this.system.levelData.level.changed) return; if (newLevel > this.system.levelData.level.current) { const maxLevel = Object.values( @@ -41,6 +43,7 @@ export default class DhpActor extends Actor { return acc; }, {}); + const featureIds = []; const domainCards = []; const experiences = []; const subclassFeatureState = { class: null, multiclass: null }; @@ -53,6 +56,7 @@ export default class DhpActor extends Actor { const advancementCards = level.selections.filter(x => x.type === 'domainCard').map(x => x.itemUuid); domainCards.push(...achievementCards, ...advancementCards); experiences.push(...Object.keys(level.achievements.experiences)); + featureIds.push(...level.selections.flatMap(x => x.featureIds)); const subclass = level.selections.find(x => x.type === 'subclass'); if (subclass) { @@ -66,13 +70,21 @@ export default class DhpActor extends Actor { multiclass = level.selections.find(x => x.type === 'multiclass'); }); + for (let featureId of featureIds) { + this.items.get(featureId).delete(); + } + if (experiences.length > 0) { - this.update({ + const getUpdate = () => ({ 'system.experiences': experiences.reduce((acc, key) => { acc[`-=${key}`] = null; return acc; }, {}) }); + this.update(getUpdate()); + if (this.system.companion) { + this.system.companion.update(getUpdate()); + } } if (subclassFeatureState.class) { @@ -114,10 +126,15 @@ export default class DhpActor extends Actor { } } }); + + if (this.system.companion) { + this.system.companion.updateLevel(newLevel); + } } } async levelUp(levelupData) { + const actions = []; const levelups = {}; for (var levelKey of Object.keys(levelupData)) { const level = levelupData[levelKey]; @@ -126,13 +143,23 @@ export default class DhpActor extends Actor { const experience = level.achievements.experiences[experienceKey]; await this.update({ [`system.experiences.${experienceKey}`]: { - description: experience.name, + name: experience.name, value: experience.modifier } }); + + if (this.system.companion) { + await this.system.companion.update({ + [`system.experiences.${experienceKey}`]: { + name: '', + value: experience.modifier + } + }); + } } let multiclass = null; + const featureAdditions = []; const domainCards = []; const subclassFeatureState = { class: null, multiclass: null }; const selections = []; @@ -141,7 +168,18 @@ export default class DhpActor extends Actor { for (var checkboxNr of Object.keys(selection)) { const checkbox = selection[checkboxNr]; - if (checkbox.type === 'multiclass') { + const tierOption = LevelOptionType[checkbox.type]; + if (tierOption.features?.length > 0) { + featureAdditions.push({ + checkbox: { + ...checkbox, + level: Number(levelKey), + optionKey: optionKey, + checkboxNr: Number(checkboxNr) + }, + features: tierOption.features + }); + } else if (checkbox.type === 'multiclass') { multiclass = { ...checkbox, level: Number(levelKey), @@ -174,6 +212,28 @@ export default class DhpActor extends Actor { } } + for (var addition of featureAdditions) { + for (var featureData of addition.features) { + const feature = new DHFeature({ + ...featureData, + description: game.i18n.localize(featureData.description) + }); + const embeddedItem = await this.createEmbeddedDocuments('Item', [ + { + ...featureData, + name: game.i18n.localize(featureData.name), + type: 'feature', + system: feature + } + ]); + addition.checkbox.featureIds = !addition.checkbox.featureIds + ? [embeddedItem[0].id] + : [...addition.checkbox.featureIds, embeddedItem[0].id]; + } + + selections.push(addition.checkbox); + } + if (multiclass) { const subclassItem = await foundry.utils.fromUuid(multiclass.secondaryData.subclass); const subclassData = subclassItem.toObject(); @@ -238,6 +298,7 @@ export default class DhpActor extends Actor { await this.update({ system: { + actions: [...this.system.actions, ...actions], levelData: { level: { current: this.system.levelData.level.changed @@ -246,6 +307,10 @@ export default class DhpActor extends Actor { } } }); + + if (this.system.companion) { + this.system.companion.updateLevel(this.system.levelData.level.changed); + } } /** @@ -266,7 +331,7 @@ export default class DhpActor extends Actor { * @param {object} [config.costs] */ async diceRoll(config) { - config.source = {...(config.source ?? {}), actor: this.uuid}; + config.source = { ...(config.source ?? {}), actor: this.uuid }; config.data = this.getRollData(); const rollClass = config.roll.lite ? CONFIG.Dice.daggerheart['DHRoll'] : this.rollClass; return await rollClass.build(config); @@ -364,6 +429,11 @@ export default class DhpActor extends Actor { } async takeDamage(damage, type) { + if (this.type === 'companion') { + await this.modifyResource([{ value: 1, type: 'stress' }]); + return; + } + const hpDamage = damage >= this.system.damageThresholds.severe ? 3 @@ -422,14 +492,17 @@ export default class DhpActor extends Actor { break; default: updates.actor.resources[`system.resources.${r.type}.value`] = Math.max( - Math.min(this.system.resources[r.type].value + r.value, (this.system.resources[r.type].maxTotal ?? this.system.resources[r.type].max)), + Math.min( + this.system.resources[r.type].value + r.value, + this.system.resources[r.type].maxTotal ?? this.system.resources[r.type].max + ), 0 ); break; } }); Object.values(updates).forEach(async u => { - console.log(updates, u) + console.log(updates, u); if (Object.keys(u.resources).length > 0) { if (game.user.isGM) { await u.target.update(u.resources); diff --git a/module/documents/item.mjs b/module/documents/item.mjs index 6158db7b..0671e7e3 100644 --- a/module/documents/item.mjs +++ b/module/documents/item.mjs @@ -55,8 +55,8 @@ export default class DHItem extends foundry.documents.Item { isInventoryItem === true ? 'Inventory Items' : isInventoryItem === false - ? 'Character Items' - : 'Other'; + ? 'Character Items' + : 'Other'; return { value: type, label, group }; } @@ -83,13 +83,14 @@ export default class DHItem extends foundry.documents.Item { { actions: this.system.actions } ), title = 'Select Action'; - + return foundry.applications.api.DialogV2.prompt({ window: { title }, content, ok: { label: title, - callback: (event, button, dialog) => this.system.actions.find(a => a._id === button.form.elements.actionId.value) + callback: (event, button, dialog) => + this.system.actions.find(a => a._id === button.form.elements.actionId.value) } }); } @@ -115,7 +116,9 @@ export default class DHItem extends foundry.documents.Item { this.type === 'ancestry' ? game.i18n.localize('DAGGERHEART.Chat.FoundationCard.AncestryTitle') : this.type === 'community' - ? game.i18n.localize('DAGGERHEART.Chat.FoundationCard.CommunityTitle') + ? game.i18n.localize('DAGGERHEART.Chat.FoundationCard.CommunityTitle') + : this.type === 'feature' + ? game.i18n.localize('TYPES.Item.feature') : game.i18n.localize('DAGGERHEART.Chat.FoundationCard.SubclassFeatureTitle'), origin: origin, img: this.img, diff --git a/module/helpers/utils.mjs b/module/helpers/utils.mjs index d812f043..7816d0f4 100644 --- a/module/helpers/utils.mjs +++ b/module/helpers/utils.mjs @@ -1,4 +1,4 @@ -import { getDiceSoNicePresets } from '../config/generalConfig.mjs'; +import { diceTypes, getDiceSoNicePresets, range } from '../config/generalConfig.mjs'; import Tagify from '@yaireo/tagify'; export const loadCompendiumOptions = async compendiums => { @@ -225,10 +225,10 @@ export const getDeleteKeys = (property, innerProperty, innerPropertyDefaultValue // Fix on Foundry native formula replacement for DH const nativeReplaceFormulaData = Roll.replaceFormulaData; -Roll.replaceFormulaData = function (formula, data={}, { missing, warn = false } = {}) { +Roll.replaceFormulaData = function (formula, data = {}, { missing, warn = false } = {}) { const terms = Object.keys(SYSTEM.GENERAL.multiplierTypes).map(type => { - return { term: type, default: 1} - }) + return { term: type, default: 1 }; + }); formula = terms.reduce((a, c) => a.replaceAll(`@${c.term}`, data[c.term] ?? c.default), formula); return nativeReplaceFormulaData(formula, data, { missing, warn }); }; @@ -258,3 +258,17 @@ export const damageKeyToNumber = key => { return 0; } }; + +export const adjustDice = (dice, decrease) => { + const diceKeys = Object.keys(diceTypes); + const index = diceKeys.indexOf(dice); + const newIndex = decrease ? Math.max(index - 1, 0) : Math.min(index + 1, diceKeys.length - 1); + return diceTypes[diceKeys[newIndex]]; +}; + +export const adjustRange = (rangeVal, decrease) => { + const rangeKeys = Object.keys(range); + const index = rangeKeys.indexOf(rangeVal); + const newIndex = decrease ? Math.max(index - 1, 0) : Math.min(index + 1, rangeKeys.length - 1); + return range[rangeKeys[newIndex]]; +}; diff --git a/module/ui/chatLog.mjs b/module/ui/chatLog.mjs index dfcc1b8e..eeb515cf 100644 --- a/module/ui/chatLog.mjs +++ b/module/ui/chatLog.mjs @@ -154,14 +154,16 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo ? message.system.targets.map(target => game.canvas.tokens.get(target.id)) : Array.from(game.user.targets); - if(message.system.onSave && event.currentTarget.dataset.targetHit) { - const pendingingSaves = message.system.targets.filter(target => target.hit && target.saved.success === null); - if(pendingingSaves.length) { + if (message.system.onSave && event.currentTarget.dataset.targetHit) { + const pendingingSaves = message.system.targets.filter( + target => target.hit && target.saved.success === null + ); + if (pendingingSaves.length) { const confirm = await foundry.applications.api.DialogV2.confirm({ - window: {title: "Pending Reaction Rolls found"}, + window: { title: 'Pending Reaction Rolls found' }, content: `

Some Tokens still need to roll their Reaction Roll.

Are you sure you want to continue ?

Undone reaction rolls will be considered as failed

` }); - if ( !confirm ) return; + if (!confirm) return; } } @@ -169,8 +171,9 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo ui.notifications.info(game.i18n.localize('DAGGERHEART.Notification.Info.NoTargetsSelected')); for (let target of targets) { let damage = message.system.roll.total; - if(message.system.onSave && message.system.targets.find(t => t.id === target.id)?.saved?.success === true) damage = Math.ceil(damage * (SYSTEM.ACTIONS.damageOnSave[message.system.onSave]?.mod ?? 1)); - + if (message.system.onSave && message.system.targets.find(t => t.id === target.id)?.saved?.success === true) + damage = Math.ceil(damage * (SYSTEM.ACTIONS.damageOnSave[message.system.onSave]?.mod ?? 1)); + await target.actor.takeDamage(damage, message.system.roll.type); } }; @@ -181,7 +184,7 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo if (targets.length === 0) ui.notifications.info(game.i18n.localize('DAGGERHEART.Notification.Info.NoTargetsSelected')); - + for (var target of targets) { await target.actor.takeHealing([{ value: message.system.roll.total, type: message.system.roll.type }]); } diff --git a/styles/daggerheart.css b/styles/daggerheart.css index b94bb248..e7b6da48 100755 --- a/styles/daggerheart.css +++ b/styles/daggerheart.css @@ -2935,6 +2935,7 @@ div.daggerheart.views.multiclass { display: flex; flex-direction: column; gap: 8px; + margin-top: 8px; } .daggerheart.levelup .levelup-navigation-container { display: flex; @@ -3096,6 +3097,13 @@ div.daggerheart.views.multiclass { align-items: center; gap: 4px; } +.daggerheart.levelup .levelup-selections-container .levelup-radio-choices { + display: flex; + gap: 8px; +} +.daggerheart.levelup .levelup-selections-container .levelup-radio-choices label { + flex: 0; +} .daggerheart.levelup .levelup-summary-container .level-achievements-container, .daggerheart.levelup .levelup-summary-container .level-advancements-container { display: flex; @@ -4211,6 +4219,14 @@ div.daggerheart.views.multiclass { scrollbar-width: thin; scrollbar-color: light-dark(#18162e, #f3c267) transparent; } +.application.sheet.daggerheart.actor.dh-style.companion .profile { + height: 80px; + width: 80px; +} +.application.sheet.daggerheart.actor.dh-style.companion .temp-container { + position: relative; + top: 32px; +} .application.sheet.daggerheart.actor.dh-style.adversary .window-content { overflow: auto; } diff --git a/styles/daggerheart.less b/styles/daggerheart.less index 4a4cf3d2..c1d19c7a 100755 --- a/styles/daggerheart.less +++ b/styles/daggerheart.less @@ -25,6 +25,8 @@ @import './less/actors/character/biography.less'; @import './less/actors/character/features.less'; +@import './less/actors/companion/sheet.less'; + @import './less/actors/adversary.less'; @import './less/actors/environment.less'; diff --git a/styles/less/actors/character/inventory.less b/styles/less/actors/character/inventory.less index c1583046..516b01b0 100644 --- a/styles/less/actors/character/inventory.less +++ b/styles/less/actors/character/inventory.less @@ -1,70 +1,70 @@ -@import '../../utils/colors.less'; -@import '../../utils/fonts.less'; - -.application.sheet.daggerheart.actor.dh-style.character { - .tab.inventory { - .search-section { - display: flex; - gap: 10px; - align-items: center; - - .search-bar { - position: relative; - color: light-dark(@dark-blue-50, @beige-50); - width: 100%; - padding-top: 5px; - - input { - border-radius: 50px; - font-family: @font-body; - background: light-dark(@dark-blue-10, @golden-10); - border: none; - outline: 2px solid transparent; - transition: all 0.3s ease; - padding: 0 20px; - - &:hover { - outline: 2px solid light-dark(@dark, @golden); - } - - &:placeholder { - color: light-dark(@dark-blue-50, @beige-50); - } - - &::-webkit-search-cancel-button { - -webkit-appearance: none; - display: none; - } - } - - .icon { - align-content: center; - height: 32px; - position: absolute; - right: 20px; - font-size: 16px; - z-index: 1; - color: light-dark(@dark-blue-50, @beige-50); - } - } - } - - .items-section { - display: flex; - flex-direction: column; - gap: 10px; - overflow-y: auto; - mask-image: linear-gradient(0deg, transparent 0%, black 5%, black 95%, transparent 100%); - padding: 20px 0; - height: 80%; - - scrollbar-width: thin; - scrollbar-color: light-dark(@dark-blue, @golden) transparent; - } - - .currency-section { - display: flex; - gap: 10px; - } - } -} +@import '../../utils/colors.less'; +@import '../../utils/fonts.less'; + +.application.sheet.daggerheart.actor.dh-style.character { + .tab.inventory { + .search-section { + display: flex; + gap: 10px; + align-items: center; + + .search-bar { + position: relative; + color: light-dark(@dark-blue-50, @beige-50); + width: 100%; + padding-top: 5px; + + input { + border-radius: 50px; + font-family: @font-body; + background: light-dark(@dark-blue-10, @golden-10); + border: none; + outline: 2px solid transparent; + transition: all 0.3s ease; + padding: 0 20px; + + &:hover { + outline: 2px solid light-dark(@dark, @golden); + } + + &:placeholder { + color: light-dark(@dark-blue-50, @beige-50); + } + + &::-webkit-search-cancel-button { + -webkit-appearance: none; + display: none; + } + } + + .icon { + align-content: center; + height: 32px; + position: absolute; + right: 20px; + font-size: 16px; + z-index: 1; + color: light-dark(@dark-blue-50, @beige-50); + } + } + } + + .items-section { + display: flex; + flex-direction: column; + gap: 10px; + overflow-y: auto; + mask-image: linear-gradient(0deg, transparent 0%, black 5%, black 95%, transparent 100%); + padding: 20px 0; + height: 80%; + + scrollbar-width: thin; + scrollbar-color: light-dark(@dark-blue, @golden) transparent; + } + + .currency-section { + display: flex; + gap: 10px; + } + } +} diff --git a/styles/less/actors/companion/sheet.less b/styles/less/actors/companion/sheet.less new file mode 100644 index 00000000..1beb28a7 --- /dev/null +++ b/styles/less/actors/companion/sheet.less @@ -0,0 +1,11 @@ +.application.sheet.daggerheart.actor.dh-style.companion { + .profile { + height: 80px; + width: 80px; + } + + .temp-container { + position: relative; + top: 32px; + } +} diff --git a/styles/levelup.less b/styles/levelup.less index 3363d0a0..0f7949ba 100644 --- a/styles/levelup.less +++ b/styles/levelup.less @@ -25,6 +25,7 @@ display: flex; flex-direction: column; gap: 8px; + margin-top: 8px; } } @@ -217,6 +218,15 @@ align-items: center; gap: 4px; } + + .levelup-radio-choices { + display: flex; + gap: 8px; + + label { + flex: 0; + } + } } .levelup-summary-container { diff --git a/system.json b/system.json index 073315eb..c1a5c501 100644 --- a/system.json +++ b/system.json @@ -209,6 +209,7 @@ "character": { "htmlFields": ["story", "description", "scars.*.description"] }, + "companion": {}, "adversary": { "htmlFields": ["description", "motivesAndTactics"] }, diff --git a/templates/sheets/actors/character/features.hbs b/templates/sheets/actors/character/features.hbs index b2851193..1ac18a1e 100644 --- a/templates/sheets/actors/character/features.hbs +++ b/templates/sheets/actors/character/features.hbs @@ -10,6 +10,12 @@ {{#if document.system.class.subclass}} {{> 'systems/daggerheart/templates/sheets/global/partials/inventory-fieldset-items.hbs' title=(concat (localize 'TYPES.Item.subclass') ' - ' document.system.class.subclass.name) type='subclass'}} {{/if}} + {{#if document.system.features}} + {{> 'systems/daggerheart/templates/sheets/global/partials/inventory-fieldset-items.hbs' title=(localize "DAGGERHEART.Sheets.PC.Features") type='features'}} + {{/if}} + {{#if document.system.companionFeatures}} + {{> 'systems/daggerheart/templates/sheets/global/partials/inventory-fieldset-items.hbs' title=(localize "DAGGERHEART.Sheets.PC.CompanionFeatures") type='companionFeatures'}} + {{/if}} {{#if document.system.community}} {{> 'systems/daggerheart/templates/sheets/global/partials/inventory-fieldset-items.hbs' title=(concat (localize 'TYPES.Item.community') ' - ' document.system.community.name) type='community'}} {{/if}} diff --git a/templates/sheets/actors/character/sidebar.hbs b/templates/sheets/actors/character/sidebar.hbs index 4060caeb..5a09dc3f 100644 --- a/templates/sheets/actors/character/sidebar.hbs +++ b/templates/sheets/actors/character/sidebar.hbs @@ -1,5 +1,5 @@