mirror of
https://github.com/Foundryborne/daggerheart.git
synced 2026-01-12 19:51:08 +01:00
497 lines
No EOL
21 KiB
JavaScript
497 lines
No EOL
21 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;
|
|
}
|
|
} |