From d0d8a922c8e93e6ef3b7fcf31d885d2e401c522f Mon Sep 17 00:00:00 2001 From: phBalance Date: Mon, 14 Oct 2024 14:25:34 -0600 Subject: [PATCH 1/6] feat(resources): allow SHIFT to override resource consumption --- module/item/item-attack.mjs | 166 +++++++++++++++++++++++------------- module/item/skill.mjs | 13 ++- 2 files changed, 113 insertions(+), 66 deletions(-) diff --git a/module/item/item-attack.mjs b/module/item/item-attack.mjs index 4922df43..3958cca9 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"; @@ -533,7 +533,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 +3066,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 +3077,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 +3135,33 @@ 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, + alias: game.user.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 +3274,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 +3308,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", @@ -3302,27 +3347,26 @@ async function spendResourcesToUse(item, enduranceReserve, endToSpend, stunToSpe const reserveEnd = parseInt(enduranceReserve?.system.value || 0); const actorNewEndurance = reserveEnd - endToSpend; - resourceUsageDescription = `Spent ${endToSpend} END from Endurance Reserve`; + resourceUsageDescription = `${endToSpend} END from Endurance Reserve`; - await enduranceReserve.update({ - "system.value": actorNewEndurance, - "system.description": enduranceReserve.system.description, - }); + if (!noResourceUse) { + await enduranceReserve.update({ + "system.value": actorNewEndurance, + "system.description": enduranceReserve.system.description, + }); + } } - } else { + } else if (endToSpend || stunToSpend) { const actorStun = actor.system.characteristics.stun.value; const actorEndurance = actor.system.characteristics.end.value; - let actorNewEndurance = actorEndurance - endToSpend; + const actorNewEndurance = actorEndurance - endToSpend; const actorChanges = {}; - if (actorNewEndurance < 0) { - const endSpentAboveZero = Math.max(actorEndurance, 0); - actorNewEndurance = Math.min(actorEndurance, 0); - + if (stunToSpend > 0) { resourceUsageDescription = ` - Spent ${endSpentAboveZero} END and ${stunToSpendObj.damage} STUN + ${endToSpend} END and ${stunToSpend} STUN 0) { + if (!noResourceUse) { + const startingCharges = parseInt(item.system.charges?.value || 0); + await item.update({ "system.charges.value": startingCharges - chargesToSpend }); + } + resourceUsageDescription = `${resourceUsageDescription}${ resourceUsageDescription ? " and " : "" }${chargesToSpend} charge${chargesToSpend > 1 ? "s" : ""}`; diff --git a/module/item/skill.mjs b/module/item/skill.mjs index ae6a117d..e64e4896 100644 --- a/module/item/skill.mjs +++ b/module/item/skill.mjs @@ -69,7 +69,7 @@ export async function createSkillPopOutFromItem(item, actor) { buttons: { rollSkill: { label: "Roll Skill", - callback: (html) => resolve(skillRoll(item, actor, html)), + callback: (target, event) => resolve(skillRoll(item, actor, target, event)), }, }, default: "rollSkill", @@ -80,7 +80,7 @@ export async function createSkillPopOutFromItem(item, actor) { }); } -async function skillRoll(item, actor, html) { +async function skillRoll(item, actor, target, event) { const token = actor.token; const speaker = ChatMessage.getSpeaker({ actor: actor, token }); speaker.alias = actor.name; @@ -91,7 +91,7 @@ async function skillRoll(item, actor, html) { warning: resourceWarning, resourcesRequired, resourcesUsedDescription, - } = await userInteractiveVerifyOptionallyPromptThenSpendResources(item, {}); + } = await userInteractiveVerifyOptionallyPromptThenSpendResources(item, { noResourceUse: event.shiftKey }); if (resourceError || resourceWarning) { const chatData = { user: game.user._id, @@ -99,11 +99,10 @@ async function skillRoll(item, actor, html) { speaker: speaker, }; - await ChatMessage.create(chatData); - return; + return ChatMessage.create(chatData); } - const formElement = html[0].querySelector("form"); + const formElement = target[0].querySelector("form"); const formData = new FormDataExtended(formElement)?.object; const skillRoller = new HeroRoller().addDice(3); @@ -184,5 +183,5 @@ async function skillRoll(item, actor, html) { speaker: speaker, }; - await ChatMessage.create(chatData); + return ChatMessage.create(chatData); } From c6b77cd71114c8effd33b8b627d6e8213a41b698 Mon Sep 17 00:00:00 2001 From: phBalance Date: Mon, 14 Oct 2024 14:31:22 -0600 Subject: [PATCH 2/6] fix(toggle): pass event through to toggle to allow SHIFT to work --- module/actor/actor-sheet.mjs | 2 +- module/utility/effects.mjs | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) 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/utility/effects.mjs b/module/utility/effects.mjs index 074a2a4f..ba6461fa 100644 --- a/module/utility/effects.mjs +++ b/module/utility/effects.mjs @@ -55,7 +55,11 @@ export async function onManageActiveEffect(event, owner) { return onActiveEffectToggle(effect); } - return item.toggle(); + return item.toggle(event); + + default: + console.error(`Unknown dataset action ${a.dataset.action} for active effect`); + break; } } From d014636833a7be972dec01548b7117c6dfbd374e Mon Sep 17 00:00:00 2001 From: phBalance Date: Mon, 14 Oct 2024 14:54:34 -0600 Subject: [PATCH 3/6] refactor(HDC OPTIONID): move HDC time calculation to separate method --- module/utility/adjustment.mjs | 55 +--------------------- module/utility/util.mjs | 86 +++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 53 deletions(-) diff --git a/module/utility/adjustment.mjs b/module/utility/adjustment.mjs index c677b0f6..847327bb 100644 --- a/module/utility/adjustment.mjs +++ b/module/utility/adjustment.mjs @@ -1,5 +1,5 @@ import { HEROSYS } from "../herosystem6e.mjs"; -import { getPowerInfo } from "./util.mjs"; +import { getPowerInfo, hdcTimeOptionIdToSeconds } from "./util.mjs"; import { RoundFavorPlayerUp } from "./round.mjs"; /** @@ -245,58 +245,7 @@ function _determineEffectDurationInSeconds(item, rawActivePointsDamage) { durationOptionId = delayedReturnRate ? delayedReturnRate.OPTIONID : "TURN"; } - let durationInSeconds = 12; - switch (durationOptionId) { - case "TURN": // Not a real OPTIONID from HD - durationInSeconds = 12; - break; - case "MINUTE": - durationInSeconds = 60; - break; - case "FIVEMINUTES": - durationInSeconds = 60 * 5; - break; - case "20MINUTES": - durationInSeconds = 60 * 20; - break; - case "HOUR": - durationInSeconds = 60 * 60; - break; - case "6HOURS": - durationInSeconds = 60 * 60 * 6; - break; - case "DAY": - durationInSeconds = 60 * 60 * 24; - break; - case "WEEK": - durationInSeconds = 604800; - break; - case "MONTH": - durationInSeconds = 2.628e6; - break; - case "SEASON": - durationInSeconds = 2.628e6 * 3; - break; - case "YEAR": - durationInSeconds = 3.154e7; - break; - case "FIVEYEARS": - durationInSeconds = 3.154e7 * 5; - break; - case "TWENTYFIVEYEARS": - durationInSeconds = 3.154e7 * 25; - break; - case "CENTURY": - durationInSeconds = 3.154e7 * 100; - break; - default: - console.error( - `DELAYEDRETURNRATE for ${item.name}/${item.system.XMLID} has unhandled option ID ${durationOptionId}`, - ); - break; - } - - return durationInSeconds; + return hdcTimeOptionIdToSeconds(durationOptionId); } function _createNewAdjustmentEffect( diff --git a/module/utility/util.mjs b/module/utility/util.mjs index 79e3e27b..836fe05b 100644 --- a/module/utility/util.mjs +++ b/module/utility/util.mjs @@ -271,3 +271,89 @@ export async function expireEffects(actor) { } await renderAdjustmentChatCards(adjustmentChatMessages); } + +/** + * A number of HDC advantages and powers have very similar OPTIONID values. + * + * @param {string} optionId + * @returns {number} + */ +export function hdcTimeOptionIdToSeconds(durationOptionId) { + let seconds = 12; + + switch (durationOptionId) { + case "EXTRAPHASE": + // TODO: This is not correct as it depends on speed and what segment we're on. + seconds = 2; + break; + + case "TURN": + seconds = 12; + break; + + case "MINUTE": + seconds = 60; + break; + + case "FIVEMINUTES": + seconds = 60 * 5; + break; + + case "20MINUTES": + case "TWENTYMINUTES": + seconds = 60 * 20; + break; + + case "HOUR": + seconds = 60 * 60; + break; + + case "6HOURS": + case "SIXHOURS": + seconds = 60 * 60 * 6; + break; + + case "DAY": + case "ONEDAY": + seconds = 60 * 60 * 24; + break; + + case "WEEK": + case "ONEWEEK": + seconds = 60 * 60 * 24 * 7; + break; + + case "MONTH": + case "ONEMONTH": + seconds = 60 * 60 * 24 * 30; + break; + + case "SEASON": + case "ONESEASON": + seconds = 60 * 60 * 24 * 90; + break; + + case "YEAR": + case "ONEYEAR": + seconds = 60 * 60 * 24 * 365; + break; + + case "FIVEYEARS": + seconds = 60 * 60 * 24 * 365 * 5; + break; + + case "TWENTYFIVEYEARS": + seconds = 60 * 60 * 24 * 365 * 25; + break; + + case "ONECENTURY": + seconds = 60 * 60 * 24 * 365 * 100; + break; + + default: + console.error(`optionID for ${item.name}/${item.system.XMLID} has unhandled option ID ${durationOptionId}`); + break; + } + + return seconds; +} From a31edd733c28e63c293cb238195a53aadf323008 Mon Sep 17 00:00:00 2001 From: phBalance Date: Mon, 14 Oct 2024 15:57:52 -0600 Subject: [PATCH 4/6] feat(toggle): toggle powers can now use STUN and end reserve --- module/item/item-attack.mjs | 103 ++++++++---------- module/item/item.mjs | 209 ++++++++++++------------------------ 2 files changed, 111 insertions(+), 201 deletions(-) diff --git a/module/item/item-attack.mjs b/module/item/item-attack.mjs index 3958cca9..3ebbc4f8 100644 --- a/module/item/item-attack.mjs +++ b/module/item/item-attack.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, @@ -3145,8 +3130,9 @@ export async function userInteractiveVerifyOptionallyPromptThenSpendResources(it if (!useResources) { const speaker = ChatMessage.getSpeaker({ actor: actor, - alias: game.user.name, }); + speaker.alias = item.actor.name; + const chatData = { user: game.user._id, type: CONST.CHAT_MESSAGE_TYPES.OTHER, @@ -3337,34 +3323,36 @@ async function spendResourcesToUse( // 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; - - resourceUsageDescription = `${endToSpend} END from Endurance Reserve`; - - if (!noResourceUse) { - await enduranceReserve.update({ - "system.value": actorNewEndurance, - "system.description": enduranceReserve.system.description, - }); - } + 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 = `${endToSpend} END from Endurance Reserve`; + + if (!noResourceUse && !noEnduranceUse) { + await enduranceReserve.update({ + "system.value": actorNewEndurance, + "system.description": enduranceReserve.system.description, + }); } - } else if (endToSpend || stunToSpend) { - const actorStun = actor.system.characteristics.stun.value; - const actorEndurance = actor.system.characteristics.end.value; - const actorNewEndurance = actorEndurance - endToSpend; + } + } else if (endToSpend || stunToSpend) { + const actorStun = actor.system.characteristics.stun.value; + const actorEndurance = actor.system.characteristics.end.value; + const actorNewEndurance = actorEndurance - endToSpend; - const actorChanges = {}; + const actorChanges = {}; - if (stunToSpend > 0) { - resourceUsageDescription = ` + if (stunToSpend > 0) { + resourceUsageDescription = ` ${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 }); } - - resourceUsageDescription = `${resourceUsageDescription}${ - resourceUsageDescription ? " and " : "" - }${chargesToSpend} charge${chargesToSpend > 1 ? "s" : ""}`; } 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} + */ async toggle(event) { let item = this; @@ -452,159 +462,50 @@ export class HeroSystem6eItem extends Item { return; } - // Spend END to toggle power on - let end = parseInt(this.system.end); - let value = parseInt(this.actor.system.characteristics.end.value); - if (end > value) { - if (event?.shiftKey) { - const speaker = ChatMessage.getSpeaker({ - actor: this, - //token, - }); - speaker["alias"] = game.user.name; - const chatData = { - user: game.user._id, - type: CONST.CHAT_MESSAGE_TYPES.OTHER, - content: `${game.user.name} used SHIFT key to force ${this.name} on.`, - whisper: whisperUserTargetsForActor(this), - speaker, - }; - ChatMessage.create(chatData); - } else { - ui.notifications.error( - `Unable to active ${this.name}. ${item.actor.name} has ${value} END. Power requires ${end} END to activate. Hold SHIFT to force.`, - ); - return; - } - } - - // Spend CHARGE to toggle power on - // Notice that item.system.charges is used to provide details - const charges = this.findModsByXmlid("CHARGES"); - if (charges) { - if (this.system.charges.value <= 0) { - if (event?.shiftKey) { - const speaker = ChatMessage.getSpeaker({ - actor: this, - //token, - }); - speaker["alias"] = game.user.name; - const chatData = { - user: game.user._id, - type: CONST.CHAT_MESSAGE_TYPES.OTHER, - content: `${game.user.name} used SHIFT key to force ${this.name} on.`, - whisper: whisperUserTargetsForActor(this), - speaker, - }; - ChatMessage.create(chatData); - } else { - ui.notifications.error( - `Unable to active ${this.name}. ${item.actor.name} has ${this.system.charges.value} CHARGES. Hold SHIFT to force.`, - ); - return; - } - } - this.system.charges.value -= 1; - this.update({ "system.charges": this.system.charges }); - - // Charges expire, find the Active Effect - const ae = this.effects.contents?.[0]; - if (ae) { - let seconds = 1; - const continuing = this.findModsByXmlid("CONTINUING"); - if (continuing) { - // TODO: Extract (look in adjustment) - switch (continuing.OPTIONID) { - case "EXTRAPHASE": - seconds = 2; - break; - case "TURN": - seconds = 12; - break; - case "MINUTE": - seconds = 60; - break; - case "FIVEMINUTES": - seconds = 60 * 5; - break; - case "TWENTYMINUTES": - seconds = 60 * 20; - break; - case "HOUR": - seconds = 60 * 60; - break; - case "SIXHOURS": - seconds = 60 * 60 * 6; - break; - case "ONEDAY": - seconds = 60 * 60 * 24; - break; - case "ONEWEEK": - seconds = 60 * 60 * 24 * 7; - break; - case "ONEMONTH": - seconds = 60 * 60 * 24 * 30; - break; - case "ONESEASON": - seconds = 60 * 60 * 24 * 90; - break; - case "ONEYEAR": - seconds = 60 * 60 * 24 * 365; - break; - case "FIVEYEARS": - seconds = 60 * 60 * 24 * 365 * 5; - break; - case "TWENTYFIVEYEARS": - seconds = 60 * 60 * 24 * 365 * 25; - break; - case "ONECENTURY": - seconds = 60 * 60 * 24 * 365 * 100; - break; - } - } - - console.log( - await ae.update({ "duration.seconds": seconds, "flags.startTime": game.time.worldTime }), - ); - } else { - console.log("No associated Active Effect", this); - } + // Make sure there are enough resources and consume them + const { + error: resourceError, + warning: resourceWarning, + resourcesUsedDescription, + } = await userInteractiveVerifyOptionallyPromptThenSpendResources(item, { + userForceOverride: !!event?.shiftKey, + }); + if (resourceError) { + return ui.notifications.error(resourceError); + } else if (resourceWarning) { + return ui.notifications.warn(resourceWarning); } - // Only spend the END if we are in combat. - if (this.actor.inCombat && end) { - await item.actor.update({ - "system.characteristics.end.value": value - end, - }); - - const speaker = ChatMessage.getSpeaker({ actor: item.actor }); - speaker["alias"] = item.actor.name; - const chatData = { - user: game.user._id, - type: CONST.CHAT_MESSAGE_TYPES.OTHER, - content: `Spent ${end} END to activate ${item.name}`, - whisper: whisperUserTargetsForActor(item.actor), - speaker, - }; - await ChatMessage.create(chatData); - } else { + const success = await RequiresASkillRollCheck(this, event); + if (!success) { const speaker = ChatMessage.getSpeaker({ actor: item.actor }); speaker["alias"] = item.actor.name; const chatData = { user: game.user._id, type: CONST.CHAT_MESSAGE_TYPES.OTHER, - content: `Activated ${item.name}`, + content: `${resourcesUsedDescription} to attempt to activate ${item.name} but attempt failed`, whisper: whisperUserTargetsForActor(item.actor), speaker, }; await ChatMessage.create(chatData); - } - const success = await RequiresASkillRollCheck(this, event); - if (!success) { return; } + const speaker = ChatMessage.getSpeaker({ actor: item.actor }); + speaker["alias"] = item.actor.name; + const chatData = { + user: game.user._id, + type: CONST.CHAT_MESSAGE_TYPES.OTHER, + content: `${resourcesUsedDescription} to activate ${item.name}`, + whisper: whisperUserTargetsForActor(item.actor), + speaker, + }; + await ChatMessage.create(chatData); + + // A continuing charge's use is tracked by an active effect. Start it. + await _startIfIsAContinuingCharge(this); + // Invisibility status effect for SIGHTGROUP? if (this.system.XMLID === "INVISIBILITY") { if (this.system.OPTIONID === "SIGHTGROUP" && !this.actor.statuses.has("invisible")) { @@ -4975,5 +4876,27 @@ export async function RequiresASkillRollCheck(item, event) { return true; } +async function _startIfIsAContinuingCharge(item) { + const charges = item.findModsByXmlid("CHARGES"); + const continuing = item.findModsByXmlid("CONTINUING"); + if (charges && continuing) { + // Charges expire, find the Active Effect + const ae = item.effects.contents?.[0]; + if (ae) { + let seconds = hdcTimeOptionIdToSeconds(continuing.OPTIONID); + if (seconds < 0) { + console.error( + `optionID for ${item.name}/${item.system.XMLID} has unhandled option ID ${continuing.OPTIONID}`, + ); + seconds = 1; + } + + console.log(await ae.update({ "duration.seconds": seconds, "flags.startTime": game.time.worldTime })); + } else { + console.log("No associated Active Effect", item); + } + } +} + // for testing and pack-load-from-config macro window.HeroSystem6eItem = HeroSystem6eItem; From d3f1fb5fa7b67bacaeffbce9843dca292b178101 Mon Sep 17 00:00:00 2001 From: phBalance Date: Mon, 14 Oct 2024 15:58:36 -0600 Subject: [PATCH 5/6] fix(duration): return -1 on error to allow better error messages --- module/utility/adjustment.mjs | 8 +++++++- module/utility/util.mjs | 4 ++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/module/utility/adjustment.mjs b/module/utility/adjustment.mjs index 847327bb..4d82033f 100644 --- a/module/utility/adjustment.mjs +++ b/module/utility/adjustment.mjs @@ -245,7 +245,13 @@ function _determineEffectDurationInSeconds(item, rawActivePointsDamage) { durationOptionId = delayedReturnRate ? delayedReturnRate.OPTIONID : "TURN"; } - return hdcTimeOptionIdToSeconds(durationOptionId); + let seconds = hdcTimeOptionIdToSeconds(durationOptionId); + if (seconds) { + console.error(`optionID for ${item.name}/${item.system.XMLID} has unhandled option ID ${durationOptionId}`); + seconds = 12; + } + + return seconds; } function _createNewAdjustmentEffect( diff --git a/module/utility/util.mjs b/module/utility/util.mjs index 836fe05b..15acf7bd 100644 --- a/module/utility/util.mjs +++ b/module/utility/util.mjs @@ -276,7 +276,7 @@ export async function expireEffects(actor) { * A number of HDC advantages and powers have very similar OPTIONID values. * * @param {string} optionId - * @returns {number} + * @returns {number} Should be >= 0 unless there is an error. */ export function hdcTimeOptionIdToSeconds(durationOptionId) { let seconds = 12; @@ -351,7 +351,7 @@ export function hdcTimeOptionIdToSeconds(durationOptionId) { break; default: - console.error(`optionID for ${item.name}/${item.system.XMLID} has unhandled option ID ${durationOptionId}`); + seconds = -1; break; } From ad4195459a956b63499072f8e91d953fae45ff94 Mon Sep 17 00:00:00 2001 From: phBalance Date: Mon, 14 Oct 2024 15:59:19 -0600 Subject: [PATCH 6/6] docs(CHANGELOG): toggle can now use STUN and END reserves --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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)