mirror of
https://github.com/Foundryborne/daggerheart.git
synced 2026-01-11 19:25:21 +01:00
* Added prettier with automatic useage on pre-commit to avoid style breakage * Ran Prettier on the project
562 lines
24 KiB
JavaScript
562 lines
24 KiB
JavaScript
import { getPathValue, getTier } from '../helpers/utils.mjs';
|
|
import { MappingField } from './fields.mjs';
|
|
|
|
const fields = foundry.data.fields;
|
|
|
|
const attributeField = () =>
|
|
new fields.SchemaField({
|
|
data: new fields.SchemaField({
|
|
value: new fields.NumberField({ initial: 0, integer: true }),
|
|
base: new fields.NumberField({ initial: 0, integer: true }),
|
|
bonus: new fields.NumberField({ initial: 0, integer: true }),
|
|
actualValue: new fields.NumberField({ initial: 0, integer: true }),
|
|
overrideValue: new fields.NumberField({ initial: 0, integer: true })
|
|
}),
|
|
levelMarks: new fields.ArrayField(new fields.NumberField({ nullable: true, initial: null, integer: true })),
|
|
levelMark: new fields.NumberField({ nullable: true, initial: null, integer: true })
|
|
});
|
|
|
|
const levelUpTier = () => ({
|
|
attributes: new MappingField(new fields.BooleanField()),
|
|
hitPointSlots: new MappingField(new fields.BooleanField()),
|
|
stressSlots: new MappingField(new fields.BooleanField()),
|
|
experiences: new MappingField(new fields.ArrayField(new fields.StringField({}))),
|
|
proficiency: new MappingField(new fields.BooleanField()),
|
|
armorOrEvasionSlot: new MappingField(new fields.StringField({})),
|
|
majorDamageThreshold2: new MappingField(new fields.BooleanField()),
|
|
severeDamageThreshold2: new MappingField(new fields.BooleanField()),
|
|
severeDamageThreshold3: new MappingField(new fields.BooleanField()),
|
|
severeDamageThreshold4: new MappingField(new fields.BooleanField()),
|
|
subclass: new MappingField(
|
|
new fields.SchemaField({
|
|
multiclass: new fields.BooleanField(),
|
|
feature: new fields.StringField({})
|
|
})
|
|
),
|
|
multiclass: new MappingField(new fields.BooleanField())
|
|
});
|
|
|
|
// const weapon = () => new fields.SchemaField({
|
|
// name: new fields.StringField({}),
|
|
// trait: new fields.StringField({}),
|
|
// range: new fields.StringField({}),
|
|
// damage: new fields.SchemaField({
|
|
// value: new fields.StringField({}),
|
|
// type: new fields.StringField({}),
|
|
// }),
|
|
// feature: new fields.StringField({}),
|
|
// img: new fields.StringField({}),
|
|
// uuid: new fields.StringField({}),
|
|
// }, { initial: null, nullable: true });
|
|
|
|
export default class DhpPC extends foundry.abstract.TypeDataModel {
|
|
static defineSchema() {
|
|
return {
|
|
resources: new fields.SchemaField({
|
|
health: new fields.SchemaField({
|
|
value: new fields.NumberField({ initial: 0, integer: true }),
|
|
min: new fields.NumberField({ initial: 0, integer: true }),
|
|
max: new fields.NumberField({ initial: 6, integer: true })
|
|
}),
|
|
stress: new fields.SchemaField({
|
|
value: new fields.NumberField({ initial: 0, integer: true }),
|
|
min: new fields.NumberField({ initial: 0, integer: true }),
|
|
max: new fields.NumberField({ initial: 6, integer: true })
|
|
}),
|
|
hope: new fields.SchemaField({
|
|
value: new fields.NumberField({ initial: -1, integer: true }), // FIXME. Logic is gte and needs -1 in PC/Hope. Change to 0
|
|
min: new fields.NumberField({ initial: 0, integer: true })
|
|
})
|
|
}),
|
|
bonuses: new fields.SchemaField({
|
|
damage: new fields.ArrayField(
|
|
new fields.SchemaField({
|
|
value: new fields.NumberField({ integer: true, initial: 0 }),
|
|
type: new fields.StringField({ nullable: true }),
|
|
initiallySelected: new fields.BooleanField(),
|
|
hopeIncrease: new fields.StringField({ initial: null, nullable: true }),
|
|
description: new fields.StringField({})
|
|
})
|
|
)
|
|
}),
|
|
attributes: new fields.SchemaField({
|
|
agility: attributeField(),
|
|
strength: attributeField(),
|
|
finesse: attributeField(),
|
|
instinct: attributeField(),
|
|
presence: attributeField(),
|
|
knowledge: attributeField()
|
|
}),
|
|
proficiency: new fields.SchemaField({
|
|
value: new fields.NumberField({ initial: 1, integer: true }),
|
|
min: new fields.NumberField({ initial: 1, integer: true }),
|
|
max: new fields.NumberField({ initial: 6, integer: true })
|
|
}),
|
|
damageThresholds: new fields.SchemaField({
|
|
minor: new fields.NumberField({ initial: 0, integer: true }),
|
|
major: new fields.NumberField({ initial: 0, integer: true }),
|
|
severe: new fields.NumberField({ initial: 0, integer: true })
|
|
}),
|
|
evasion: new fields.NumberField({ initial: 0, integer: true }),
|
|
// armor: new fields.SchemaField({
|
|
// value: new fields.NumberField({ initial: 0, integer: true }),
|
|
// customValue: new fields.NumberField({ initial: null, nullable: true }),
|
|
// }),
|
|
experiences: new fields.ArrayField(
|
|
new fields.SchemaField({
|
|
id: new fields.StringField({ required: true }),
|
|
level: new fields.NumberField({ required: true, integer: true }),
|
|
description: new fields.StringField({}),
|
|
value: new fields.NumberField({ integer: true, nullable: true, initial: null })
|
|
}),
|
|
{
|
|
initial: [
|
|
{ id: foundry.utils.randomID(), level: 1, description: '', value: 2 },
|
|
{ id: foundry.utils.randomID(), level: 1, description: '', value: 2 }
|
|
]
|
|
}
|
|
),
|
|
gold: new fields.SchemaField({
|
|
coins: new fields.NumberField({ initial: 0, integer: true }),
|
|
handfulls: new fields.NumberField({ initial: 0, integer: true }),
|
|
bags: new fields.NumberField({ initial: 0, integer: true }),
|
|
chests: new fields.NumberField({ initial: 0, integer: true })
|
|
}),
|
|
pronouns: new fields.StringField({}),
|
|
domainData: new fields.SchemaField({
|
|
maxLoadout: new fields.NumberField({ initial: 2, integer: true }),
|
|
maxCards: new fields.NumberField({ initial: 2, integer: true })
|
|
}),
|
|
levelData: new fields.SchemaField({
|
|
currentLevel: new fields.NumberField({ initial: 1, integer: true }),
|
|
changedLevel: new fields.NumberField({ initial: 1, integer: true }),
|
|
levelups: new MappingField(
|
|
new fields.SchemaField({
|
|
level: new fields.NumberField({ required: true, integer: true }),
|
|
tier1: new fields.SchemaField({
|
|
...levelUpTier()
|
|
}),
|
|
tier2: new fields.SchemaField(
|
|
{
|
|
...levelUpTier()
|
|
},
|
|
{ nullable: true, initial: null }
|
|
),
|
|
tier3: new fields.SchemaField(
|
|
{
|
|
...levelUpTier()
|
|
},
|
|
{ nullable: true, initial: null }
|
|
)
|
|
})
|
|
)
|
|
}),
|
|
story: new fields.SchemaField({
|
|
background: new fields.HTMLField(),
|
|
appearance: new fields.HTMLField(),
|
|
connections: new fields.HTMLField(),
|
|
scars: new fields.ArrayField(
|
|
new fields.SchemaField({
|
|
name: new fields.StringField({}),
|
|
description: new fields.HTMLField()
|
|
})
|
|
)
|
|
}),
|
|
description: new fields.StringField({}),
|
|
//Temporary until new FoundryVersion fix --> See Armor.Mjs DataPreparation
|
|
armorMarks: new fields.SchemaField({
|
|
max: new fields.NumberField({ initial: 6, integer: true }),
|
|
value: new fields.NumberField({ initial: 0, integer: true })
|
|
})
|
|
};
|
|
}
|
|
|
|
get canLevelUp() {
|
|
// return Object.values(this.levels.data).some(x => !x.completed);
|
|
return this.levelData.currentLevel !== this.levelData.changedLevel;
|
|
}
|
|
|
|
get tier() {
|
|
return this.#getTier(this.levelData.currentLevel);
|
|
}
|
|
|
|
get ancestry() {
|
|
return this.parent.items.find(x => x.type === 'ancestry') ?? null;
|
|
}
|
|
|
|
get class() {
|
|
return this.parent.items.find(x => x.type === 'class' && !x.system.multiclass) ?? null;
|
|
}
|
|
|
|
get multiclass() {
|
|
return this.parent.items.find(x => x.type === 'class' && x.system.multiclass) ?? null;
|
|
}
|
|
|
|
get multiclassSubclass() {
|
|
return this.parent.items.find(x => x.type === 'subclass' && x.system.multiclass) ?? null;
|
|
}
|
|
|
|
get subclass() {
|
|
return this.parent.items.find(x => x.type === 'subclass' && !x.system.multiclass) ?? null;
|
|
}
|
|
|
|
get subclassFeatures() {
|
|
const subclass = this.subclass;
|
|
const multiclass = this.multiclassSubclass;
|
|
const subclassItems = this.parent.items.filter(x => x.type === 'feature' && x.system.type === 'subclass');
|
|
return {
|
|
subclass: !subclass
|
|
? {}
|
|
: {
|
|
foundation: subclassItems.filter(x =>
|
|
subclass.system.foundationFeature.abilities.some(ability => ability.uuid === x.uuid)
|
|
),
|
|
specialization: subclassItems.filter(x =>
|
|
subclass.system.specializationFeature.abilities.some(ability => ability.uuid === x.uuid)
|
|
),
|
|
mastery: subclassItems.filter(x =>
|
|
subclass.system.masteryFeature.abilities.some(ability => ability.uuid === x.uuid)
|
|
)
|
|
},
|
|
multiclassSubclass: !multiclass
|
|
? {}
|
|
: {
|
|
foundation: subclassItems.filter(x =>
|
|
multiclass.system.foundationFeature.abilities.some(ability => ability.uuid === x.uuid)
|
|
),
|
|
specialization: subclassItems.filter(x =>
|
|
multiclass.system.specializationFeature.abilities.some(ability => ability.uuid === x.uuid)
|
|
),
|
|
mastery: subclassItems.filter(x =>
|
|
multiclass.system.masteryFeature.abilities.some(ability => ability.uuid === x.uuid)
|
|
)
|
|
}
|
|
};
|
|
}
|
|
|
|
get community() {
|
|
return this.parent.items.find(x => x.type === 'community') ?? null;
|
|
}
|
|
|
|
get classFeatures() {
|
|
return this.parent.items.filter(
|
|
x => x.type === 'feature' && x.system.type === SYSTEM.ITEM.featureTypes.class.id && !x.system.multiclass
|
|
);
|
|
}
|
|
|
|
get multiclassFeatures() {
|
|
return this.parent.items.filter(
|
|
x => x.type === 'feature' && x.system.type === SYSTEM.ITEM.featureTypes.class.id && x.system.multiclass
|
|
);
|
|
}
|
|
|
|
get domains() {
|
|
const classDomains = this.class ? this.class.system.domains : [];
|
|
const multiclassDomains = this.multiclass ? this.multiclass.system.domains : [];
|
|
return [...classDomains, ...multiclassDomains];
|
|
}
|
|
|
|
get domainCards() {
|
|
const domainCards = this.parent.items.filter(x => x.type === 'domainCard');
|
|
const loadout = domainCards.filter(x => !x.system.inVault);
|
|
const vault = domainCards.filter(x => x.system.inVault);
|
|
|
|
return {
|
|
loadout: loadout,
|
|
vault: vault,
|
|
total: [...loadout, ...vault]
|
|
};
|
|
}
|
|
|
|
get armor() {
|
|
return this.parent.items.find(x => x.type === 'armor');
|
|
}
|
|
|
|
get activeWeapons() {
|
|
const primaryWeapon = this.parent.items.find(
|
|
x => x.type === 'weapon' && x.system.active && !x.system.secondary
|
|
);
|
|
const secondaryWeapon = this.parent.items.find(
|
|
x => x.type === 'weapon' && x.system.active && x.system.secondary
|
|
);
|
|
return {
|
|
primary: this.#weaponData(primaryWeapon),
|
|
secondary: this.#weaponData(secondaryWeapon),
|
|
burden: this.getBurden(primaryWeapon, secondaryWeapon)
|
|
};
|
|
}
|
|
|
|
get inventoryWeapons() {
|
|
const inventoryWeaponFirst = this.parent.items.find(x => x.type === 'weapon' && x.system.inventoryWeapon === 1);
|
|
const inventoryWeaponSecond = this.parent.items.find(
|
|
x => x.type === 'weapon' && x.system.inventoryWeapon === 2
|
|
);
|
|
return {
|
|
first: this.#weaponData(inventoryWeaponFirst),
|
|
second: this.#weaponData(inventoryWeaponSecond)
|
|
};
|
|
}
|
|
|
|
get totalAttributeMarks() {
|
|
return Object.keys(this.levelData.levelups).reduce((nr, level) => {
|
|
const nrAttributeMarks = Object.keys(this.levelData.levelups[level]).reduce((nr, tier) => {
|
|
nr += Object.keys(this.levelData.levelups[level][tier]?.attributes ?? {}).length * 2;
|
|
|
|
return nr;
|
|
}, 0);
|
|
|
|
nr.push(...Array(nrAttributeMarks).fill(Number.parseInt(level)));
|
|
|
|
return nr;
|
|
}, []);
|
|
}
|
|
|
|
get availableAttributeMarks() {
|
|
const attributeMarks = Object.keys(this.attributes).flatMap(y => this.attributes[y].levelMarks);
|
|
return this.totalAttributeMarks.reduce((acc, attribute) => {
|
|
if (!attributeMarks.findSplice(x => x === attribute)) {
|
|
acc.push(attribute);
|
|
}
|
|
|
|
return acc;
|
|
}, []);
|
|
}
|
|
|
|
get effects() {
|
|
return this.parent.items.reduce((acc, item) => {
|
|
const effects = item.system.effectData;
|
|
if (effects && !item.system.disabled) {
|
|
for (var key in effects) {
|
|
const effect = effects[key];
|
|
for (var effectEntry of effect) {
|
|
if (!acc[key]) acc[key] = [];
|
|
acc[key].push({ name: item.name, value: effectEntry });
|
|
}
|
|
}
|
|
}
|
|
|
|
return acc;
|
|
}, {});
|
|
}
|
|
|
|
get refreshableFeatures() {
|
|
return this.parent.items.reduce(
|
|
(acc, x) => {
|
|
if (x.type === 'feature' && x.system.refreshData.type) {
|
|
acc[x.system.refreshData.type].push(x);
|
|
}
|
|
|
|
return acc;
|
|
},
|
|
{ shortRest: [], longRest: [] }
|
|
);
|
|
}
|
|
|
|
#weaponData(weapon) {
|
|
return weapon
|
|
? {
|
|
name: weapon.name,
|
|
trait: CONFIG.daggerheart.ACTOR.abilities[weapon.system.trait].name, //Should not be done in data?
|
|
range: CONFIG.daggerheart.GENERAL.range[weapon.system.range],
|
|
damage: {
|
|
value: weapon.system.damage.value,
|
|
type: CONFIG.daggerheart.GENERAL.damageTypes[weapon.system.damage.type]
|
|
},
|
|
feature: CONFIG.daggerheart.ITEM.weaponFeatures[weapon.system.feature],
|
|
img: weapon.img,
|
|
uuid: weapon.uuid
|
|
}
|
|
: null;
|
|
}
|
|
|
|
prepareDerivedData() {
|
|
this.resources.hope.max = 6 - this.story.scars.length;
|
|
if (this.resources.hope.value >= this.resources.hope.max) {
|
|
this.resources.hope.value = Math.max(this.resources.hope.max - 1, 0);
|
|
}
|
|
|
|
for (var attributeKey in this.attributes) {
|
|
const attribute = this.attributes[attributeKey];
|
|
|
|
attribute.levelMark = attribute.levelMarks.find(x => this.isSameTier(x)) ?? null;
|
|
|
|
const actualValue = attribute.data.base + attribute.levelMarks.length + attribute.data.bonus;
|
|
attribute.data.actualValue = actualValue;
|
|
attribute.data.value = attribute.data.overrideValue
|
|
? attribute.data.overrideValue
|
|
: attribute.data.actualValue;
|
|
}
|
|
|
|
this.evasion = this.class?.system?.evasion ?? 0;
|
|
// this.armor.value = this.activeArmor?.baseScore ?? 0;
|
|
this.damageThresholds = this.class?.system?.damageThresholds ?? { minor: 0, major: 0, severe: 0 };
|
|
|
|
this.applyLevels();
|
|
this.applyEffects();
|
|
}
|
|
|
|
applyLevels() {
|
|
let healthBonus = 0,
|
|
stressBonus = 0,
|
|
proficiencyBonus = 0,
|
|
evasionBonus = 0,
|
|
armorBonus = 0,
|
|
minorThresholdBonus = 0,
|
|
majorThresholdBonus = 0,
|
|
severeThresholdBonus = 0;
|
|
let experienceBonuses = {};
|
|
let advancementFirst = null,
|
|
advancementSecond = null;
|
|
for (var level in this.levelData.levelups) {
|
|
var levelData = this.levelData.levelups[level];
|
|
for (var tier in levelData) {
|
|
var tierData = levelData[tier];
|
|
if (tierData) {
|
|
healthBonus += Object.keys(tierData.hitPointSlots).length;
|
|
stressBonus += Object.keys(tierData.stressSlots).length;
|
|
proficiencyBonus += Object.keys(tierData.proficiency).length;
|
|
advancementFirst =
|
|
Object.keys(tierData.subclass).length > 0 && level >= 5 && level <= 7
|
|
? { ...tierData.subclass[0], tier: getTier(Number.parseInt(level), true) }
|
|
: advancementFirst;
|
|
advancementSecond =
|
|
Object.keys(tierData.subclass).length > 0 && level >= 8 && level <= 10
|
|
? { ...tierData.subclass[0], tier: getTier(Number.parseInt(level), true) }
|
|
: advancementSecond;
|
|
|
|
for (var index in Object.keys(tierData.experiences)) {
|
|
for (var experienceKey in tierData.experiences[index]) {
|
|
var experience = tierData.experiences[index][experienceKey];
|
|
experienceBonuses[experience] = experienceBonuses[experience]
|
|
? experienceBonuses[experience] + 1
|
|
: 1;
|
|
}
|
|
}
|
|
|
|
evasionBonus += Object.keys(tierData.armorOrEvasionSlot).filter(
|
|
x => tierData.armorOrEvasionSlot[x] === 'evasion'
|
|
).length;
|
|
armorBonus += Object.keys(tierData.armorOrEvasionSlot).filter(
|
|
x => tierData.armorOrEvasionSlot[x] === 'armor'
|
|
).length;
|
|
|
|
majorThresholdBonus += Object.keys(tierData.majorDamageThreshold2).length * 2;
|
|
severeThresholdBonus += Object.keys(tierData.severeDamageThreshold2).length * 2;
|
|
severeThresholdBonus += Object.keys(tierData.severeDamageThreshold3).length * 3;
|
|
severeThresholdBonus += Object.keys(tierData.severeDamageThreshold4).length * 4;
|
|
}
|
|
}
|
|
}
|
|
|
|
this.resources.health.max += healthBonus;
|
|
this.resources.stress.max += stressBonus;
|
|
this.proficiency.value += proficiencyBonus;
|
|
this.evasion += evasionBonus;
|
|
this.armorMarks = {
|
|
max: this.armor ? this.armor.system.marks.max + armorBonus : 0,
|
|
value: this.armor ? this.armor.system.marks.value : 0
|
|
};
|
|
this.damageThresholds.minor += minorThresholdBonus;
|
|
this.damageThresholds.major += majorThresholdBonus;
|
|
this.damageThresholds.severe += severeThresholdBonus;
|
|
|
|
this.experiences = this.experiences.map(x => ({ ...x, value: x.value + (experienceBonuses[x.id] ?? 0) }));
|
|
|
|
const subclassFeatures = this.subclassFeatures;
|
|
if (advancementFirst) {
|
|
if (advancementFirst.multiclass) {
|
|
this.multiclassSubclass.system[`${advancementFirst.feature}Feature`].unlocked = true;
|
|
this.multiclassSubclass.system[`${advancementFirst.feature}Feature`].tier = advancementFirst.tier;
|
|
subclassFeatures.multiclassSubclass[advancementFirst.feature].forEach(x => (x.system.disabled = false));
|
|
} else {
|
|
this.subclass.system[`${advancementFirst.feature}Feature`].unlocked = true;
|
|
this.subclass.system[`${advancementFirst.feature}Feature`].tier = advancementFirst.tier;
|
|
subclassFeatures.subclass[advancementFirst.feature].forEach(x => (x.system.disabled = false));
|
|
}
|
|
}
|
|
if (advancementSecond) {
|
|
if (advancementSecond.multiclass) {
|
|
this.multiclassSubclass.system[`${advancementSecond.feature}Feature`].unlocked = true;
|
|
this.multiclassSubclass.system[`${advancementSecond.feature}Feature`].tier = advancementSecond.tier;
|
|
subclassFeatures.multiclassSubclass[advancementSecond.feature].forEach(
|
|
x => (x.system.disabled = false)
|
|
);
|
|
} else {
|
|
this.subclass.system[`${advancementSecond.feature}Feature`].unlocked = true;
|
|
this.subclass.system[`${advancementSecond.feature}Feature`].tier = advancementSecond.tier;
|
|
subclassFeatures.subclass[advancementSecond.feature].forEach(x => (x.system.disabled = false));
|
|
}
|
|
}
|
|
|
|
//General progression
|
|
for (var i = 0; i < this.levelData.currentLevel; i++) {
|
|
const tier = getTier(i + 1);
|
|
if (tier !== 'tier0') {
|
|
this.domainData.maxLoadout = Math.min(this.domainData.maxLoadout + 1, 5);
|
|
this.domainData.maxCards += 1;
|
|
}
|
|
|
|
switch (tier) {
|
|
case 'tier1':
|
|
this.damageThresholds.severe += 2;
|
|
break;
|
|
case 'tier2':
|
|
this.damageThresholds.major += 1;
|
|
this.damageThresholds.severe += 3;
|
|
break;
|
|
case 'tier3':
|
|
this.damageThresholds.major += 2;
|
|
this.damageThresholds.severe += 4;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
applyEffects() {
|
|
const effects = this.effects;
|
|
for (var key in effects) {
|
|
const effectType = effects[key];
|
|
for (var effect of effectType) {
|
|
switch (key) {
|
|
case SYSTEM.EFFECTS.effectTypes.health.id:
|
|
this.resources.health.max += effect.value.valueData.value;
|
|
break;
|
|
case SYSTEM.EFFECTS.effectTypes.stress.id:
|
|
this.resources.stress.max += effect.value.valueData.value;
|
|
break;
|
|
case SYSTEM.EFFECTS.effectTypes.damage.id:
|
|
this.bonuses.damage.push({
|
|
value: getPathValue(effect.value.valueData.value, this),
|
|
type: 'physical',
|
|
description: effect.name,
|
|
hopeIncrease: effect.value.valueData.hopeIncrease,
|
|
initiallySelected: effect.value.initiallySelected,
|
|
appliesOn: effect.value.appliesOn
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
getBurden(primary, secondary) {
|
|
const twoHanded =
|
|
primary?.system?.burden === 'twoHanded' ||
|
|
secondary?.system?.burden === 'twoHanded' ||
|
|
(primary?.system?.burden === 'oneHanded' && secondary?.system?.burden === 'oneHanded');
|
|
const oneHanded =
|
|
!twoHanded && (primary?.system?.burden === 'oneHanded' || secondary?.system?.burden === 'oneHanded');
|
|
|
|
return twoHanded ? 'twoHanded' : oneHanded ? 'oneHanded' : null;
|
|
}
|
|
|
|
isSameTier(level) {
|
|
return this.#getTier(this.levelData.currentLevel) === this.#getTier(level);
|
|
}
|
|
|
|
#getTier(level) {
|
|
if (level >= 8) return 3;
|
|
else if (level >= 5) return 2;
|
|
else if (level >= 2) return 1;
|
|
else return 0;
|
|
}
|
|
}
|