[Feature] Beastform Compendium (#434)

* Various fixes

* Added fixes to make beastforms work

* .

* Added all SRD beastforms
This commit is contained in:
WBHarry 2025-07-27 21:26:28 +02:00 committed by GitHub
parent 0fe6c4066a
commit 187a0dc090
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
80 changed files with 6189 additions and 89 deletions

View file

@ -123,13 +123,15 @@ export default class BeastformDialog extends HandlebarsApplicationMixin(Applicat
);
const compendiumBeastforms = await game.packs.get(`daggerheart.beastforms`)?.getDocuments();
const beastformTiers = [...(compendiumBeastforms ? compendiumBeastforms : []), ...game.items].reduce(
const beastformTiers = [...game.items, ...(compendiumBeastforms ? compendiumBeastforms : [])].reduce(
(acc, x) => {
const tier = CONFIG.DH.GENERAL.tiers[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: {} };
if (Object.values(acc[tier.id].values).find(existing => existing.value.name === x.name)) return acc;
acc[tier.id].values[x.uuid] = {
selected: this.selected?.uuid == x.uuid,
value: x,

View file

@ -123,7 +123,14 @@ export default function DHApplicationMixin(Base) {
/**@inheritdoc */
async _onFirstRender(context, options) {
await super._onFirstRender(context, options);
this.relatedDocs.filter(doc => doc).map(doc => (doc.apps[this.id] = this));
const docs = [];
for (var docData of this.relatedDocs) {
const doc = await foundry.utils.fromUuid(docData.uuid);
docs.push(doc);
}
docs.filter(doc => doc).map(doc => (doc.apps[this.id] = this));
if (!!this.options.contextMenus.length) this._createContextMenus();
}

View file

@ -249,12 +249,20 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
const target = event.target.closest('fieldset.drop-section');
const item = await fromUuid(data.uuid);
if (item?.type === 'feature') {
await this.document.update({
'system.features': [...this.document.system.features, { type: target.dataset.type, item }].map(x => ({
...x,
item: x.item?.uuid
}))
});
if (target.dataset.type) {
await this.document.update({
'system.features': [...this.document.system.features, { type: target.dataset.type, item }].map(
x => ({
...x,
item: x.item?.uuid
})
)
});
} else {
await this.document.update({
'system.features': [...this.document.system.features, item].map(x => x.uuid)
});
}
}
}
}

View file

@ -66,8 +66,8 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo
super.close(options);
}
async getActor(id) {
return await fromUuid(id);
async getActor(uuid) {
return await foundry.utils.fromUuid(uuid);
}
getAction(actor, itemId, actionId) {

View file

@ -108,7 +108,7 @@ export const diceCompare = {
}
};
export const advandtageState = {
export const advantageState = {
disadvantage: {
label: 'DAGGERHEART.GENERAL.Disadvantage.full',
value: -1

View file

@ -164,7 +164,8 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
title: this.item.name,
source: {
item: this.item._id,
action: this._id
action: this._id,
actor: this.actor.uuid
},
dialog: {},
type: this.type,
@ -191,7 +192,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
difficulty: this.roll?.difficulty,
formula: this.roll.getFormula(),
bonus: this.roll.bonus,
advantage: CONFIG.DH.ACTIONS.advandtageState[this.roll.advState].value
advantage: CONFIG.DH.ACTIONS.advantageState[this.roll.advState].value
};
if (this.roll?.type === 'diceSet') roll.lite = true;
@ -256,6 +257,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
/* EFFECTS */
async applyEffects(event, data, targets) {
targets ??= data.system.targets;
const force = true; /* Where should this come from? */
if (!this.effects?.length || !targets.length) return;
let effects = this.effects;
targets.forEach(async token => {

View file

@ -4,15 +4,13 @@ import DHBaseAction from './baseAction.mjs';
export default class DhBeastformAction extends DHBaseAction {
static extraSchemas = [...super.extraSchemas, 'beastform'];
async use(event, ...args) {
async use(_event, ...args) {
const beastformConfig = this.prepareBeastformConfig();
const abort = await this.handleActiveTransformations();
if (abort) return;
const item = args[0];
const { selected, evolved, hybrid } = await BeastformDialog.configure(beastformConfig, item);
const { selected, evolved, hybrid } = await BeastformDialog.configure(beastformConfig, this.item);
if (!selected) return;
await this.transform(selected, evolved, hybrid);

View file

@ -1,10 +1,23 @@
import DHBaseActorSettings from '../../applications/sheets/api/actor-setting.mjs';
const resistanceField = reductionLabel =>
const resistanceField = (resistanceLabel, immunityLabel, reductionLabel) =>
new foundry.data.fields.SchemaField({
resistance: new foundry.data.fields.BooleanField({ initial: false }),
immunity: new foundry.data.fields.BooleanField({ initial: false }),
reduction: new foundry.data.fields.NumberField({ integer: true, initial: 0, label: reductionLabel })
resistance: new foundry.data.fields.BooleanField({
initial: false,
label: `${resistanceLabel}.label`,
hint: `${resistanceLabel}.hint`
}),
immunity: new foundry.data.fields.BooleanField({
initial: false,
label: `${immunityLabel}.label`,
hint: `${immunityLabel}.hint`
}),
reduction: new foundry.data.fields.NumberField({
integer: true,
initial: 0,
label: `${reductionLabel}.label`,
hint: `${reductionLabel}.hint`
})
});
/**
@ -40,8 +53,16 @@ export default class BaseDataActor extends foundry.abstract.TypeDataModel {
if (this.metadata.isNPC) schema.description = new fields.HTMLField({ required: true, nullable: true });
if (this.metadata.hasResistances)
schema.resistance = new fields.SchemaField({
physical: resistanceField('DAGGERHEART.GENERAL.DamageResistance.physicalReduction'),
magical: resistanceField('DAGGERHEART.GENERAL.DamageResistance.magicalReduction')
physical: resistanceField(
'DAGGERHEART.GENERAL.DamageResistance.physicalResistance',
'DAGGERHEART.GENERAL.DamageResistance.physicalImmunity',
'DAGGERHEART.GENERAL.DamageResistance.physicalReduction'
),
magical: resistanceField(
'DAGGERHEART.GENERAL.DamageResistance.magicalResistance',
'DAGGERHEART.GENERAL.DamageResistance.magicalImmunity',
'DAGGERHEART.GENERAL.DamageResistance.magicalReduction'
)
});
return schema;
}

View file

@ -45,12 +45,12 @@ export default class DhCharacter extends BaseDataActor {
severe: new fields.NumberField({
integer: true,
initial: 0,
label: 'DAGGERHEART.GENERAL.DamageThresholds.majorThreshold'
label: 'DAGGERHEART.GENERAL.DamageThresholds.severeThreshold'
}),
major: new fields.NumberField({
integer: true,
initial: 0,
label: 'DAGGERHEART.GENERAL.DamageThresholds.severeThreshold'
label: 'DAGGERHEART.GENERAL.DamageThresholds.majorThreshold'
})
}),
experiences: new fields.TypedObjectField(
@ -112,7 +112,7 @@ export default class DhCharacter extends BaseDataActor {
value: {
custom: {
enabled: true,
formula: '@system.rules.attack.damage.value'
formula: '@profd4'
}
}
}
@ -244,10 +244,19 @@ export default class DhCharacter extends BaseDataActor {
}),
attack: new fields.SchemaField({
damage: new fields.SchemaField({
value: new fields.StringField({
diceIndex: new fields.NumberField({
integer: true,
min: 0,
max: 5,
initial: 0,
label: 'DAGGERHEART.GENERAL.Rules.attack.damage.dice.label',
hint: 'DAGGERHEART.GENERAL.Rules.attack.damage.dice.hint'
}),
bonus: new fields.NumberField({
required: true,
initial: '@profd4',
label: 'DAGGERHEART.GENERAL.Rules.attack.damage.value.label'
initial: 0,
min: 0,
label: 'DAGGERHEART.GENERAL.Rules.attack.damage.bonus.label'
})
}),
roll: new fields.SchemaField({
@ -462,6 +471,12 @@ export default class DhCharacter extends BaseDataActor {
};
}
get basicAttackDamageDice() {
const diceTypes = Object.keys(CONFIG.DH.GENERAL.diceTypes);
const attackDiceIndex = Math.max(Math.min(this.rules.attack.damage.diceIndex, 5), 0);
return diceTypes[attackDiceIndex];
}
static async unequipBeforeEquip(itemToEquip) {
const primary = this.primaryWeapon,
secondary = this.secondaryWeapon;
@ -547,12 +562,16 @@ export default class DhCharacter extends BaseDataActor {
const baseHope = this.resources.hope.value + (this.companion?.system?.resources?.hope ?? 0);
this.resources.hope.value = Math.min(baseHope, this.resources.hope.max);
this.attack.roll.trait = this.rules.attack.roll.trait ?? this.attack.roll.trait;
this.attack.damage.parts[0].value.custom.formula = `@prof${this.basicAttackDamageDice}${this.rules.attack.damage.bonus ? ` + ${this.rules.attack.damage.bonus}` : ''}`;
}
getRollData() {
const data = super.getRollData();
return {
...data,
basicAttackDamageDice: this.basicAttackDamageDice,
tier: this.tier,
level: this.levelData.level.current
};

View file

@ -8,25 +8,38 @@ export class DHActionRollData extends foundry.abstract.DataModel {
trait: new fields.StringField({ nullable: true, initial: null, choices: CONFIG.DH.ACTOR.abilities }),
difficulty: new fields.NumberField({ nullable: true, initial: null, integer: true, min: 0 }),
bonus: new fields.NumberField({ nullable: true, initial: null, integer: true }),
advState: new fields.StringField({ choices: CONFIG.DH.ACTIONS.advandtageState, initial: 'neutral' }),
advState: new fields.StringField({
choices: CONFIG.DH.ACTIONS.advantageState,
initial: 'neutral'
}),
diceRolling: new fields.SchemaField({
multiplier: new fields.StringField({
choices: CONFIG.DH.GENERAL.diceSetNumbers,
initial: 'prof',
label: 'Dice Number'
label: 'DAGGERHEART.ACTIONS.RollField.diceRolling.multiplier'
}),
flatMultiplier: new fields.NumberField({
nullable: true,
initial: 1,
label: 'DAGGERHEART.ACTIONS.RollField.diceRolling.flatMultiplier'
}),
flatMultiplier: new fields.NumberField({ nullable: true, initial: 1, label: 'Flat Multiplier' }),
dice: new fields.StringField({
choices: CONFIG.DH.GENERAL.diceTypes,
initial: 'd6',
label: 'Dice Type'
initial: CONFIG.DH.GENERAL.diceTypes.d6,
label: 'DAGGERHEART.ACTIONS.RollField.diceRolling.dice'
}),
compare: new fields.StringField({
choices: CONFIG.DH.ACTIONS.diceCompare,
initial: 'above',
label: 'Should be'
nullable: true,
initial: null,
label: 'DAGGERHEART.ACTIONS.RollField.diceRolling.compare'
}),
treshold: new fields.NumberField({ initial: 1, integer: true, min: 1, label: 'Treshold' })
treshold: new fields.NumberField({
integer: true,
nullable: true,
initial: null,
label: 'DAGGERHEART.ACTIONS.RollField.diceRolling.threshold'
})
}),
useDefault: new fields.BooleanField({ initial: false })
};
@ -41,7 +54,11 @@ export class DHActionRollData extends foundry.abstract.DataModel {
this.diceRolling.multiplier === 'flat'
? this.diceRolling.flatMultiplier
: `@${this.diceRolling.multiplier}`;
formula = `${multiplier}${this.diceRolling.dice}cs${CONFIG.DH.ACTIONS.diceCompare[this.diceRolling.compare].operator}${this.diceRolling.treshold}`;
if (this.diceRolling.compare && this.diceRolling.threshold) {
formula = `${multiplier}${this.diceRolling.dice}cs${CONFIG.DH.ACTIONS.diceCompare[this.diceRolling.compare].operator}${this.diceRolling.treshold}`;
} else {
formula = `${multiplier}${this.diceRolling.dice}`;
}
break;
default:
formula = '';

View file

@ -35,7 +35,13 @@ export default class FormulaField extends foundry.data.fields.StringField {
/** @inheritDoc */
_validateType(value) {
const roll = new Roll(value.replace(/@([a-z.0-9_-]+)/gi, '1'));
/* A bit suss, but seems to work */
let roll = null;
try {
roll = new Roll(value.replace(/@([a-z.0-9_-]+)/gi, '1'));
} catch (_) {
roll = new Roll(value.replace(/@([a-z.0-9_-]+)/gi, 'd6'));
}
roll.evaluateSync({ strict: false });
if (this.options.deterministic && !roll.isDeterministic)
throw new Error(`must not contain dice terms: ${value}`);

View file

@ -1,5 +1,4 @@
import AttachableItem from './attachableItem.mjs';
import { ActionsField } from '../fields/actionField.mjs';
import { armorFeatures } from '../../config/itemConfig.mjs';
export default class DHArmor extends AttachableItem {

View file

@ -94,10 +94,13 @@ export default class DHBeastform extends BaseDataItem {
return false;
}
const features = await this.parent.parent.createEmbeddedDocuments(
'Item',
this.features.map(x => x.toObject())
);
const beastformFeatures = [];
for (let featureData of this.features) {
const feature = await foundry.utils.fromUuid(featureData.uuid);
beastformFeatures.push(feature.toObject());
}
const features = await this.parent.parent.createEmbeddedDocuments('Item', beastformFeatures);
const extraEffects = await this.parent.parent.createEmbeddedDocuments(
'ActiveEffect',
@ -152,12 +155,14 @@ export default class DHBeastform extends BaseDataItem {
_onCreate(_data, _options, userId) {
if (userId !== game.user.id) return;
this.parent.createEmbeddedDocuments('ActiveEffect', [
{
type: 'beastform',
name: game.i18n.localize('DAGGERHEART.ITEMS.Beastform.beastformEffect'),
img: 'icons/creatures/abilities/paw-print-pair-purple.webp'
}
]);
if (!this.parent.effects.find(x => x.type === 'beastform')) {
this.parent.createEmbeddedDocuments('ActiveEffect', [
{
type: 'beastform',
name: game.i18n.localize('DAGGERHEART.ITEMS.Beastform.beastformEffect'),
img: 'icons/creatures/abilities/paw-print-pair-purple.webp'
}
]);
}
}
}

View file

@ -22,14 +22,14 @@ function getDualityMessage(roll) {
: game.i18n.localize('DAGGERHEART.GENERAL.duality');
const advantage = roll.advantage
? CONFIG.DH.ACTIONS.advandtageState.advantage.value
? CONFIG.DH.ACTIONS.advantageState.advantage.value
: roll.disadvantage
? CONFIG.DH.ACTIONS.advandtageState.disadvantage.value
? CONFIG.DH.ACTIONS.advantageState.disadvantage.value
: undefined;
const advantageLabel =
advantage === CONFIG.DH.ACTIONS.advandtageState.advantage.value
advantage === CONFIG.DH.ACTIONS.advantageState.advantage.value
? 'Advantage'
: advantage === CONFIG.DH.ACTIONS.advandtageState.disadvantage.value
: advantage === CONFIG.DH.ACTIONS.advantageState.disadvantage.value
? 'Disadvantage'
: undefined;

View file

@ -271,14 +271,14 @@ export function addLinkedItemsDiff(changedItems, currentItems, options) {
newItems
.difference(prevItems)
.map(item => item?.item ?? item)
.filter(x => (typeof x === 'object' ? x.item : x))
.filter(x => (typeof x === 'object' ? x?.item : x))
);
options.toUnlink = Array.from(
prevItems
.difference(newItems)
.map(item => item?.item?.uuid ?? item?.uuid ?? item)
.filter(x => (typeof x === 'object' ? x.item : x))
.filter(x => (typeof x === 'object' ? x?.item : x))
);
}
}