import { SYSTEM } from "../config/system.mjs" import FTLNomadRoll from "../documents/roll.mjs" export default class FTLNomadProtagonist extends foundry.abstract.TypeDataModel { static defineSchema() { const fields = foundry.data.fields const requiredInteger = { required: true, nullable: false, integer: true } const schema = {} schema.description = new fields.HTMLField({ required: true, textSearch: true }) schema.notes = new fields.HTMLField({ required: true, textSearch: true }) schema.name = new fields.StringField({ required: true, nullable: false, initial: "" }) schema.concept = new fields.StringField({ required: true, nullable: false, initial: "" }) schema.species = new fields.StringField({ required: true, nullable: false, initial: "" }) schema.archetype = new fields.StringField({ required: true, nullable: false, initial: "" }) // Carac const skillField = (label) => { const schema = { value: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }), } return new fields.SchemaField(schema, { label }) } schema.skills = new fields.SchemaField( Object.values(SYSTEM.SKILLS).reduce((obj, characteristic) => { obj[characteristic.id] = skillField(characteristic.label) return obj }, {}), ) schema.wp = new fields.SchemaField({ value: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }), max: new fields.NumberField({ ...requiredInteger, initial: 3, min: 0 }), exhausted: new fields.BooleanField({ required: true, initial: false }) }) schema.hp = new fields.SchemaField({ value: new fields.NumberField({ ...requiredInteger, initial: 1, min: 0 }), max: new fields.NumberField({ ...requiredInteger, initial: 1, min: 0 }), stunned: new fields.BooleanField({ required: true, initial: false }) }) schema.san = new fields.SchemaField({ value: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }), max: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }), recovery: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }), violence: new fields.ArrayField(new fields.BooleanField(), { required: true, initial: [false, false, false], min:3, max:3}), helplessness: new fields.ArrayField(new fields.BooleanField(), { required: true, initial: [false, false, false], min:3, max:3 }), breakingPoint: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }), insanity: new fields.StringField({ required: true, nullable: false, initial: "none", choices:SYSTEM.INSANITY }), }) schema.damageBonus = new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }) schema.resources = new fields.SchemaField({ value: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }), // Unused but kept for compatibility permanentRating: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }), hand: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }), currentHand: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }), stowed: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }), currentStowed: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }), storage: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }), currentStorage: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }), checks: new fields.ArrayField(new fields.BooleanField(), { required: true, initial: [false, false, false], min:3, max:3 }), nbValidChecks: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }) }) schema.biodata = new fields.SchemaField({ age: new fields.NumberField({ ...requiredInteger, initial: 15, min: 6 }), archetype: new fields.StringField({ required: true, nullable: false, initial: "" }), height: new fields.NumberField({ ...requiredInteger, initial: 170, min: 50 }), gender: new fields.StringField({ required: true, nullable: false, initial: "" }), home: new fields.StringField({ required: true, nullable: false, initial: "" }), birthplace: new fields.StringField({ required: true, nullable: false, initial: "" }), eyes: new fields.StringField({ required: true, nullable: false, initial: "" }), hair: new fields.StringField({ required: true, nullable: false, initial: "" }), harshness: new fields.StringField({ required: true, nullable: false, initial: "normal", choices:SYSTEM.HARSHNESS }), adaptedToViolence: new fields.BooleanField({ required: true, initial: false }), adaptedToHelplessness: new fields.BooleanField({ required: true, initial: false }) }) return schema } /** @override */ static LOCALIZATION_PREFIXES = ["FTLNOMAD.Protagonist"] prepareDerivedData() { super.prepareDerivedData(); let updates = {} if ( this.wp.max !== this.characteristics.pow.value) { updates[`system.wp.max`] = this.characteristics.pow.value } let hpMax = Math.round((this.characteristics.con.value + this.characteristics.str.value) / 2) if ( this.hp.max !== hpMax) { updates[`system.hp.max`] = hpMax } // Get Unnatural skill for MAX SAN let unnatural = this.parent.items.find(i => i.type === "skill" && i.name.toLowerCase() === game.i18n.localize("FTLNOMAD.Skill.Unnatural").toLowerCase()) let minus = 0 if (unnatural) { minus = unnatural.system.skillTotal } let maxSan = Math.max(99 - minus, 0) if ( this.san.max !== maxSan) { updates[`system.san.max`] = maxSan } let recoverySan = this.characteristics.pow.value * 5 if (recoverySan > this.san.max) { recoverySan = this.san.max } if ( this.san.recovery !== recoverySan) { updates[`system.san.recovery`] = recoverySan } let dmgBonus = 0 if (this.characteristics.str.value <= 4) { dmgBonus = -2 } else if (this.characteristics.str.value <= 8) { dmgBonus = -1 } else if (this.characteristics.str.value <= 12) { dmgBonus = 0 } else if (this.characteristics.str.value <= 16) { dmgBonus = 1 } else if (this.characteristics.str.value <= 20) { dmgBonus = 2 } if ( this.damageBonus !== dmgBonus) { updates[`system.damageBonus`] = dmgBonus } // Sanity check if (this.san.value > this.san.max) { updates[`system.san.value`] = this.san.max } if (this.wp.value > this.wp.max) { updates[`system.wp.value`] = this.wp.max } if (this.hp.value > this.hp.max) { updates[`system.hp.value`] = this.hp.max } if (this.resources.permanentRating < 0) { updates[`system.resources.permanentRating`] = 0 } let resourceIndex = Math.max(Math.min(this.resources.permanentRating, 20), 0) let breakdown = SYSTEM.RESOURCE_BREAKDOWN[resourceIndex] if (this.resources.hand !== breakdown.hand) { updates[`system.resources.hand`] = breakdown.hand } if (this.resources.stowed !== breakdown.stowed) { updates[`system.resources.stowed`] = breakdown.stowed } if (this.resources.storage !== breakdown.storage) { updates[`system.resources.storage`] = breakdown.storage + (this.resources.permanentRating - resourceIndex) } if (this.resources.nbValidChecks !== breakdown.checks) { updates[`system.resources.nbValidChecks`] = breakdown.checks } if (Object.keys(updates).length > 0) { this.parent.update(updates) } } isLowWP() { return this.wp.value <= 2 } isZeroWP() { return this.wp.value === 0 } isExhausted() { return this.wp.exhausted } modifyWP(value) { let updates = {} let wp = Math.max(Math.min(this.wp.value + value, this.wp.max), 0) if ( this.wp.value !== wp) { updates[`system.wp.value`] = wp } if (Object.keys(updates).length > 0) { this.parent.update(updates) } } setBP() { let updates = {} let bp = Math.max(this.san.value - this.characteristics.pow.value, 0) if ( this.san.breakingPoint !== bp) { updates[`system.san.breakingPoint`] = bp } if (Object.keys(updates).length > 0) { this.parent.update(updates) } } /** */ /** * Rolls a dice for a character. * @param {("save"|"resource|damage")} rollType The type of the roll. * @param {number} rollItem The target value for the roll. Which caracteristic or resource. If the roll is a damage roll, this is the id of the item. * @returns {Promise} - A promise that resolves to null if the roll is cancelled. */ async roll(rollType, rollItem) { let opponentTarget const hasTarget = opponentTarget !== undefined let roll = await CthulhuEternalRoll.prompt({ rollType, rollItem, isLowWP: this.isLowWP(), isZeroWP: this.isZeroWP(), isExhausted: this.isExhausted(), actorId: this.parent.id, actorName: this.parent.name, actorImage: this.parent.img, hasTarget, target: opponentTarget }) if (!roll) return null await roll.toMessage({}, { rollMode: roll.options.rollMode }) } }