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", "single": "Miss",
"plural": "Miss" "plural": "Miss"
}, },
"missingX": "Missing {x}",
"maxWithThing": "Max {thing}", "maxWithThing": "Max {thing}",
"missingDragDropThing": "Drop {thing} here", "missingDragDropThing": "Drop {thing} here",
"multiclass": "Multiclass", "multiclass": "Multiclass",
@ -2533,6 +2534,9 @@
"recovery": { "label": "Recovery" }, "recovery": { "label": "Recovery" },
"type": { "label": "Type" }, "type": { "label": "Type" },
"value": { "label": "Value" } "value": { "label": "Value" }
},
"identifier": {
"label": "Identifier"
} }
}, },
"Ancestry": { "Ancestry": {
@ -2823,6 +2827,15 @@
"hideObserverPermissionInChat": { "hideObserverPermissionInChat": {
"label": "Hide Chat Info From Players", "label": "Hide Chat Info From Players",
"hint": "Information such as hit/miss on attack rolls against adversaries will be hidden" "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", "subclassesAlreadyPresent": "You already have a class and multiclass subclass",
"noDiceSystem": "Your selected dice {system} does not have a {faces} dice", "noDiceSystem": "Your selected dice {system} does not have a {faces} dice",
"gmMenuRefresh": "You refreshed all actions and resources {types}", "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", "gmRequired": "This action requires an online GM",
"gmOnly": "This can only be accessed by the GM", "gmOnly": "This can only be accessed by the GM",
"noActorOwnership": "You do not have permissions for this character", "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 } '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 = { 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)) if (equipment.includes(type))
presets.filter = { presets.filter = {
@ -610,7 +613,8 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl
[foundry.utils.randomID()]: {} [foundry.utils.randomID()]: {}
}; };
} else if (item.type === 'subclass' && event.target.closest('.subclass-card')) { } 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')); ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.subclassNotInClass'));
return; return;
} }

View file

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

View file

@ -85,6 +85,14 @@ export default class Party extends DHBaseActorSheet {
/* Prepare Context */ /* 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) { async _preparePartContext(partId, context, options) {
context = await super._preparePartContext(partId, context, options); context = await super._preparePartContext(partId, context, options);
switch (partId) { switch (partId) {

View file

@ -104,9 +104,10 @@ export default class ClassSheet extends DHBaseItemSheet {
} }
/**@inheritdoc */ /**@inheritdoc */
async _prepareContext(_options) { async _prepareContext(options) {
const context = await super._prepareContext(_options); const context = await super._prepareContext(options);
context.domains = this.document.system.domains; context.domains = this.document.system.domains;
context.subclasses = await this.document.system.fetchSubclasses();
return context; return context;
} }
@ -128,20 +129,8 @@ export default class ClassSheet extends DHBaseItemSheet {
const item = await fromUuid(data.uuid); const item = await fromUuid(data.uuid);
const itemType = data.type === 'ActiveEffect' ? data.type : item.type; const itemType = data.type === 'ActiveEffect' ? data.type : item.type;
const target = event.target.closest('fieldset.drop-section'); const target = event.target.closest('fieldset.drop-section');
if (itemType === 'subclass') {
if (item.system.linkedClass) { if (['feature', 'ActiveEffect'].includes(itemType)) {
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)) {
super._onDrop(event); super._onDrop(event);
} else if (this.document.parent?.type !== 'character') { } else if (this.document.parent?.type !== 'character') {
if (itemType === 'weapon') { if (itemType === 'weapon') {
@ -200,12 +189,6 @@ export default class ClassSheet extends DHBaseItemSheet {
static async #removeItemFromCollection(_event, element) { static async #removeItemFromCollection(_event, element) {
const { uuid, target } = element.dataset; const { uuid, target } = element.dataset;
const prop = foundry.utils.getProperty(this.document.system, target); 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) }); 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() { get relatedDocs() {
return this.document.system.features.map(x => x.item); 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)); (await foundry.applications.ux.TextEditor.implementation.enrichHTML(item.description));
} }
this.fieldFilter = this._createFieldFilter(); this.fieldFilter = await this._createFieldFilter();
if (this.presets?.filter) { if (this.presets?.filter) {
Object.entries(this.presets.filter).forEach(([k, v]) => { 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'); 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); if (typeof f.field === 'string') f.field = foundry.utils.getProperty(game, f.field);
else if (typeof f.choices === 'function') { 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 // 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.name ??= f.key;
f.value = this.presets?.filter?.[f.name]?.value ?? null; f.value = this.presets?.filter?.[f.name]?.value ?? null;
}); }
return filters; return filters;
} }

View file

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

View file

@ -200,7 +200,7 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
const features = []; const features = [];
for (let f of this.features) { for (let f of this.features) {
const fBase = f.item ?? f; 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( features.push(
foundry.utils.mergeObject( foundry.utils.mergeObject(
feature.toObject(), 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' }), evasion: new fields.NumberField({ initial: 0, integer: true, label: 'DAGGERHEART.GENERAL.evasion' }),
features: new ItemLinkFields(), features: new ItemLinkFields(),
subclasses: new ForeignDocumentUUIDArrayField({ type: 'Item', required: false }),
inventory: new fields.SchemaField({ inventory: new fields.SchemaField({
take: new ForeignDocumentUUIDArrayField({ type: 'Item', required: false }), take: new ForeignDocumentUUIDArrayField({ type: 'Item', required: false }),
choiceA: 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); 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) { async _preCreate(data, options, user) {
if (this.actor?.type === 'character') { if (this.actor?.type === 'character') {
const levelupAuto = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Automation).levelupAuto; 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(), features: new ItemLinkFields(),
featureState: new fields.NumberField({ required: true, initial: 1, min: 1 }), featureState: new fields.NumberField({ required: true, initial: 1, min: 1 }),
isMulticlass: new fields.BooleanField({ initial: false }), 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')); ui.notifications.warn(game.i18n.localize('DAGGERHEART.UI.Notifications.missingClass'));
return false; 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')); ui.notifications.error(game.i18n.localize('DAGGERHEART.UI.Notifications.subclassNotInClass'));
return false; return false;
} }

View file

@ -1,4 +1,5 @@
import { defaultRestOptions } from '../../config/generalConfig.mjs'; import { defaultRestOptions } from '../../config/generalConfig.mjs';
import { resetAndRerenderActors } from '../../helpers/utils.mjs';
import { ActionsField } from '../fields/actionField.mjs'; import { ActionsField } from '../fields/actionField.mjs';
const currencyField = (initial, label, icon) => const currencyField = (initial, label, icon) =>
@ -209,7 +210,7 @@ export default class DhHomebrew extends foundry.abstract.DataModel {
} }
this.refreshConfig(); this.refreshConfig();
this.#resetActors(); resetAndRerenderActors();
} }
/** Update config values based on homebrew data. Make sure the references don't change */ /** 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 { 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 { export default class DhMetagaming extends foundry.abstract.DataModel {
static defineSchema() { static defineSchema() {
const fields = foundry.data.fields; const fields = foundry.data.fields;
@ -6,7 +8,24 @@ export default class DhMetagaming extends foundry.abstract.DataModel {
initial: false, initial: false,
label: 'DAGGERHEART.SETTINGS.Metagaming.FIELDS.hideObserverPermissionInChat.label', label: 'DAGGERHEART.SETTINGS.Metagaming.FIELDS.hideObserverPermissionInChat.label',
hint: 'DAGGERHEART.SETTINGS.Metagaming.FIELDS.hideObserverPermissionInChat.hint' 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 metagamingSettings = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Metagaming);
const chatData = await this._prepareChatRenderContext({ flavor, isPrivate, ...options }); const chatData = await this._prepareChatRenderContext({ flavor, isPrivate, ...options });
return foundry.applications.handlebars.renderTemplate(template, { return foundry.applications.handlebars.renderTemplate(template, {
...chatData,
roll: this, roll: this,
...chatData,
parent: chatData.parent, parent: chatData.parent,
targetMode: chatData.targetMode, targetMode: chatData.targetMode,
areas: chatData.action?.areas, 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 * 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. * 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, { game.settings.register(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Metagaming, {
scope: 'world', scope: 'world',
config: false, config: false,
type: DhMetagaming type: DhMetagaming,
onChange: value => {
value.handleChange();
}
}); });
game.settings.register(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, { game.settings.register(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew, {

View file

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

View file

@ -19,6 +19,7 @@
&:last-child { &:last-child {
margin-bottom: 0px; margin-bottom: 0px;
} }
}
.feature-line { .feature-line {
display: grid; display: grid;
align-items: center; align-items: center;
@ -34,6 +35,14 @@
border-radius: 6px; border-radius: 6px;
border: none; border: none;
} }
.image-icon {
font-size: 26px;
width: 40px;
height: 40px;
display: flex;
justify-content: center;
align-items: center;
}
.controls { .controls {
display: flex; display: flex;
justify-content: center; justify-content: center;
@ -46,4 +55,3 @@
} }
} }
} }
}

View file

@ -5,20 +5,12 @@
// Theme header backgrounds // Theme header backgrounds
.appTheme({ .appTheme({
.character-header-sheet { .character-header-sheet {
.trait {
background: url(../assets/svg/trait-shield.svg) no-repeat;
}
.character-row .domains-section img { .character-row .domains-section img {
filter: @golden-filter; filter: @golden-filter;
} }
} }
}, { }, {
.character-header-sheet { .character-header-sheet {
.trait {
background: url('../assets/svg/trait-shield-light.svg') no-repeat;
}
.character-row .domains-section img { .character-row .domains-section img {
filter: brightness(0) saturate(100%); filter: brightness(0) saturate(100%);
} }
@ -33,13 +25,19 @@
.name-row { .name-row {
display: flex; display: flex;
gap: 5px; gap: 6px;
align-items: start; align-items: start;
justify-content: space-between; justify-content: space-between;
padding: 0; padding: 0;
padding-top: 5px; padding-top: 5px;
flex: 1; flex: 1;
[contenteditable],
input {
border: 1px solid @soft-shadow;
background-color: light-dark(@dark-15, @soft-white-shadow);
}
h1 { h1 {
display: flex; display: flex;
flex: 1; flex: 1;
@ -65,14 +63,16 @@
.label { .label {
display: flex; display: flex;
align-items: center; align-items: baseline;
gap: 4px; gap: 4px;
} }
input { input {
border: none;
width: 40px; width: 40px;
padding: 0; padding: 0;
text-align: center; text-align: center;
font-weight: 600;
} }
.level-button { .level-button {
@ -101,7 +101,7 @@
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
padding: 5px 0; padding: 5px 0;
margin-bottom: 10px; margin-bottom: 8px;
font-size: var(--font-size-12); font-size: var(--font-size-12);
color: light-dark(@dark-blue, @golden); color: light-dark(@dark-blue, @golden);
@ -131,7 +131,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
padding: 0; padding: 0;
margin-bottom: 15px; margin-bottom: 12px;
.resource-section { .resource-section {
display: flex; display: flex;
@ -168,7 +168,7 @@
.domains-section { .domains-section {
position: relative; position: relative;
display: flex; display: flex;
gap: 10px; gap: 4px;
background-color: light-dark(transparent, @dark-blue); background-color: light-dark(transparent, @dark-blue);
color: light-dark(@dark-blue, @golden); color: light-dark(@dark-blue, @golden);
padding: 5px 10px; padding: 5px 10px;
@ -183,6 +183,7 @@
font-weight: bold; font-weight: bold;
text-transform: uppercase; text-transform: uppercase;
color: light-dark(@dark-blue, @golden); color: light-dark(@dark-blue, @golden);
margin-right: 4px;
} }
.domain { .domain {
@ -217,37 +218,110 @@
.character-traits { .character-traits {
display: flex; display: flex;
justify-content: space-between;
padding: 0; padding: 0;
margin-bottom: 15px; margin-bottom: 15px;
justify-content: space-between;
max-width: 38.5rem;
gap: 0.5rem;
padding-left: 0.5rem;
.trait { .trait {
height: 60px; --color-border: light-dark(@semi-transparent-dark-blue, @golden-60);
width: 60px;
cursor: pointer; cursor: pointer;
position: relative;
.trait-name {
display: flex; display: flex;
align-items: center; align-items: center;
padding-top: 5px; justify-content: center;
color: light-dark(@dark-blue, @golden); flex-direction: column;
font-size: var(--font-size-14); 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; 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; align-items: center;
justify-content: center; justify-content: center;
gap: 3px;
i { .tier-mark {
line-height: 17px; position: absolute;
font-size: var(--font-size-10); 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-area {
--background: light-dark(#e8e6e3, @dark-blue);
display: flex;
position: relative;
.trait-value { .trait-value {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 600;
font-size: var(--font-size-20); font-size: var(--font-size-20);
text-align: center; 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 { .death-roll-btn {
display: none; 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 { .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 { .actors-dragger {
display: flex; display: flex;
align-items: center; align-items: center;

View file

@ -10,4 +10,8 @@
font-family: @font-body; font-family: @font-body;
color: light-dark(@chat-blue-bg, @beige-50); 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: #f3c267;
--golden-10: #f3c26710; --golden-10: #f3c26710;
--golden-40: #f3c26740; --golden-40: #f3c26740;
--golden-60: #f3c26760;
--golden-90: #f3c26790; --golden-90: #f3c26790;
--golden-bg: #f3c2671a; --golden-bg: #f3c2671a;
--golden-secondary: #eaaf42; --golden-secondary: #eaaf42;
@ -89,6 +90,7 @@
@golden: var(--golden, #f3c267); @golden: var(--golden, #f3c267);
@golden-10: var(--golden-10, #f3c26710); @golden-10: var(--golden-10, #f3c26710);
@golden-40: var(--golden-40, #f3c26740); @golden-40: var(--golden-40, #f3c26740);
@golden-60: var(--golden-60, #f3c26760);
@golden-90: var(--golden-90, #f3c26790); @golden-90: var(--golden-90, #f3c26790);
@golden-bg: var(--golden-bg, #f3c2671a); @golden-bg: var(--golden-bg, #f3c2671a);
@golden-secondary: var(--golden-secondary, #eaaf42); @golden-secondary: var(--golden-secondary, #eaaf42);

View file

@ -5,6 +5,11 @@
--dh-font-title: 'Cinzel Decorative'; --dh-font-title: 'Cinzel Decorative';
--dh-font-subtitle: 'Cinzel'; --dh-font-subtitle: 'Cinzel';
--dh-font-body: 'Montserrat'; --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"; @font-title: ~"var(--dh-font-title, 'Cinzel Decorative'), serif";

View file

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

View file

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

View file

@ -1,7 +1,14 @@
<header class='adversary-header-sheet'> <header class='adversary-header-sheet'>
<line-div></line-div> <line-div></line-div>
<div class="name-row"> <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>
<div class="tags"> <div class="tags">
<div class="tag"> <div class="tag">

View file

@ -1,7 +1,14 @@
<header class="character-header-sheet"> <header class="character-header-sheet">
<line-div></line-div> <line-div></line-div>
<div class="name-row"> <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'> <div class='level-div'>
<h3 class='label'> <h3 class='label'>
{{#if @root.editable}} {{#if @root.editable}}
@ -125,15 +132,16 @@
<div class="character-traits"> <div class="character-traits">
{{#each this.attributes as |attribute key|}} {{#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 {{#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="trait-name">
<span>{{localize (concat 'DAGGERHEART.CONFIG.Traits.' key '.short')}}</span> <div class="tier-mark {{#if attribute.tierMarked}}marked{{/if}}"></div>
{{#if attribute.tierMarked}} <span>{{attribute.label}}</span>
<i class='fa-solid fa-circle'></i>
{{else}}
<i class='fa-regular fa-circle'></i>
{{/if}}
</div> </div>
<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"> <div class="trait-value">
{{#if (gt attribute.value 0)}} {{#if (gt attribute.value 0)}}
<span>+{{attribute.value}}</span> <span>+{{attribute.value}}</span>
@ -141,6 +149,12 @@
<span>{{attribute.value}}</span> <span>{{attribute.value}}</span>
{{/if}} {{/if}}
</div> </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> </div>
{{/each}} {{/each}}
</div> </div>

View file

@ -1,17 +1,6 @@
<aside class="character-sidebar-sheet"> <aside class="character-sidebar-sheet">
<div class="portrait {{#if isDeath}}death-roll{{/if}}"> <div class="portrait {{#if isDeath}}death-roll{{/if}}">
<img src="{{document.img}}" alt="{{document.name}}" data-action='editImage' data-edit="img"> <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 <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> isDeath}}data-action="makeDeathMove" {{/if}}><i class="fa-solid fa-skull death-save"></i></a>
</div> </div>

View file

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

View file

@ -2,7 +2,9 @@
<img class='profile' src='{{source.img}}' data-action='editImage' data-edit='img' /> <img class='profile' src='{{source.img}}' data-action='editImage' data-edit='img' />
<div class='item-container'> <div class='item-container'>
<div class="item-info"> <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>
</div> </div>
</header> </header>

View file

@ -23,6 +23,7 @@
</button> </button>
</div> </div>
{{#if @root.showStats}}
<ul class="actors-list"> <ul class="actors-list">
{{#each partyMembers as |member id|}} {{#each partyMembers as |member id|}}
<li class="actor-resources"> <li class="actor-resources">
@ -160,6 +161,26 @@
</li> </li>
{{/each}} {{/each}}
</ul> </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}} {{#unless document.system.partyMembers.length}}
<div class="actors-dragger"> <div class="actors-dragger">
<span>{{localize "DAGGERHEART.GENERAL.dropActorsHere"}}</span> <span>{{localize "DAGGERHEART.GENERAL.dropActorsHere"}}</span>

View file

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

View file

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

View file

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

View file

@ -3,6 +3,35 @@
data-tab='{{tabs.features.id}}' data-tab='{{tabs.features.id}}'
data-group='{{tabs.features.group}}' 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"> <fieldset class="drop-section" data-type="foundation">
<legend> <legend>
{{localize "DAGGERHEART.GENERAL.Tabs.foundation"}} {{localize "DAGGERHEART.GENERAL.Tabs.foundation"}}