import { BoLRoll } from "../controllers/bol-rolls.js"; /** * BoL Macro API — accessible via game.bol.macros * * Usage examples (in a Foundry macro): * * game.bol.macros.rollAttribute("vigor") * game.bol.macros.rollAttribute("mind") * * game.bol.macros.rollAptitude("melee") * game.bol.macros.rollAptitude("ranged") * game.bol.macros.rollAptitude("def") * game.bol.macros.rollAptitude("init") * * game.bol.macros.rollWeapon("Épée courte") // by name (partial match) * game.bol.macros.rollWeapon(0) // by index (first weapon) * * game.bol.macros.rollSpell("Boule de feu") // by name (partial match) * game.bol.macros.rollSpell(0) // by index * * game.bol.macros.rollAlchemy("Potion de soin") // by name (partial match) * game.bol.macros.rollAlchemy(0) // by index * * game.bol.macros.rollHoroscope("minor") * game.bol.macros.rollHoroscope("major") * * // Generic dispatcher: * game.bol.macros.roll("attribute", "vigor") * game.bol.macros.roll("aptitude", "melee") * game.bol.macros.roll("weapon", "Épée courte") * game.bol.macros.roll("spell", "Boule de feu") * game.bol.macros.roll("alchemy", 0) * game.bol.macros.roll("horoscope", "minor") */ export class Macros { /* -------------------------------------------- */ /** * Resolves the actor for macro use: * - If multiple tokens are selected → error (always) * - If exactly one token is selected → use it (GM or player) * - If no token selected and user is GM → error (GM must select a token) * - If no token selected and user is a player → use their assigned character * @returns {Actor|null} */ static getSpeakersActor() { const tokens = canvas.tokens?.controlled ?? [] if (tokens.length > 1) { ui.notifications.warn(game.i18n.localize('BOL.notification.MacroMultipleTokensSelected')) return null } if (tokens.length === 1) { return tokens[0].actor ?? null } // No token selected if (game.user.isGM) { ui.notifications.error(game.i18n.localize("BOL.notification.MacroNoTokenSelected")) return null } // Player: fall back to their assigned character const actor = game.user.character if (!actor) { ui.notifications.error(game.i18n.localize("BOL.notification.MacroNoTokenSelected")) return null } return actor } /* -------------------------------------------- */ /** * Finds an item on an actor by name (partial, case-insensitive) or index. * @param {Actor} actor * @param {string} type - item type: "weapon", "spell", "alchemy" * @param {string|number} nameOrIndex * @returns {object|undefined} */ static _findItem(actor, type, nameOrIndex) { const items = actor.items.filter(i => i.type === type) if (items.length === 0) { ui.notifications.warn(`${actor.name} : aucun(e) ${type} trouvé(e).`) return undefined } if (nameOrIndex === undefined || nameOrIndex === null) { if (items.length === 1) return foundry.utils.duplicate(items[0]) const names = items.map((it, i) => `[${i}] ${it.name}`).join(', ') ui.notifications.warn(`Précisez le nom ou l'index : ${names}`) return undefined } if (typeof nameOrIndex === "number") { const item = items[nameOrIndex] if (!item) { ui.notifications.warn(`${actor.name} : index ${nameOrIndex} invalide pour ${type}.`) return undefined } return foundry.utils.duplicate(item) } const lower = String(nameOrIndex).toLowerCase() const found = items.find(i => i.name.toLowerCase().includes(lower)) if (!found) { ui.notifications.warn(`${actor.name} : ${type} "${nameOrIndex}" introuvable.`) return undefined } return foundry.utils.duplicate(found) } /* -------------------------------------------- */ /** * Roll an attribute check. * @param {string} key - "vigor" | "agility" | "mind" | "appeal" * @param {Actor} [actor] - optional, defaults to selected/owned token */ static rollAttribute(key = "vigor", actor = undefined) { actor = actor ?? this.getSpeakersActor() if (!actor) return if (!actor.system.attributes[key]) { ui.notifications.warn(`Attribut inconnu : "${key}". Valeurs : vigor, agility, mind, appeal`) return } return BoLRoll.attributeCheck(actor, key) } /* -------------------------------------------- */ /** * Roll an aptitude check. * @param {string} key - "init" | "melee" | "ranged" | "def" * @param {Actor} [actor] - optional, defaults to selected/owned token */ static rollAptitude(key = "melee", actor = undefined) { actor = actor ?? this.getSpeakersActor() if (!actor) return if (!actor.system.aptitudes[key]) { ui.notifications.warn(`Aptitude inconnue : "${key}". Valeurs : init, melee, ranged, def`) return } return BoLRoll.aptitudeCheck(actor, key) } /* -------------------------------------------- */ /** * Roll a weapon attack. * @param {string|number} [nameOrIndex] - weapon name (partial) or index. Defaults to first weapon if only one. * @param {Actor} [actor] - optional, defaults to selected/owned token */ static rollWeapon(nameOrIndex = undefined, actor = undefined) { actor = actor ?? this.getSpeakersActor() if (!actor) return const weapon = this._findItem(actor, "weapon", nameOrIndex) if (!weapon) return return BoLRoll.weaponCheckWithWeapon(actor, weapon) } /* -------------------------------------------- */ /** * Roll a spell check. * @param {string|number} [nameOrIndex] - spell name (partial) or index * @param {Actor} [actor] - optional, defaults to selected/owned token */ static rollSpell(nameOrIndex = undefined, actor = undefined) { actor = actor ?? this.getSpeakersActor() if (!actor) return if ((actor.system.resources.power?.value ?? 1) <= 0) { ui.notifications.warn("Plus assez de points de Pouvoir !") return } const spell = this._findItem(actor, "spell", nameOrIndex) if (!spell) return return BoLRoll.spellCheckWithSpell(actor, spell) } /* -------------------------------------------- */ /** * Roll an alchemy check. * @param {string|number} [nameOrIndex] - alchemy item name (partial) or index * @param {Actor} [actor] - optional, defaults to selected/owned token */ static rollAlchemy(nameOrIndex = undefined, actor = undefined) { actor = actor ?? this.getSpeakersActor() if (!actor) return const alchemy = this._findItem(actor, "alchemy", nameOrIndex) if (!alchemy) return return BoLRoll.alchemyCheckWithItem(actor, alchemy) } /* -------------------------------------------- */ /** * Roll a horoscope check. * @param {"minor"|"major"} [type="minor"] * @param {Actor} [actor] - optional, defaults to selected/owned token */ static rollHoroscope(type = "minor", actor = undefined) { actor = actor ?? this.getSpeakersActor() if (!actor) return return BoLRoll.horoscopeCheck(actor, undefined, type) } /* -------------------------------------------- */ /** * Generic roll dispatcher. * @param {"attribute"|"aptitude"|"weapon"|"spell"|"alchemy"|"horoscope"} type * @param {string|number} [key] - attribute/aptitude key, item name/index, or horoscope type * @param {Actor} [actor] - optional, defaults to selected/owned token */ static roll(type, key = undefined, actor = undefined) { switch (type) { case "attribute": return this.rollAttribute(key ?? "vigor", actor) case "aptitude": return this.rollAptitude(key ?? "melee", actor) case "weapon": return this.rollWeapon(key, actor) case "spell": return this.rollSpell(key, actor) case "alchemy": return this.rollAlchemy(key, actor) case "horoscope": return this.rollHoroscope(key ?? "minor", actor) default: ui.notifications.warn(`Type de jet inconnu : "${type}". Types valides : attribute, aptitude, weapon, spell, alchemy, horoscope`) } } /* -------------------------------------------- */ // Kept for backward-compat (previously called by old macros) static rollMacro = async function (rollType, key) { const actor = Macros.getSpeakersActor() if (!actor) return return Macros.roll(rollType, key, actor) } }