Files

258 lines
9.0 KiB
JavaScript

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<AwERoll|null>} 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<Roll>}
*/
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: `<strong>${weaponItem.name}</strong> — ${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<AwERoll|null>} 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: `<p>${game.i18n.format("AWEMMY.Kit.Used", { name: item.name })}</p>`
})
}
/**
* 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<void>}
*/
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 => `<li>${s}</li>`).join("")
const content = `
<div class="awemmy-rest-message">
<h3><i class="fa-solid fa-moon"></i> ${game.i18n.format("AWEMMY.Rest.LongRestTitle", { name: this.name })}</h3>
${summary.length ? `<ul>${bulletList}</ul>` : `<p>${game.i18n.localize("AWEMMY.Rest.AlreadyRested")}</p>`}
</div>`
await ChatMessage.create({ speaker: ChatMessage.getSpeaker({ actor: this }), content })
}
}