Fix conflict

This commit is contained in:
Dapoolp 2025-06-27 18:01:49 +02:00
commit 38d228c53e
20 changed files with 324 additions and 70 deletions

View file

@ -1113,8 +1113,9 @@
"faith": "Faith",
"ShortRest": "Take a Short Rest",
"LongRest": "Take a Long Rest",
"CharacterSetup": "Character setup isn't done yet",
"Level": "Level",
"LevelUp": "Level Up!",
"LevelUp": "You can level up",
"Tabs": {
"Features": "Features",
"Inventory": "Inventory",
@ -1212,7 +1213,9 @@
"NewScar": "New Scar",
"DeleteConfirmation": "Are you sure you want to delete the item - {item}?",
"Errors": {
"missingClassOrSubclass": "The character doesn't have a class and subclass"
"missingClassOrSubclass": "The character doesn't have a class and subclass",
"tooHighLevel": "You cannot raise the character level past the maximum",
"tooLowLevel": "You cannot lower the character level below starting level"
}
},
"Adversary": {

View file

@ -5,6 +5,7 @@ import AncestrySelectionDialog from '../ancestrySelectionDialog.mjs';
import DaggerheartSheet from './daggerheart-sheet.mjs';
import { abilities } from '../../config/actorConfig.mjs';
import DhlevelUp from '../levelup.mjs';
import DhCharacterCreation from '../characterCreation.mjs';
const { ActorSheetV2 } = foundry.applications.sheets;
const { TextEditor } = foundry.applications.ux;
@ -46,7 +47,7 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) {
useAdvancementCard: this.useAdvancementCard,
useAdvancementAbility: this.useAdvancementAbility,
toggleEquipItem: this.toggleEquipItem,
levelup: this.openLevelUp,
levelManagement: this.levelManagement,
editImage: this._onEditImage
},
window: {
@ -216,7 +217,7 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) {
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
// htmlElement.querySelector('.level-value').addEventListener('change', this.onLevelChange.bind(this));
htmlElement.querySelector('.level-value')?.addEventListener('change', this.onLevelChange.bind(this));
// To Remove when ContextMenu Handler is made
htmlElement
.querySelectorAll('[data-item-id]')
@ -417,7 +418,19 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) {
}
}
static openLevelUp() {
static levelManagement() {
if (this.document.system.needsCharacterSetup) {
this.characterSetup();
} else {
this.openLevelUp();
}
}
characterSetup() {
new DhCharacterCreation(this.document).render(true);
}
openLevelUp() {
if (!this.document.system.class.value || !this.document.system.class.subclass) {
ui.notifications.error(game.i18n.localize('DAGGERHEART.Sheets.PC.Errors.missingClassOrSubclass'));
return;

View file

@ -4,20 +4,56 @@ const { ItemSheetV2 } = foundry.applications.sheets;
export default class DomainCardSheet extends DHItemSheetV2(ItemSheetV2) {
static DEFAULT_OPTIONS = {
classes: ['domain-card'],
position: { width: 450, height: 700 }
position: { width: 450, height: 700 },
actions: {
addEffect: this.addEffect,
editEffect: this.editEffect,
removeEffect: this.removeEffect
}
};
static PARTS = {
header: { template: 'systems/daggerheart/templates/sheets/items/domainCard/header.hbs' },
tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
description: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-description.hbs' },
settings: {
template: 'systems/daggerheart/templates/sheets/items/domainCard/settings.hbs',
scrollable: ['.settings']
},
actions: {
template: 'systems/daggerheart/templates/sheets/global/tabs/tab-actions.hbs',
scrollable: ['.actions']
},
settings: {
template: 'systems/daggerheart/templates/sheets/items/domainCard/settings.hbs',
scrollable: ['.settings']
effects: {
template: 'systems/daggerheart/templates/sheets/global/tabs/tab-effects.hbs',
scrollable: ['.effects']
}
};
static TABS = {
...super.TABS,
effects: {
active: false,
cssClass: '',
group: 'primary',
id: 'effects',
icon: null,
label: 'DAGGERHEART.Sheets.Feature.Tabs.Effects'
}
};
static async addEffect() {
await this.document.createEmbeddedDocuments('ActiveEffect', [
{ name: game.i18n.localize('DAGGERHEART.Feature.NewEffect') }
]);
}
static async editEffect(_, target) {
const effect = this.document.effects.get(target.dataset.effect);
effect.sheet.render(true);
}
static async removeEffect(_, target) {
await this.document.effects.get(target.dataset.effect).delete();
}
}

View file

@ -393,6 +393,14 @@ export const abilityCosts = {
slayer: {
id: 'slayer',
label: 'Slayer Dice'
},
tide: {
id: 'tide',
label: 'Tide'
},
chaos: {
id: 'chaos',
label: 'Chaos'
}
};

View file

@ -68,7 +68,13 @@ export default class DhpAdversary extends BaseDataActor {
name: new fields.StringField(),
value: new fields.NumberField({ required: true, integer: true, initial: 1 })
})
)
),
bonuses: new fields.SchemaField({
difficulty: new fields.SchemaField({
all: new fields.NumberField({ integer: true, initial: 0 }),
reaction: new fields.NumberField({ integer: true, initial: 0 })
})
})
};
}
}

View file

@ -38,7 +38,9 @@ export default class DhCharacter extends BaseDataActor {
resources: new fields.SchemaField({
hitPoints: resourceField(6),
stress: resourceField(6),
hope: resourceField(6)
hope: resourceField(6),
tokens: new fields.ObjectField(),
dice: new fields.ObjectField()
}),
traits: new fields.SchemaField({
agility: attributeField(),
@ -93,9 +95,22 @@ export default class DhCharacter extends BaseDataActor {
}),
levelData: new fields.EmbeddedDataField(DhPCLevelData),
bonuses: new fields.SchemaField({
attack: new fields.NumberField({ integer: true, initial: 0 }),
spellcast: new fields.NumberField({ integer: true, initial: 0 }),
armorScore: new fields.NumberField({ integer: true, initial: 0 })
armorScore: new fields.NumberField({ integer: true, initial: 0 }),
damageThresholds: new fields.SchemaField({
severe: new fields.NumberField({ integer: true, initial: 0 }),
major: new fields.NumberField({ integer: true, initial: 0 })
}),
roll: new fields.SchemaField({
attack: new fields.NumberField({ integer: true, initial: 0 }),
spellcast: new fields.NumberField({ integer: true, initial: 0 }),
action: new fields.NumberField({ integer: true, initial: 0 }),
hopeOrFear: new fields.NumberField({ integer: true, initial: 0 })
}),
damage: new fields.SchemaField({
all: new fields.NumberField({ integer: true, initial: 0 }),
physical: new fields.NumberField({ integer: true, initial: 0 }),
magic: new fields.NumberField({ integer: true, initial: 0 })
})
}),
rules: new fields.SchemaField({
maxArmorMarked: new fields.SchemaField({
@ -107,7 +122,15 @@ export default class DhCharacter extends BaseDataActor {
severe: stressDamageReductionRule(),
major: stressDamageReductionRule(),
minor: stressDamageReductionRule()
})
}),
strangePatterns: new fields.NumberField({
integer: true,
min: 1,
max: 12,
nullable: true,
initial: null
}),
runeWard: new fields.BooleanField({ initial: false })
})
};
}
@ -128,6 +151,10 @@ export default class DhCharacter extends BaseDataActor {
return this.parent.items.find(x => x.type === 'community') ?? null;
}
get needsCharacterSetup() {
return !this.class.value || !this.class.subclass;
}
get domains() {
const classDomains = this.class.value ? this.class.value.system.domains : [];
const multiclassDomains = this.multiclass.value ? this.multiclass.value.system.domains : [];
@ -270,7 +297,11 @@ export default class DhCharacter extends BaseDataActor {
const data = super.getRollData();
return {
...data,
tier: this.tier
...this.resources.tokens,
...this.resources.dice,
...this.bonuses,
tier: this.tier,
level: this.levelData.level.current
};
}
}

View file

@ -16,10 +16,18 @@ export default class DHDomainCard extends BaseDataItem {
const fields = foundry.data.fields;
return {
...super.defineSchema(),
domain: new fields.StringField({ choices: SYSTEM.DOMAIN.domains, required: true, blank: true }),
domain: new fields.StringField({
choices: SYSTEM.DOMAIN.domains,
required: true,
initial: SYSTEM.DOMAIN.domains.arcana.id
}),
level: new fields.NumberField({ initial: 1, integer: true }),
recallCost: new fields.NumberField({ initial: 0, integer: true }),
type: new fields.StringField({ choices: SYSTEM.DOMAIN.cardTypes, required: true, blank: true }),
type: new fields.StringField({
choices: SYSTEM.DOMAIN.cardTypes,
required: true,
initial: SYSTEM.DOMAIN.cardTypes.ability.id
}),
foundation: new fields.BooleanField({ initial: false }),
inVault: new fields.BooleanField({ initial: false }),
actions: new fields.ArrayField(new ActionField())

View file

@ -4,6 +4,10 @@ export default class DhActiveEffect extends ActiveEffect {
return !this.parent.system.equipped;
}
if (this.parent.type === 'domainCard') {
return this.parent.system.inVault;
}
return super.isSuppressed;
}

View file

@ -29,10 +29,22 @@ export default class DhpActor extends Actor {
if (this.type !== 'character' || newLevel === this.system.levelData.level.changed) return;
if (newLevel > this.system.levelData.level.current) {
await this.update({ 'system.levelData.level.changed': newLevel });
const maxLevel = Object.values(
game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.LevelTiers).tiers
).reduce((acc, tier) => Math.max(acc, tier.levels.end), 0);
if (newLevel > maxLevel) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.Sheets.PC.Errors.tooHighLevel'));
}
await this.update({ 'system.levelData.level.changed': Math.min(newLevel, maxLevel) });
} else {
const usedLevel = Math.max(newLevel, 1);
if (newLevel < 1) {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.Sheets.PC.Errors.tooLowLevel'));
}
const updatedLevelups = Object.keys(this.system.levelData.levelups).reduce((acc, level) => {
if (Number(level) > newLevel) acc[`-=${level}`] = null;
if (Number(level) > usedLevel) acc[`-=${level}`] = null;
return acc;
}, {});
@ -42,7 +54,7 @@ export default class DhpActor extends Actor {
const subclassFeatureState = { class: null, multiclass: null };
let multiclass = null;
Object.keys(this.system.levelData.levelups)
.filter(x => x > newLevel)
.filter(x => x > usedLevel)
.forEach(levelKey => {
const level = this.system.levelData.levelups[levelKey];
const achievementCards = level.achievements.domainCards.map(x => x.itemUuid);
@ -103,8 +115,8 @@ export default class DhpActor extends Actor {
system: {
levelData: {
level: {
current: newLevel,
changed: newLevel
current: usedLevel,
changed: usedLevel
},
levelups: updatedLevelups
}
@ -400,7 +412,7 @@ export default class DhpActor extends Actor {
}
async takeHealing(resources) {
resources.forEach(r => r.value *= -1);
resources.forEach(r => (r.value *= -1));
await this.modifyResource(resources);
}
@ -410,16 +422,16 @@ export default class DhpActor extends Actor {
resources.forEach(r => {
switch (r.type) {
case 'armorStack':
updates.armor.resources['system.marks.value'] = Math.max(Math.min(
this.system.armor.system.marks.value + r.value,
this.system.armorScore
), 0);
updates.armor.resources['system.marks.value'] = Math.max(
Math.min(this.system.armor.system.marks.value + r.value, this.system.armorScore),
0
);
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].max
), 0);
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].max),
0
);
break;
}
});

View file

@ -189,22 +189,20 @@ export const tagifyElement = (element, options, onChange, tagifyOptions = {}) =>
}
});
const onSelect = async event => {
const inputElement = event.detail.tagify.DOM.originalInput;
const selectedOptions = event.detail?.value ? JSON.parse(event.detail.value) : [];
const unusedDropDownItems = event.detail.tagify.suggestedListItems;
const missingOptions = Object.keys(options).filter(x => !unusedDropDownItems.find(item => item.value === x));
const removedItem = missingOptions.find(x => !selectedOptions.find(item => item.value === x));
const addedItem = removedItem
? null
: selectedOptions.find(x => !missingOptions.find(item => item === x.value));
const changedItem = { option: removedItem ?? addedItem.value, removed: Boolean(removedItem) };
onChange(selectedOptions, changedItem, inputElement);
};
tagifyElement.on('change', onSelect);
tagifyElement.on('add', event => {
const input = event.detail.tagify.DOM.originalInput;
const currentList = input.value ? JSON.parse(input.value) : [];
onChange([...currentList, event.detail.data], { option: event.detail.data.value, removed: false }, input);
});
tagifyElement.on('remove', event => {
const input = event.detail.tagify.DOM.originalInput;
const currentList = input.value ? JSON.parse(input.value) : [];
onChange(
currentList.filter(x => x.value !== event.detail.data.value),
{ option: event.detail.data.value, removed: true },
event.detail.tagify.DOM.originalInput
);
});
};
export const getDeleteKeys = (property, innerProperty, innerPropertyDefaultValue) => {

View file

@ -8,6 +8,7 @@
/* Background */
/* Duality */
/* Fear */
@import '../node_modules/@yaireo/tagify/dist/tagify.css';
.daggerheart.sheet.class .editor {
height: 500px;
}
@ -3611,16 +3612,16 @@ div.daggerheart.views.multiclass {
}
.application.sheet.daggerheart.actor.dh-style.character .character-header-sheet .name-row {
display: flex;
gap: 20px;
align-items: baseline;
gap: 5px;
align-items: center;
justify-content: space-between;
padding: 0;
padding-top: 5px;
flex: 1;
}
.application.sheet.daggerheart.actor.dh-style.character .character-header-sheet .name-row input[type='text'] {
font-size: 32px;
height: 42px;
width: 380px;
text-align: start;
border: 1px solid transparent;
outline: 2px solid transparent;
@ -3631,6 +3632,36 @@ div.daggerheart.views.multiclass {
}
.application.sheet.daggerheart.actor.dh-style.character .character-header-sheet .name-row .level-div {
white-space: nowrap;
display: flex;
justify-content: end;
}
.application.sheet.daggerheart.actor.dh-style.character .character-header-sheet .name-row .level-div .label {
display: flex;
align-items: center;
gap: 4px;
}
.application.sheet.daggerheart.actor.dh-style.character .character-header-sheet .name-row .level-div input {
width: 40px;
padding: 0;
text-align: center;
}
.application.sheet.daggerheart.actor.dh-style.character .character-header-sheet .name-row .level-div .level-button {
color: light-dark(#222, #efe6d8);
font-size: 18px;
line-height: 1;
min-height: unset;
height: min-content;
padding: 4px;
font-family: 'Cinzel', serif;
margin: 0;
font-weight: normal;
border-color: light-dark(#18162e, #f3c267);
background-color: light-dark(transparent, #0e0d15);
}
.application.sheet.daggerheart.actor.dh-style.character .character-header-sheet .name-row .level-div .level-button:hover {
background-image: none;
background-color: var(--color-warm-2);
filter: drop-shadow(0 0 3px lightgray);
}
.application.sheet.daggerheart.actor.dh-style.character .character-header-sheet .character-details {
display: flex;
@ -4200,6 +4231,9 @@ div.daggerheart.views.multiclass {
gap: 10px;
grid-template-columns: 1fr 1.5fr 1.5fr;
}
.application.sheet.daggerheart.dh-style.class .tab.settings .fieldsets-section .drop-section {
width: 100%;
}
.application.sheet.daggerheart.dh-style.class .tab.settings .list-items {
margin-bottom: 10px;
width: 100%;
@ -4230,6 +4264,22 @@ div.daggerheart.views.multiclass {
justify-content: center;
gap: 10px;
}
@keyframes glow {
0% {
box-shadow: 0 0 1px 1px #f3c267;
}
100% {
box-shadow: 0 0 2px 2px #f3c267;
}
}
@keyframes glow-dark {
0% {
box-shadow: 0 0 1px 1px #18162e;
}
100% {
box-shadow: 0 0 2px 2px #18162e;
}
}
@font-face {
font-family: 'Cinzel';
font-style: normal;
@ -4413,6 +4463,9 @@ div.daggerheart.views.multiclass {
background: light-dark(rgba(0, 0, 0, 0.3), #18162e);
color: light-dark(#18162e, #f3c267);
}
.application.sheet.dh-style button.glow {
animation: glow 0.75s infinite alternate;
}
.application.sheet.dh-style select {
background: light-dark(transparent, transparent);
color: light-dark(#222, #efe6d8);
@ -4636,6 +4689,9 @@ div.daggerheart.views.multiclass {
.system-daggerheart.theme-light .tagify__dropdown .tagify__dropdown__item--active {
color: #efe6d8;
}
.theme-light .application.sheet.dh-style button.glow {
animation: glow-dark 0.75s infinite alternate;
}
.theme-light .application .component.dh-style.card-preview-container {
background-image: url('../assets/parchments/dh-parchment-light.png');
}

View file

@ -45,6 +45,7 @@
@import './less/global/feature-section.less';
@import './less/global/inventory-item.less';
@import './less/global/inventory-fieldset-items.less';
@import '../node_modules/@yaireo/tagify/dist/tagify.css';
.daggerheart {
.vertical-separator {

View file

@ -9,16 +9,16 @@
.name-row {
display: flex;
gap: 20px;
align-items: baseline;
gap: 5px;
align-items: center;
justify-content: space-between;
padding: 0;
padding-top: 5px;
flex: 1;
input[type='text'] {
font-size: 32px;
height: 42px;
width: 380px;
text-align: start;
border: 1px solid transparent;
outline: 2px solid transparent;
@ -31,6 +31,40 @@
.level-div {
white-space: nowrap;
display: flex;
justify-content: end;
.label {
display: flex;
align-items: center;
gap: 4px;
}
input {
width: 40px;
padding: 0;
text-align: center;
}
.level-button {
color: light-dark(@dark, @beige);
font-size: 18px;
line-height: 1;
min-height: unset;
height: min-content;
padding: 4px;
font-family: 'Cinzel', serif;
margin: 0;
font-weight: normal;
border-color: light-dark(@dark-blue, @golden);
background-color: light-dark(transparent, @deep-black);
&:hover {
background-image: none;
background-color: var(--color-warm-2);
filter: drop-shadow(0 0 3px lightgray);
}
}
}
}

View file

@ -48,6 +48,10 @@
background: light-dark(@light-black, @dark-blue);
color: light-dark(@dark-blue, @golden);
}
&.glow {
animation: glow 0.75s infinite alternate;
}
}
select {
@ -336,16 +340,24 @@
}
}
.theme-light .application .component.dh-style.card-preview-container {
background-image: url('../assets/parchments/dh-parchment-light.png');
.preview-text-container {
background-image: url(../assets/parchments/dh-parchment-dark.png);
.theme-light .application {
&.sheet.dh-style {
button.glow {
animation: glow-dark 0.75s infinite alternate;
}
}
.preview-selected-icon-container {
background-image: url(../assets/parchments/dh-parchment-dark.png);
color: var(--color-light-5);
.component.dh-style.card-preview-container {
background-image: url('../assets/parchments/dh-parchment-light.png');
.preview-text-container {
background-image: url(../assets/parchments/dh-parchment-dark.png);
}
.preview-selected-icon-container {
background-image: url(../assets/parchments/dh-parchment-dark.png);
color: var(--color-light-5);
}
}
}

View file

@ -7,6 +7,10 @@
display: grid;
gap: 10px;
grid-template-columns: 1fr 1.5fr 1.5fr;
.drop-section {
width: 100%;
}
}
.list-items {

View file

@ -19,3 +19,23 @@
@soft-shadow: rgba(0, 0, 0, 0.05);
@gradient-hp: linear-gradient(15deg, rgb(70, 20, 10) 0%, rgb(190, 0, 0) 42%, rgb(252, 176, 69) 100%);
@gradient-stress: linear-gradient(15deg, rgb(130, 59, 1) 0%, rgb(252, 142, 69) 65%, rgb(190, 0, 0) 100%);
@keyframes glow {
0% {
box-shadow: 0 0 1px 1px @golden;
}
100% {
box-shadow: 0 0 2px 2px @golden;
}
}
@keyframes glow-dark {
0% {
box-shadow: 0 0 1px 1px @dark-blue;
}
100% {
box-shadow: 0 0 2px 2px @dark-blue;
}
}

View file

@ -12,8 +12,16 @@
<div class='level-div'>
<h3 class='label'>
{{#if (or document.system.needsCharacterSetup document.system.levelData.canLevelUp)}}
<button
class="level-button glow" data-tooltip="{{#if document.system.needsCharacterSetup}}{{localize "DAGGERHEART.Sheets.PC.CharacterSetup"}}{{else}}{{localize "DAGGERHEART.Sheets.PC.LevelUp"}}{{/if}}"
data-action="levelManagement"
>
<i class="fa-solid fa-triangle-exclamation"></i>
</button>
{{/if}}
{{localize 'DAGGERHEART.Sheets.PC.Level'}}
{{document.system.levelData.level.current}}
<input type="text" data-dtype="Number" class="level-value" value={{#if document.system.needsCharacterSetup}}0{{else}}{{document.system.levelData.level.changed}}{{/if}} {{#if document.system.needsCharacterSetup}}disabled{{/if}} />
</h3>
</div>
</div>

View file

@ -6,12 +6,12 @@
<div class='status-value'>
<p><input class="bar-input" name="system.resources.hitPoints.value" value="{{document.system.resources.hitPoints.value}}" type="number"></p>
<p>/</p>
<p class="bar-label">{{document.system.resources.hitPoints.max}}</p>
<p class="bar-label">{{document.system.resources.hitPoints.maxTotal}}</p>
</div>
<progress
class='progress-bar'
value='{{document.system.resources.hitPoints.value}}'
max='{{document.system.resources.hitPoints.max}}'
max='{{document.system.resources.hitPoints.maxTotal}}'
></progress>
<div class="status-label">
<h4>Health</h4>
@ -22,12 +22,12 @@
<div class='status-value'>
<p><input class="bar-input" name="system.resources.stress.value" value="{{document.system.resources.stress.value}}" type="number"></p>
<p>/</p>
<p class="bar-label">{{document.system.resources.stress.max}}</p>
<p class="bar-label">{{document.system.resources.stress.maxTotal}}</p>
</div>
<progress
class='progress-bar stress-color'
value='{{document.system.resources.stress.value}}'
max='{{document.system.resources.stress.max}}'
max='{{document.system.resources.stress.maxTotal}}'
></progress>
<div class="status-label">
<h4>Stress</h4>

View file

@ -7,11 +7,11 @@
<legend>{{localize tabs.settings.label}}</legend>
<span>{{localize "DAGGERHEART.Sheets.DomainCard.Type"}}</span>
{{formField systemFields.type value=source.system.type localize=true blank=""}}
{{formField systemFields.type value=source.system.type localize=true}}
<span>{{localize "DAGGERHEART.Sheets.DomainCard.Foundation"}}</span>
{{formField systemFields.foundation value=source.system.foundation }}
<span>{{localize "DAGGERHEART.Sheets.DomainCard.Domain"}}</span>
{{formField systemFields.domain value=source.system.domain localize=true blank=""}}
{{formField systemFields.domain value=source.system.domain localize=true}}
<span>{{localize "DAGGERHEART.Sheets.DomainCard.Level"}}</span>
{{formField systemFields.level value=source.system.level data-dtype="Number"}}
<span>{{localize "DAGGERHEART.Sheets.DomainCard.RecallCost"}}</span>

View file

@ -4,7 +4,7 @@
<h1 class='item-name'><input type='text' name='name' value='{{source.name}}' /></h1>
<div class='item-description'>
<h3>{{localize 'TYPES.Item.subclass'}}</h3>
<h3>{{localize (concat 'DAGGERHEART.Abilities.' source.system.spellcastingTrait '.name')}}</h3>
{{#if source.system.spellcastingTrait}}<h3>{{localize (concat 'DAGGERHEART.Abilities.' source.system.spellcastingTrait '.name')}}</h3>{{/if}}
</div>
</div>
</header>