import { burden } from '../../config/generalConfig.mjs'; import ForeignDocumentUUIDField from '../fields/foreignDocumentUUIDField.mjs'; import DhLevelData from '../levelData.mjs'; import BaseDataActor from './base.mjs'; import { attributeField, resourceField, stressDamageReductionRule, bonusField } from '../fields/actorField.mjs'; import { ActionField } from '../fields/actionField.mjs'; export default class DhCharacter extends BaseDataActor { static LOCALIZATION_PREFIXES = ['DAGGERHEART.ACTORS.Character']; static get metadata() { return foundry.utils.mergeObject(super.metadata, { label: 'TYPES.Actor.character', type: 'character', isNPC: false }); } static defineSchema() { const fields = foundry.data.fields; return { ...super.defineSchema(), resources: new fields.SchemaField({ hitPoints: resourceField(0, 'DAGGERHEART.GENERAL.HitPoints.plural', true), stress: resourceField(6, 'DAGGERHEART.GENERAL.stress', true), hope: resourceField(6, 'DAGGERHEART.GENERAL.hope') }), traits: new fields.SchemaField({ agility: attributeField('DAGGERHEART.CONFIG.Traits.agility.name'), strength: attributeField('DAGGERHEART.CONFIG.Traits.strength.name'), finesse: attributeField('DAGGERHEART.CONFIG.Traits.finesse.name'), instinct: attributeField('DAGGERHEART.CONFIG.Traits.instinct.name'), presence: attributeField('DAGGERHEART.CONFIG.Traits.presence.name'), knowledge: attributeField('DAGGERHEART.CONFIG.Traits.knowledge.name') }), proficiency: new fields.NumberField({ initial: 1, integer: true, label: 'DAGGERHEART.GENERAL.proficiency' }), evasion: new fields.NumberField({ initial: 0, integer: true, label: 'DAGGERHEART.GENERAL.evasion' }), armorScore: new fields.NumberField({ integer: true, initial: 0, label: 'DAGGERHEART.GENERAL.armorScore' }), damageThresholds: new fields.SchemaField({ severe: new fields.NumberField({ integer: true, initial: 0, label: 'DAGGERHEART.GENERAL.DamageThresholds.severeThreshold' }), major: new fields.NumberField({ integer: true, initial: 0, label: 'DAGGERHEART.GENERAL.DamageThresholds.majorThreshold' }) }), experiences: new fields.TypedObjectField( new fields.SchemaField({ name: new fields.StringField(), value: new fields.NumberField({ integer: true, initial: 0 }) }) ), 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 }) }), scars: new fields.TypedObjectField( new fields.SchemaField({ name: new fields.StringField({}), description: new fields.StringField() }) ), biography: new fields.SchemaField({ background: new fields.HTMLField(), connections: new fields.HTMLField(), characteristics: new fields.SchemaField({ pronouns: new fields.StringField({}), age: new fields.StringField({}), faith: new fields.StringField({}) }) }), class: new fields.SchemaField({ value: new ForeignDocumentUUIDField({ type: 'Item', nullable: true }), subclass: new ForeignDocumentUUIDField({ type: 'Item', nullable: true }) }), multiclass: new fields.SchemaField({ value: new ForeignDocumentUUIDField({ type: 'Item', nullable: true }), subclass: new ForeignDocumentUUIDField({ type: 'Item', nullable: true }) }), attack: new ActionField({ initial: { name: 'Attack', img: 'icons/skills/melee/unarmed-punch-fist-yellow-red.webp', _id: foundry.utils.randomID(), systemPath: 'attack', chatDisplay: false, type: 'attack', range: 'melee', target: { type: 'any', amount: 1 }, roll: { type: 'attack', trait: 'strength' }, damage: { parts: [ { type: ['physical'], value: { custom: { enabled: true, formula: '@profd4' } } } ] } } }), advantageSources: new fields.ArrayField(new fields.StringField(), { label: 'DAGGERHEART.ACTORS.Character.advantageSources.label', hint: 'DAGGERHEART.ACTORS.Character.advantageSources.hint' }), disadvantageSources: new fields.ArrayField(new fields.StringField(), { label: 'DAGGERHEART.ACTORS.Character.disadvantageSources.label', hint: 'DAGGERHEART.ACTORS.Character.disadvantageSources.hint' }), levelData: new fields.EmbeddedDataField(DhLevelData), bonuses: new fields.SchemaField({ roll: new fields.SchemaField({ attack: bonusField('DAGGERHEART.GENERAL.Roll.attack'), spellcast: bonusField('DAGGERHEART.GENERAL.Roll.spellcast'), trait: bonusField('DAGGERHEART.GENERAL.Roll.trait'), action: bonusField('DAGGERHEART.GENERAL.Roll.action'), reaction: bonusField('DAGGERHEART.GENERAL.Roll.reaction'), primaryWeapon: bonusField('DAGGERHEART.GENERAL.Roll.primaryWeaponAttack'), secondaryWeapon: bonusField('DAGGERHEART.GENERAL.Roll.secondaryWeaponAttack') }), damage: new fields.SchemaField({ physical: bonusField('DAGGERHEART.GENERAL.Damage.physicalDamage'), magical: bonusField('DAGGERHEART.GENERAL.Damage.magicalDamage'), primaryWeapon: bonusField('DAGGERHEART.GENERAL.Damage.primaryWeapon'), secondaryWeapon: bonusField('DAGGERHEART.GENERAL.Damage.secondaryWeapon') }), healing: bonusField('DAGGERHEART.GENERAL.Healing.healingAmount'), range: new fields.SchemaField({ weapon: new fields.NumberField({ integer: true, initial: 0, label: 'DAGGERHEART.GENERAL.Range.weapon' }), spell: new fields.NumberField({ integer: true, initial: 0, label: 'DAGGERHEART.GENERAL.Range.spell' }), other: new fields.NumberField({ integer: true, initial: 0, label: 'DAGGERHEART.GENERAL.Range.other' }) }), rally: new fields.ArrayField(new fields.StringField(), { label: 'DAGGERHEART.CLASS.Feature.rallyDice' }), rest: new fields.SchemaField({ shortRest: new fields.SchemaField({ shortMoves: new fields.NumberField({ required: true, integer: true, min: 0, initial: 0, label: 'DAGGERHEART.GENERAL.Bonuses.rest.shortRest.shortRestMoves.label', hint: 'DAGGERHEART.GENERAL.Bonuses.rest.shortRest.shortRestMoves.hint' }), longMoves: new fields.NumberField({ required: true, integer: true, min: 0, initial: 0, label: 'DAGGERHEART.GENERAL.Bonuses.rest.shortRest.longRestMoves.label', hint: 'DAGGERHEART.GENERAL.Bonuses.rest.shortRest.longRestMoves.hint' }) }), longRest: new fields.SchemaField({ shortMoves: new fields.NumberField({ required: true, integer: true, min: 0, initial: 0, label: 'DAGGERHEART.GENERAL.Bonuses.rest.longRest.shortRestMoves.label', hint: 'DAGGERHEART.GENERAL.Bonuses.rest.longRest.shortRestMoves.hint' }), longMoves: new fields.NumberField({ required: true, integer: true, min: 0, initial: 0, label: 'DAGGERHEART.GENERAL.Bonuses.rest.longRest.longRestMoves.label', hint: 'DAGGERHEART.GENERAL.Bonuses.rest.longRest.longRestMoves.hint' }) }) }), maxLoadout: new fields.NumberField({ integer: true, initial: 0, label: 'DAGGERHEART.GENERAL.Bonuses.maxLoadout.label' }) }), companion: new ForeignDocumentUUIDField({ type: 'Actor', nullable: true, initial: null }), rules: new fields.SchemaField({ damageReduction: new fields.SchemaField({ maxArmorMarked: new fields.SchemaField({ value: new fields.NumberField({ required: true, integer: true, initial: 1, label: 'DAGGERHEART.GENERAL.Rules.damageReduction.maxArmorMarkedBonus' }), stressExtra: new fields.NumberField({ required: true, integer: true, initial: 0, label: 'DAGGERHEART.GENERAL.Rules.damageReduction.maxArmorMarkedStress.label', hint: 'DAGGERHEART.GENERAL.Rules.damageReduction.maxArmorMarkedStress.hint' }) }), stressDamageReduction: new fields.SchemaField({ severe: stressDamageReductionRule('DAGGERHEART.GENERAL.Rules.damageReduction.stress.severe'), major: stressDamageReductionRule('DAGGERHEART.GENERAL.Rules.damageReduction.stress.major'), minor: stressDamageReductionRule('DAGGERHEART.GENERAL.Rules.damageReduction.stress.minor') }), increasePerArmorMark: new fields.NumberField({ integer: true, initial: 1, label: 'DAGGERHEART.GENERAL.Rules.damageReduction.increasePerArmorMark.label', hint: 'DAGGERHEART.GENERAL.Rules.damageReduction.increasePerArmorMark.hint' }), magical: new fields.BooleanField({ initial: false }), physical: new fields.BooleanField({ initial: false }) }), attack: new fields.SchemaField({ damage: new fields.SchemaField({ 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: 0, min: 0, label: 'DAGGERHEART.GENERAL.Rules.attack.damage.bonus.label' }) }), roll: new fields.SchemaField({ trait: new fields.StringField({ required: true, choices: CONFIG.DH.ACTOR.abilities, nullable: true, initial: null, label: 'DAGGERHEART.GENERAL.Rules.attack.roll.trait.label' }) }) }), weapon: new fields.SchemaField({ /* Unimplemented -> Should remove the lowest damage dice from weapon damage -> Reflect this in the chat message somehow so players get feedback that their choice is helping them. */ dropLowestDamageDice: new fields.BooleanField({ initial: false }), /* Unimplemented -> Should flip any lowest possible dice rolls for weapon damage to highest -> Reflect this in the chat message somehow so players get feedback that their choice is helping them. */ flipMinDiceValue: new fields.BooleanField({ intial: false }) }), runeWard: new fields.BooleanField({ initial: false }) }) }; } get tier() { const currentLevel = this.levelData.level.current; return currentLevel === 1 ? 1 : Object.values(game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LevelTiers).tiers).find( tier => currentLevel >= tier.levels.start && currentLevel <= tier.levels.end ).tier; } get ancestry() { return this.parent.items.find(x => x.type === 'ancestry') ?? null; } get community() { return this.parent.items.find(x => x.type === 'community') ?? null; } get features() { return this.parent.items.filter(x => x.type === 'feature') ?? []; } get companionFeatures() { return this.companion ? this.companion.items.filter(x => x.type === 'feature') : []; } get needsCharacterSetup() { return !this.class.value || !this.class.subclass; } get spellcastModifier() { const subClasses = this.parent.items.filter(x => x.type === 'subclass') ?? []; return Math.max(subClasses?.map(sc => this.traits[sc.system.spellcastingTrait]?.value)); } get spellcastingModifiers() { return { main: this.class.subclass?.system?.spellcastingTrait, multiclass: this.multiclass.subclass?.system?.spellcastingTrait }; } get domains() { const classDomains = this.class.value ? this.class.value.system.domains : []; const multiclassDomains = this.multiclass.value ? this.multiclass.value.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 loadoutSlot() { const loadoutCount = this.domainCards.loadout?.length ?? 0, max = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).maxLoadout + this.bonuses.maxLoadout; return { current: loadoutCount, available: Math.max(max - loadoutCount, 0), max }; } get armor() { return this.parent.items.find(x => x.type === 'armor' && x.system.equipped); } get activeBeastform() { return this.parent.effects.find(x => x.type === 'beastform'); } get usedUnarmed() { const primaryWeaponEquipped = this.primaryWeapon?.system?.equipped; const secondaryWeaponEquipped = this.secondaryWeapon?.system?.equipped; return !primaryWeaponEquipped && !secondaryWeaponEquipped ? { ...this.attack, uuid: this.attack.uuid, id: this.attack.id, name: this.activeBeastform ? 'DAGGERHEART.ITEMS.Beastform.attackName' : this.attack.name, img: this.activeBeastform ? 'icons/creatures/claws/claw-straight-brown.webp' : this.attack.img, actor: this.parent } : null; } get sheetLists() { const ancestryFeatures = [], communityFeatures = [], classFeatures = [], subclassFeatures = [], companionFeatures = [], features = []; for (let item of this.parent.items) { if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.ancestry.id) { ancestryFeatures.push(item); } else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.community.id) { communityFeatures.push(item); } else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.class.id) { classFeatures.push(item); } else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.subclass.id) { if (this.class.subclass) { const subclassState = this.class.subclass.system.featureState; const subType = item.system.subType; if ( subType === CONFIG.DH.ITEM.featureSubTypes.foundation || (subType === CONFIG.DH.ITEM.featureSubTypes.specialization && subclassState >= 2) || (subType === CONFIG.DH.ITEM.featureSubTypes.mastery && subclassState >= 3) ) { subclassFeatures.push(item); } } } else if (item.system.originItemType === CONFIG.DH.ITEM.featureTypes.companion.id) { companionFeatures.push(item); } else if (item.type === 'feature' && !item.system.type) { features.push(item); } } return { ancestryFeatures: { title: `${game.i18n.localize('TYPES.Item.ancestry')} - ${this.ancestry?.name}`, type: 'ancestry', values: ancestryFeatures }, communityFeatures: { title: `${game.i18n.localize('TYPES.Item.community')} - ${this.community?.name}`, type: 'community', values: communityFeatures }, classFeatures: { title: `${game.i18n.localize('TYPES.Item.class')} - ${this.class.value?.name}`, type: 'class', values: classFeatures }, subclassFeatures: { title: `${game.i18n.localize('TYPES.Item.subclass')} - ${this.class.subclass?.name}`, type: 'subclass', values: subclassFeatures }, companionFeatures: { title: game.i18n.localize('DAGGERHEART.ACTORS.Character.companionFeatures'), type: 'companion', values: companionFeatures }, features: { title: game.i18n.localize('DAGGERHEART.GENERAL.features'), type: 'feature', values: features } }; } get primaryWeapon() { return this.parent.items.find(x => x.type === 'weapon' && x.system.equipped && !x.system.secondary); } get secondaryWeapon() { return this.parent.items.find(x => x.type === 'weapon' && x.system.equipped && x.system.secondary); } get getWeaponBurden() { return this.primaryWeapon?.system?.burden === burden.twoHanded.value || (this.primaryWeapon && this.secondaryWeapon) ? burden.twoHanded.value : this.primaryWeapon || this.secondaryWeapon ? burden.oneHanded.value : null; } get deathMoveViable() { return this.resources.hitPoints.max > 0 && this.resources.hitPoints.value >= this.resources.hitPoints.max; } get armorApplicableDamageTypes() { return { physical: !this.rules.damageReduction.magical, magical: !this.rules.damageReduction.physical }; } 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; if (itemToEquip.system.secondary) { if (primary && primary.burden === CONFIG.DH.GENERAL.burden.twoHanded.value) { await primary.update({ 'system.equipped': false }); } if (secondary) { await secondary.update({ 'system.equipped': false }); } } else { if (secondary && itemToEquip.system.burden === CONFIG.DH.GENERAL.burden.twoHanded.value) { await secondary.update({ 'system.equipped': false }); } if (primary) { await primary.update({ 'system.equipped': false }); } } } prepareBaseData() { this.evasion = this.class.value?.system?.evasion ?? 0; const currentLevel = this.levelData.level.current; const currentTier = currentLevel === 1 ? null : Object.values(game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.LevelTiers).tiers).find( tier => currentLevel >= tier.levels.start && currentLevel <= tier.levels.end ).tier; for (let levelKey in this.levelData.levelups) { const level = this.levelData.levelups[levelKey]; this.proficiency += level.achievements.proficiency; for (let selection of level.selections) { switch (selection.type) { case 'trait': selection.data.forEach(data => { this.traits[data].value += 1; this.traits[data].tierMarked = selection.tier === currentTier; }); break; case 'hitPoint': this.resources.hitPoints.max += selection.value; break; case 'stress': this.resources.stress.max += selection.value; break; case 'evasion': this.evasion += selection.value; break; case 'proficiency': this.proficiency += selection.value; break; case 'experience': Object.keys(this.experiences).forEach(key => { const experience = this.experiences[key]; experience.value += selection.value; }); break; } } } const armor = this.armor; this.armorScore = armor ? armor.system.baseScore : 0; this.damageThresholds = { major: armor ? armor.system.baseThresholds.major + this.levelData.level.current : this.levelData.level.current, severe: armor ? armor.system.baseThresholds.severe + this.levelData.level.current : this.levelData.level.current * 2 }; this.resources.hope.max -= Object.keys(this.scars).length; this.resources.hitPoints.max = this.class.value?.system?.hitPoints ?? 0; } prepareDerivedData() { 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.resources.armor = { value: this.armor.system.marks.value, max: this.armorScore, isReversed: true }; 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 }; } async _preDelete() { if (this.companion) { this.companion.updateLevel(1); } } }