Finished Evolved

This commit is contained in:
WBHarry 2025-07-18 15:05:12 +02:00
parent 9f22545f7d
commit 011f5d2b14
16 changed files with 378 additions and 162 deletions

View file

@ -6,13 +6,17 @@ export default class BeastformDialog extends HandlebarsApplicationMixin(Applicat
this.configData = configData;
this.selected = null;
this.evolved = { form: null };
this.hybrid = null;
this._dragDrop = this._createDragDropHandlers();
}
static DEFAULT_OPTIONS = {
tag: 'form',
classes: ['daggerheart', 'views', 'dh-style', 'beastform-selection'],
position: {
width: 600,
width: 'auto',
height: 'auto'
},
actions: {
@ -23,7 +27,8 @@ export default class BeastformDialog extends HandlebarsApplicationMixin(Applicat
handler: this.updateBeastform,
submitOnChange: true,
submitOnClose: false
}
},
dragDrop: [{ dragSelector: '.beastform-container', dropSelector: '.advanced-form-container' }]
};
get title() {
@ -37,29 +42,73 @@ export default class BeastformDialog extends HandlebarsApplicationMixin(Applicat
}
};
// _attachPartListeners(partId, htmlElement, options) {
// super._attachPartListeners(partId, htmlElement, options);
_createDragDropHandlers() {
return this.options.dragDrop.map(d => {
d.callbacks = {
dragstart: this._onDragStart.bind(this),
drop: this._onDrop.bind(this)
};
return new foundry.applications.ux.DragDrop.implementation(d);
});
}
// htmlElement.querySelector('');
// }
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
this._dragDrop.forEach(d => d.bind(htmlElement));
}
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.beastformTiers = game.items.reduce((acc, x) => {
const tier = CONFIG.DH.GENERAL.tiers[x.system.tier];
if (x.type !== 'beastform' || tier.value > this.configData.tierLimit) return acc;
context.selected = this.selected;
context.selectedBeastformEffect = this.selected?.effects?.find?.(x => x.type === 'beastform');
if (!acc[tier.value]) acc[tier.value] = { label: game.i18n.localize(tier.label), values: {} };
acc[tier.value].values[x.uuid] = { selected: this.selected == x.uuid, value: x };
context.evolved = this.evolved;
context.hybrid = this.hybrid;
return acc;
}, {}); // Also get from compendium when added
context.canSubmit = this.selected;
const maximumDragTier = Math.max(
this.selected?.system?.evolved?.maximumTier ?? 0,
this.selected?.system?.hybrid?.maximumTier ?? 0
);
const compendiumBeastforms = await game.packs.get(`daggerheart.beastforms`)?.getDocuments();
context.beastformTiers = [...(compendiumBeastforms ? compendiumBeastforms : []), ...game.items].reduce(
(acc, x) => {
const tier = CONFIG.DH.GENERAL.tiersAlternate[x.system.tier];
if (x.type !== 'beastform' || tier.id > this.configData.tierLimit) return acc;
if (!acc[tier.id]) acc[tier.id] = { label: game.i18n.localize(tier.label), values: {} };
acc[tier.id].values[x.uuid] = {
selected: this.selected?.uuid == x.uuid,
value: x,
draggable: maximumDragTier ? x.system.tier <= maximumDragTier : false
};
return acc;
},
{}
); // Also get from compendium when added
context.canSubmit = this.canSubmit();
return context;
}
canSubmit() {
if (this.selected) {
switch (this.selected.system.beastformType) {
case 'normal':
return true;
case 'evolved':
return this.evolved.form;
}
}
return false;
}
static updateBeastform(event, _, formData) {
this.selected = foundry.utils.mergeObject(this.selected, formData.object);
@ -67,12 +116,26 @@ export default class BeastformDialog extends HandlebarsApplicationMixin(Applicat
}
static async selectBeastform(_, target) {
this.selected = this.selected === target.dataset.uuid ? null : target.dataset.uuid;
const beastform = this.selected ? await foundry.utils.fromUuid(this.selected) : null;
if (beastform && beastform.system.beastformType !== CONFIG.DH.ITEM.beastformTypes.normal.id) {
this.element.classList.add('expanded');
this.element.querySelectorAll('.beastform-container ').forEach(element => {
if (element.dataset.uuid === target.dataset.uuid && this.selected?.uuid !== target.dataset.uuid) {
element.classList.remove('inactive');
} else {
element.classList.add('inactive');
}
});
const uuid = this.selected?.uuid === target.dataset.uuid ? null : target.dataset.uuid;
this.selected = uuid ? await foundry.utils.fromUuid(uuid) : null;
if (this.selected && this.selected.system.beastformType !== CONFIG.DH.ITEM.beastformTypes.normal.id) {
this.element.querySelector('.advanced-container').classList.add('expanded');
} else {
this.element.classList.remove('expanded');
this.element.querySelector('.advanced-container').classList.remove('expanded');
}
if (this.selected) {
if (this.selected.system.beastformType !== 'evolved') this.evolved.form = null;
if (this.selected.system.beastformType !== 'hybrid') this.hybrid = null;
}
this.render();
@ -84,14 +147,55 @@ export default class BeastformDialog extends HandlebarsApplicationMixin(Applicat
/** @override */
_onClose(options = {}) {
if (!options.submitted) this.config = false;
if (!options.submitted) this.selected = null;
}
static async configure(configData) {
return new Promise(resolve => {
const app = new this(configData);
app.addEventListener('close', () => resolve(app.selected), { once: true });
app.addEventListener(
'close',
() => resolve({ selected: app.selected, evolved: app.evolved, hybrid: app.hybrid }),
{ once: true }
);
app.render({ force: true });
});
}
async _onDragStart(event) {
const target = event.currentTarget;
if (!this.selected) {
event.preventDefault();
return;
}
const draggedForm = await foundry.utils.fromUuid(target.dataset.uuid);
if (this.selected.system.beastformType === 'evolved') {
if (draggedForm.system.tier > this.selected.system.evolved.maximumTier) {
event.preventDefault();
return;
}
}
if (this.selected.system.beastformType === 'hybrid') {
if (draggedForm.system.tier > this.selected.system.hybrid.maximumTier) {
event.preventDefault();
return;
}
}
event.dataTransfer.setData('text/plain', JSON.stringify(target.dataset));
event.dataTransfer.setDragImage(target, 60, 0);
}
async _onDrop(event) {
event.stopPropagation();
const data = foundry.applications.ux.TextEditor.getDragEventData(event);
const item = await fromUuid(data.uuid);
if (event.target.closest('.advanced-form-container.evolved')) {
this.evolved.form = item;
}
this.render();
}
}

View file

@ -1,4 +1,5 @@
import DHBaseItemSheet from '../api/base-item.mjs';
import Tagify from '@yaireo/tagify';
export default class BeastformSheet extends DHBaseItemSheet {
/**@inheritdoc */
@ -30,6 +31,17 @@ export default class BeastformSheet extends DHBaseItemSheet {
}
};
_attachPartListeners(partId, htmlElement, options) {
super._attachPartListeners(partId, htmlElement, options);
const advantageOnInput = htmlElement.querySelector('.advantageon-input');
if (advantageOnInput) {
const tagifyElement = new Tagify(advantageOnInput);
tagifyElement.on('add', this.advantageOnAdd.bind(this));
tagifyElement.on('remove', this.advantageOnRemove.bind(this));
}
}
/**@inheritdoc */
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
@ -52,4 +64,16 @@ export default class BeastformSheet extends DHBaseItemSheet {
return context;
}
async advantageOnAdd(event) {
await this.document.update({
'system.advantageOn': [...this.document.system.advantageOn, event.detail.data.value]
});
}
async advantageOnRemove(event) {
await this.document.update({
'system.advantageOn': this.document.system.advantageOn.filter(x => x !== event.detail.data.value)
});
}
}

View file

@ -99,7 +99,7 @@ export default class DhHotbar extends foundry.applications.ui.Hotbar {
async createItemMacro(data, slot) {
const macro = await Macro.implementation.create({
name: `${game.i18n.localize('Display')} ${name}`,
name: data.name,
type: CONST.MACRO_TYPES.SCRIPT,
img: data.img,
command: `await game.system.api.applications.ui.DhHotbar.useItem("${data.uuid}");`
@ -109,7 +109,7 @@ export default class DhHotbar extends foundry.applications.ui.Hotbar {
async createActionMacro(data, slot) {
const macro = await Macro.implementation.create({
name: `${game.i18n.localize('Display')} ${name}`,
name: data.data.name,
type: CONST.MACRO_TYPES.SCRIPT,
img: data.data.img,
command: `await game.system.api.applications.ui.DhHotbar.useAction("${data.data.itemUuid}", "${data.data.id}");`
@ -119,7 +119,7 @@ export default class DhHotbar extends foundry.applications.ui.Hotbar {
async createAttackMacro(data, slot) {
const macro = await Macro.implementation.create({
name: `${game.i18n.localize('Display')} ${name}`,
name: data.name,
type: CONST.MACRO_TYPES.SCRIPT,
img: data.img,
command: `await game.system.api.applications.ui.DhHotbar.useAttack("${data.actorUuid}");`

View file

@ -1,5 +1,6 @@
export const abilities = {
agility: {
id: 'agility',
label: 'DAGGERHEART.CONFIG.Traits.agility.name',
verbs: [
'DAGGERHEART.CONFIG.Traits.agility.verb.sprint',
@ -8,6 +9,7 @@ export const abilities = {
]
},
strength: {
id: 'strength',
label: 'DAGGERHEART.CONFIG.Traits.strength.name',
verbs: [
'DAGGERHEART.CONFIG.Traits.strength.verb.lift',
@ -16,6 +18,7 @@ export const abilities = {
]
},
finesse: {
id: 'finesse',
label: 'DAGGERHEART.CONFIG.Traits.finesse.name',
verbs: [
'DAGGERHEART.CONFIG.Traits.finesse.verb.control',
@ -24,6 +27,7 @@ export const abilities = {
]
},
instinct: {
id: 'instinct',
label: 'DAGGERHEART.CONFIG.Traits.instinct.name',
verbs: [
'DAGGERHEART.CONFIG.Traits.instinct.verb.perceive',
@ -32,6 +36,7 @@ export const abilities = {
]
},
presence: {
id: 'presence',
label: 'DAGGERHEART.CONFIG.Traits.presence.name',
verbs: [
'DAGGERHEART.CONFIG.Traits.presence.verb.charm',
@ -40,6 +45,7 @@ export const abilities = {
]
},
knowledge: {
id: 'knowledge',
label: 'DAGGERHEART.CONFIG.Traits.knowledge.name',
verbs: [
'DAGGERHEART.CONFIG.Traits.knowledge.verb.recall',

View file

@ -279,6 +279,25 @@ export const tiers = {
}
};
export const tiersAlternate = {
1: {
id: 1,
label: 'DAGGERHEART.GENERAL.Tiers.tier1'
},
2: {
id: 2,
label: 'DAGGERHEART.GENERAL.Tiers.tier2'
},
3: {
id: 3,
label: 'DAGGERHEART.GENERAL.Tiers.tier3'
},
4: {
id: 4,
label: 'DAGGERHEART.GENERAL.Tiers.tier4'
}
};
export const diceTypes = {
d4: 'd4',
d6: 'd6',

View file

@ -10,10 +10,10 @@ export default class DhBeastformAction extends DHBaseAction {
const abort = await this.handleActiveTransformations();
if (abort) return;
const beastformUuid = await BeastformDialog.configure(beastformConfig);
if (!beastformUuid) return;
const { selected, evolved, hybrid } = await BeastformDialog.configure(beastformConfig);
if (!selected) return;
await this.transform(beastformUuid);
await this.transform(selected, evolved, hybrid);
}
prepareBeastformConfig(config) {
@ -29,9 +29,26 @@ export default class DhBeastformAction extends DHBaseAction {
};
}
async transform(beastformUuid) {
const beastform = await foundry.utils.fromUuid(beastformUuid);
this.actor.createEmbeddedDocuments('Item', [beastform.toObject()]);
async transform(selectedForm, evolvedData, hybridData) {
const formData = evolvedData?.form ? evolvedData.form.toObject() : selectedForm.toObject();
const beastformEffect = formData.effects.find(x => x.type === 'beastform');
if (!beastformEffect) {
ui.notifications.error('DAGGERHEART.UI.Notifications.beastformMissingEffect');
return;
}
if (evolvedData?.form) {
const evolvedForm = selectedForm.effects.find(x => x.type === 'beastform');
if (!evolvedForm) {
ui.notifications.error('DAGGERHEART.UI.Notifications.beastformMissingEffect');
return;
}
beastformEffect.changes = [...beastformEffect.changes, ...evolvedForm.changes];
formData.system.features = [...formData.system.features, ...selectedForm.system.features.map(x => x.uuid)];
}
this.actor.createEmbeddedDocuments('Item', [formData]);
}
async handleActiveTransformations() {

View file

@ -24,10 +24,11 @@ export default class DHBeastform extends BaseDataItem {
choices: CONFIG.DH.ITEM.beastformTypes,
initial: CONFIG.DH.ITEM.beastformTypes.normal.id
}),
tier: new fields.StringField({
tier: new fields.NumberField({
required: true,
choices: CONFIG.DH.GENERAL.tiers,
initial: CONFIG.DH.GENERAL.tiers.tier1.id
integer: true,
choices: CONFIG.DH.GENERAL.tiersAlternate,
initial: CONFIG.DH.GENERAL.tiersAlternate[1].id
}),
tokenImg: new fields.FilePathField({
initial: 'icons/svg/mystery-man.svg',
@ -43,21 +44,30 @@ export default class DHBeastform extends BaseDataItem {
height: new fields.NumberField({ integer: true, min: 1, initial: null, nullable: true }),
width: new fields.NumberField({ integer: true, min: 1, initial: null, nullable: true })
}),
mainTrait: new fields.StringField({
required: true,
choices: CONFIG.DH.ACTOR.abilities,
initial: CONFIG.DH.ACTOR.abilities.agility.id
}),
examples: new fields.StringField(),
advantageOn: new fields.StringField(),
advantageOn: new fields.ArrayField(new fields.StringField()),
features: new ForeignDocumentUUIDArrayField({ type: 'Item' }),
evolved: new fields.SchemaField({
maximumTier: new fields.StringField({
maximumTier: new fields.NumberField({
integer: true,
choices: CONFIG.DH.GENERAL.tiersAlternate
}),
mainTraitBonus: new fields.NumberField({
required: true,
choices: CONFIG.DH.GENERAL.tiers,
initial: CONFIG.DH.GENERAL.tiers.tier1.id
integer: true,
min: 0,
initial: 0
})
}),
hybrid: new fields.SchemaField({
maximumTier: new fields.StringField({
required: true,
choices: CONFIG.DH.GENERAL.tiers,
initial: CONFIG.DH.GENERAL.tiers.tier1.id,
maximumTier: new fields.NumberField({
integer: true,
choices: CONFIG.DH.GENERAL.tiersAlternate,
label: 'DAGGERHEART.ITEMS.Beastform.FIELDS.evolved.maximumTier.label'
}),
beastformOptions: new fields.NumberField({ required: true, integer: true, initial: 2, min: 2 }),
@ -92,7 +102,10 @@ export default class DHBeastform extends BaseDataItem {
const beastformEffect = this.parent.effects.find(x => x.type === 'beastform');
await beastformEffect.updateSource({
changes: [...beastformEffect.changes, { key: 'system.advantageSources', mode: 2, value: this.advantageOn }],
changes: [
...beastformEffect.changes,
{ key: 'system.advantageSources', mode: 2, value: this.advantageOn.join(', ') }
],
system: {
characterTokenData: {
tokenImg: this.parent.parent.prototypeToken.texture.src,

View file

@ -1,80 +1,10 @@
import { diceTypes, getDiceSoNicePresets, range } from '../config/generalConfig.mjs';
import Tagify from '@yaireo/tagify';
export const loadCompendiumOptions = async compendiums => {
const compendiumValues = [];
for (var compendium of compendiums) {
const values = await getCompendiumOptions(compendium);
compendiumValues.push(values);
}
return compendiumValues;
};
const getCompendiumOptions = async compendium => {
const compendiumPack = await game.packs.get(compendium);
const values = [];
for (var value of compendiumPack.index) {
const document = await compendiumPack.getDocument(value._id);
values.push(document);
}
return values;
};
export const getWidthOfText = (txt, fontsize, allCaps, bold) => {
const text = allCaps ? txt.toUpperCase() : txt;
if (getWidthOfText.c === undefined) {
getWidthOfText.c = document.createElement('canvas');
getWidthOfText.ctx = getWidthOfText.c.getContext('2d');
}
var fontspec = `${bold ? 'bold' : ''} ${fontsize}px` + ' ' + 'Signika, sans-serif';
if (getWidthOfText.ctx.font !== fontspec) getWidthOfText.ctx.font = fontspec;
return getWidthOfText.ctx.measureText(text).width;
};
export const padArray = (arr, len, fill) => {
return arr.concat(Array(len).fill(fill)).slice(0, len);
};
export const getTier = (level, asNr) => {
switch (Math.floor((level + 1) / 3)) {
case 1:
return asNr ? 1 : 'tier1';
case 2:
return asNr ? 2 : 'tier2';
case 3:
return asNr ? 3 : 'tier3';
default:
return asNr ? 0 : 'tier0';
}
};
export const capitalize = string => {
return string.charAt(0).toUpperCase() + string.slice(1);
};
export const getPathValue = (path, entity, numeric) => {
const pathValue = foundry.utils.getProperty(entity, path);
if (pathValue) return numeric ? Number.parseInt(pathValue) : pathValue;
return numeric ? Number.parseInt(path) : path;
};
export const generateId = (title, length) => {
const id = title
.split(' ')
.map((w, i) => {
const p = w.slugify({ replacement: '', strict: true });
return i ? p.titleCase() : p;
})
.join('');
return Number.isNumeric(length) ? id.slice(0, length).padEnd(length, '0') : id;
};
export function rollCommandToJSON(text) {
if (!text) return {};