Merge branch 'main' into feature/granular-action-outcomes

This commit is contained in:
WBHarry 2026-05-10 18:21:10 +02:00
commit f260d221a8
36 changed files with 540 additions and 348 deletions

View file

@ -2443,6 +2443,7 @@
"single": "Miss",
"plural": "Miss"
},
"missingX": "Missing {x}",
"maxWithThing": "Max {thing}",
"missingDragDropThing": "Drop {thing} here",
"multiclass": "Multiclass",
@ -2533,6 +2534,9 @@
"recovery": { "label": "Recovery" },
"type": { "label": "Type" },
"value": { "label": "Value" }
},
"identifier": {
"label": "Identifier"
}
},
"Ancestry": {
@ -2823,6 +2827,15 @@
"hideObserverPermissionInChat": {
"label": "Hide Chat Info From Players",
"hint": "Information such as hit/miss on attack rolls against adversaries will be hidden"
},
"hidePartyStats": {
"label": "Hide Party Stats",
"hint": "Resources and stats in the party sheet's member list will be hidden to the following users, even if the user is part of the same party",
"choices": {
"never": "Never, always show",
"players": "Hide From Players",
"always": "Hide from Everyone"
}
}
}
},
@ -3211,7 +3224,6 @@
"subclassesAlreadyPresent": "You already have a class and multiclass subclass",
"noDiceSystem": "Your selected dice {system} does not have a {faces} dice",
"gmMenuRefresh": "You refreshed all actions and resources {types}",
"subclassAlreadyLinked": "{name} is already a subclass in the class {class}. Remove it from there if you want it to be a subclass to this class.",
"gmRequired": "This action requires an online GM",
"gmOnly": "This can only be accessed by the GM",
"noActorOwnership": "You do not have permissions for this character",

View file

@ -439,10 +439,13 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
'system.domain': { key: 'system.domain', value: this.setup.class?.system.domains ?? null }
};
if (type === 'subclasses')
if (type === 'subclasses') {
const classItem = this.setup.class;
const uuid = classItem?._stats.compendiumSource ?? classItem?.uuid;
presets.filter = {
'system.linkedClass.uuid': { key: 'system.linkedClass.uuid', value: this.setup.class?.uuid }
'system.linkedClass': { key: 'system.linkedClass', value: uuid }
};
}
if (equipment.includes(type))
presets.filter = {
@ -610,7 +613,8 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
[foundry.utils.randomID()]: {}
};
} else if (item.type === 'subclass' && event.target.closest('.subclass-card')) {
if (this.setup.class.system.subclasses.every(subclass => subclass.uuid !== item.uuid)) {
const classSubclasses = await this.setup.class.system.fetchSubclasses();
if (classSubclasses.every(subclass => subclass.uuid !== item.uuid)) {
ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.subclassNotInClass'));
return;
}

View file

@ -209,8 +209,9 @@ export default class CharacterSheet extends DHBaseActorSheet {
context.attributes = Object.keys(this.document.system.traits).reduce((acc, key) => {
acc[key] = {
...this.document.system.traits[key],
name: game.i18n.localize(CONFIG.DH.ACTOR.abilities[key].name),
verbs: CONFIG.DH.ACTOR.abilities[key].verbs.map(x => game.i18n.localize(x))
label: _loc(CONFIG.DH.ACTOR.abilities[key].label),
verbs: CONFIG.DH.ACTOR.abilities[key].verbs.map(x => game.i18n.localize(x)),
isSpellcasting: this.document.system.spellcastModifierTrait?.key === key
};
return acc;
@ -227,7 +228,7 @@ export default class CharacterSheet extends DHBaseActorSheet {
context.resources.stress.max < maxResource ? maxResource - context.resources.stress.max : 0;
context.equippedItems = sortBy(
this.document.items.filter(i => i.system.equipped),
this.document.items.filter(i => i.system.equipped && (i.type === 'weapon' || i.usable)),
i => (i.type === 'weapon' ? (i.system.secondary ? 1 : 0) : 2)
);

View file

@ -85,6 +85,14 @@ export default class Party extends DHBaseActorSheet {
/* Prepare Context */
/* -------------------------------------------- */
async _prepareContext(options) {
const context = await super._prepareContext(options);
const settings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Metagaming);
context.showStats =
settings.hidePartyStats === 'never' || (settings.hidePartyStats === 'players' && game.user.isGM);
return context;
}
async _preparePartContext(partId, context, options) {
context = await super._preparePartContext(partId, context, options);
switch (partId) {

View file

@ -104,9 +104,10 @@ export default class ClassSheet extends DHBaseItemSheet {
}
/**@inheritdoc */
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
async _prepareContext(options) {
const context = await super._prepareContext(options);
context.domains = this.document.system.domains;
context.subclasses = await this.document.system.fetchSubclasses();
return context;
}
@ -128,20 +129,8 @@ export default class ClassSheet extends DHBaseItemSheet {
const item = await fromUuid(data.uuid);
const itemType = data.type === 'ActiveEffect' ? data.type : item.type;
const target = event.target.closest('fieldset.drop-section');
if (itemType === 'subclass') {
if (item.system.linkedClass) {
return ui.notifications.warn(
game.i18n.format('DAGGERHEART.UI.Notifications.subclassAlreadyLinked', {
name: item.name,
class: this.document.name
})
);
}
await item.update({ 'system.linkedClass': this.document.uuid });
await this.document.update({
'system.subclasses': [...this.document.system.subclasses.map(x => x.uuid), item.uuid]
});
} else if (['feature', 'ActiveEffect'].includes(itemType)) {
if (['feature', 'ActiveEffect'].includes(itemType)) {
super._onDrop(event);
} else if (this.document.parent?.type !== 'character') {
if (itemType === 'weapon') {
@ -200,12 +189,6 @@ export default class ClassSheet extends DHBaseItemSheet {
static async #removeItemFromCollection(_event, element) {
const { uuid, target } = element.dataset;
const prop = foundry.utils.getProperty(this.document.system, target);
if (target === 'subclasses') {
const subclass = await foundry.utils.fromUuid(uuid);
await subclass?.update({ 'system.linkedClass': null });
}
await this.document.update({ [`system.${target}`]: prop.filter(i => i && i.uuid !== uuid).map(x => x.uuid) });
}

View file

@ -40,4 +40,36 @@ export default class SubclassSheet extends DHBaseItemSheet {
get relatedDocs() {
return this.document.system.features.map(x => x.item);
}
async _prepareContext(options) {
const context = await super._prepareContext(options);
if (this.document.system.linkedClass) {
const classData = await fromUuid(this.document.system.linkedClass);
context.class = classData ?? {
name: _loc('DAGGERHEART.GENERAL.missingX', { x: _loc('TYPES.Item.class') }),
missing: true
};
}
return context;
}
async _onDrop(event) {
event.stopPropagation();
const data = TextEditor.getDragEventData(event);
const item = await fromUuid(data.uuid);
const itemType = data.type === 'ActiveEffect' ? data.type : item.type;
if (itemType === 'class') {
const uuid = item._stats.compendiumSource ?? item.uuid;
if (this.document.system.linkedClass !== uuid) {
await this.document.update({ 'system.linkedClass': uuid });
// Re-render all class sheets for instant feedback
for (const app of foundry.applications.instances.values()) {
if (app.document?.type === 'class') app.render();
}
}
return;
}
return super._onDrop(event);
}
}

View file

@ -277,7 +277,7 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
(await foundry.applications.ux.TextEditor.implementation.enrichHTML(item.description));
}
this.fieldFilter = this._createFieldFilter();
this.fieldFilter = await this._createFieldFilter();
if (this.presets?.filter) {
Object.entries(this.presets.filter).forEach(([k, v]) => {
@ -355,12 +355,12 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
);
}
_createFieldFilter() {
async _createFieldFilter() {
const filters = ItemBrowser.getFolderConfig(this.selectedMenu.data, 'filters');
filters.forEach(f => {
for (const f of filters) {
if (typeof f.field === 'string') f.field = foundry.utils.getProperty(game, f.field);
else if (typeof f.choices === 'function') {
f.choices = f.choices(this.items);
f.choices = await f.choices(this.items);
}
// Clear field label so template uses our custom label parameter
@ -370,7 +370,8 @@ export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
f.name ??= f.key;
f.value = this.presets?.filter?.[f.name]?.value ?? null;
});
}
return filters;
}

View file

@ -383,7 +383,8 @@ export const typeConfig = {
{
key: 'system.linkedClass',
label: 'TYPES.Item.class',
format: linkedClass => linkedClass?.name ?? 'DAGGERHEART.UI.ItemBrowser.missing'
format: linkedClass =>
foundry.utils.fromUuidSync(linkedClass)?.name ?? 'DAGGERHEART.UI.ItemBrowser.missing'
},
{
key: 'system.spellcastingTrait',
@ -393,15 +394,18 @@ export const typeConfig = {
],
filters: [
{
key: 'system.linkedClass.uuid',
key: 'system.linkedClass',
label: 'TYPES.Item.class',
choices: items => {
const list = items
.filter(item => item.system.linkedClass)
.map(item => ({
value: item.system.linkedClass.uuid,
label: item.system.linkedClass.name
}));
choices: async items => {
const list = [];
for (const item of items.filter(item => item.system.linkedClass)) {
const linkedClass = await foundry.utils.fromUuid(item.system.linkedClass);
list.push({
value: linkedClass.uuid,
label: linkedClass.name
});
}
return list.reduce((a, c) => {
if (!a.find(i => i.value === c.value)) a.push(c);
return a;

View file

@ -200,7 +200,7 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
const features = [];
for (let f of this.features) {
const fBase = f.item ?? f;
const feature = fBase.system ? fBase : await foundry.utils.fromUuid(fBase.uuid);
const feature = fBase.pack ? await foundry.utils.fromUuid(fBase.uuid) : fBase;
features.push(
foundry.utils.mergeObject(
feature.toObject(),

View file

@ -30,7 +30,6 @@ export default class DHClass extends BaseDataItem {
}),
evasion: new fields.NumberField({ initial: 0, integer: true, label: 'DAGGERHEART.GENERAL.evasion' }),
features: new ItemLinkFields(),
subclasses: new ForeignDocumentUUIDArrayField({ type: 'Item', required: false }),
inventory: new fields.SchemaField({
take: new ForeignDocumentUUIDArrayField({ type: 'Item', required: false }),
choiceA: new ForeignDocumentUUIDArrayField({ type: 'Item', required: false }),
@ -70,6 +69,24 @@ export default class DHClass extends BaseDataItem {
return this.features.filter(x => x.type === CONFIG.DH.ITEM.featureSubTypes.class).map(x => x.item);
}
async fetchSubclasses() {
const uuids = [this.parent.uuid, this.parent._stats?.compendiumSource].filter(u => !!u);
const subclasses = game.items.filter(x => x.type === 'subclass' && uuids.includes(x.system.linkedClass));
for (const pack of game.packs) {
const indexes = await pack.getIndex({ fields: ['system.linkedClass'] });
for (const index of indexes) {
if (index.type !== 'subclass') continue;
if (!uuids.includes(index.system?.linkedClass)) continue;
if (subclasses.find(x => x.uuid === index.uuid)) continue;
const subclass = await foundry.utils.fromUuid(index.uuid);
subclasses.push(subclass);
}
}
return subclasses;
}
async _preCreate(data, options, user) {
if (this.actor?.type === 'character') {
const levelupAuto = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).levelupAuto;

View file

@ -28,7 +28,7 @@ export default class DHSubclass extends BaseDataItem {
features: new ItemLinkFields(),
featureState: new fields.NumberField({ required: true, initial: 1, min: 1 }),
isMulticlass: new fields.BooleanField({ initial: false }),
linkedClass: new ForeignDocumentUUIDField({ type: 'Item', nullable: true, initial: null })
linkedClass: new fields.DocumentUUIDField({ type: 'Item', nullable: true, initial: null })
};
}
@ -83,7 +83,8 @@ export default class DHSubclass extends BaseDataItem {
ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.missingClass'));
return false;
}
if (actorClass.system.subclasses.every(x => x.uuid !== dataUuid)) {
if ((await actorClass.system.fetchSubclasses()).every(x => x.uuid !== dataUuid)) {
ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.subclassNotInClass'));
return false;
}

View file

@ -1,4 +1,5 @@
import { defaultRestOptions } from '../../config/generalConfig.mjs';
import { resetAndRerenderActors } from '../../helpers/utils.mjs';
import { ActionsField } from '../fields/actionField.mjs';
const currencyField = (initial, label, icon) =>
@ -209,7 +210,7 @@ export default class DhHomebrew extends foundry.abstract.DataModel {
}
this.refreshConfig();
this.#resetActors();
resetAndRerenderActors();
}
/** Update config values based on homebrew data. Make sure the references don't change */
@ -230,29 +231,6 @@ export default class DhHomebrew extends foundry.abstract.DataModel {
});
}
}
/**
* Triggers a reset and non-forced re-render on all given actors (if given)
* or all world actors and actors in all scenes to show immediate results for a changed setting.
*/
#resetActors() {
const actors = new Set(
[
game.actors.contents,
game.scenes.contents.flatMap(s => s.tokens.contents).flatMap(t => t.actor ?? [])
].flat()
);
for (const actor of actors) {
for (const app of Object.values(actor.apps)) {
for (const element of app.element?.querySelectorAll('prose-mirror.active')) {
element.open = false; // This triggers a save
}
}
actor.reset();
actor.render();
}
}
}
export class Resource extends foundry.abstract.DataModel {

View file

@ -1,3 +1,5 @@
import { resetAndRerenderActors } from '../../helpers/utils.mjs';
export default class DhMetagaming extends foundry.abstract.DataModel {
static defineSchema() {
const fields = foundry.data.fields;
@ -6,7 +8,24 @@ export default class DhMetagaming extends foundry.abstract.DataModel {
initial: false,
label: 'DAGGERHEART.SETTINGS.Metagaming.FIELDS.hideObserverPermissionInChat.label',
hint: 'DAGGERHEART.SETTINGS.Metagaming.FIELDS.hideObserverPermissionInChat.hint'
}),
hidePartyStats: new fields.StringField({
initial: 'never',
label: 'DAGGERHEART.SETTINGS.Metagaming.FIELDS.hidePartyStats.label',
hint: 'DAGGERHEART.SETTINGS.Metagaming.FIELDS.hidePartyStats.hint',
required: true,
nullable: false,
choices: {
never: 'DAGGERHEART.SETTINGS.Metagaming.FIELDS.hidePartyStats.choices.never',
players: 'DAGGERHEART.SETTINGS.Metagaming.FIELDS.hidePartyStats.choices.players',
always: 'DAGGERHEART.SETTINGS.Metagaming.FIELDS.hidePartyStats.choices.always'
}
})
};
}
/** Invoked by the setting when data changes */
handleChange() {
resetAndRerenderActors();
}
}

View file

@ -141,8 +141,8 @@ export default class DHRoll extends Roll {
const metagamingSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Metagaming);
const chatData = await this._prepareChatRenderContext({ flavor, isPrivate, ...options });
return foundry.applications.handlebars.renderTemplate(template, {
...chatData,
roll: this,
...chatData,
parent: chatData.parent,
targetMode: chatData.targetMode,
areas: chatData.action?.areas,

View file

@ -793,6 +793,26 @@ export function getArmorSources(actor) {
});
}
/**
* Triggers a reset and non-forced re-render on all given actors (if given)
* or all world actors and actors in all scenes to show immediate results for a changed setting.
*/
export function resetAndRerenderActors() {
const actors = new Set(
[game.actors.contents, game.scenes.contents.flatMap(s => s.tokens.contents).flatMap(t => t.actor ?? [])].flat()
);
for (const actor of actors) {
for (const app of Object.values(actor.apps)) {
for (const element of app.element?.querySelectorAll('prose-mirror.active')) {
element.open = false; // This triggers a save
}
}
actor.reset();
actor.render();
}
}
/**
* Returns an array sorted by a function that returns a thing to compare, or an array to compare in order
* Similar to lodash's sortBy function.
@ -841,3 +861,11 @@ export function createShallowProxy(obj) {
}
});
}
export function camelize(str) {
return str
.replace(/(?:^\w|[A-Z]|\b\w)/g, (part, index) => {
return index === 0 ? part.toLowerCase() : part.toUpperCase();
})
.replace(/\s+/g, '');
}

View file

@ -91,7 +91,10 @@ const registerMenuSettings = () => {
game.settings.register(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Metagaming, {
scope: 'world',
config: false,
type: DhMetagaming
type: DhMetagaming,
onChange: value => {
value.handleChange();
}
});
game.settings.register(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, {

View file

@ -251,8 +251,7 @@
a:hover,
a.active {
font-weight: bold;
text-shadow: 0 0 8px light-dark(@dark-blue, @golden);
text-shadow: 0 0 1px currentColor, 0 0 1px currentColor, 0 0 8px light-dark(@dark-blue, @golden);
}
fieldset {

View file

@ -19,28 +19,36 @@
&:last-child {
margin-bottom: 0px;
}
.feature-line {
display: grid;
}
.feature-line {
display: grid;
align-items: center;
grid-template-columns: 1fr 4fr 1fr;
h4 {
font-weight: lighter;
color: light-dark(@dark, @beige);
}
.image {
height: 40px;
width: 40px;
object-fit: cover;
border-radius: 6px;
border: none;
}
.image-icon {
font-size: 26px;
width: 40px;
height: 40px;
display: flex;
justify-content: center;
align-items: center;
grid-template-columns: 1fr 4fr 1fr;
h4 {
font-weight: lighter;
color: light-dark(@dark, @beige);
}
.image {
height: 40px;
width: 40px;
object-fit: cover;
border-radius: 6px;
border: none;
}
.controls {
display: flex;
justify-content: center;
gap: 10px;
a {
text-shadow: none;
}
}
.controls {
display: flex;
justify-content: center;
gap: 10px;
a {
text-shadow: none;
}
}
}

View file

@ -5,20 +5,12 @@
// Theme header backgrounds
.appTheme({
.character-header-sheet {
.trait {
background: url(../assets/svg/trait-shield.svg) no-repeat;
}
.character-row .domains-section img {
filter: @golden-filter;
}
}
}, {
.character-header-sheet {
.trait {
background: url('../assets/svg/trait-shield-light.svg') no-repeat;
}
.character-row .domains-section img {
filter: brightness(0) saturate(100%);
}
@ -33,13 +25,19 @@
.name-row {
display: flex;
gap: 5px;
gap: 6px;
align-items: start;
justify-content: space-between;
padding: 0;
padding-top: 5px;
flex: 1;
[contenteditable],
input {
border: 1px solid @soft-shadow;
background-color: light-dark(@dark-15, @soft-white-shadow);
}
h1 {
display: flex;
flex: 1;
@ -65,14 +63,16 @@
.label {
display: flex;
align-items: center;
align-items: baseline;
gap: 4px;
}
input {
border: none;
width: 40px;
padding: 0;
text-align: center;
font-weight: 600;
}
.level-button {
@ -101,7 +101,7 @@
display: flex;
justify-content: space-between;
padding: 5px 0;
margin-bottom: 10px;
margin-bottom: 8px;
font-size: var(--font-size-12);
color: light-dark(@dark-blue, @golden);
@ -131,7 +131,7 @@
display: flex;
align-items: center;
padding: 0;
margin-bottom: 15px;
margin-bottom: 12px;
.resource-section {
display: flex;
@ -168,7 +168,7 @@
.domains-section {
position: relative;
display: flex;
gap: 10px;
gap: 4px;
background-color: light-dark(transparent, @dark-blue);
color: light-dark(@dark-blue, @golden);
padding: 5px 10px;
@ -183,6 +183,7 @@
font-weight: bold;
text-transform: uppercase;
color: light-dark(@dark-blue, @golden);
margin-right: 4px;
}
.domain {
@ -217,37 +218,110 @@
.character-traits {
display: flex;
justify-content: space-between;
padding: 0;
margin-bottom: 15px;
justify-content: space-between;
max-width: 38.5rem;
gap: 0.5rem;
padding-left: 0.5rem;
.trait {
height: 60px;
width: 60px;
--color-border: light-dark(@semi-transparent-dark-blue, @golden-60);
cursor: pointer;
position: relative;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
min-width: 4.375rem;
.trait-name {
position: relative;
background-color: light-dark(@semi-transparent-dark-blue, @golden-40);
border: 1px solid var(--color-border);
border-radius: 3px;
color: light-dark(var(--color-light-1), @golden);
font-size: var(--font-size-12);
font-weight: 600;
height: 1rem;
line-height: 1rem;
white-space: nowrap;
width: 100%;
padding: 0 0.1876px 0 0.375rem;
margin-right: 0.125rem; /* makes it center SLIGHTLY */
text-shadow: 1px 1px 2px @light-black;
display: flex;
align-items: center;
padding-top: 5px;
color: light-dark(@dark-blue, @golden);
font-size: var(--font-size-14);
font-weight: 600;
align-items: center;
justify-content: center;
gap: 3px;
i {
line-height: 17px;
font-size: var(--font-size-10);
.tier-mark {
position: absolute;
background-color: @dark-blue;
border: 1px solid light-dark(@dark-blue, @golden);
border-radius: 50%;
width: 1rem;
height: 1rem;
right: calc(100% - 0.4375rem);
display: flex;
justify-content: center;
align-items: center;
&.marked::before {
content: ' ';
position: absolute;
width: 0.5rem;
height: 0.5rem;
border-radius: 50%;
background-color: @golden;
}
}
}
.trait-value {
font-style: normal;
font-weight: 400;
font-size: var(--font-size-20);
text-align: center;
.trait-value-area {
--background: light-dark(#e8e6e3, @dark-blue);
display: flex;
position: relative;
.trait-value {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
font-style: normal;
font-weight: 600;
font-size: var(--font-size-20);
text-align: center;
margin-bottom: 0.375rem;
}
.spellcasting-mark {
position: absolute;
border: 1px solid light-dark(@dark-blue, @golden);
color: @golden;
left: 0;
right: 0;
bottom: -0.375rem;
margin-inline: auto;
border-radius: 50%;
width: 1.125rem;
height: 1.125rem;
background: radial-gradient(190.63% 190.63% at 50% -80.63%, #18152E 70%, #4D4494 80%, #A0837E 90%, var(--color-border) 100%);
font-size: var(--font-size-9);
text-shadow: 0 0 2px @light-black;
display: flex;
align-items: center;
justify-content: center;
}
}
&:hover {
.trait-name {
color: light-dark(@dark, @beige);
text-shadow: 0 0 8px light-dark(@dark-80, @beige-80);
}
}
}
}

View file

@ -74,62 +74,6 @@
.death-roll-btn {
display: none;
}
.icons-list {
position: absolute;
display: flex;
flex-direction: column;
gap: 5px;
align-items: end;
justify-content: center;
top: 45px;
right: 10px;
.spellcast-icon {
display: flex;
align-items: center;
justify-content: end;
text-align: center;
padding-right: 8px;
max-width: 50px;
height: 50px;
font-size: 1.2rem;
background: light-dark(@dark-blue-60, @dark-golden-80);
backdrop-filter: blur(8px);
border: 4px double light-dark(@beige, @golden);
color: light-dark(@beige, @golden);
border-radius: 999px;
transition: all 0.3s ease;
.spellcast-label {
font-size: var(--font-size-14);
opacity: 0;
margin-right: 0.3rem;
transition: all 0.3s ease;
}
i {
height: 24px;
width: 24px;
align-content: center;
margin-right: 3px;
}
&:not(.no-label):hover {
max-width: 300px;
padding: 0 10px;
border-radius: 60px;
.spellcast-label {
opacity: 1;
}
i {
margin-right: 0px;
}
}
}
}
}
.info-section {

View file

@ -280,6 +280,17 @@ body.game:is(.performance-low, .noblur) {
}
}
.actors-list.limited {
.actor-resources {
display: flex;
align-items: center;
}
.actor-img-frame {
width: 3rem;
height: 3rem;
}
}
.actors-dragger {
display: flex;
align-items: center;

View file

@ -10,4 +10,8 @@
font-family: @font-body;
color: light-dark(@chat-blue-bg, @beige-50);
}
button.plain.inline-control {
flex: 0 0 auto;
}
}

View file

@ -5,6 +5,7 @@
--golden: #f3c267;
--golden-10: #f3c26710;
--golden-40: #f3c26740;
--golden-60: #f3c26760;
--golden-90: #f3c26790;
--golden-bg: #f3c2671a;
--golden-secondary: #eaaf42;
@ -89,6 +90,7 @@
@golden: var(--golden, #f3c267);
@golden-10: var(--golden-10, #f3c26710);
@golden-40: var(--golden-40, #f3c26740);
@golden-60: var(--golden-60, #f3c26760);
@golden-90: var(--golden-90, #f3c26790);
@golden-bg: var(--golden-bg, #f3c2671a);
@golden-secondary: var(--golden-secondary, #eaaf42);

View file

@ -5,6 +5,11 @@
--dh-font-title: 'Cinzel Decorative';
--dh-font-subtitle: 'Cinzel';
--dh-font-body: 'Montserrat';
/* Include missing font sizes */
--font-size-8: 0.5rem;
--font-size-9: 0.5625rem;
--font-size-22: 1.375rem;
}
@font-title: ~"var(--dh-font-title, 'Cinzel Decorative'), serif";

View file

@ -2,15 +2,15 @@
"id": "daggerheart",
"title": "Daggerheart",
"description": "An unofficial implementation of the Daggerheart system",
"version": "2.2.3",
"version": "2.2.4",
"compatibility": {
"minimum": "14.359",
"verified": "14.360",
"verified": "14.361",
"maximum": "14"
},
"url": "https://github.com/Foundryborne/daggerheart",
"manifest": "https://raw.githubusercontent.com/Foundryborne/daggerheart/v14/system.json",
"download": "https://github.com/Foundryborne/daggerheart/releases/download/2.2.3/system.zip",
"download": "https://github.com/Foundryborne/daggerheart/releases/download/2.2.4/system.zip",
"authors": [
{
"name": "WBHarry"

View file

@ -1,3 +1,4 @@
<div>
{{formGroup settingFields.schema.fields.hideObserverPermissionInChat value=settingFields._source.hideObserverPermissionInChat localize=true}}
{{formGroup settingFields.schema.fields.hidePartyStats value=settingFields._source.hidePartyStats localize=true}}
</div>

View file

@ -1,7 +1,14 @@
<header class='adversary-header-sheet'>
<line-div></line-div>
<div class="name-row">
<h1 class='input actor-name' contenteditable="plaintext-only" data-property="name" placeholder="{{localize "DAGGERHEART.GENERAL.actorName"}}">{{source.name}}</h1>
<h1
class="input actor-name"
contenteditable="plaintext-only"
data-property="name"
placeholder="{{localize "DAGGERHEART.GENERAL.actorName"}}"
autocomplete="off"
spellcheck="false"
>{{source.name}}</h1>
</div>
<div class="tags">
<div class="tag">

View file

@ -1,7 +1,14 @@
<header class="character-header-sheet">
<line-div></line-div>
<div class="name-row">
<h1 class="actor-name input" contenteditable="plaintext-only" data-property="name" placeholder="{{localize "DAGGERHEART.GENERAL.actorName"}}">{{source.name}}</h1>
<h1
class="actor-name input"
contenteditable="plaintext-only"
data-property="name"
placeholder="{{localize "DAGGERHEART.GENERAL.actorName"}}"
autocomplete="off"
spellcheck="false"
>{{source.name}}</h1>
<div class='level-div'>
<h3 class='label'>
{{#if @root.editable}}
@ -125,20 +132,27 @@
<div class="character-traits">
{{#each this.attributes as |attribute key|}}
<div class="trait" data-tooltip="<b>{{localize (concat 'DAGGERHEART.CONFIG.Traits.' key '.name') }}:</b><br>{{#each attribute.verbs}}{{this}}<br>{{/each}}" data-action="rollAttribute" data-attribute="{{key}}" data-value="{{attribute.value}}">
<div class="trait-name">
<span>{{localize (concat 'DAGGERHEART.CONFIG.Traits.' key '.short')}}</span>
{{#if attribute.tierMarked}}
<i class='fa-solid fa-circle'></i>
{{else}}
<i class='fa-regular fa-circle'></i>
{{/if}}
<div class="trait {{#if isSpellcasting}}with-spellcasting{{/if}}" data-tooltip="<b>{{attribute.label}}:</b><br>{{#each attribute.verbs}}{{this}}<br>{{/each}}" data-action="rollAttribute" data-attribute="{{key}}" data-value="{{attribute.value}}">
<div class="trait-name">
<div class="tier-mark {{#if attribute.tierMarked}}marked{{/if}}"></div>
<span>{{attribute.label}}</span>
</div>
<div class="trait-value">
{{#if (gt attribute.value 0)}}
<span>+{{attribute.value}}</span>
{{else}}
<span>{{attribute.value}}</span>
<div class="trait-value-area">
<svg width="52" height="46" viewBox="0 0 52 46" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M 0,0 H 52 L 51.0714,18.254 48.781,24.0952 47.5745,39.9596 26,46 4.42553,39.9596 3.219,24.0952 0.928571,18.254 Z" fill="var(--color-border)"/>
<path d="M 3.312,5.357 8.58596,0 H 43.414 l 5.274,5.357 -1.8797,28.846 c -0.2431,2.605 -2.1461,4.7522 -4.7031,5.3064 l -14.8343,3.2151 c -0.8375,0.1816 -1.7043,0.1816 -2.5418,0 L 9.89482,39.5094 C 7.33785,38.9552 5.43478,36.808 5.19169,34.203 Z" fill="var(--background)"/>
</svg>
<div class="trait-value">
{{#if (gt attribute.value 0)}}
<span>+{{attribute.value}}</span>
{{else}}
<span>{{attribute.value}}</span>
{{/if}}
</div>
{{#if isSpellcasting}}
<div class="spellcasting-mark" data-tooltip="DAGGERHEART.ITEMS.Subclass.spellcastingTrait">
<i class="fa-solid fa-wand-magic-sparkles" inert></i>
</div>
{{/if}}
</div>
</div>

View file

@ -1,17 +1,6 @@
<aside class="character-sidebar-sheet">
<div class="portrait {{#if isDeath}}death-roll{{/if}}">
<img src="{{document.img}}" alt="{{document.name}}" data-action='editImage' data-edit="img">
{{#if document.system.spellcastModifierTrait.key}}
<div class="icons-list">
<span class="spellcast-icon {{#if isDeath}}no-label{{/if}}">
<span class="spellcast-label">
{{localize "DAGGERHEART.ITEMS.Subclass.spellcastingTrait"}}:
{{localize (concat 'DAGGERHEART.CONFIG.Traits.' document.system.spellcastModifierTrait.key '.short')}}
</span>
<i class="fa-solid fa-wand-sparkles"></i>
</span>
</div>
{{/if}}
<a class="death-roll-btn" data-tooltip="DAGGERHEART.UI.Tooltip.makeDeathMove" {{#if
isDeath}}data-action="makeDeathMove" {{/if}}><i class="fa-solid fa-skull death-save"></i></a>
</div>

View file

@ -6,6 +6,8 @@
name='name'
value='{{document.name}}'
placeholder='{{localize "DAGGERHEART.GENERAL.actorName"}}'
autocomplete="off"
spellcheck="false"
/>
</h1>
{{#if useResourcePips}}

View file

@ -2,7 +2,9 @@
<img class='profile' src='{{source.img}}' data-action='editImage' data-edit='img' />
<div class='item-container'>
<div class="item-info">
<h1 class='item-name'><input type='text' name='name' value='{{source.name}}' /></h1>
<h1 class='item-name'>
<input type='text' name='name' value='{{source.name}}' autocomplete="off" spellcheck="false"/>
</h1>
</div>
</div>
</header>

View file

@ -23,143 +23,164 @@
</button>
</div>
<ul class="actors-list">
{{#each partyMembers as |member id|}}
<li class="actor-resources">
<div class="actor-img-frame">
<img class="actor-img" src="{{member.img}}">
{{#if member.weapons}}
<div class="equipped-weapons">
{{#each member.weapons as |weapon|}}
<img src="{{weapon.img}}" data-tooltip="{{weapon.name}}"/>
{{/each}}
</div>
{{/if}}
{{#if member.evasion includeZero=true}}
<div class="evasion" data-tooltip="DAGGERHEART.GENERAL.evasion">{{member.evasion}}</div>
{{/if}}
{{#if member.difficulty includeZero=true}}
<div class="evasion" data-tooltip="DAGGERHEART.GENERAL.difficulty">{{member.difficulty}}</div>
{{/if}}
{{#unless (eq member.type 'companion')}}
<div class="threshold-section">
<h4 class="threshold-label">{{localize "DAGGERHEART.ACTORS.Party.Thresholds.minor"}}</h4>
<h4 class="threshold-value">{{member.damageThresholds.major}}</h4>
<h4 class="threshold-label">{{localize "DAGGERHEART.ACTORS.Party.Thresholds.major"}}</h4>
<h4 class="threshold-value">{{member.damageThresholds.severe}}</h4>
<h4 class="threshold-label">{{localize "DAGGERHEART.ACTORS.Party.Thresholds.severe"}}</h4>
</div>
{{/unless}}
</div>
<header>
<h2 class="actor-name">
<a data-action="openDocument" data-uuid="{{member.uuid}}">{{member.name}}</a>
<a class="delete-icon" data-action="deletePartyMember" data-uuid="{{member.uuid}}"><i class="fa-regular fa-times" inert></i></a>
</h2>
<div>
{{#unless (or (eq member.type 'companion') (eq member.type 'adversary')) }}
<div class="hope-section">
<h4>{{localize "DAGGERHEART.GENERAL.hope"}}</h4>
{{#times member.resources.hope.max}}
<span class='hope-value' data-action='toggleHope' data-actor-id="{{member.uuid}}" data-value="{{add this 1}}">
{{#if (gte member.resources.hope.value (add this 1))}}
<i class='fa-solid fa-diamond'></i>
{{else}}
<i class='fa-regular fa-circle'></i>
{{/if}}
</span>
{{/times}}
{{#if @root.showStats}}
<ul class="actors-list">
{{#each partyMembers as |member id|}}
<li class="actor-resources">
<div class="actor-img-frame">
<img class="actor-img" src="{{member.img}}">
{{#if member.weapons}}
<div class="equipped-weapons">
{{#each member.weapons as |weapon|}}
<img src="{{weapon.img}}" data-tooltip="{{weapon.name}}"/>
{{/each}}
</div>
{{/if}}
{{#if member.evasion includeZero=true}}
<div class="evasion" data-tooltip="DAGGERHEART.GENERAL.evasion">{{member.evasion}}</div>
{{/if}}
{{#if member.difficulty includeZero=true}}
<div class="evasion" data-tooltip="DAGGERHEART.GENERAL.difficulty">{{member.difficulty}}</div>
{{/if}}
{{#unless (eq member.type 'companion')}}
<div class="threshold-section">
<h4 class="threshold-label">{{localize "DAGGERHEART.ACTORS.Party.Thresholds.minor"}}</h4>
<h4 class="threshold-value">{{member.damageThresholds.major}}</h4>
<h4 class="threshold-label">{{localize "DAGGERHEART.ACTORS.Party.Thresholds.major"}}</h4>
<h4 class="threshold-value">{{member.damageThresholds.severe}}</h4>
<h4 class="threshold-label">{{localize "DAGGERHEART.ACTORS.Party.Thresholds.severe"}}</h4>
</div>
{{/unless}}
</div>
{{#if member.subtitle}}
<span class="subtitle">{{member.subtitle}}</span>
{{/if}}
</header>
<section class="body">
<section class="resources">
{{#unless (eq member.type 'companion') }}
<header>
<h2 class="actor-name">
<a data-action="openDocument" data-uuid="{{member.uuid}}">{{member.name}}</a>
<a class="delete-icon" data-action="deletePartyMember" data-uuid="{{member.uuid}}"><i class="fa-regular fa-times" inert></i></a>
</h2>
<div>
{{#unless (or (eq member.type 'companion') (eq member.type 'adversary')) }}
<div class="hope-section">
<h4>{{localize "DAGGERHEART.GENERAL.hope"}}</h4>
{{#times member.resources.hope.max}}
<span class='hope-value' data-action='toggleHope' data-actor-id="{{member.uuid}}" data-value="{{add this 1}}">
{{#if (gte member.resources.hope.value (add this 1))}}
<i class='fa-solid fa-diamond'></i>
{{else}}
<i class='fa-regular fa-circle'></i>
{{/if}}
</span>
{{/times}}
</div>
{{/unless}}
</div>
{{#if member.subtitle}}
<span class="subtitle">{{member.subtitle}}</span>
{{/if}}
</header>
<section class="body">
<section class="resources">
{{#unless (eq member.type 'companion') }}
<div class="slot-section">
<div class="slot-label" data-tooltip="DAGGERHEART.GENERAL.HitPoints.plural">
<span class="label">
<i class="fa-solid fa-heart" inert></i>
</span>
<span class="value">
<span class="current">{{member.resources.hitPoints.value}}</span>
/
<span class="max">{{member.resources.hitPoints.max}}</span>
</span>
</div>
<div class="slot-bar">
{{#times member.resources.hitPoints.max}}
<span class='slot {{#if (gte member.resources.hitPoints.value (add this 1))}}filled{{/if}}'
data-action='toggleHitPoints' data-actor-id="{{member.uuid}}" data-value="{{add this 1}}">
</span>
{{/times}}
</div>
</div>
{{/unless}}
<div class="slot-section">
<div class="slot-label" data-tooltip="DAGGERHEART.GENERAL.HitPoints.plural">
<div class="slot-label" data-tooltip="DAGGERHEART.GENERAL.stress">
<span class="label">
<i class="fa-solid fa-heart" inert></i>
<i class="fa-solid fa-bolt" inert></i>
</span>
<span class="value">
<span class="current">{{member.resources.hitPoints.value}}</span>
<span class="current">{{member.resources.stress.value}}</span>
/
<span class="max">{{member.resources.hitPoints.max}}</span>
<span class="max">{{member.resources.stress.max}}</span>
</span>
</div>
<div class="slot-bar">
{{#times member.resources.hitPoints.max}}
<span class='slot {{#if (gte member.resources.hitPoints.value (add this 1))}}filled{{/if}}'
data-action='toggleHitPoints' data-actor-id="{{member.uuid}}" data-value="{{add this 1}}">
{{#times member.resources.stress.max}}
<span class='slot {{#if (gte member.resources.stress.value (add this 1))}}filled{{/if}}'
data-action='toggleStress' data-actor-id="{{member.uuid}}" data-value="{{add this 1}}">
</span>
{{/times}}
</div>
</div>
{{/unless}}
<div class="slot-section">
<div class="slot-label" data-tooltip="DAGGERHEART.GENERAL.stress">
<span class="label">
<i class="fa-solid fa-bolt" inert></i>
</span>
<span class="value">
<span class="current">{{member.resources.stress.value}}</span>
/
<span class="max">{{member.resources.stress.max}}</span>
</span>
</div>
<div class="slot-bar">
{{#times member.resources.stress.max}}
<span class='slot {{#if (gte member.resources.stress.value (add this 1))}}filled{{/if}}'
data-action='toggleStress' data-actor-id="{{member.uuid}}" data-value="{{add this 1}}">
</span>
{{/times}}
</div>
</div>
{{#if member.armorScore.max}}
<div class="slot-section">
<div class="slot-label" data-tooltip="DAGGERHEART.GENERAL.armorSlots">
<span class="label">
<i class="fa-solid fa-shield" inert></i>
</span>
<span class="value">
<span class="current">{{member.armorScore.value}}</span>
/
<span class="max">{{member.armorScore.max}}</span>
</span>
</div>
<div class="slot-bar">
{{#times member.armorScore.max}}
<a class='armor-slot' data-action='toggleArmorSlot' data-actor-id="{{member.uuid}}" data-value="{{add this 1}}">
{{#if (gte member.armorScore.value (add this 1))}}
<i class="fa-solid fa-shield"></i>
{{else}}
<i class="fa-solid fa-shield-halved"></i>
{{/if}}
</a>
{{/times}}
{{#if member.armorScore.max}}
<div class="slot-section">
<div class="slot-label" data-tooltip="DAGGERHEART.GENERAL.armorSlots">
<span class="label">
<i class="fa-solid fa-shield" inert></i>
</span>
<span class="value">
<span class="current">{{member.armorScore.value}}</span>
/
<span class="max">{{member.armorScore.max}}</span>
</span>
</div>
<div class="slot-bar">
{{#times member.armorScore.max}}
<a class='armor-slot' data-action='toggleArmorSlot' data-actor-id="{{member.uuid}}" data-value="{{add this 1}}">
{{#if (gte member.armorScore.value (add this 1))}}
<i class="fa-solid fa-shield"></i>
{{else}}
<i class="fa-solid fa-shield-halved"></i>
{{/if}}
</a>
{{/times}}
</div>
</div>
{{/if}}
</section>
{{#if member.traits}}
<div class="traits">
{{#each member.traits as |trait|}}
<span class="trait">
<span class="label">{{trait.label}}</span>
<span class="value">{{trait.value}}</span>
</span>
{{/each}}
</div>
{{/if}}
</section>
{{#if member.traits}}
<div class="traits">
{{#each member.traits as |trait|}}
<span class="trait">
<span class="label">{{trait.label}}</span>
<span class="value">{{trait.value}}</span>
</span>
{{/each}}
</div>
{{/if}}
</section>
</li>
{{/each}}
</ul>
</li>
{{/each}}
</ul>
{{else}}
<ul class="actors-list limited">
{{#each partyMembers as |member id|}}
<li class="actor-resources">
<div class="actor-img-frame">
<img class="actor-img" src="{{member.img}}">
</div>
<header>
<h2 class="actor-name">
<a data-action="openDocument" data-uuid="{{member.uuid}}">{{member.name}}</a>
<a class="delete-icon" data-action="deletePartyMember" data-uuid="{{member.uuid}}"><i class="fa-regular fa-times" inert></i></a>
</h2>
{{#if member.subtitle}}
<span class="subtitle">{{member.subtitle}}</span>
{{/if}}
</header>
</li>
{{/each}}
</ul>
{{/if}}
{{#unless document.system.partyMembers.length}}
<div class="actors-dragger">
<span>{{localize "DAGGERHEART.GENERAL.dropActorsHere"}}</span>

View file

@ -27,10 +27,7 @@
<fieldset>
<legend>{{localize "TYPES.Item.subclass"}}</legend>
<div class="feature-list">
{{#unless source.system.subclasses}}
<div class="drag-area">{{localize "DAGGERHEART.GENERAL.missingDragDropThing" thing=(localize "DAGGERHEART.GENERAL.subclasses")}}</div>
{{/unless}}
{{#each source.system.subclasses as |subclass index|}}
{{#each subclasses as |subclass index|}}
<li class='feature-item'>
<div class='feature-line'>
<img class='image' src='{{subclass.img}}' />
@ -44,16 +41,7 @@
data-item-uuid={{subclass.uuid}}
data-tooltip='{{localize "DAGGERHEART.UI.Tooltip.openItemWorld"}}'
>
<i class="fa-solid fa-globe"></i>
</a>
<a
class='effect-control'
data-action='removeItemFromCollection'
data-target="subclasses"
data-uuid="{{subclass.uuid}}"
data-tooltip='{{localize "CONTROLS.CommonDelete"}}'
>
<i class='fas fa-trash'></i>
<i class="fa-solid fa-globe" inert></i>
</a>
</div>
</div>

View file

@ -3,7 +3,6 @@
<div class='item-info'>
<h1 class='item-name'><input type='text' name='name' value='{{source.name}}' /></h1>
<div class='item-description'>
<h3>{{localize 'TYPES.Item.class'}}</h3>
<h3 class="form-fields domain-section">
<span>{{localize "DAGGERHEART.GENERAL.Domain.plural"}}</span>
<input class="domain-input" value="{{domains}}" />

View file

@ -1,8 +1,10 @@
<div class="item-description-outer-container">
<div class="item-description-container">
<h4>{{localize "DAGGERHEART.ITEMS.Subclass.spellcastTrait"}}</h4>
<span>{{spellcastTrait}}</span>
</div>
{{#if spellcastTrait}}
<div class="item-description-container">
<h4>{{localize "DAGGERHEART.ITEMS.Subclass.spellcastTrait"}}</h4>
<span>{{spellcastTrait}}</span>
</div>
{{/if}}
<div class="item-description-container">
<h4>{{localize "DAGGERHEART.ITEMS.Subclass.foundationFeatures"}}</h4>
{{#each foundationFeatures as | feature |}}

View file

@ -3,6 +3,35 @@
data-tab='{{tabs.features.id}}'
data-group='{{tabs.features.group}}'
>
<fieldset>
<legend>{{localize "TYPES.Item.class"}}</legend>
{{#if class}}
<div class="feature-list">
<li class="feature-line">
{{#if class.missing}}
<i class="fa-solid fa-link-slash hint image-icon" inert></i>
<span class="hint">{{class.name}}</span>
{{else}}
<img class="image" src="{{class.img}}" />
<span>{{class.name}}</span>
<div class='controls'>
<a
class='effect-control'
data-action='editDoc'
data-item-uuid={{class.uuid}}
data-tooltip='{{localize "DAGGERHEART.UI.Tooltip.openItemWorld"}}'
>
<i class="fa-solid fa-globe" inert></i>
</a>
</div>
{{/if}}
</li>
</div>
{{else}}
<div class="drag-area">{{localize "DAGGERHEART.GENERAL.missingDragDropThing" thing=(localize "TYPES.Item.class")}}</div>
{{/if}}
</fieldset>
<fieldset class="drop-section" data-type="foundation">
<legend>
{{localize "DAGGERHEART.GENERAL.Tabs.foundation"}}