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} + */ 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; 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); } diff --git a/module/utility/adjustment.mjs b/module/utility/adjustment.mjs index c677b0f6..4d82033f 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,13 @@ 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; + let seconds = hdcTimeOptionIdToSeconds(durationOptionId); + if (seconds) { + console.error(`optionID for ${item.name}/${item.system.XMLID} has unhandled option ID ${durationOptionId}`); + seconds = 12; } - return durationInSeconds; + return seconds; } function _createNewAdjustmentEffect( 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; } } diff --git a/module/utility/util.mjs b/module/utility/util.mjs index 79e3e27b..15acf7bd 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} Should be >= 0 unless there is an error. + */ +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: + seconds = -1; + break; + } + + return seconds; +}