[Feature] 155 - New Adversary Sheet (#228)

* rework adversary templates, add setting dialog to adversary and fix erratas

* fix errata in adversary data model

* developing experience setting page

* Experience settings update

* finish actions setting tab

* Fixed Notes enriched path and adversary-settings form id

* Fixed UseItem and ToChat

* Fixed Firefox progress bar (HP/Stress) (#230)

* insert prose-mirror style and inicial implement to damage settings

* fix character import relative paths and remove effects from standard adversary attack

---------

Co-authored-by: WBHarry <williambjrklund@gmail.com>
Co-authored-by: GyroFalc <43814421+GyroFalc@users.noreply.github.com>
This commit is contained in:
Murilo Brito 2025-07-01 17:07:12 -03:00 committed by GitHub
parent 8b834036fa
commit 5ffb22fcc5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
42 changed files with 2123 additions and 254 deletions

View file

@ -1,6 +1,7 @@
export { default as DhCharacterSheet } from './sheets/character.mjs';
export { default as DhCharacterSheet } from './sheets/actors/character.mjs';
export { default as DhpAdversarySheet } from './sheets/actors/adversary.mjs';
export { default as DhpAdversarySheet } from './sheets/actors/adversary.mjs';
export { default as DhCompanionSheet } from './sheets/companion.mjs';
export { default as DhpAdversarySheet } from './sheets/adversary.mjs';
export { default as DhpClassSheet } from './sheets/items/class.mjs';
export { default as DhpSubclass } from './sheets/items/subclass.mjs';
export { default as DhpFeatureSheet } from './sheets/items/feature.mjs';

View file

@ -1,20 +1,23 @@
import DHActionConfig from '../config/Action.mjs';
import DaggerheartSheet from './daggerheart-sheet.mjs';
import DHActionConfig from '../../config/Action.mjs';
import DaggerheartSheet from '../daggerheart-sheet.mjs';
import DHAdversarySettings from '../applications/adversary-settings.mjs';
const { ActorSheetV2 } = foundry.applications.sheets;
export default class AdversarySheet extends DaggerheartSheet(ActorSheetV2) {
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ['daggerheart', 'sheet', 'actor', 'dh-style', 'adversary'],
position: { width: 450, height: 1000 },
position: { width: 660, height: 766 },
actions: {
reactionRoll: this.reactionRoll,
attackRoll: this.attackRoll,
useItem: this.useItem,
toChat: this.toChat,
attackConfigure: this.attackConfigure,
addExperience: this.addExperience,
removeExperience: this.removeExperience,
toggleHP: this.toggleHP,
toggleStress: this.toggleStress
toggleStress: this.toggleStress,
openSettings: this.openSettings
},
form: {
handler: this.updateForm,
@ -24,28 +27,37 @@ export default class AdversarySheet extends DaggerheartSheet(ActorSheetV2) {
};
static PARTS = {
sidebar: { template: 'systems/daggerheart/templates/sheets/actors/adversary/sidebar.hbs' },
header: { template: 'systems/daggerheart/templates/sheets/actors/adversary/header.hbs' },
tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
main: { template: 'systems/daggerheart/templates/sheets/actors/adversary/main.hbs' },
information: { template: 'systems/daggerheart/templates/sheets/actors/adversary/information.hbs' }
actions: { template: 'systems/daggerheart/templates/sheets/actors/adversary/actions.hbs' },
notes: { template: 'systems/daggerheart/templates/sheets/actors/adversary/notes.hbs' },
effects: { template: 'systems/daggerheart/templates/sheets/actors/adversary/effects.hbs' }
};
static TABS = {
main: {
actions: {
active: true,
cssClass: '',
group: 'primary',
id: 'main',
id: 'actions',
icon: null,
label: 'DAGGERHEART.Sheets.Adversary.Tabs.Main'
label: 'DAGGERHEART.General.tabs.actions'
},
information: {
notes: {
active: false,
cssClass: '',
group: 'primary',
id: 'information',
id: 'notes',
icon: null,
label: 'DAGGERHEART.Sheets.Adversary.Tabs.Information'
label: 'DAGGERHEART.Sheets.Adversary.Tabs.notes'
},
effects: {
active: false,
cssClass: '',
group: 'primary',
id: 'effects',
icon: null,
label: 'DAGGERHEART.Sheets.Adversary.Tabs.effects'
}
};
@ -56,10 +68,15 @@ export default class AdversarySheet extends DaggerheartSheet(ActorSheetV2) {
context.systemFields.attack.fields = this.document.system.attack.schema.fields;
context.getEffectDetails = this.getEffectDetails.bind(this);
context.isNPC = true;
console.log(context)
return context;
}
getAction(element) {
const itemId = (element.target ?? element).closest('[data-item-id]').dataset.itemId,
item = this.document.system.actions.find(x => x.id === itemId);
return item;
}
static async updateForm(event, _, formData) {
await this.document.update(formData.object);
this.render();
@ -86,8 +103,40 @@ export default class AdversarySheet extends DaggerheartSheet(ActorSheetV2) {
return {};
}
static async attackRoll(event) {
this.actor.system.attack.use(event);
static async openSettings() {
await new DHAdversarySettings(this.document).render(true);
}
static async useItem(event) {
const action = this.getAction(event) ?? this.actor.system.attack;
action.use(event);
}
static async toChat(event, button) {
if (button?.dataset?.type === 'experience') {
const experience = this.document.system.experiences[button.dataset.uuid];
const cls = getDocumentClass('ChatMessage');
const systemData = {
name: game.i18n.localize('DAGGERHEART.General.Experience.Single'),
description: `${experience.name} ${
experience.modifier < 0 ? experience.modifier : `+${experience.modifier}`
}`
};
const msg = new cls({
type: 'abilityUse',
user: game.user.id,
system: systemData,
content: await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/chat/ability-use.hbs',
systemData
)
});
cls.create(msg.toObject());
} else {
const item = this.getAction(event) ?? this.document.system.attack;
item.toChat(this.document.id);
}
}
static async attackConfigure(event) {

View file

@ -1,11 +1,11 @@
import { capitalize } from '../../helpers/utils.mjs';
import DhpDeathMove from '../deathMove.mjs';
import DhpDowntime from '../downtime.mjs';
import AncestrySelectionDialog from '../ancestrySelectionDialog.mjs';
import DaggerheartSheet from './daggerheart-sheet.mjs';
import { abilities } from '../../config/actorConfig.mjs';
import DhCharacterlevelUp from '../levelup/characterLevelup.mjs';
import DhCharacterCreation from '../characterCreation.mjs';
import { capitalize } from '../../../helpers/utils.mjs';
import DhpDeathMove from '../../deathMove.mjs';
import DhpDowntime from '../../downtime.mjs';
import AncestrySelectionDialog from '../../ancestrySelectionDialog.mjs';
import DaggerheartSheet from '.././daggerheart-sheet.mjs';
import { abilities } from '../../../config/actorConfig.mjs';
import DhCharacterlevelUp from '../../levelup/characterLevelup.mjs';
import DhCharacterCreation from '../../characterCreation.mjs';
const { ActorSheetV2 } = foundry.applications.sheets;
const { TextEditor } = foundry.applications.ux;
@ -727,9 +727,7 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) {
const cls = getDocumentClass('ChatMessage');
const systemData = {
name: game.i18n.localize('DAGGERHEART.General.Experience.Single'),
description: `${experience.description} ${
experience.total < 0 ? experience.total : `+${experience.total}`
}`
description: `${experience.name} ${experience.total < 0 ? experience.total : `+${experience.total}`}`
};
const msg = new cls({
type: 'abilityUse',

View file

@ -0,0 +1,181 @@
import DHActionConfig from '../../config/Action.mjs';
import DHBaseItemSheet from '../api/base-item.mjs';
import { actionsTypes } from '../../../data/_module.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class DHAdversarySettings extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(actor) {
super({});
this.actor = actor;
}
get title() {
return `${game.i18n.localize('DAGGERHEART.Sheets.TABS.settings')}`;
}
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ['daggerheart', 'dh-style', 'dialog', 'adversary-settings'],
window: {
icon: 'fa-solid fa-wrench',
resizable: false
},
position: { width: 455, height: 'auto' },
actions: {
addExperience: this.#addExperience,
removeExperience: this.#removeExperience,
addAction: this.#addAction,
editAction: this.#editAction,
removeAction: this.#removeAction
},
form: {
handler: this.updateForm,
submitOnChange: true,
closeOnSubmit: false
}
};
static PARTS = {
header: {
id: 'header',
template: 'systems/daggerheart/templates/sheets/applications/adversary-settings/header.hbs'
},
tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' },
details: {
id: 'details',
template: 'systems/daggerheart/templates/sheets/applications/adversary-settings/details.hbs'
},
attack: {
id: 'attack',
template: 'systems/daggerheart/templates/sheets/applications/adversary-settings/attack.hbs'
},
experiences: {
id: 'experiences',
template: 'systems/daggerheart/templates/sheets/applications/adversary-settings/experiences.hbs'
},
actions: {
id: 'actions',
template: 'systems/daggerheart/templates/sheets/applications/adversary-settings/actions.hbs'
}
};
static TABS = {
details: {
active: true,
cssClass: '',
group: 'primary',
id: 'details',
icon: null,
label: 'DAGGERHEART.General.tabs.details'
},
attack: {
active: false,
cssClass: '',
group: 'primary',
id: 'attack',
icon: null,
label: 'DAGGERHEART.General.tabs.attack'
},
experiences: {
active: false,
cssClass: '',
group: 'primary',
id: 'experiences',
icon: null,
label: 'DAGGERHEART.General.tabs.experiences'
},
actions: {
active: false,
cssClass: '',
group: 'primary',
id: 'actions',
icon: null,
label: 'DAGGERHEART.General.tabs.actions'
}
};
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.document = this.actor;
context.tabs = this._getTabs(this.constructor.TABS);
context.systemFields = this.actor.system.schema.fields;
context.systemFields.attack.fields = this.actor.system.attack.schema.fields;
context.isNPC = true;
return context;
}
_getTabs(tabs) {
for (const v of Object.values(tabs)) {
v.active = this.tabGroups[v.group] ? this.tabGroups[v.group] === v.id : v.active;
v.cssClass = v.active ? 'active' : '';
}
return tabs;
}
static async #addExperience() {
const newExperience = {
name: 'Experience',
modifier: 0
};
await this.actor.update({ [`system.experiences.${foundry.utils.randomID()}`]: newExperience });
this.render();
}
static async #removeExperience(_, target) {
await this.actor.update({ [`system.experiences.-=${target.dataset.experience}`]: null });
this.render();
}
static async #addAction(_event, _button) {
const actionType = await DHBaseItemSheet.selectActionType();
if (!actionType) return;
try {
const cls = actionsTypes[actionType] ?? actionsTypes.attack,
action = new cls(
{
_id: foundry.utils.randomID(),
type: actionType,
name: game.i18n.localize(SYSTEM.ACTIONS.actionTypes[actionType].name),
...cls.getSourceConfig(this.actor)
},
{
parent: this.actor
}
);
console.log(action);
await this.actor.update({ 'system.actions': [...this.actor.system.actions, action] });
await new DHActionConfig(this.actor.system.actions[this.actor.system.actions.length - 1]).render({
force: true
});
this.render();
} catch (error) {
console.log(error);
}
}
static async #editAction(event, target) {
event.stopPropagation();
const actionIndex = target.dataset.index;
await new DHActionConfig(this.actor.system.actions[actionIndex]).render({
force: true
});
}
static async #removeAction(event, target) {
event.stopPropagation();
const actionIndex = target.dataset.index;
await this.actor.update({
'system.actions': this.actor.system.actions.filter((_, index) => index !== Number.parseInt(actionIndex))
});
this.render();
}
static async updateForm(event, _, formData) {
await this.actor.update(formData.object);
this.render();
}
}

View file

@ -1,42 +1,42 @@
export const actionTypes = {
attack: {
id: 'attack',
name: 'DAGGERHEART.Actions.Types.Attack.Name',
name: 'DAGGERHEART.Actions.Types.attack.name',
icon: 'fa-swords'
},
// spellcast: {
// id: 'spellcast',
// name: 'DAGGERHEART.Actions.Types.Spellcast.Name',
// name: 'DAGGERHEART.Actions.Types.spellcast.name',
// icon: 'fa-book-sparkles'
// },
healing: {
id: 'healing',
name: 'DAGGERHEART.Actions.Types.Healing.Name',
name: 'DAGGERHEART.Actions.Types.healing.name',
icon: 'fa-kit-medical'
},
// resource: {
// id: 'resource',
// name: 'DAGGERHEART.Actions.Types.Resource.Name',
// name: 'DAGGERHEART.Actions.Types.resource.name',
// icon: 'fa-honey-pot'
// },
damage: {
id: 'damage',
name: 'DAGGERHEART.Actions.Types.Damage.Name',
name: 'DAGGERHEART.Actions.Types.damage.name',
icon: 'fa-bone-break'
},
summon: {
id: 'summon',
name: 'DAGGERHEART.Actions.Types.Summon.Name',
name: 'DAGGERHEART.Actions.Types.summon.name',
icon: 'fa-ghost'
},
effect: {
id: 'effect',
name: 'DAGGERHEART.Actions.Types.Effect.Name',
name: 'DAGGERHEART.Actions.Types.effect.name',
icon: 'fa-person-rays'
},
macro: {
id: 'macro',
name: 'DAGGERHEART.Actions.Types.Macro.Name',
name: 'DAGGERHEART.Actions.Types.macro.name',
icon: 'fa-scroll'
}
};

View file

@ -83,53 +83,53 @@ export const featureProperties = {
export const adversaryTypes = {
bruiser: {
id: 'bruiser',
label: 'DAGGERHEART.Adversary.Type.Bruiser.label',
description: 'DAGGERHEART.Adversary.Bruiser.Description'
label: 'DAGGERHEART.Adversary.Type.bruiser.label',
description: 'DAGGERHEART.Adversary.bruiser.description'
},
horde: {
id: 'horde',
label: 'DAGGERHEART.Adversary.Type.Horde.label',
description: 'DAGGERHEART.Adversary.Horde.Description'
label: 'DAGGERHEART.Adversary.Type.horde.label',
description: 'DAGGERHEART.Adversary.horde.description'
},
leader: {
id: 'leader',
label: 'DAGGERHEART.Adversary.Type.Leader.label',
description: 'DAGGERHEART.Adversary.Leader.Description'
label: 'DAGGERHEART.Adversary.Type.leader.label',
description: 'DAGGERHEART.Adversary.leader.description'
},
minion: {
id: 'minion',
label: 'DAGGERHEART.Adversary.Type.Minion.label',
description: 'DAGGERHEART.Adversary.Minion.Description'
label: 'DAGGERHEART.Adversary.Type.minion.label',
description: 'DAGGERHEART.Adversary.minion.description'
},
ranged: {
id: 'ranged',
label: 'DAGGERHEART.Adversary.Type.Ranged.label',
description: 'DAGGERHEART.Adversary.Ranged.Description'
label: 'DAGGERHEART.Adversary.Type.ranged.label',
description: 'DAGGERHEART.Adversary.ranged.description'
},
skulk: {
id: 'skulk',
label: 'DAGGERHEART.Adversary.Type.Skulk.label',
description: 'DAGGERHEART.Adversary.Skulk.Description'
label: 'DAGGERHEART.Adversary.Type.skulk.label',
description: 'DAGGERHEART.Adversary.skulk.description'
},
social: {
id: 'social',
label: 'DAGGERHEART.Adversary.Type.Social.label',
description: 'DAGGERHEART.Adversary.Social.Description'
label: 'DAGGERHEART.Adversary.Type.social.label',
description: 'DAGGERHEART.Adversary.social.description'
},
solo: {
id: 'solo',
label: 'DAGGERHEART.Adversary.Type.Solo.label',
description: 'DAGGERHEART.Adversary.Solo.Description'
label: 'DAGGERHEART.Adversary.Type.solo.label',
description: 'DAGGERHEART.Adversary.solo.description'
},
standard: {
id: 'standard',
label: 'DAGGERHEART.Adversary.Type.Standard.label',
description: 'DAGGERHEART.Adversary.Standard.Description'
label: 'DAGGERHEART.Adversary.Type.standard.label',
description: 'DAGGERHEART.Adversary.standard.description'
},
support: {
id: 'support',
label: 'DAGGERHEART.Adversary.Type.Support.label',
description: 'DAGGERHEART.Adversary.Support.Description'
label: 'DAGGERHEART.Adversary.Type.support.label',
description: 'DAGGERHEART.Adversary.support.description'
}
};

View file

@ -712,14 +712,14 @@ export const valueTypes = {
export const actionTypes = {
passive: {
id: 'passive',
label: 'DAGGERHEART.ActionType.Passive'
label: 'DAGGERHEART.ActionType.passive'
},
action: {
id: 'action',
label: 'DAGGERHEART.ActionType.Action'
label: 'DAGGERHEART.ActionType.action'
},
reaction: {
id: 'reaction',
label: 'DAGGERHEART.ActionType.Reaction'
label: 'DAGGERHEART.ActionType.reaction'
}
};

View file

@ -531,6 +531,29 @@ export class DHBaseAction extends foundry.abstract.DataModel {
}
}
/* SAVE */
async toChat(origin) {
const cls = getDocumentClass('ChatMessage');
const systemData = {
title: game.i18n.localize('DAGGERHEART.ActionType.action'),
origin: origin,
img: this.img,
name: this.name,
description: this.description,
actions: []
};
const msg = new cls({
type: 'abilityUse',
user: game.user.id,
system: systemData,
content: await foundry.applications.handlebars.renderTemplate(
'systems/daggerheart/templates/chat/ability-use.hbs',
systemData
)
});
cls.create(msg.toObject());
}
}
export class DHDamageAction extends DHBaseAction {

View file

@ -30,8 +30,11 @@ export default class DhpAdversary extends BaseDataActor {
choices: SYSTEM.ACTOR.adversaryTypes,
initial: SYSTEM.ACTOR.adversaryTypes.standard.id
}),
motivesAndTactics: new fields.HTMLField(),
description: new fields.StringField(),
motivesAndTactics: new fields.StringField(),
notes: new fields.HTMLField(),
difficulty: new fields.NumberField({ required: true, initial: 1, integer: true }),
hordeHp: new fields.NumberField({ required: true, initial: 1, integer: true }),
damageThresholds: new fields.SchemaField({
major: new fields.NumberField({ required: true, initial: 0, integer: true }),
severe: new fields.NumberField({ required: true, initial: 0, integer: true })
@ -40,6 +43,7 @@ export default class DhpAdversary extends BaseDataActor {
hitPoints: resourceField(),
stress: resourceField()
}),
actions: new fields.ArrayField(new ActionField()),
attack: new ActionField({
initial: {
name: 'Attack',
@ -66,7 +70,7 @@ export default class DhpAdversary extends BaseDataActor {
experiences: new fields.TypedObjectField(
new fields.SchemaField({
name: new fields.StringField(),
value: new fields.NumberField({ required: true, integer: true, initial: 1 })
modifier: new fields.NumberField({ required: true, integer: true, initial: 1 })
})
),
bonuses: new fields.SchemaField({