import AwERoll from "./roll.mjs" import { SYSTEM } from "../config/system.mjs" export default class AwEActor extends Actor { /** @override */ prepareData() { super.prepareData() } /** @override */ prepareBaseData() {} /** @override */ prepareDerivedData() { const actorData = this this._prepareCharacterData(actorData) this._prepareCreatureData(actorData) } /** * Prepare character data. * @param {object} actorData - The actor data. */ _prepareCharacterData(actorData) { if (actorData.type !== "character") return } /** * Prepare creature data. * @param {object} actorData - The actor data. */ _prepareCreatureData(actorData) { if (actorData.type !== "creature") return } /** * Roll a weapon attack: attack roll + damage if hit. * @param {Item} weaponItem - The weapon item being used. * @param {object} options - Additional roll options. * @returns {Promise} The evaluated roll, or null if cancelled. */ async rollWeapon(weaponItem, options = {}) { const attrId = weaponItem.system.attackAttribute const attribute = this.system.attributes[attrId] if (!attribute) return null const knowledgeBonuses = this.#buildItemBonuses() const { conditionBonus, conditionLabels } = this.#buildConditionOptions() const roll = await AwERoll.prompt({ attributeKey: attrId, modifier: attribute.mod ?? 0, attributeBonus: attribute.bonus ?? 0, knowledgeBonuses, conditionBonus, conditionLabels, actorId: this.id, actorName: this.name, actorImage: this.img, sourceItemName: weaponItem.name, sourceItemImg: weaponItem.img, damageFormula: weaponItem.system.damageFormula, damageType: weaponItem.system.damageType, ...options }) // Remove consumed conditions if (roll && this.statuses.has("edge")) await this.toggleStatusEffect("edge") return roll } /** * Roll weapon damage directly (no attack roll). * @param {Item} weaponItem - The weapon item. * @returns {Promise} */ async rollDamage(weaponItem) { const formula = weaponItem.system.damageFormula if (!formula) return ui.notifications.warn(game.i18n.localize("AWEMMY.Weapon.NoDamageFormula")) const roll = new Roll(formula) await roll.evaluate() const typeStr = weaponItem.system.damageType ? ` (${weaponItem.system.damageType})` : "" await roll.toMessage({ speaker: ChatMessage.getSpeaker({ actor: this }), flavor: `${weaponItem.name} — ${game.i18n.localize("AWEMMY.Weapon.DamageRoll")}${typeStr}` }) return roll } /** * Roll an attribute check. * @param {string} attributeId - The attribute to roll. * @param {object} options - Roll options (passed through to AwERoll.prompt). * @returns {Promise} The evaluated roll, or null if cancelled. */ async rollAttribute(attributeId, options = {}) { const attribute = this.system.attributes[attributeId] if (!attribute) return null const knowledgeBonuses = this.#buildItemBonuses() const { conditionBonus, conditionLabels } = this.#buildConditionOptions() const roll = await AwERoll.prompt({ attributeKey: attributeId, modifier: attribute.mod ?? 0, attributeBonus: attribute.bonus ?? 0, knowledgeBonuses, conditionBonus, conditionLabels, actorId: this.id, actorName: this.name, actorImage: this.img, ...options }) // Remove consumed conditions if (roll && this.statuses.has("edge")) await this.toggleStatusEffect("edge") return roll } /** Collect roll bonuses from all item types that declare a rollBonus or knowledgeBonus. */ #buildItemBonuses() { const entries = [ // Field items use the original knowledgeBonus field ...(this.itemTypes.field ?? []).map(i => ({ label: i.name, bonus: i.system.knowledgeBonus ?? "" })), // Ability, equipment, specialization use rollBonus ...(this.itemTypes.ability ?? []).map(i => ({ label: i.name, bonus: i.system.rollBonus ?? "" })), ...(this.itemTypes.equipment ?? []).map(i => ({ label: i.name, bonus: i.system.rollBonus ?? "" })), ...(this.itemTypes.specialization ?? []).map(i => ({ label: i.name, bonus: i.system.rollBonus ?? "" })), ] return entries.filter(e => e.bonus !== "") } #buildConditionOptions() { let conditionBonus = 0 const conditionLabels = [] if (this.statuses.has("edge")) { conditionBonus += 2 conditionLabels.push({ label: game.i18n.localize("AWEMMY.Condition.Edge"), bonus: 2 }) } if (this.statuses.has("prone")) { conditionBonus -= 2 conditionLabels.push({ label: game.i18n.localize("AWEMMY.Condition.Prone"), bonus: -2 }) } if (this.statuses.has("jumbled")) { conditionBonus -= 2 conditionLabels.push({ label: game.i18n.localize("AWEMMY.Condition.Jumbled"), bonus: -2 }) } if (this.statuses.has("inhibited")) { const penalty = this.system.inhibitedPenalty ?? 2 conditionBonus -= penalty conditionLabels.push({ label: game.i18n.localize("AWEMMY.Condition.Inhibited"), bonus: -penalty }) } return { conditionBonus, conditionLabels } } /** * Use a kit item: post a chat message. * @param {string} kitId - The kit item ID. */ async useKit(kitId) { const item = this.items.get(kitId) if (!item) return await ChatMessage.create({ speaker: ChatMessage.getSpeaker({ actor: this }), content: `

${game.i18n.format("AWEMMY.Kit.Used", { name: item.name })}

` }) } /** * Use an ability item: check daily/FP constraints, deduct FP, mark used, post chat card. * @param {string} abilityId - The ability item ID. */ async useAbility(abilityId) { const item = this.items.get(abilityId) if (!item) return const sys = item.system const isDaily = sys.isDaily if (isDaily && sys.usedToday) { ui.notifications.warn(game.i18n.format("AWEMMY.Ability.AlreadyUsed", { name: item.name })) return } if (sys.flowPointCost > 0) { const fp = this.system.flowPoints.value if (fp < sys.flowPointCost) { ui.notifications.warn(game.i18n.format("AWEMMY.Ability.NotEnoughFP", { name: item.name, cost: sys.flowPointCost, current: fp })) return } await this.update({ "system.flowPoints.value": fp - sys.flowPointCost }) } if (isDaily) await item.update({ "system.usedToday": true }) const abilityTypeLabel = game.i18n.localize(SYSTEM.ABILITY_TYPE[sys.abilityType]?.label ?? sys.abilityType) const costLabel = game.i18n.localize(SYSTEM.ABILITY_COST[sys.cost]?.label ?? sys.cost) const enrichedDescription = await foundry.applications.ux.TextEditor.implementation.enrichHTML(sys.description ?? "", { async: true, relativeTo: item }) const content = await foundry.applications.handlebars.renderTemplate( "systems/fvtt-adventures-with-emmy/templates/ability-use.hbs", { name: item.name, img: item.img, costLabel, abilityTypeLabel, traits: sys.traits ?? [], frequency: sys.frequency, trigger: sys.trigger, requirements: sys.requirements, flowPointCost: sys.flowPointCost || 0, description: enrichedDescription } ) await ChatMessage.create({ speaker: ChatMessage.getSpeaker({ actor: this }), content }) } /** * Reset all once-per-day abilities (usedToday → false). * @returns {Promise} */ async resetDailyAbilities() { const dailyAbilities = this.itemTypes.ability.filter(i => i.system.usedToday) if (!dailyAbilities.length) return 0 await this.updateEmbeddedDocuments("Item", dailyAbilities.map(i => ({ _id: i.id, "system.usedToday": false }))) return dailyAbilities.length } /** * Perform a long rest: restore HP, reset daily abilities, post chat card. * No confirmation dialog — caller is responsible for confirming if needed. */ async longRest() { const sys = this.system const updates = {} const summary = [] const hpMissing = sys.hp.max - sys.hp.value if (hpMissing > 0) { updates["system.hp.value"] = sys.hp.max summary.push(game.i18n.format("AWEMMY.Rest.HPRestored", { amount: hpMissing, max: sys.hp.max })) } if (Object.keys(updates).length > 0) await this.update(updates) const resetCount = await this.resetDailyAbilities() if (resetCount > 0) { summary.push(game.i18n.format("AWEMMY.Rest.AbilitiesReset", { count: resetCount })) } const bulletList = summary.map(s => `
  • ${s}
  • `).join("") const content = `

    ${game.i18n.format("AWEMMY.Rest.LongRestTitle", { name: this.name })}

    ${summary.length ? `
      ${bulletList}
    ` : `

    ${game.i18n.localize("AWEMMY.Rest.AlreadyRested")}

    `}
    ` await ChatMessage.create({ speaker: ChatMessage.getSpeaker({ actor: this }), content }) } }