diff --git a/CHANGELOG.md b/CHANGELOG.md index 8435e0fa..92e978fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ ## Version 4.0.2 (So far...) [Hero System 6e (Unofficial) v2](https://github.com/dmdorman/hero6e-foundryvtt) - Strength rolls now use endurance. [#1253](https://github.com/dmdorman/hero6e-foundryvtt/issues/1253) -- Skills can now use STUN for END and END reserves. +- Skill rolls and toggle activations can now use STUN for END and END reserves. They can also use SHIFT to override resource consumption. - Encumbrance related improvements. - Fix for 5e DAMAGE RESISTANCE and PD/ED purchased as a power, where the PD/ED was counted twice. [#1297](https://github.com/dmdorman/hero6e-foundryvtt/issues/1297) - Improved KNOWLEDGE_SKILL descriptions. [#1278](https://github.com/dmdorman/hero6e-foundryvtt/issues/1278) diff --git a/module/actor/actor-sheet.mjs b/module/actor/actor-sheet.mjs index 87e94246..d536fb4a 100644 --- a/module/actor/actor-sheet.mjs +++ b/module/actor/actor-sheet.mjs @@ -1380,7 +1380,7 @@ export class HeroSystemActorSheet extends ActorSheet { }); if (confirmed) { - await ae.parent.toggle(); + await ae.parent.toggle(event); } continue; } diff --git a/module/item/item-attack.mjs b/module/item/item-attack.mjs index 4922df43..3ebbc4f8 100644 --- a/module/item/item-attack.mjs +++ b/module/item/item-attack.mjs @@ -1,5 +1,5 @@ import { HEROSYS } from "../herosystem6e.mjs"; -import { getPowerInfo, getCharacteristicInfoArrayForActor } from "../utility/util.mjs"; +import { getPowerInfo, getCharacteristicInfoArrayForActor, whisperUserTargetsForActor } from "../utility/util.mjs"; import { determineDefense } from "../utility/defense.mjs"; import { HeroSystem6eActorActiveEffects } from "../actor/actor-active-effects.mjs"; import { RoundFavorPlayerDown, RoundFavorPlayerUp } from "../utility/round.mjs"; @@ -67,21 +67,6 @@ function isStunBasedEffectRoll(item) { export async function AttackOptions(item) { const actor = item.actor; const token = actor.getActiveTokens()[0]; - - // if (!actor.canAct(true, event)) { - // return; - // } - - // if ( - // item?.system?.XMLID === "MINDSCAN" && - // !game.user.isGM && - // game.settings.get(game.system.id, "SecretMindScan") - // ) { - // return ui.notifications.error( - // `${item.name} has several secret components that the GM does not wish to reveal. The Game Master is required to roll this attack on your behalf. This "Secret Mind Scan" can be disabled in the settings by the GM.`, - // ); - // } - const data = { item: item, actor: actor, @@ -533,7 +518,10 @@ export async function AttackToHit(item, options) { warning: resourceWarning, resourcesRequired, resourcesUsedDescription, - } = await userInteractiveVerifyOptionallyPromptThenSpendResources(item, options); + } = await userInteractiveVerifyOptionallyPromptThenSpendResources(item, { + ...options, + ...{ noResourceUse: false }, + }); if (resourceError) { return ui.notifications.error(resourceError); } else if (resourceWarning) { @@ -3063,6 +3051,8 @@ async function _calcKnockback(body, item, options, knockbackMultiplier) { * @returns Object discriminated union based on error or warning being falsy/truthy */ export async function userInteractiveVerifyOptionallyPromptThenSpendResources(item, options) { + const useResources = !options.noResourceUse; + // What resources are required to activate this power? const resourcesRequired = calculateRequiredResourcesToUse(item, options); @@ -3072,47 +3062,56 @@ export async function userInteractiveVerifyOptionallyPromptThenSpendResources(it const reserveEnd = parseInt(enduranceReserve?.system.value || 0); const actorEndurance = actor.system.characteristics.end.value; - // Does the actor have enough charges available? - if (resourcesRequired.charges > 0 && resourcesRequired.charges > startingCharges) { - return { - error: `${item.name} does not have ${resourcesRequired.charges} charge${ - resourcesRequired.charges > 1 ? "s" : "" - } remaining.`, - }; - } - // Does the actor have enough endurance available? - let actualStunCostObj = null; - if (item.system.USE_END_RESERVE) { - if (enduranceReserve) { - if (resourcesRequired.end > reserveEnd) { + let actualStunDamage = 0; + let actualStunRoller = null; + if (resourcesRequired.end) { + if (item.system.USE_END_RESERVE) { + if (enduranceReserve) { + if (resourcesRequired.end > reserveEnd && useResources) { + return { + error: `${item.name} needs ${resourcesRequired.end} END but ${enduranceReserve.name} only has ${reserveEnd} END.`, + }; + } + } else { return { - error: `${item.name} needs ${resourcesRequired.end} END but ${enduranceReserve.name} only has ${reserveEnd} END.`, + error: `${item.name} needs an endurance reserve to spend END but none found.`, }; } } else { - return { - error: `${item.name} needs an endurance reserve to spend END but none found.`, - }; - } - } else { - if (resourcesRequired.end > actorEndurance) { - // Is the actor willing to use STUN to make up for the lack of END? - const potentialStunCost = calculateRequiredStunDiceForLackOfEnd(actor, resourcesRequired.end); + if (resourcesRequired.end > actorEndurance && useResources) { + // Is the actor willing to use STUN to make up for the lack of END? + const potentialStunCost = calculateRequiredStunDiceForLackOfEnd(actor, resourcesRequired.end); - const confirmed = await Dialog.confirm({ - title: "USING STUN FOR ENDURANCE", - content: `
${item.name} requires ${resourcesRequired.end} END. + const confirmed = await Dialog.confirm({ + title: "USING STUN FOR ENDURANCE", + content: `
${item.name} requires ${resourcesRequired.end} END. ${actor.name} has ${actorEndurance} END. Do you want to take ${potentialStunCost.stunDice}d6 STUN damage to make up for the lack of END?
`, - }); - if (!confirmed) { - return { - warning: `${item.name} needs ${resourcesRequired.end} END but ${actor.name} only has ${actorEndurance} END. The player is not spending STUN to make up the difference.`, - }; + }); + if (!confirmed) { + return { + warning: `${item.name} needs ${resourcesRequired.end} END but ${actor.name} only has ${actorEndurance} END. The player is not spending STUN to make up the difference.`, + }; + } + + ({ damage: actualStunDamage, roller: actualStunRoller } = await rollStunForEnd( + potentialStunCost.stunDice, + )); + + resourcesRequired.end = potentialStunCost.endSpentAboveZero; } + } + } - actualStunCostObj = await rollStunForEnd(potentialStunCost.stunDice); + // Does the actor have enough charges available? + if (resourcesRequired.charges > 0) { + if (resourcesRequired.charges > startingCharges && useResources) { + return { + error: `${item.name} does not have ${resourcesRequired.charges} charge${ + resourcesRequired.charges > 1 ? "s" : "" + } remaining.`, + }; } } @@ -3121,13 +3120,34 @@ export async function userInteractiveVerifyOptionallyPromptThenSpendResources(it item, enduranceReserve, resourcesRequired.end, - actualStunCostObj, + actualStunDamage, + actualStunRoller, resourcesRequired.charges, + !useResources, ); + // Let users know what resources were not consumed + if (!useResources) { + const speaker = ChatMessage.getSpeaker({ + actor: actor, + }); + speaker.alias = item.actor.name; + + const chatData = { + user: game.user._id, + type: CONST.CHAT_MESSAGE_TYPES.OTHER, + content: `${game.user.name} is using SHIFT to override using ${resourcesUsedDescription} for ${item.name}`, + whisper: whisperUserTargetsForActor(this), + speaker, + }; + await ChatMessage.create(chatData); + } + return { resourcesRequired, - resourcesUsedDescription, + resourcesUsedDescription: useResources + ? `Spent ${resourcesUsedDescription}` + : `${resourcesUsedDescription} overridden with SHIFT`, }; } @@ -3240,7 +3260,7 @@ function calculateRequiredStunDiceForLackOfEnd(actor, enduranceToUse) { let stunDice = 0; if (enduranceToUse > 0 && actorEnd - enduranceToUse < 0) { - // 1d6 STUN for each 2 END spent beyond 0 END - always round END use up to the nearest larger 2 END + // 1d6 STUN for each 2 END spent beyond 0 END - always round up endSpentAboveZero = Math.max(actorEnd, 0); stunDice = Math.ceil(Math.abs(enduranceToUse - endSpentAboveZero) / 2); } @@ -3274,17 +3294,28 @@ async function rollStunForEnd(stunDice) { } /** + * Spend all resources (END, STUN, charges) provided. Assumes numbers are possible. * * @param {HeroSystem6eItem} item * @param {HeroSystem6eItem} enduranceReserve * @param {number} endToSpend - * @param {Object} stunToSpendObj + * @param {number} stunToSpend + * @param {HeroRoller} stunToSpendRoller * @param {number} chargesToSpend + * @param {boolean} noResourceUse - true if you would like to simulate the resources being used without using them (aka dry run) * @returns */ -async function spendResourcesToUse(item, enduranceReserve, endToSpend, stunToSpendObj, chargesToSpend) { +async function spendResourcesToUse( + item, + enduranceReserve, + endToSpend, + stunToSpend, + stunToSpendRoller, + chargesToSpend, + noResourceUse, +) { const actor = item.actor; - let resourceUsageDescription; + let resourceUsageDescription = ""; // Deduct endurance // none: "No Automation", @@ -3292,37 +3323,38 @@ async function spendResourcesToUse(item, enduranceReserve, endToSpend, stunToSpe // pcEndOnly: "PCs (end) and NPCs (end, stun, body)", // all: "PCs and NPCs (end, stun, body)" const automation = game.settings.get(HEROSYS.module, "automation"); - if ( - automation === "all" || - (automation === "npcOnly" && actor.type == "npc") || - (automation === "pcEndOnly" && actor.type === "pc") - ) { - if (item.system.USE_END_RESERVE) { - if (enduranceReserve) { - const reserveEnd = parseInt(enduranceReserve?.system.value || 0); - const actorNewEndurance = reserveEnd - endToSpend; + const actorInCombat = actor.inCombat; + const noEnduranceUse = + actorInCombat && // TODO: Not sure if we should have this or not. We had it in toggle() but not elsewhere. + (automation === "all" || + (automation === "npcOnly" && actor.type == "npc") || + (automation === "pcEndOnly" && actor.type === "pc")); + + if (item.system.USE_END_RESERVE) { + if (enduranceReserve) { + const reserveEnd = parseInt(enduranceReserve?.system.value || 0); + const actorNewEndurance = reserveEnd - endToSpend; - resourceUsageDescription = `Spent ${endToSpend} END from Endurance Reserve`; + resourceUsageDescription = `${endToSpend} END from Endurance Reserve`; + if (!noResourceUse && !noEnduranceUse) { await enduranceReserve.update({ "system.value": actorNewEndurance, "system.description": enduranceReserve.system.description, }); } - } else { - const actorStun = actor.system.characteristics.stun.value; - const actorEndurance = actor.system.characteristics.end.value; - let actorNewEndurance = actorEndurance - endToSpend; - - const actorChanges = {}; + } + } else if (endToSpend || stunToSpend) { + const actorStun = actor.system.characteristics.stun.value; + const actorEndurance = actor.system.characteristics.end.value; + const actorNewEndurance = actorEndurance - endToSpend; - if (actorNewEndurance < 0) { - const endSpentAboveZero = Math.max(actorEndurance, 0); - actorNewEndurance = Math.min(actorEndurance, 0); + const actorChanges = {}; - resourceUsageDescription = ` + if (stunToSpend > 0) { + resourceUsageDescription = ` - Spent ${endSpentAboveZero} END and ${stunToSpendObj.damage} STUN + ${endToSpend} END and ${stunToSpend} STUN 0) { resourceUsageDescription = `${resourceUsageDescription}${ resourceUsageDescription ? " and " : "" }${chargesToSpend} charge${chargesToSpend > 1 ? "s" : ""}`; + + if (!noResourceUse) { + const startingCharges = parseInt(item.system.charges?.value || 0); + await item.update({ "system.charges.value": startingCharges - chargesToSpend }); + } } return resourceUsageDescription; diff --git a/module/item/item.mjs b/module/item/item.mjs index ee0993a4..72fdd79b 100644 --- a/module/item/item.mjs +++ b/module/item/item.mjs @@ -1,6 +1,6 @@ import { HEROSYS } from "../herosystem6e.mjs"; import { HeroSystem6eActor } from "../actor/actor.mjs"; -import * as ItemAttack from "../item/item-attack.mjs"; +import { AttackOptions, userInteractiveVerifyOptionallyPromptThenSpendResources } from "../item/item-attack.mjs"; import { createSkillPopOutFromItem } from "../item/skill.mjs"; import { enforceManeuverLimits } from "../item/manuever.mjs"; import { @@ -9,7 +9,12 @@ import { determineMaxAdjustment, } from "../utility/adjustment.mjs"; import { onActiveEffectToggle } from "../utility/effects.mjs"; -import { getPowerInfo, getModifierInfo, whisperUserTargetsForActor } from "../utility/util.mjs"; +import { + getPowerInfo, + getModifierInfo, + hdcTimeOptionIdToSeconds, + whisperUserTargetsForActor, +} from "../utility/util.mjs"; import { RoundFavorPlayerDown, RoundFavorPlayerUp } from "../utility/round.mjs"; import { convertToDcFromItem, getDiceFormulaFromItemDC, CombatSkillLevelsForAttack } from "../utility/damage.mjs"; import { getSystemDisplayUnits } from "../utility/units.mjs"; @@ -264,7 +269,7 @@ export class HeroSystem6eItem extends Item { case "TELEKINESIS": case "TRANSFER": case "TRANSFORM": - return ItemAttack.AttackOptions(this, event); + return AttackOptions(this, event); case "ABSORPTION": case "DISPEL": @@ -297,11 +302,11 @@ export class HeroSystem6eItem extends Item { case "TRIP": default: ui.notifications.warn(`${this.system.XMLID} roll is not fully supported`); - return ItemAttack.AttackOptions(this, event); + return AttackOptions(this, event); } case "defense": - return this.toggle(); + return this.toggle(event); case "skill": default: { @@ -444,6 +449,11 @@ export class HeroSystem6eItem extends Item { ChatMessage.create(chatData); } + /** + * + * @param {Event} [event] + * @returns {Promise