From b7f962b22df95e6f62266af755023a3d52b94046 Mon Sep 17 00:00:00 2001 From: WBHarry Date: Sun, 29 Jun 2025 22:58:21 +0200 Subject: [PATCH] Temp --- lang/en.json | 22 +- .../applications/levelup/characterLevelup.mjs | 381 ++++++++++++++++++ .../applications/levelup/companionLevelup.mjs | 172 ++++++++ module/applications/{ => levelup}/levelup.mjs | 186 +-------- module/applications/sheets/character.mjs | 4 +- module/applications/sheets/companion.mjs | 25 +- module/data/actor/character.mjs | 1 + module/data/actor/companion.mjs | 50 ++- module/data/levelTier.mjs | 111 ++++- module/data/levelup.mjs | 4 +- module/helpers/utils.mjs | 16 +- styles/daggerheart.css | 8 + styles/levelup.less | 10 + .../sheets/actors/companion/tempMain.hbs | 14 +- templates/views/levelup/tabs/selections.hbs | 11 + templates/views/levelup/tabs/summary.hbs | 41 +- 16 files changed, 844 insertions(+), 212 deletions(-) create mode 100644 module/applications/levelup/characterLevelup.mjs create mode 100644 module/applications/levelup/companionLevelup.mjs rename module/applications/{ => levelup}/levelup.mjs (74%) diff --git a/lang/en.json b/lang/en.json index 4a02ae96..619fb3dc 100755 --- a/lang/en.json +++ b/lang/en.json @@ -482,7 +482,15 @@ "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." }, "Tier2": { "Label": "Levels 2-4", @@ -953,7 +961,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", @@ -971,7 +981,10 @@ "multiclass": "Multiclass", "traits": "Increased Traits", "experienceIncreases": "Experience Increases", - "damageThresholds": "Damage Thresholds" + "damageThresholds": "Damage Thresholds", + "vicious": "Vicious", + "damageIncreased": "Damage Increased: {damage}", + "rangeIncreased": "Range Increased: {range}" }, "notifications": { "info": { @@ -1252,7 +1265,8 @@ "name": { "label": "Attack Name" } } }, - "Experiences": "Experiences" + "Experiences": "Experiences", + "Level": "Level" }, "Adversary": { "FIELDS": { diff --git a/module/applications/levelup/characterLevelup.mjs b/module/applications/levelup/characterLevelup.mjs new file mode 100644 index 00000000..58e0ca95 --- /dev/null +++ b/module/applications/levelup/characterLevelup.mjs @@ -0,0 +1,381 @@ +import LevelUpBase from './levelup.mjs'; +import { DhLevelup } from '../../data/levelup.mjs'; +import { domains } from '../../config/domainConfig.mjs'; + +export default class DhCharacterLevelUp extends LevelUpBase { + constructor(actor) { + super(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)); + } + + 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].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); + 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 + } + }; + + 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..46399c99 --- /dev/null +++ b/module/applications/levelup/companionLevelup.mjs @@ -0,0 +1,172 @@ +import BaseLevelUp from './levelup.mjs'; +import { defaultCompanionTier } 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 = 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].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 + }; + }); + + 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 { current: currentActorLevel, changed: changedActorLevel } = this.actor.system.levelData.level; + const levelKeys = Object.keys(this.levelup.levels); + let achievementExperiences = []; + for (var levelKey of levelKeys) { + const level = this.levelup.levels[levelKey]; + if (Number(levelKey) < this.levelup.startLevel) continue; + + achievementExperiences = level.achievements.experiences + ? Object.values(level.achievements.experiences).reduce((acc, experience) => { + if (experience.name) acc.push(experience); + return acc; + }, []) + : []; + } + + context.achievements = {}; + + 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 'resilient': + case 'aware': + advancement[choiceKey] = advancement[choiceKey] + ? advancement[choiceKey] + Number(checkbox.value) + : Number(checkbox.value); + break; + case 'intelligent': + 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)]]; + } + } + } + } + + 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 + } + }; + + 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 74% rename from module/applications/levelup.mjs rename to module/applications/levelup/levelup.mjs index 3094c5bb..ebd5d7e2 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'; @@ -118,181 +113,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); diff --git a/module/applications/sheets/character.mjs b/module/applications/sheets/character.mjs index 941ed1c8..b2723346 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; @@ -425,7 +425,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) { diff --git a/module/applications/sheets/companion.mjs b/module/applications/sheets/companion.mjs index b438550e..4a07204a 100644 --- a/module/applications/sheets/companion.mjs +++ b/module/applications/sheets/companion.mjs @@ -1,3 +1,4 @@ +import DhCompanionlevelUp from '../levelup/companionLevelup.mjs'; import DaggerheartSheet from './daggerheart-sheet.mjs'; const { ActorSheetV2 } = foundry.applications.sheets; @@ -7,7 +8,8 @@ export default class DhCompanionSheet extends DaggerheartSheet(ActorSheetV2) { classes: ['daggerheart', 'sheet', 'actor', 'dh-style', 'companion'], position: { width: 700, height: 1000 }, actions: { - attackRoll: this.attackRoll + attackRoll: this.attackRoll, + levelUp: this.levelUp }, form: { handler: this.updateForm, @@ -20,6 +22,12 @@ export default class DhCompanionSheet extends DaggerheartSheet(ActorSheetV2) { 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; @@ -35,7 +43,22 @@ export default class DhCompanionSheet extends DaggerheartSheet(ActorSheetV2) { this.render(); } + async onPartnerChange(event) { + if (event.target.value) { + const partner = game.actors.find(a => a.uuid === event.target.value); + await partner.update({ 'system.companion': this.document.uuid }); + } else { + await this.document.system.partner.update({ 'system.companion': null }); + } + + await this.document.update({ 'system.partner': event.target.value }); + } + static async attackRoll(event) { this.actor.system.attack.use(event); } + + static async levelUp() { + new DhCompanionlevelUp(this.document).render(true); + } } diff --git a/module/data/actor/character.mjs b/module/data/actor/character.mjs index ce77e4ae..1a3c91ca 100644 --- a/module/data/actor/character.mjs +++ b/module/data/actor/character.mjs @@ -115,6 +115,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 }), diff --git a/module/data/actor/companion.mjs b/module/data/actor/companion.mjs index 24f7906c..404a92f4 100644 --- a/module/data/actor/companion.mjs +++ b/module/data/actor/companion.mjs @@ -2,6 +2,7 @@ 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']; @@ -9,7 +10,7 @@ export default class DhCompanion extends BaseDataActor { static get metadata() { return foundry.utils.mergeObject(super.metadata, { label: 'TYPES.Actor.companion', - type: 'character' + type: 'companion' }); } @@ -23,7 +24,8 @@ export default class DhCompanion extends BaseDataActor { 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 }), @@ -31,7 +33,7 @@ export default class DhCompanion extends BaseDataActor { }), experiences: new fields.TypedObjectField( new fields.SchemaField({ - description: new fields.StringField({}), + name: new fields.StringField({}), value: new fields.NumberField({ integer: true, initial: 0 }), bonus: new fields.NumberField({ integer: true, initial: 0 }) }), @@ -43,7 +45,7 @@ export default class DhCompanion extends BaseDataActor { } ), attack: new ActionField({ - base: { + initial: { name: 'Attack', _id: foundry.utils.randomID(), systemPath: 'attack', @@ -60,7 +62,11 @@ export default class DhCompanion extends BaseDataActor { damage: { parts: [ { - multiplier: 'flat' + multiplier: 'flat', + value: { + dice: 'd6', + multiplier: 'flat' + } } ] } @@ -74,6 +80,36 @@ export default class DhCompanion extends BaseDataActor { 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 'lightInTheDark': + this.resources.hope += selection.value; + break; + case 'vicious': + if (selection.data === '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 'resilient': + this.resources.stress.bonus += selection.value; + break; + case 'aware': + this.evasion.bonus += selection.value; + break; + case 'intelligent': + Object.keys(this.experiences).forEach(key => { + const experience = this.experiences[key]; + experience.bonus += selection.value; + }); + break; + } + } + } } prepareDerivedData() { @@ -92,4 +128,8 @@ export default class DhCompanion extends BaseDataActor { ...data }; } + + _preDelete() { + /* Null Character Companion field */ + } } diff --git a/module/data/levelTier.mjs b/module/data/levelTier.mjs index 6cf11252..ec8e98cd 100644 --- a/module/data/levelTier.mjs +++ b/module/data/levelTier.mjs @@ -58,6 +58,37 @@ class DhLevelOption extends foundry.abstract.DataModel { } } +export const CompanionLevelOptionType = { + lightInTheDark: { + id: 'lightInTheDark', + label: 'Light In The Dark' + }, + createComfort: { + id: 'createComfort', + label: 'Create Comfort' + }, + armored: { + id: 'armored', + label: 'Armored' + }, + vicious: { + id: 'vicious', + label: 'Viscious' + }, + resilient: { + id: 'resilient', + label: 'Resilient' + }, + bonded: { + id: 'bonded', + label: 'Bonded' + }, + aware: { + id: 'aware', + label: 'Aware' + } +}; + export const LevelOptionType = { trait: { id: 'trait', @@ -106,7 +137,8 @@ export const LevelOptionType = { multiclass: { id: 'multiclass', label: 'Multiclass' - } + }, + ...CompanionLevelOptionType }; export const defaultLevelTiers = { @@ -338,3 +370,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: 2 + }, + lightInTheDark: { + label: 'DAGGERHEART.LevelUp.Options.lightInTheDark', + checkboxSelections: 1, + minCost: 1, + type: CompanionLevelOptionType.lightInTheDark.id, + value: 1 + }, + creatureComfort: { + label: 'DAGGERHEART.LevelUp.Options.creatureComfort', + checkboxSelections: 1, + minCost: 1, + type: CompanionLevelOptionType.createComfort.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 + }, + resilient: { + label: 'DAGGERHEART.LevelUp.Options.resilient', + checkboxSelections: 3, + minCost: 1, + type: CompanionLevelOptionType.resilient.id, + value: 1 + }, + bonded: { + label: 'DAGGERHEART.LevelUp.Options.bonded', + checkboxSelections: 1, + minCost: 1, + type: CompanionLevelOptionType.bonded.id, + value: 1 + }, + aware: { + label: 'DAGGERHEART.LevelUp.Options.aware', + checkboxSelections: 3, + minCost: 1, + type: CompanionLevelOptionType.aware.id, + value: 2, + amount: 1 + } + } + } + } +}; diff --git a/module/data/levelup.mjs b/module/data/levelup.mjs index a964d716..b71bf4c7 100644 --- a/module/data/levelup.mjs +++ b/module/data/levelup.mjs @@ -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,8 @@ export class DhLevelup extends foundry.abstract.DataModel { case 'experience': case 'domainCard': case 'subclass': + case 'vicious': + case 'intelligent': return checkbox.data.length === (checkbox.amount ?? 1); case 'multiclass': const classSelected = checkbox.data.length === 1; diff --git a/module/helpers/utils.mjs b/module/helpers/utils.mjs index d62ff148..af3ce16b 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 => { @@ -259,3 +259,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/styles/daggerheart.css b/styles/daggerheart.css index aaae3beb..0478ee9a 100755 --- a/styles/daggerheart.css +++ b/styles/daggerheart.css @@ -2933,6 +2933,7 @@ div.daggerheart.views.multiclass { display: flex; flex-direction: column; gap: 8px; + margin-top: 8px; } .daggerheart.levelup .levelup-navigation-container { display: flex; @@ -3094,6 +3095,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; 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/templates/sheets/actors/companion/tempMain.hbs b/templates/sheets/actors/companion/tempMain.hbs index d12d60c7..ef106b72 100644 --- a/templates/sheets/actors/companion/tempMain.hbs +++ b/templates/sheets/actors/companion/tempMain.hbs @@ -3,7 +3,7 @@
- {{selectOptions playerCharacters selected=source.system.partner.uuid labelAttr="name" valueAttr="key" blank=""}}
@@ -15,7 +15,7 @@
{{#each source.system.experiences as |experience key|}}
- +
{{/each}} @@ -30,4 +30,14 @@
+ +
+
+
+ + + +
+
+
\ No newline at end of file diff --git a/templates/views/levelup/tabs/selections.hbs b/templates/views/levelup/tabs/selections.hbs index 03145347..6c1b8aa3 100644 --- a/templates/views/levelup/tabs/selections.hbs +++ b/templates/views/levelup/tabs/selections.hbs @@ -111,5 +111,16 @@ {{/if}} + + {{#if this.vicious}} +
+

{{localize "DAGGERHEART.Application.LevelUp.summary.vicious"}}

+ {{#each this.vicious}} +
+ {{radioBoxes (concat "levelup." this.path ".data") @root.viciousChoices checked=this.data}} +
+ {{/each}} +
+ {{/if}} \ No newline at end of file diff --git a/templates/views/levelup/tabs/summary.hbs b/templates/views/levelup/tabs/summary.hbs index c1c1d13f..68086895 100644 --- a/templates/views/levelup/tabs/summary.hbs +++ b/templates/views/levelup/tabs/summary.hbs @@ -17,19 +17,21 @@ {{/if}} -
-
{{localize "DAGGERHEART.Application.LevelUp.summary.damageThresholds"}}{{#if this.levelAchievements.damageThresholds.unarmored}}({{localize "DAGGERHEART.General.unarmored"}}){{/if}}
-
- {{localize "DAGGERHEART.Application.LevelUp.summary.damageThresholdMajorIncrease" threshold=this.achievements.damageThresholds.major.old }} - - {{this.achievements.damageThresholds.major.new}} + {{#if this.achievements.damageThresholds}} +
+
{{localize "DAGGERHEART.Application.LevelUp.summary.damageThresholds"}}{{#if this.levelAchievements.damageThresholds.unarmored}}({{localize "DAGGERHEART.General.unarmored"}}){{/if}}
+
+ {{localize "DAGGERHEART.Application.LevelUp.summary.damageThresholdMajorIncrease" threshold=this.achievements.damageThresholds.major.old }} + + {{this.achievements.damageThresholds.major.new}} +
+
+ {{localize "DAGGERHEART.Application.LevelUp.summary.damageThresholdSevereIncrease" threshold=this.achievements.damageThresholds.severe.old }} + + {{this.achievements.damageThresholds.severe.new}} +
-
- {{localize "DAGGERHEART.Application.LevelUp.summary.damageThresholdSevereIncrease" threshold=this.achievements.damageThresholds.severe.old }} - - {{this.achievements.damageThresholds.severe.new}} -
-
+ {{/if}} {{#if this.achievements.domainCards.shown}}
{{localize "DAGGERHEART.Application.LevelUp.summary.domainCards"}}
@@ -151,6 +153,21 @@
{{/with}} {{/if}} + + {{#if this.advancements.vicious.damage}} +
+ {{localize "DAGGERHEART.Application.LevelUp.summary.damageIncreased" damage=this.advancements.vicious.damage.old }} + + {{this.advancements.vicious.damage.new}} +
+ {{/if}} + {{#if this.advancements.vicious.range}} +
+ {{localize "DAGGERHEART.Application.LevelUp.summary.rangeIncreased" range=this.advancements.vicious.range.old }} + + {{this.advancements.vicious.range.new}} +
+ {{/if}}