diff --git a/.prettierignore b/.prettierignore index 9ac7bc89..af6d6223 100644 --- a/.prettierignore +++ b/.prettierignore @@ -6,3 +6,6 @@ # Ignore all yaml files **/*.yml + +# Ignore all SCSS files +**/*.scss diff --git a/CHANGELOG.md b/CHANGELOG.md index d261dda7..ae3ecec4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,12 @@ - The character sheet's power tab now has the dice icon for attack powers to initiate attacks. - Fix for movement powers toggles. [#533](https://github.com/dmdorman/hero6e-foundryvtt/issues/533) -- Adjustment powers should now respect uploaded multi sources and targets when triggering. +- Adjustment powers should now respect uploaded multi sources and targets when triggering. They should also respect maximum amounts for absorption, aid, and transfer for 5e and 6e. - No adjustment powers should be killing attacks that are enhanced by strength. - Compound powers now show proper indices. +- Correct resistant power defense (yes it's a silly thing but we were accidentally boosting it and consequently mostly likely doubling power defenses). +- Calculated power defense will now be shown on the character sheet. +- Defensive powers qualifying for the adjustment multiplier now match 5e more strict list. ## Version 3.0.53 diff --git a/lang/en.json b/lang/en.json index a4f5dfd4..cdd794bb 100644 --- a/lang/en.json +++ b/lang/en.json @@ -7,7 +7,7 @@ }, "fullHealthConfirm": { "Title": "Confirm Full Health", - "Content": "Are you sure you want to restore all characteristics to max, remove status effects, and remote temporary effects?" + "Content": "Are you sure you want to restore all characteristics to max, remove status effects, and remove temporary effects?" } } }, @@ -24,7 +24,7 @@ "ToHitModifier": "To Hit Modifier", "UsesStrength": "Uses Strength", "UsesTk": "Uses Telekinesis Strength", - "Value": "Damage dice", + "Value": "Damage Dice", "StunBodyDamage": "Stun and/or Body Damage", "OcvMod": "OCV Modifier", "DcvMod": "DCV Modifier", @@ -55,11 +55,13 @@ "OCV": "OCV", "DCV": "DCV", "Char": "Char", + "CharTitle": "Characteristic Name's Abbreviation.", "Base": "Base", "BaseTitle": "Starting amount you get for free.", "Cost": "Cost", "CostTitle": "Cost paid that are over the base amount.", "Notes": "Notes", + "NotesTitle": "Additional crucial information relating to the characteristic.", "Max": "Max", "MaxTitle": "Maximum value including all powers and effects.", "Core": "Core", diff --git a/module/actor/actor-active-effects.js b/module/actor/actor-active-effects.js index 6f1a832f..2422b08c 100644 --- a/module/actor/actor-active-effects.js +++ b/module/actor/actor-active-effects.js @@ -205,7 +205,7 @@ export class HeroSystem6eActorActiveEffects extends ActiveEffect { name: "EFFECT.StatusSilenced", icon: "icons/svg/silenced.svg", }; - static freightenedEffect = { + static frightenedEffect = { id: "fear", name: "EFFECT.StatusFear", icon: "icons/svg/terror.svg", diff --git a/module/actor/actor-sheet.js b/module/actor/actor-sheet.js index c33e1ebf..e905317f 100644 --- a/module/actor/actor-sheet.js +++ b/module/actor/actor-sheet.js @@ -555,7 +555,7 @@ export class HeroSystemActorSheet extends ActorSheet { damageNegationValue /*knockbackResistance*/, , defenseTagsP, - ] = determineDefense.call(this, this.actor, pdAttack); + ] = determineDefense(this.actor, pdAttack); defense.PD = defenseValue; defense.rPD = resistantValue; defense.PDtags = "PHYSICAL DEFENSE\n"; @@ -601,7 +601,7 @@ export class HeroSystemActorSheet extends ActorSheet { damageNegationValueE /* knockbackResistanceE */, , defenseTagsE, - ] = determineDefense.call(this, this.actor, edAttack); + ] = determineDefense(this.actor, edAttack); defense.ED = defenseValueE; defense.rED = resistantValueE; defense.EDtags = "ENERGY DEFENSE\n"; @@ -648,7 +648,7 @@ export class HeroSystemActorSheet extends ActorSheet { damageNegationValueM /*knockbackResistanceM*/, , defenseTagsM, - ] = determineDefense.call(this, this.actor, mdAttack); + ] = determineDefense(this.actor, mdAttack); defense.MD = defenseValueM; defense.rMD = resistantValueM; defense.MDtags = "MENTAL DEFENSE\n"; @@ -677,8 +677,8 @@ export class HeroSystemActorSheet extends ActorSheet { // Defense POWD const drainContentsAttack = ` - - + + `; const drainAttack = await new HeroSystem6eItem( @@ -695,7 +695,7 @@ export class HeroSystemActorSheet extends ActorSheet { , , defenseTagsPOWD, - ] = determineDefense.call(this, this.actor, drainAttack); + ] = determineDefense(this.actor, drainAttack); defense.POWD = defenseValuePOWD; defense.rPOWD = resistantValuePOWD; defense.POWDtags = "POWER DEFENSE\n"; @@ -1120,10 +1120,15 @@ export class HeroSystemActorSheet extends ActorSheet { async _onUnlockCharacteristic(event) { event.preventDefault(); + // The event will not be generated from the disabled input (since disabled elements + // don't generally allow mouse events) but rather from the enclosing td element. + // Find its child input element + const input = event.target.querySelector("input"); + // Find all associated Active Effects let activeEffects = Array.from( this.actor.allApplicableEffects(), - ).filter((o) => o.changes.find((p) => p.key === event.target.name)); + ).filter((o) => o.changes.find((p) => p.key === input.name)); for (let ae of activeEffects) { // Delete status if (ae.statuses) { diff --git a/module/actor/actor.js b/module/actor/actor.js index 1effc9ce..ba954063 100644 --- a/module/actor/actor.js +++ b/module/actor/actor.js @@ -66,7 +66,6 @@ export class HeroSystem6eActor extends Actor { // TODO: Allow for a non-statusEffects ActiveEffect (like from a power) async addActiveEffect(activeEffect) { const newEffect = foundry.utils.deepClone(activeEffect); - newEffect.label = `${game.i18n.localize(newEffect.label)}`; // Check for standard StatusEffects // statuses appears to be necessary to associate with StatusEffects @@ -773,41 +772,37 @@ export class HeroSystem6eActor extends Actor { async FullHealth() { // Remove all status effects for (let status of this.statuses) { - let ae = Array.from(this.effects).find((o) => - o.statuses.has(status), + let ae = Array.from(this.effects).find((effect) => + effect.statuses.has(status), ); await ae.delete(); } // Remove temporary effects - let tempEffects = Array.from(this.effects).filter( + const tempEffects = Array.from(this.effects).filter( (o) => parseInt(o.duration?.seconds || 0) > 0, ); - for (let ae of tempEffects) { + for (const ae of tempEffects) { await ae.delete(); } - // Set Characterstics VALUE to MAX - for (let char of Object.keys(this.system.characteristics)) { - let value = parseInt(this.system.characteristics[char].value); - let max = parseInt(this.system.characteristics[char].max); + // Set Characteristics VALUE to MAX + const characteristicChanges = {}; + for (const char of Object.keys(this.system.characteristics)) { + const value = parseInt(this.system.characteristics[char].value); + const max = parseInt(this.system.characteristics[char].max); if (value != max) { - //this.actor.system.characteristics[char].value = max; - await this.update({ - [`system.characteristics.${char}.value`]: max, - }); + characteristicChanges[`system.characteristics.${char}.value`] = + max; } } + if (Object.keys(characteristicChanges).length > 0) { + await this.update(characteristicChanges); + } - // Set Charges to max - for (let item of this.items.filter( - (o) => - o.system.charges?.max && - o.system.charges.value != o.system.charges.max, - )) { - await item.update({ - [`system.charges.value`]: item.system.charges.max, - }); + // Reset all items + for (const item of this.items) { + await item.resetToOriginal(); } // We just cleared encumbrance, check if it applies again @@ -918,7 +913,7 @@ export class HeroSystem6eActor extends Actor { 0, ); - // FIXME: This is, but should never be, called with this.system[characteristic] being undefined. Need to reorder the loading + // TODO: FIXME: This is, but should never be, called with this.system[characteristic] being undefined. Need to reorder the loading // mechanism to ensure that we do something more similar to a load, transform, and extract pipeline so that we // not invoked way too many times and way too early. const charBase = (characteristicUpperCase) => { @@ -1131,7 +1126,7 @@ export class HeroSystem6eActor extends Actor { content += `
  • ${retainDamage.end} END used
  • `; content += `

    Do you want to apply this damage after the upload?

    `; const confirmed = await Dialog.confirm({ - title: "Retain damage after upload?", //game.i18n.localize("HERO6EFOUNDRYVTTV2.confirms.deleteConfirm.Title"), + title: "Retain damage after upload?", content: content, }); if (confirmed === null) { @@ -1215,7 +1210,6 @@ export class HeroSystem6eActor extends Actor { !this.system.CHARACTER.TEMPLATE.includes("6E.") && !this.system.is5e ) { - //changes[`system.is5e`] = true this.system.is5e = true; } if ( @@ -1223,12 +1217,10 @@ export class HeroSystem6eActor extends Actor { this.system.CHARACTER.TEMPLATE.includes("6E.") && this.system.is5e ) { - //changes[`system.is5e`] = false this.system.is5e = false; } } if (this.system.COM && !this.system.is5e) { - //changes[`system.is5e`] = true this.system.is5e = true; } @@ -1249,18 +1241,28 @@ export class HeroSystem6eActor extends Actor { type: itemTag.toLowerCase().replace(/s$/, ""), system: system, }; + + // Hack in some basic information with names. + // TODO: This should be turned into some kind of short version of the description + // and it should probably be done when building the description switch (system.XMLID) { case "FOLLOWER": itemData.name = "Followers"; break; + case "ABSORPTION": case "AID": + case "DISPEL": case "DRAIN": + case "HEALING": + case "TRANSFER": + case "SUPPRESS": if (!system.NAME) { itemData.name = system?.ALIAS + " " + system?.INPUT; } break; } + if (this.id) { const item = await HeroSystem6eItem.create(itemData, { parent: this, @@ -1324,6 +1326,17 @@ export class HeroSystem6eActor extends Actor { }`, { console: true, permanent: true }, ); + } else { + const maxAllowedEffects = + item.numberOfSimultaneousAdjustmentEffects(); + if ( + result.reducesArray.length > maxAllowedEffects.maxReduces || + result.enhancesArray.length > maxAllowedEffects.maxEnhances + ) { + await ui.notifications.warn( + `${this.name} has too many adjustment targets defined for ${item.name}.`, + ); + } } } diff --git a/module/herosystem6e.js b/module/herosystem6e.js index 26f8ee08..cd7140e2 100644 --- a/module/herosystem6e.js +++ b/module/herosystem6e.js @@ -26,7 +26,10 @@ import { extendTokenConfig } from "./bar3/extendTokenConfig.js"; import { HeroRuler } from "./ruler.js"; import { initializeHandlebarsHelpers } from "./handlebars-helpers.js"; import { getPowerInfo } from "./utility/util.js"; -import { AdjustmentMultiplier } from "./utility/adjustment.js"; +import { + performAdjustment, + renderAdjustmentChatCards, +} from "./utility/adjustment.js"; import { migrateWorld } from "./migration.js"; Hooks.once("init", async function () { @@ -527,9 +530,6 @@ Hooks.on("updateWorldTime", async (worldTime, options) => { // Active Effects for (let ae of actor.temporaryEffects) { - let fade = 0; - let name = ae.name; - // Determine XMLID, ITEM, ACTOR let origin = await fromUuid(ae.origin); let item = origin instanceof HeroSystem6eItem ? origin : null; @@ -566,149 +566,55 @@ Hooks.on("updateWorldTime", async (worldTime, options) => { d = ae._prepareDuration(); if (game.user.isGM) await ae.update({ duration: ae.duration }); - // Fade by 5 ActivePoints - let _fade = Math.min(ae.flags.activePoints, 5); - ae.flags.activePoints -= _fade; - fade += _fade; - - if ( - ae.changes.length > 0 && - ae.flags.target && - powerInfo && - powerInfo.powerType.includes("adjustment") - ) { - let value = parseInt(ae.changes[0].value); - let XMLID = ae.flags.XMLID; - let target = ae.flags.target; - const powerTargetX = actor.items.find( - (o) => o.uuid === target, - ); - let source = ae.flags.source; - let ActivePoints = parseInt(ae.flags.activePoints); - let _value = parseInt(ae.changes[0].value); - let _APtext = - _value === ActivePoints ? "" : ` (${ActivePoints}AP)`; - - const powerInfoX = getPowerInfo({ - xmlid: target.toUpperCase(), - actor: aeActor, - }); - const costPerPointX = - (powerTargetX - ? parseFloat( - powerTargetX.system.activePoints / - powerTargetX.system.value, - ) - : parseFloat( - powerInfoX?.cost || powerInfoX?.costPerLevel, - )) * - AdjustmentMultiplier(target.toUpperCase(), aeActor); - - let costPerPoint = costPerPointX; - let newLevels = parseInt(ActivePoints / costPerPoint); - ae.changes[0].value = - value < 0 ? -parseInt(newLevels) : parseInt(newLevels); - ae.name = `${XMLID} ${parseInt( - ae.changes[0].value, - ).signedString()}${_APtext} ${ - powerTargetX?.name || target.toUpperCase() - } [${source}]`; - - // If ActivePoints <= 0 then remove effect - if (ae.flags.activePoints <= 0) { - //content += ` Effect deleted.`; - if (game.user.isGM) await ae.delete(); + // What is this effect related to? + if (ae.flags.type === "adjustment") { + // Fade by 5 Active Points + let _fade; + if (ae.flags.activePoints < 0) { + _fade = Math.max(ae.flags.activePoints, -5); } else { - //await to make sure MAX is updated before VALUE - if (game.user.isGM) - await ae.update({ - name: ae.name, - changes: ae.changes, - flags: ae.flags, - }); + _fade = Math.min(ae.flags.activePoints, 5); } - // DRAIN fade (increase VALUE) - if (value < 0) { - let delta = -value - newLevels; - let newValue = Math.min( - parseInt(actor.system.characteristics[target].max), - parseInt( - actor.system.characteristics[target].value, - ) + delta, - ); + renderAdjustmentChatCards( + await performAdjustment( + item, + ae.flags.target[0], + -_fade, + -_fade, + "None - Beneficial", + true, + actor, + ), + ); + } else if (ae.flags.XMLID === "naturalBodyHealing") { + let bodyValue = parseInt( + actor.system.characteristics.body.value, + ); + let bodyMax = parseInt( + actor.system.characteristics.body.max, + ); + bodyValue = Math.min(bodyValue + 1, bodyMax); + // await + if (game.user.isGM) actor.update({ - [`system.characteristics.${target}.value`]: - newValue, + "system.characteristics.body.value": bodyValue, }); - } - // AID fade (VALUE = max) - else if (actor.system.characteristics?.[target]) { - let newValue = Math.min( - parseInt(actor.system.characteristics[target].max), - parseInt( - actor.system.characteristics[target].value, - ), - ); - if (game.user.isGM) - actor.update({ - [`system.characteristics.${target}.value`]: - newValue, - }); + if (bodyValue === bodyMax) { + if (game.user.isGM) ae.delete(); + break; + } else { + //await ae.update({ duration: ae.duration }); } - - if (ae.flags.activePoints <= 0) break; } else { - // No changes defined - - // Natural Body Healing - if (ae.flags.XMLID === "naturalBodyHealing") { - let bodyValue = parseInt( - actor.system.characteristics.body.value, - ); - let bodyMax = parseInt( - actor.system.characteristics.body.max, - ); - bodyValue = Math.min(bodyValue + 1, bodyMax); - // await - if (game.user.isGM) - actor.update({ - "system.characteristics.body.value": bodyValue, - }); - - if (bodyValue === bodyMax) { - if (game.user.isGM) ae.delete(); - break; - } else { - //await ae.update({ duration: ae.duration }); - } - } else { - // Default is to delete the expired AE - if (powerInfo) { - if (game.user.isGM) await ae.delete(); - break; - } + // Default is to delete the expired AE + if (powerInfo) { + if (game.user.isGM) await ae.delete(); + break; } } } - - if (fade) { - let content = `${actor.name}: ${name} fades by ${fade} active points.`; - if (ae.flags.activePoints <= 0) { - content += ` Effect deleted.`; - } - - const chatData = { - user: game.user.id, //ChatMessage.getWhisperRecipients('GM'), - whisper: ChatMessage.getWhisperRecipients("GM"), - speaker: ChatMessage.getSpeaker({ actor: this }), - blind: true, - content: content, - }; - //await - ChatMessage.create(chatData); - } } // Out of combat recovery. When SimpleCalendar is used to advance time. @@ -808,7 +714,7 @@ Hooks.on("updateWorldTime", async (worldTime, options) => { deltaMs > 100 ) { return ui.notifications.warn( - `updateWorldTime took ${deltaMs} ms. This routine handles AID/DRAIN fades and END/BODY recovery for all actors, and all tokens on this scene. If this occures on a regular basis, then there may be a performance issue that needs to be addressed by the developer.`, + `updateWorldTime took ${deltaMs} ms. This routine handles adjustment fades and END/BODY recovery for all actors, and all tokens on this scene. If this occurs on a regular basis, then there may be a performance issue that needs to be addressed by the developer.`, ); } }); diff --git a/module/item/item-attack.js b/module/item/item-attack.js index 49ec80bb..cbcaaf9b 100644 --- a/module/item/item-attack.js +++ b/module/item/item-attack.js @@ -9,7 +9,10 @@ import { convertToDcFromItem, convertFromDC, } from "../utility/damage.js"; -import { AdjustmentMultiplier } from "../utility/adjustment.js"; +import { + performAdjustment, + renderAdjustmentChatCards, +} from "../utility/adjustment.js"; import { RequiresASkillRollCheck } from "../item/item.js"; import { ItemAttackFormApplication } from "../item/item-attack-application.js"; @@ -1815,7 +1818,7 @@ export async function _onApplyDamageToSpecificToken(event, tokenId) { } effectsFinal = effectsFinal.replace(/; $/, ""); - let cardData = { + const cardData = { item: item, // dice rolls roll: newRoll, @@ -1898,366 +1901,77 @@ async function _onApplyAdjustmentToSpecificToken( `Attack details are no longer available.`, ); } - - const template = - "systems/hero6efoundryvttv2/templates/chat/apply-adjustment-card.hbs"; - const token = canvas.tokens.get(tokenId); - if (!item.actor) { return ui.notifications.error( `Attack details are no longer available.`, ); } + const token = canvas.tokens.get(tokenId); if ( item.actor.id === token.actor.id && ["DISPEL", "DRAIN", "SUPPRESS", "TRANSFER"].includes(item.system.XMLID) ) { await ui.notifications.warn( - `${item.system.XMLID} attacker (${item.actor.name}) and defender (${token.actor.name}) are the same.`, + `${item.system.XMLID} attacker/source (${item.actor.name}) and defender/target (${token.actor.name}) are the same.`, ); } - let levelsX = 0; - let levelsY = 0; - let ActivePoints = parseInt(damageData.stundamage); - - const _inputs = item.system.INPUT.split(","); - const count = item.numberOfSimultaneousAdjustmentEffects(_inputs); - for (let i = 0; i < count; i++) { - const input = _inputs?.[i]?.toUpperCase()?.trim() || ""; - - // TRANSFER X to Y (ABSORB, AID, DISPEL, DRAIN, HEALING, and SUPPRESS only have X) - let xmlidX = input.split(" to ")[0]?.trim() || ""; - let xmlidY = input.split(" to ")[1]?.trim() || ""; - - // Apply the ADJUSTMENT to a CHARACTERISTIC - let keyX = xmlidX.toLowerCase(); - let keyY = xmlidY.toLowerCase(); + const rawActivePointsDamage = parseInt(damageData.stundamage); + const actualActivePointDamage = Math.max( + 0, + rawActivePointsDamage - + damageData.defenseValue - + damageData.resistantValue, + ); - // Or a POWER - const powerTargetX = token.actor.items.find( - (o) => o.name.toUpperCase().trim() === keyX.toUpperCase().trim(), + const { valid, reducesArray, enhancesArray } = + item.splitAdjustmentSourceAndTarget(); + if (!valid) { + return ui.notifications.error( + `Invalid adjustment sources/targets provided. Compute effects manually.`, ); - if (powerTargetX) { - keyX = powerTargetX.system.XMLID; - } - - if (token.actor.system.characteristics?.[keyX] || powerTargetX) { - // Power Defense vs DRAIN - if ( - ["DISPEL", "DRAIN", "SUPPRESS", "TRANSFER"].includes( - item.system.XMLID, - ) - ) { - ActivePoints = Math.max( - 0, - ActivePoints - - (damageData.defenseValue + damageData.resistantValue), - ); - } - - const powerInfoX = getPowerInfo({ - xmlid: keyX.toUpperCase(), - actor: item.actor, - }); - let costPerPointX = - (powerTargetX - ? parseFloat( - powerTargetX.system.activePoints / - powerTargetX.system.value, - ) - : parseFloat( - powerInfoX?.cost || powerInfoX?.costPerLevel, - )) * AdjustmentMultiplier(keyX.toUpperCase(), item.actor); - levelsX = parseInt(ActivePoints / costPerPointX); - - const powerInfoY = getPowerInfo({ - xmlid: keyY.toUpperCase(), - actor: item.actor, - }); - let costPerPointY = - parseFloat(powerInfoY?.cost || powerInfoY?.costPerLevel) * - AdjustmentMultiplier(keyY.toUpperCase(), item.actor); - levelsY = parseInt(ActivePoints / costPerPointY); - - let _APtext = - levelsX === ActivePoints ? "" : ` (${ActivePoints}AP)`; - - // Check for previous ADJUSTMENT from same source - // TODO: Variable Effect may result in multiple changes on same AE. - let prevEffectX = token.actor.effects.find( - (o) => - o.origin === item.uuid && - o.flags.target === (powerTargetX?.uuid || keyX), - ); - let prevEffectY = item.actor.effects.find( - (o) => - o.flags?.XMLID === item.system.XMLID && - o.flags?.keyY === keyY, - ); - if (prevEffectX) { - // Maximum Effect (ActivePoints) - let maxEffect = 0; - for (let term of JSON.parse(damageData.terms)) { - maxEffect += - parseInt(term.faces) * parseInt(term.number) || 0; - } - - let newActivePoints = - (prevEffectX.flags?.activePoints || 0) + ActivePoints; - if (newActivePoints > maxEffect) { - ActivePoints = maxEffect - prevEffectX.flags.activePoints; - newActivePoints = maxEffect; - } - - let newLevelsX = newActivePoints / costPerPointX; - levelsX = - newLevelsX - - Math.abs(parseInt(prevEffectX.changes[0].value)); - - _APtext = - newLevelsX === newActivePoints - ? "" - : ` (${newActivePoints}AP)`; - - prevEffectX.changes[0].value = [ - "DISPEL", - "DRAIN", - "SUPPRESS", - "TRANSFER", - ].includes(item.system.XMLID) - ? -parseInt(newLevelsX) - : parseInt(newLevelsX); - (prevEffectX.name = - `${item.system.XMLID} ${ - ["DISPEL", "DRAIN", "SUPPRESS", "TRANSFER"].includes( - item.system.XMLID, - ) - ? -parseInt(newLevelsX) - : parseInt(newLevelsX).signedString() - }${_APtext}` + - (token.actor.system.characteristics?.[keyX] - ? ` ${keyX.toUpperCase()}` - : ` ${powerTargetX.name}`) + - ` [${item.actor.name}]`), - (prevEffectX.flags.activePoints = newActivePoints); - - await prevEffectX.update({ - name: prevEffectX.name, - changes: prevEffectX.changes, - flags: prevEffectX.flags, - }); - - if (item.system.XMLID === "TRANSFER" && keyY && prevEffectY) { - let newLevelsY = newActivePoints / costPerPointY; - levelsY = - newLevelsY - - Math.abs(parseInt(prevEffectY.changes[0].value)); - - prevEffectY.changes[0].value = parseInt(newLevelsY); - prevEffectY.name = `${item.system.XMLID} +${parseInt( - newLevelsY, - )} ${keyY.toUpperCase()} [${item.actor.name}]`; - prevEffectY.flags.activePoints = newActivePoints; - await prevEffectY.update({ - name: prevEffectY.name, - changes: prevEffectY.changes, - flags: prevEffectY.flags, - }); - - let newValueY = - item.actor.system.characteristics[keyY].value + - parseInt(levelsY); - await item.actor.update({ - [`system.characteristics.${keyY}.value`]: newValueY, - }); - } - } else { - // Create new ActiveEffect - let activeEffect = { - name: - `${item.system.XMLID} ${ - [ - "DISPEL", - "DRAIN", - "SUPPRESS", - "TRANSFER", - ].includes(item.system.XMLID) - ? -parseInt(levelsX) - : parseInt(levelsX).signedString() - }${_APtext}` + - (token.actor.system.characteristics?.[keyX] - ? ` ${keyX.toUpperCase()}` - : ` ${powerTargetX.name}`) + - ` [${item.actor.name}]`, - id: `${item.system.XMLID}.${item.id}.${ - powerTargetX?.name || keyX - }`, - icon: item.img, - changes: [ - { - // system.value is transferred to the actor, so not very useful, - // but we can enumerate via item.effects when determining value. - key: token.actor.system.characteristics?.[keyX] - ? "system.characteristics." + keyX + ".max" - : "system.value", - value: [ - "DISPEL", - "DRAIN", - "SUPPRESS", - "TRANSFER", - ].includes(item.system.XMLID) - ? -parseInt(levelsX) - : parseInt(levelsX), - mode: CONST.ACTIVE_EFFECT_MODES.ADD, - }, - ], - duration: { - seconds: 12, - }, - flags: { - activePoints: ActivePoints, - XMLID: item.system.XMLID, - source: item.actor.name, - target: powerTargetX?.uuid || keyX, - keyX: keyX, - keyY: keyY, - }, - origin: item.uuid, - }; - - // DELAYEDRETURNRATE - let delayedReturnRate = - item.findModsByXmlid("DELAYEDRETURNRATE"); - if (delayedReturnRate) { - switch (delayedReturnRate.OPTIONID) { - case "MINUTE": - activeEffect.duration.seconds = 60; - break; - case "FIVEMINUTES": - activeEffect.duration.seconds = 60 * 5; - break; - case "20MINUTES": - activeEffect.duration.seconds = 60 * 20; - break; - case "HOUR": - activeEffect.duration.seconds = 60 * 60; - break; - case "6HOURS": - activeEffect.duration.seconds = 60 * 60 * 6; - break; - case "DAY": - activeEffect.duration.seconds = 60 * 60 * 24; - break; - case "WEEK": - activeEffect.duration.seconds = 604800; - break; - case "MONTH": - activeEffect.duration.seconds = 2.628e6; - break; - case "SEASON": - activeEffect.duration.seconds = 2.628e6 * 3; - break; - case "YEAR": - activeEffect.duration.seconds = 3.154e7; - break; - case "FIVEYEARS": - activeEffect.duration.seconds = 3.154e7 * 5; - break; - case "TWENTYFIVEYEARS": - activeEffect.duration.seconds = 3.154e7 * 25; - break; - case "CENTURY": - activeEffect.duration.seconds = 3.154e7 * 100; - break; - default: - await ui.notifications.error( - `DELAYEDRETURNRATE has unhandled option ${delayedReturnRate?.OPTIONID}`, - ); - } - } - - if (ActivePoints > 0) { - await token.actor.addActiveEffect(activeEffect); - - if (item.system.XMLID === "TRANSFER" && keyY) { - let activeEffectY = - foundry.utils.deepClone(activeEffect); - activeEffectY.id = `${item.system.XMLID}.${item.id}.${keyY}`; - activeEffectY.name = `${item.system.XMLID} +${parseInt( - levelsY, - )} ${keyY.toUpperCase()} [${item.actor.name}]`; - activeEffectY.changes[0].key = - "system.characteristics." + keyY + ".max"; - activeEffectY.changes[0].value = parseInt(levelsY); - activeEffectY.flags.target = keyY; - await item.actor.addActiveEffect(activeEffectY); - - let newValueY = - item.actor.system.characteristics[keyY].value + - parseInt(levelsY); - if (token.actor.system.characteristics?.[keyX]) { - await item.actor.update({ - [`system.characteristics.${keyY}.value`]: - newValueY, - }); - } - } - } - } - - // Add levels to value - if (token.actor.system.characteristics?.[keyX]) { - let newValue = - token.actor.system.characteristics[keyX].value + - (["DISPEL", "DRAIN", "SUPPRESS", "TRANSFER"].includes( - item.system.XMLID, - ) - ? -parseInt(levelsX) - : parseInt(levelsX)); - await token.actor.update({ - [`system.characteristics.${keyX}.value`]: newValue, - }); - } - } - - let cardData = { - item: item, - // dice rolls - - // stun - stunDamage: ActivePoints, - levelsX: ["DISPEL", "DRAIN", "SUPPRESS", "TRANSFER"].includes( - item.system.XMLID, - ) - ? -parseInt(levelsX) - : parseInt(levelsX), - levelsY: parseInt(levelsY), - xmlidX: xmlidX, - xmlidY: xmlidY, - - // effects - //effects: effectsFinal, - - // defense - defense: defense, - - // misc - targetToken: token, - }; - - // render card - let cardHtml = await renderTemplate(template, cardData); - let speaker = ChatMessage.getSpeaker({ actor: item.actor }); - - const chatData = { - user: game.user._id, - content: cardHtml, - speaker: speaker, - }; + } - await ChatMessage.create(chatData); + const reductionChatMessages = []; + const reductionTargetActor = token.actor; + for (const reduce of reducesArray) { + reductionChatMessages.push( + await performAdjustment( + item, + reduce, + rawActivePointsDamage, + actualActivePointDamage, + defense, + false, + reductionTargetActor, + ), + ); + } + if (reductionChatMessages.length > 0) { + await renderAdjustmentChatCards(reductionChatMessages); + } + + const enhancementChatMessages = []; + const enhancementTargetActor = + item.system.XMLID === "TRANSFER" ? item.actor : token.actor; + for (const enhance of enhancesArray) { + enhancementChatMessages.push( + await performAdjustment( + item, + enhance, + -rawActivePointsDamage, + item.system.XMLID === "TRANSFER" + ? -actualActivePointDamage + : -rawActivePointsDamage, + "None - Beneficial", + false, + enhancementTargetActor, + ), + ); + } + if (enhancementChatMessages.length > 0) { + await renderAdjustmentChatCards(enhancementChatMessages); } } diff --git a/module/item/item-sheet.js b/module/item/item-sheet.js index 5d56f07f..cc832994 100644 --- a/module/item/item-sheet.js +++ b/module/item/item-sheet.js @@ -1,6 +1,9 @@ import { HeroSystem6eItem } from "./item.js"; import { editSubItem, deleteSubItem } from "../powers/powers.js"; -import { adjustmentSources } from "../utility/adjustment.js"; +import { + adjustmentSourcesPermissive, + adjustmentSourcesStrict, +} from "../utility/adjustment.js"; import { getPowerInfo } from "../utility/util.js"; /** @@ -180,7 +183,16 @@ export class HeroSystem6eItemSheet extends ItemSheet { ) { const { enhances, reduces } = item.splitAdjustmentSourceAndTarget(); - data.possibleSources = adjustmentSources(this.actor); + const enhancesValidator = + item.system.XMLID === "AID" || + item.system.XMLID === "ABSORPTION" || + item.system.XMLID === "TRANSFER" + ? adjustmentSourcesStrict + : adjustmentSourcesPermissive; + + data.possibleEnhances = enhancesValidator(this.actor); + data.possibleReduces = adjustmentSourcesPermissive(this.actor); + data.enhances = enhances ? enhances .split(",") @@ -322,11 +334,6 @@ export class HeroSystem6eItemSheet extends ItemSheet { return; } - // Adjustment Powers - if (expandedData.xmlidX || expandedData.xmlidY) { - expandedData.system.INPUT = `${expandedData.xmlidX} to ${expandedData.xmlidY}`; - } - // Endurance Reserve if (expandedData.rec) { let power = this.item.system.powers.find( @@ -340,13 +347,30 @@ export class HeroSystem6eItemSheet extends ItemSheet { } } - // AID - if (expandedData.inputs && this.item.system.XMLID === "AID") { - const array = []; - for (let i of Object.keys(expandedData.inputs)) { - array.push(expandedData.inputs[i]); + // A select list of possible adjustment targets on the character + if ( + (expandedData.reduces || expandedData.enhances) && + (this.item.system.XMLID === "ABSORPTION" || + this.item.system.XMLID === "AID" || + this.item.system.XMLID === "HEALING" || + this.item.system.XMLID === "DISPEL" || + this.item.system.XMLID === "DRAIN" || + this.item.system.XMLID === "SUPPRESS" || + this.item.system.XMLID === "TRANSFER") + ) { + let newInputStr; + + if (this.item.system.XMLID === "TRANSFER") { + newInputStr = `${Object.values(expandedData.reduces).join( + ", ", + )} -> ${Object.values(expandedData.enhances).join(", ")}`; + } else { + newInputStr = Object.values( + expandedData.reduces || expandedData.enhances, + ).join(", "); } - await this.item.update({ "system.INPUT": array.join(", ") }); + + await this.item.update({ "system.INPUT": newInputStr }); } let description = this.item.system.description; @@ -456,8 +480,10 @@ export class HeroSystem6eItemSheet extends ItemSheet { const effectId = $(event.currentTarget) .closest("[data-effect-id]") .data().effectId; - const effect = this.actor.effects.get(effectId); + const effect = this.item.effects.get(effectId); + if (!effect) return; + const confirmed = await Dialog.confirm({ title: game.i18n.localize( "HERO6EFOUNDRYVTTV2.confirms.deleteConfirm.Title", @@ -468,7 +494,7 @@ export class HeroSystem6eItemSheet extends ItemSheet { }); if (confirmed) { - effect.delete(); + await effect.delete(); this.render(); } } @@ -483,8 +509,8 @@ export class HeroSystem6eItemSheet extends ItemSheet { .closest("[data-effect-id]") .data().effectId; let effect = this.document.effects.get(effectId); - if (!effect && this.actor) { - effect = this.actor.effects.get(effectId); + if (!effect && this.document.actor) { + effect = this.document.actor.effects.get(effectId); } effect.sheet.render(true); diff --git a/module/item/item.js b/module/item/item.js index a61f1396..a1f71785 100644 --- a/module/item/item.js +++ b/module/item/item.js @@ -2,7 +2,11 @@ import { HEROSYS } from "../herosystem6e.js"; import * as Attack from "../item/item-attack.js"; import { createSkillPopOutFromItem } from "../item/skill.js"; import { enforceManeuverLimits } from "../item/manuever.js"; -import { adjustmentSources } from "../utility/adjustment.js"; +import { + adjustmentSourcesPermissive, + adjustmentSourcesStrict, + determineMaxAdjustment, +} from "../utility/adjustment.js"; import { onActiveEffectToggle } from "../utility/effects.js"; import { getPowerInfo, getModifierInfo } from "../utility/util.js"; import { RoundFavorPlayerDown, RoundFavorPlayerUp } from "../utility/round.js"; @@ -77,14 +81,54 @@ export class HeroSystem6eItem extends Item { super.prepareData(); } - async _onUpdate(data, options, userId) { - super._onUpdate(data, options, userId); + async _onUpdate(changed, options, userId) { + super._onUpdate(changed, options, userId); + + // If our value has changed, we need to rebuild this item. + if (changed.system?.value != null) { + // TODO: Update everything! + changed = this.calcItemPoints() || changed; + + // DESCRIPTION + const oldDescription = this.system.description; + this.updateItemDescription(); + changed = oldDescription != this.system.description || changed; + + // Save changes + await this.update({ system: this.system }); + } if (this.actor && this.type === "equipment") { this.actor.applyEncumbrancePenalty(); } } + /** + * Reset an item back to its default state. + */ + async resetToOriginal() { + // Set Charges to max + if ( + this.system.charges && + this.system.charges.value !== this.system.charges.max + ) { + await this.update({ + [`system.charges.value`]: this.system.charges.max, + }); + } + + // Remove temporary effects + const effectPromises = Promise.all( + this.effects.map(async (effect) => await effect.delete()), + ); + + await effectPromises; + + if (this.system.value !== this.system.max) { + await this.update({ ["system.value"]: this.system.max }); + } + } + // Largely used to determine if we can drag to hotbar isRollable() { switch (this.system?.subType || this.type) { @@ -112,11 +156,16 @@ export class HeroSystem6eItem extends Item { case "EGOATTACK": case "AID": case "DRAIN": + case "HEALING": + case "TRANSFER": case "STRIKE": case "FLASH": case undefined: return await Attack.AttackOptions(this, event); + case "ABSORPTION": + case "DISPEL": + case "SUPPRESS": default: if ( !this.system.EFFECT || @@ -2146,6 +2195,15 @@ export class HeroSystem6eItem extends Item { } break; + case "INCREASEDMAX": + // Typical ALIAS would be "Increased Maximum (+34 points)". Provide total as well. + _adderArray.push( + `${adder.ALIAS} (${determineMaxAdjustment( + this, + )} total points)`, + ); + break; + default: if (adder.ALIAS.trim()) { _adderArray.push(adder.ALIAS); @@ -2715,93 +2773,53 @@ export class HeroSystem6eItem extends Item { } } - // ENTANGLE (not implemented) + // Specific power overrides if (xmlid == "ENTANGLE") { this.system.class = "entangle"; this.system.usesStrength = false; this.system.noHitLocations = true; this.system.knockbackMultiplier = 0; - } - - // DARKNESS (not implemented) - if (xmlid == "DARKNESS") { + } else if (xmlid == "DARKNESS") { this.system.class = "darkness"; this.system.usesStrength = false; this.system.noHitLocations = true; - } - - // IMAGES (not implemented) - if (xmlid == "IMAGES") { + } else if (xmlid == "IMAGES") { this.system.class = "images"; this.system.usesStrength = false; this.system.noHitLocations = true; - } - - // ABSORPTION - if (xmlid == "ABSORPTION") { - this.system.class = "absorb"; + } else if (xmlid == "ABSORPTION") { + this.system.class = "adjustment"; this.system.usesStrength = false; this.system.noHitLocations = true; - } - - // AID - if (xmlid == "AID") { - this.system.class = "aid"; + } else if (xmlid == "AID") { + this.system.class = "adjustment"; this.system.usesStrength = false; this.system.noHitLocations = true; - } - - // DISPEL - if (xmlid == "DISPEL") { - this.system.class = "dispel"; + } else if (xmlid == "DISPEL") { + this.system.class = "adjustment"; this.system.usesStrength = false; this.system.noHitLocations = true; - } - - // DRAIN - if (xmlid == "DRAIN") { - this.system.class = "drain"; + } else if (xmlid == "DRAIN") { + this.system.class = "adjustment"; this.system.usesStrength = false; this.system.noHitLocations = true; - } - - // HEALING - if (xmlid == "HEALING") { - this.system.class = "healing"; + } else if (xmlid == "HEALING") { + this.system.class = "adjustment"; this.system.usesStrength = false; this.system.noHitLocations = true; - } - - // HEALING - if (xmlid == "SUPPRESSION") { - this.system.class = "suppression"; + } else if (xmlid == "SUPPRESS") { + this.system.class = "adjustment"; this.system.usesStrength = false; this.system.noHitLocations = true; - } - - // TRANSFER - if (xmlid == "TRANSFER") { - this.system.class = "transfer"; + } else if (xmlid == "TRANSFER") { + this.system.class = "adjustment"; this.system.usesStrength = false; this.system.noHitLocations = true; - } - - // MINDSCAN - if (xmlid == "MINDSCAN") { + } else if (xmlid == "MINDSCAN") { this.system.class = "mindscan"; this.system.usesStrength = false; this.system.noHitLocations = true; - } - - // DISPEL - if (xmlid == "DISPEL") { - this.system.class = "dispel"; - this.system.usesStrength = false; - this.system.noHitLocations = true; - } - - // MENTALBLAST - if (xmlid == "EGOATTACK") { + } else if (xmlid == "EGOATTACK") { this.system.class = "mental"; this.system.targets = "dmcv"; this.system.uses = "omcv"; @@ -2809,10 +2827,7 @@ export class HeroSystem6eItem extends Item { this.system.usesStrength = false; this.system.stunBodyDamage = "stunonly"; this.system.noHitLocations = true; - } - - // MINDCONTROL - if (xmlid == "MINDCONTROL") { + } else if (xmlid == "MINDCONTROL") { this.system.class = "mindcontrol"; this.system.targets = "dmcv"; this.system.uses = "omcv"; @@ -2820,10 +2835,7 @@ export class HeroSystem6eItem extends Item { this.system.usesStrength = false; this.system.stunBodyDamage = "stunonly"; this.system.noHitLocations = true; - } - - // TELEPATHY - if (xmlid == "TELEPATHY") { + } else if (xmlid == "TELEPATHY") { this.system.class = "telepathy"; this.system.targets = "dmcv"; this.system.uses = "omcv"; @@ -2831,17 +2843,11 @@ export class HeroSystem6eItem extends Item { this.system.usesStrength = false; //this.system.stunBodyDamage = "stunonly" this.system.noHitLocations = true; - } - - // CHANGEENVIRONMENT - if (xmlid == "CHANGEENVIRONMENT") { + } else if (xmlid == "CHANGEENVIRONMENT") { this.system.class = "change enviro"; this.system.usesStrength = false; this.system.noHitLocations = true; - } - - // FLASH - if (xmlid == "FLASH") { + } else if (xmlid == "FLASH") { this.system.class = "flash"; this.system.usesStrength = false; this.system.noHitLocations = true; @@ -3120,17 +3126,24 @@ export class HeroSystem6eItem extends Item { } } - _areAllAdjustmentTargetsInListValid(targetsList) { + _areAllAdjustmentTargetsInListValid(targetsList, mustBeStrict) { if (!targetsList) return false; + // ABSORPTION, AID, and TRANSFER target characteristics/powers are the only adjustment powers that must match + // the character's characteristics/powers (i.e. they can't create new characteristics or powers). All others just + // have to match actual possible characteristics/powers. + const validator = + this.system.XMLID === "AID" || + this.system.XMLID === "ABSORPTION" || + (this.system.XMLID === "TRANSFER" && mustBeStrict) + ? adjustmentSourcesStrict + : adjustmentSourcesPermissive; + const validList = Object.keys(validator(this.actor)); + const adjustmentTargets = targetsList.split(","); for (const rawAdjustmentTarget of adjustmentTargets) { const upperCasedInput = rawAdjustmentTarget.toUpperCase().trim(); - if ( - !Object.keys(adjustmentSources(this.actor)).includes( - upperCasedInput, - ) - ) { + if (!validList.includes(upperCasedInput)) { return false; } } @@ -3138,11 +3151,25 @@ export class HeroSystem6eItem extends Item { return true; } - // valid: boolean If true the enhances and reduces lists are valid, otherwise ignore them. + /** + * + * If valid, the enhances and reduces lists are valid, otherwise ignore them. + * + * @typedef { Object } AdjustmentSourceAndTarget + * @property { boolean } valid - if any of the reduces and enhances fields are valid + * @property { string } reduces - things that are reduced (aka from) + * @property { string } enhances - things that are enhanced (aka to) + * @property { string[] } reducesArray + * @property { string[] } enhancesArray + */ + /** + * + * @returns { AdjustmentSourceAndTarget } + */ splitAdjustmentSourceAndTarget() { let valid; - let reduces; - let enhances; + let reduces = ""; + let enhances = ""; if (this.system.XMLID === "TRANSFER") { // Should be something like "STR,CON -> DEX,SPD" @@ -3153,14 +3180,20 @@ export class HeroSystem6eItem extends Item { valid = this._areAllAdjustmentTargetsInListValid( splitSourcesAndTargets[0], + false, ) && this._areAllAdjustmentTargetsInListValid( splitSourcesAndTargets[1], + true, ); enhances = splitSourcesAndTargets[1]; reduces = splitSourcesAndTargets[0]; } else { - valid = this._areAllAdjustmentTargetsInListValid(this.system.INPUT); + valid = this._areAllAdjustmentTargetsInListValid( + this.system.INPUT, + this.system.XMLID === "AID" || + this.system.XMLID === "ABSORPTION", + ); if ( this.system.XMLID === "AID" || @@ -3175,34 +3208,98 @@ export class HeroSystem6eItem extends Item { return { valid: valid, - reduces: reduces || "", - enhances: enhances || "", + + reduces: reduces, + enhances: enhances, + reducesArray: reduces + ? reduces.split(",").map((str) => str.trim()) + : [], + enhancesArray: enhances + ? enhances.split(",").map((str) => str.trim()) + : [], }; } - numberOfSimultaneousAdjustmentEffects(inputs) { + static _maxNumOf5eAdjustmentEffects(mod) { + if (!mod) return 1; + + switch (mod.BASECOST) { + case "0.5": + return 2; + case "1.0": + return 4; + case "2.0": + // All of a type. Assume this is just infinite (pick a really big number). + return 10000; + default: + return 1; + } + } + + numberOfSimultaneousAdjustmentEffects() { if (this.actor.system.is5e) { // In 5e, the number of simultaneous effects is based on the VARIABLEEFFECT modifier. - const variableEffect = this.findModsByXmlid("VARIABLEEFFECT"); - - if (!variableEffect) return 1; - - switch (variableEffect.BASECOST) { - case "0.5": - return 2; - case "1.0": - return 4; - case "2.0": - // All of a type. Assume this is just everything listed in the inputs - return inputs.length; - default: - return 1; + const variableEffect = this.findModsByXmlid("VARIABLEEFFECT"); // From for TRANSFER and everything else + const variableEffect2 = this.findModsByXmlid("VARIABLEEFFECT2"); // To for TRANSFER + + if (this.system.XMLID === "TRANSFER") { + return { + maxReduces: + HeroSystem6eItem._maxNumOf5eAdjustmentEffects( + variableEffect, + ), + maxEnhances: + HeroSystem6eItem._maxNumOf5eAdjustmentEffects( + variableEffect2, + ), + }; + } else if ( + this.system.XMLID === "AID" || + this.system.XMLID === "ABSORPTION" || + this.system.XMLID === "HEALING" + ) { + return { + maxReduces: 0, + maxEnhances: + HeroSystem6eItem._maxNumOf5eAdjustmentEffects( + variableEffect, + ), + }; + } else { + return { + maxReduces: + HeroSystem6eItem._maxNumOf5eAdjustmentEffects( + variableEffect, + ), + maxEnhances: 0, + }; } } - // In 6e, the number of simultaneous effects is LEVELS in EXPANDEDEFFECT modifier if available or - // it is just 1. - return this.findModsByXmlid("EXPANDEDEFFECT")?.LEVELS || 1; + // In 6e, the number of simultaneous effects is LEVELS in the EXPANDEDEFFECT modifier, if available, or + // it is just 1. There is no TRANSFER in 6e. + const maxCount = this.findModsByXmlid("EXPANDEDEFFECT")?.LEVELS || 1; + if ( + this.system.XMLID === "AID" || + this.system.XMLID === "ABSORPTION" || + this.system.XMLID === "HEALING" + ) { + return { + maxReduces: 0, + maxEnhances: maxCount, + }; + } else { + return { + maxReduces: maxCount, + maxEnhances: 0, + }; + } + } + + async addActiveEffect(activeEffect) { + const newEffect = foundry.utils.deepClone(activeEffect); + + return this.createEmbeddedDocuments("ActiveEffect", [newEffect]); } } diff --git a/module/migration.js b/module/migration.js index 6368cb3b..caedb4fb 100644 --- a/module/migration.js +++ b/module/migration.js @@ -1,5 +1,7 @@ import { HeroSystem6eItem } from "./item/item.js"; import { getPowerInfo } from "./utility/util.js"; +import { determineCostPerActivePoint } from "./utility/adjustment.js"; +import { RoundFavorPlayerUp } from "./utility/round.js"; function getAllActorsInGame() { return [ @@ -332,6 +334,28 @@ export async function migrateWorld() { } } + // if lastMigration < 3.0.54 + // Update item.system.class from specific adjustment powers to the general adjustment + // Active Effects for adjustments changed format + if (foundry.utils.isNewerVersion("3.0.54", lastMigration)) { + const queue = getAllActorsInGame(); + let dateNow = new Date(); + + for (const [index, actor] of queue.entries()) { + if (new Date() - dateNow > 4000) { + ui.notifications.info( + `Migrating actor's items to 3.0.54: (${ + queue.length - index + } actors remaining)`, + ); + dateNow = new Date(); + } + + await migrate_actor_items_to_3_0_54(actor); + await migrate_actor_active_effects_to_3_0_54(actor); + } + } + // Reparse all items (description, cost, etc) on every migration { let d = new Date(); @@ -358,9 +382,6 @@ async function migrateActorCostDescription(actor) { try { if (!actor) return false; - if (actor.name === `Jack "Iron Shin" Daniels`) - console.log.apply(actor.name); - let itemsChanged = false; for (let item of actor.items) { await item._postUpload(); @@ -394,6 +415,185 @@ async function migrateActorCostDescription(actor) { } } +async function migrate_actor_active_effects_to_3_0_54(actor) { + for (const activeEffect of actor.temporaryEffects) { + // Is it possibly an old style adjustment effect? + if (activeEffect.changes.length > 0 && activeEffect.flags.target) { + const origin = await fromUuid(activeEffect.origin); + const item = origin instanceof HeroSystem6eItem ? origin : null; + + const powerInfo = getPowerInfo({ + actor: actor, + xmlid: activeEffect?.flags?.XMLID, + item: item, + }); + + // Confirm the power associated with is an adjustment power type + if (!powerInfo || !powerInfo.powerType.includes("adjustment")) { + continue; + } + + // Make sure it's not a new style adjustment active effect already (just a dev possibility) + if (activeEffect.flags.type === "adjustment") { + continue; + } + + const presentAdjustmentActiveEffect = activeEffect; + const potentialCharacteristic = + presentAdjustmentActiveEffect.flags.keyX; + + const costPerActivePoint = determineCostPerActivePoint( + potentialCharacteristic, + null, // TODO: Correct, as we don't support powers right now? + actor, + ); + const activePointsThatShouldBeAffected = Math.trunc( + presentAdjustmentActiveEffect.flags.activePoints / + costPerActivePoint, + ); + + const newFormatAdjustmentActiveEffect = { + name: `${presentAdjustmentActiveEffect.flags.XMLID} ${Math.abs( + activePointsThatShouldBeAffected, + )} ${presentAdjustmentActiveEffect.flags.target} (${Math.abs( + presentAdjustmentActiveEffect.flags.activePoints, + )} AP) [by ${presentAdjustmentActiveEffect.flags.source}]`, + id: presentAdjustmentActiveEffect.id, // No change + icon: presentAdjustmentActiveEffect.icon, // No change + changes: presentAdjustmentActiveEffect.changes, // No change but for 5e there may be additional indices (see below) + duration: presentAdjustmentActiveEffect.duration, // No change even though it might be wrong for transfer it's too complicated to try to figure it out + flags: { + type: "adjustment", // New + version: 2, // New + + activePoints: + presentAdjustmentActiveEffect.flags.activePoints, // No change + XMLID: presentAdjustmentActiveEffect.flags.XMLID, // No change + source: presentAdjustmentActiveEffect.flags.source, // No change + target: [presentAdjustmentActiveEffect.flags.target], // Now an array + key: presentAdjustmentActiveEffect.flags.keyX, // Name change + }, + origin: presentAdjustmentActiveEffect.origin, // No change + }; + + // If 5e we may have additional changes + if ( + actor.system.is5e && + actor.system.characteristics?.[potentialCharacteristic] + ) { + if (potentialCharacteristic === "dex") { + const charValue = + actor.system.characteristics[potentialCharacteristic] + .value; + const lift = RoundFavorPlayerUp( + (charValue - + actor.system.characteristics[ + potentialCharacteristic + ].core) / + 3, + ); + + newFormatAdjustmentActiveEffect.changes.push({ + key: actor.system.characteristics[ + potentialCharacteristic + ] + ? `system.characteristics.ocv.max` + : "system.value", + value: lift, + mode: CONST.ACTIVE_EFFECT_MODES.ADD, + }); + newFormatAdjustmentActiveEffect.flags.target.push("ocv"); + + newFormatAdjustmentActiveEffect.changes.push({ + key: actor.system.characteristics[ + potentialCharacteristic + ] + ? `system.characteristics.dcv.max` + : "system.value", + value: lift, + mode: CONST.ACTIVE_EFFECT_MODES.ADD, + }); + newFormatAdjustmentActiveEffect.flags.target.push("dcv"); + + const changes = { + [`system.characteristics.${newFormatAdjustmentActiveEffect.flags.target[1]}.value`]: + actor.system.characteristics.ocv.value + lift, + [`system.characteristics.${newFormatAdjustmentActiveEffect.flags.target[2]}.value`]: + actor.system.characteristics.dcv.value + lift, + }; + + await actor.update(changes); + } else if (potentialCharacteristic === "ego") { + const charValue = + actor.system.characteristics[potentialCharacteristic] + .value; + const lift = RoundFavorPlayerUp( + (charValue - + actor.system.characteristics[ + potentialCharacteristic + ].core) / + 3, + ); + + newFormatAdjustmentActiveEffect.changes.push({ + key: actor.system.characteristics[ + potentialCharacteristic + ] + ? `system.characteristics.omcv.max` + : "system.value", + value: lift, + mode: CONST.ACTIVE_EFFECT_MODES.ADD, + }); + newFormatAdjustmentActiveEffect.flags.target.push("omcv"); + + newFormatAdjustmentActiveEffect.changes.push({ + key: actor.system.characteristics[ + potentialCharacteristic + ] + ? `system.characteristics.dmcv.max` + : "system.value", + value: lift, + mode: CONST.ACTIVE_EFFECT_MODES.ADD, + }); + newFormatAdjustmentActiveEffect.flags.target.push("dmcv"); + + const changes = { + [`system.characteristics.${newFormatAdjustmentActiveEffect.flags.target[1]}.value`]: + actor.system.characteristics.omcv.value + lift, + [`system.characteristics.${newFormatAdjustmentActiveEffect.flags.target[2]}.value`]: + actor.system.characteristics.dmcv.value + lift, + }; + + await actor.update(changes); + } + } + + // Delete old active effect and create the new one + await presentAdjustmentActiveEffect.delete(); + await actor.addActiveEffect(newFormatAdjustmentActiveEffect); + } + } +} + +async function migrate_actor_items_to_3_0_54(actor) { + for (const item of actor.items) { + // Give all adjustment powers the new "adjustment" class for simplicity. + if ( + item.system.XMLID === "ABSORPTION" || + item.system.XMLID === "AID" || + item.system.XMLID === "DISPEL" || + item.system.XMLID === "DRAIN" || + item.system.XMLID === "HEALING" || + item.system.XMLID === "TRANSFER" || + item.system.XMLID === "SUPPRESS" + ) { + await item.update({ + "system.class": "adjustment", + }); + } + } +} + async function migrate_actor_items_to_3_0_53(actor) { for (const item of actor.items) { // Get rid of item.system.characteristic and replace with diff --git a/module/testing/testing-upload.js b/module/testing/testing-upload.js index f3e61166..bb7646ef 100644 --- a/module/testing/testing-upload.js +++ b/module/testing/testing-upload.js @@ -2071,7 +2071,7 @@ export function registerUploadTests(quench) { it("description", function () { assert.equal( item.system.description, - "Suppress Flight 7 1/2d6, Armor Piercing (+1/2) (42 Active Points); Range Based On Strength (-1/4)", + "Suppress Flight 5 1/2d6, Armor Piercing (+1/2) (42 Active Points); Range Based On Strength (-1/4)", ); }); @@ -2134,7 +2134,7 @@ export function registerUploadTests(quench) { it("description", function () { assert.equal( item.system.description, - "Aid CON 3d6 + 1 (+1 pip; Increased Maximum (+8 points)), Continuous (+1) (74 Active Points); Crew-Served (2 people; -1/4)", + "Aid CON 3d6 + 1 (+1 pip; Increased Maximum (+8 points) (27 total points)), Continuous (+1) (74 Active Points); Crew-Served (2 people; -1/4)", ); }); diff --git a/module/utility/adjustment.js b/module/utility/adjustment.js index 8c51c67c..457429b4 100644 --- a/module/utility/adjustment.js +++ b/module/utility/adjustment.js @@ -1,22 +1,60 @@ -import { getPowerInfo } from "../utility/util.js"; +import { getPowerInfo } from "./util.js"; +import { determineExtraDiceDamage } from "./damage.js"; +import { RoundFavorPlayerUp } from "./round.js"; -export function adjustmentSources(actor) { +/** + * Return the full list of possible powers and characteristics. No skills, talents, or perks. + */ +export function adjustmentSourcesPermissive(actor) { let choices = {}; - let powers = CONFIG.HERO.powers.filter( - (o) => - (o.powerType?.includes("characteristic") || - o.powerType?.includes("movement")) && - !o.ignoreFor?.includes(actor.type) && - !o.ignoreFor?.includes(actor.system.is5e ? "5e" : "6e") && - (!o.onlyFor || o.onlyFor.includes(actor.type)), + const powers = CONFIG.HERO.powers.filter( + (power) => + !power.powerType?.includes("skill") && + !power.powerType?.includes("perk") && + !power.powerType?.includes("talent"), + ); + + for (const power of powers) { + let key = power.key; + choices[key.toUpperCase()] = key.toUpperCase(); + } + + // Add * to defensive powers + for (let key of Object.keys(choices)) { + if (defensivePowerAdjustmentMultiplier(key, actor) > 1) { + choices[key] += "*"; + } + } + + choices[""] = ""; + choices = Object.keys(choices) + .sort() + .reduce((obj, key) => { + obj[key] = choices[key]; + return obj; + }, {}); + + return choices; +} + +export function adjustmentSourcesStrict(actor) { + let choices = {}; + + const powers = CONFIG.HERO.powers.filter( + (power) => + (power.powerType?.includes("characteristic") || + power.powerType?.includes("movement")) && + !power.ignoreFor?.includes(actor.type) && + !power.ignoreFor?.includes(actor.system.is5e ? "5e" : "6e") && + (!power.onlyFor || power.onlyFor.includes(actor.type)), ); // Attack powers for (const item of actor.items.filter( - (o) => o.type === "power" && o.system.XMLID != "MULTIPOWER", + (item) => item.type === "power" && item.system.XMLID != "MULTIPOWER", )) { - powers.push({ key: item.name }); + powers.push({ key: item.system.XMLID }); } for (const power of powers) { @@ -26,7 +64,7 @@ export function adjustmentSources(actor) { // Add * to defensive powers for (let key of Object.keys(choices)) { - if (AdjustmentMultiplier(key, actor) > 1) { + if (defensivePowerAdjustmentMultiplier(key, actor) > 1) { choices[key] += "*"; } } @@ -42,8 +80,25 @@ export function adjustmentSources(actor) { return choices; } -export function AdjustmentMultiplier(XMLID, actor) { +// 5e (pg 114) indicates PD, ED, and defensive powers +const defensiveCharacteristics5e = ["PD", "ED"]; + +// 6e (V1 pg 135) +const defensiveCharacteristics6e = [ + "CON", + "DCV", + "DMCV", + "PD", + "ED", + "REC", + "END", + "BODY", + "STUN", +]; + +export function defensivePowerAdjustmentMultiplier(XMLID, actor) { if (!XMLID) return 1; + let configPowerInfo = getPowerInfo({ xmlid: XMLID, actor: actor }); if (!configPowerInfo) { if (actor) { @@ -55,20 +110,567 @@ export function AdjustmentMultiplier(XMLID, actor) { } if (!configPowerInfo) return 1; } - if ( - [ - "CON", - "DCV", - "DMCV", - "PD", - "ED", - "REC", - "END", - "BODY", - "STUN", - ].includes(XMLID) - ) + + const defenseCharacteristics = actor.system.is5e + ? defensiveCharacteristics5e + : defensiveCharacteristics6e; + if (defenseCharacteristics.includes(XMLID)) { return 2; + } + if (configPowerInfo.powerType?.includes("defense")) return 2; + return 1; } + +export function determineMaxAdjustment(item) { + const reallyBigInteger = 1000000; + + // Certain adjustment powers have no fixed limit. Give them a large integer. + if ( + item.system.XMLID !== "ABSORPTION" && + item.system.XMLID !== "AID" && + item.system.XMLID !== "TRANSFER" + ) { + return reallyBigInteger; + } + + if (item.actor.system.is5e) { + // Max pips in a roll is starting max base. + let maxAdjustment = item.system.dice * 6; + + const extraDice = determineExtraDiceDamage(item); + switch (extraDice) { + case "+1": + maxAdjustment = maxAdjustment + 1; + break; + case "1d3": + maxAdjustment = maxAdjustment + 3; + break; + default: + break; + } + + // Add INCREASEDMAX if available. + const increaseMax = item.system.ADDER?.find( + (adder) => adder.XMLID === "INCREASEDMAX", + ); + maxAdjustment = maxAdjustment + (parseInt(increaseMax?.LEVELS) || 0); + + return maxAdjustment; + } else { + if (item.system.XMLID === "ABSORPTION") { + let maxAdjustment = item.system.LEVELS * 2; + + const increasedMax = item.system.MODIFIER?.find( + (mod) => mod.XMLID === "INCREASEDMAX", + ); + if (increasedMax) { + // Each level is 2x + maxAdjustment = + maxAdjustment * Math.pow(2, parseInt(increasedMax.LEVELS)); + } + return maxAdjustment; + } + + let maxAdjustment = item.system.dice * 6; + + const extraDice = determineExtraDiceDamage(item); + switch (extraDice) { + case "+1": + maxAdjustment = maxAdjustment + 1; + break; + case "1d3": + maxAdjustment = maxAdjustment + 3; + break; + default: + break; + } + return maxAdjustment; + } +} + +export function determineCostPerActivePoint( + potentialCharacteristic, + powerTargetX, + targetActor, +) { + // TODO: Not sure we need to use the characteristic here... + const powerInfo = getPowerInfo({ + xmlid: potentialCharacteristic.toUpperCase(), + actor: targetActor, + }); + return ( + (powerTargetX + ? parseFloat( + powerTargetX.system.activePoints / powerTargetX.system.value, + ) + : parseFloat(powerInfo?.cost || powerInfo?.costPerLevel)) * + defensivePowerAdjustmentMultiplier( + potentialCharacteristic.toUpperCase(), + targetActor, + ) + ); +} + +function _findExistingMatchingEffect( + item, + potentialCharacteristic, + powerTargetName, + targetSystem, +) { + // TODO: Variable Effect may result in multiple changes on same AE. + return targetSystem.effects.find( + (effect) => + effect.origin === item.uuid && + effect.flags.target[0] === + (powerTargetName?.uuid || potentialCharacteristic), + ); +} + +function _createCharacteristicAEChangeBlock( + potentialCharacteristic, + targetSystem, +) { + // TODO: Calculate this earlier so we don't have the logic in here + return { + key: + targetSystem.system.characteristics?.[potentialCharacteristic] != + null + ? `system.characteristics.${potentialCharacteristic}.max` + : "system.max", + value: 0, + mode: CONST.ACTIVE_EFFECT_MODES.ADD, + }; +} + +async function _createNewAdjustmentEffect( + item, + potentialCharacteristic, // TODO: By this point we should know which it is. + powerTargetName, + rawActivePointsDamage, + targetActor, + targetSystem, +) { + // Create new ActiveEffect + // TODO: Add a document field + const activeEffect = { + name: `${item.system.XMLID} 0 ${ + powerTargetName?.name || potentialCharacteristic // TODO: This will need to change for multiple effects + } (0 AP) [by ${item.actor.name}]`, + id: `${item.system.XMLID}.${item.id}.${ + powerTargetName?.name || potentialCharacteristic // TODO: This will need to change for multiple effects + }`, + icon: item.img, + changes: [ + _createCharacteristicAEChangeBlock( + potentialCharacteristic, + targetSystem, + ), + ], + duration: { + seconds: 12, + }, + flags: { + type: "adjustment", + version: 2, + activePoints: 0, + XMLID: item.system.XMLID, + source: targetActor.name, + target: [powerTargetName?.uuid || potentialCharacteristic], + key: potentialCharacteristic, + }, + origin: item.uuid, + + transfer: true, + disabled: false, + }; + + // If this is 5e then some characteristics are entirely calculated based on + // those. We only need to worry about 2 (DEX -> OCV & DCV and EGO -> OMCV & DMCV) + // as figured characteristics aren't adjusted. + if (targetActor.system.is5e) { + if (potentialCharacteristic === "dex") { + activeEffect.changes.push( + _createCharacteristicAEChangeBlock("ocv", targetSystem), + ); + activeEffect.flags.target.push("ocv"); + + activeEffect.changes.push( + _createCharacteristicAEChangeBlock("dcv", targetSystem), + ); + activeEffect.flags.target.push("dcv"); + } else if (potentialCharacteristic === "ego") { + activeEffect.changes.push( + _createCharacteristicAEChangeBlock("omcv", targetSystem), + ); + activeEffect.flags.target.push("omcv"); + + activeEffect.changes.push( + _createCharacteristicAEChangeBlock("dmcv", targetSystem), + ); + activeEffect.flags.target.push("dmcv"); + } + } + + // DELAYEDRETURNRATE (loss for TRANSFER and all other adjustments) and DELAYEDRETURNRATE2 (gain for TRANSFER) + const dRR = item.findModsByXmlid("DELAYEDRETURNRATE"); + const dRR2 = item.findModsByXmlid("DELAYEDRETURNRATE2"); + const delayedReturnRate = + rawActivePointsDamage > 0 + ? dRR + : item.system.XMLID === "TRANSFER" + ? dRR2 + : dRR; + if (delayedReturnRate) { + switch (delayedReturnRate.OPTIONID) { + case "MINUTE": + activeEffect.duration.seconds = 60; + break; + case "FIVEMINUTES": + activeEffect.duration.seconds = 60 * 5; + break; + case "20MINUTES": + activeEffect.duration.seconds = 60 * 20; + break; + case "HOUR": + activeEffect.duration.seconds = 60 * 60; + break; + case "6HOURS": + activeEffect.duration.seconds = 60 * 60 * 6; + break; + case "DAY": + activeEffect.duration.seconds = 60 * 60 * 24; + break; + case "WEEK": + activeEffect.duration.seconds = 604800; + break; + case "MONTH": + activeEffect.duration.seconds = 2.628e6; + break; + case "SEASON": + activeEffect.duration.seconds = 2.628e6 * 3; + break; + case "YEAR": + activeEffect.duration.seconds = 3.154e7; + break; + case "FIVEYEARS": + activeEffect.duration.seconds = 3.154e7 * 5; + break; + case "TWENTYFIVEYEARS": + activeEffect.duration.seconds = 3.154e7 * 25; + break; + case "CENTURY": + activeEffect.duration.seconds = 3.154e7 * 100; + break; + default: + await ui.notifications.error( + `DELAYEDRETURNRATE has unhandled option ${delayedReturnRate?.OPTIONID}`, + ); + } + } + + return activeEffect; +} + +export async function performAdjustment( + item, + targetedPower, + rawActivePointsDamage, + activePointDamage, + defenseDescription, + isFade, + targetActor, +) { + const targetName = targetedPower.toUpperCase(); + + // Search the target for this power. + // TODO: will return first matching power. How can we distinguish without making users + // setup the item for a specific? Will likely need to provide a dialog. That gets + // us into the thorny question of what powers have been discovered. + const targetPower = targetActor.items.find( + (item) => item.system.XMLID === targetName, + ); + const potentialCharacteristic = targetPower + ? targetPower.system.XMLID + : targetName.toLowerCase(); + const targetCharacteristic = + targetActor.system.characteristics?.[potentialCharacteristic]; + + // A target we understand? + // TODO: Targeting a movement power that the targetActor doesn't have will still succeed. This seems wrong. + // Why are flying, teleportation, etc characteristics? + if (!targetCharacteristic && !targetPower) { + // Can't find anything to link this against...meh. Might be caught by the validity check above. + return; + } + + // Characteristics target an actor, and powers target an item + const targetSystem = + targetActor.system.characteristics?.[potentialCharacteristic] != null + ? targetActor + : targetPower; + const targetValuePath = + targetSystem.system.characteristics?.[potentialCharacteristic] != null + ? `system.characteristics.${potentialCharacteristic}.value` + : `system.value`; + const targetValue = + targetActor.system.characteristics?.[potentialCharacteristic] != null + ? targetActor.system.characteristics?.[potentialCharacteristic] + .value + : targetPower.system.value; + const targetMax = + targetActor.system.characteristics?.[potentialCharacteristic] != null + ? targetActor.system.characteristics?.[potentialCharacteristic].max + : targetPower.system.max; + + // Check for previous adjustment (i.e ActiveEffect) from same power against this target + // and calculate the total effect + const existingEffect = _findExistingMatchingEffect( + item, + potentialCharacteristic, + targetPower, + targetSystem, + ); + + // Shortcut here in case we have no existing effect and 0 damage is done. + if (!existingEffect && activePointDamage === 0) { + return _generateAdjustmentChatCard( + item, + activePointDamage, + 0, + 0, + 0, + defenseDescription, + potentialCharacteristic, + isFade, + false, + targetActor, + ); + } + + const activeEffect = + existingEffect || + (await _createNewAdjustmentEffect( + item, + potentialCharacteristic, + targetPower, + rawActivePointsDamage, + targetActor, + targetSystem, + )); + let totalNewActivePoints = + activePointDamage + activeEffect.flags.activePoints; + let activePointEffectLostDueToMax = 0; + + // Clamp max change to the max allowed by the power. + // TODO: Healing may not raise max or value above max. + // TODO: Combined effects may not exceed the largest source's maximum for a single target. + if (totalNewActivePoints < 0) { + const max = Math.max( + totalNewActivePoints, + -determineMaxAdjustment(item), + ); + activePointEffectLostDueToMax = totalNewActivePoints - max; + totalNewActivePoints = max; + } else { + const min = Math.min( + totalNewActivePoints, + determineMaxAdjustment(item), + ); + activePointEffectLostDueToMax = totalNewActivePoints - min; + totalNewActivePoints = min; + } + + // Determine how many points of effect there are based on the cost + const costPerActivePoint = determineCostPerActivePoint( + potentialCharacteristic, + targetPower, + targetActor, + ); + const activePointsThatShouldBeAffected = Math.trunc( + totalNewActivePoints / costPerActivePoint, + ); + const activePointAffectedDifference = + activePointsThatShouldBeAffected - + Math.trunc(activeEffect.flags.activePoints / costPerActivePoint); + + // Calculate the effect's max value(s) + activeEffect.changes[0].value = + activeEffect.changes[0].value - activePointAffectedDifference; + + // If this is 5e then some characteristics are calculated (not figured) based on + // those. We only need to worry about 2: DEX -> OCV & DCV and EGO -> OMCV & DMCV. + // These 2 characteristics are always at indices 2 and 3 + + // TODO: This really only works when there is 1 effect happening to the characteristic. + // To fix would require separate boost tracking along with fractional boosts or + // not tracking the changes to OCV and DCV as active effects but have them recalculated + // as the characteristic max and value are changing. + if (targetActor.system.is5e && activeEffect.changes[1]) { + const newCalculatedValue = RoundFavorPlayerUp( + (targetMax - activePointAffectedDifference) / 3, + ); + const oldCalculatedValue = RoundFavorPlayerUp(targetMax / 3); + + activeEffect.changes[1].value = + parseInt(activeEffect.changes[1].value) + + (newCalculatedValue - oldCalculatedValue); + + activeEffect.changes[2].value = + parseInt(activeEffect.changes[2].value) + + (newCalculatedValue - oldCalculatedValue); + } + + // Update the effect max value(s) + activeEffect.name = `${item.system.XMLID} ${Math.abs( + activePointsThatShouldBeAffected, + )} ${targetPower?.name || potentialCharacteristic} (${Math.abs( + totalNewActivePoints, + )} AP) [by ${item.actor.name}]`; + + activeEffect.flags.activePoints = totalNewActivePoints; + + const isEffectFinished = activeEffect.flags.activePoints === 0 && isFade; + if (isEffectFinished) { + await activeEffect.delete(); + } else if (!existingEffect) { + await targetSystem.addActiveEffect(activeEffect); + } else { + await activeEffect.update({ + name: activeEffect.name, + changes: activeEffect.changes, + flags: activeEffect.flags, + }); + } + + // Calculate the effect value(s) + // TODO: Pretty sure recovery isn't working as expected for defensive items + // TODO: Pretty sure recovery isn't working as expected for expended characteristics (need separate category keeping: value, max, boost) + const newValue = targetValue - activePointAffectedDifference; + const changes = { + [targetValuePath]: newValue, + }; + + if (targetActor.system.is5e && activeEffect.flags.target[1]) { + const newCalculatedValue = RoundFavorPlayerUp( + (targetMax - activePointAffectedDifference) / 3, + ); + const oldCalculatedValue = RoundFavorPlayerUp(targetMax / 3); + const char1Value = + targetActor.system.characteristics[activeEffect.flags.target[1]] + .value; + const char2Value = + targetActor.system.characteristics[activeEffect.flags.target[2]] + .value; + + changes[ + `system.characteristics.${activeEffect.flags.target[1]}.value` + ] = char1Value + (newCalculatedValue - oldCalculatedValue); + changes[ + `system.characteristics.${activeEffect.flags.target[2]}.value` + ] = char2Value + (newCalculatedValue - oldCalculatedValue); + } + + // Update the effect value(s) + await targetSystem.update(changes); + + return _generateAdjustmentChatCard( + item, + activePointDamage, + activePointAffectedDifference, + totalNewActivePoints, + activePointEffectLostDueToMax, + defenseDescription, + potentialCharacteristic, + isFade, + isEffectFinished, + targetActor, + ); +} + +function _generateAdjustmentChatCard( + item, + activePointDamage, + activePointAffectedDifference, + totalActivePointEffect, + activePointEffectLostDueToMax, + defenseDescription, + potentialCharacteristic, // TODO: Power? + isFade, + isEffectFinished, + targetActor, +) { + const cardData = { + item: item, + + adjustmentDamageRaw: activePointDamage, + adjustmentTotalActivePointEffect: totalActivePointEffect, + defenseDescription: defenseDescription, + + adjustment: { + adjustmentDamageThisApplication: activePointAffectedDifference, + adjustmentTarget: potentialCharacteristic.toUpperCase(), + activePointEffectLostDueToMax, + }, + + isFade, + isEffectFinished, + + targetActor: targetActor, + }; + + return cardData; +} + +/** + * + * Renders and creates a number of related adjustment chat messages for the same target + * + * @param {*} cardOrCards + * @returns void + */ +export async function renderAdjustmentChatCards(cardOrCards) { + if (!Array.isArray(cardOrCards)) { + cardOrCards = [cardOrCards]; + } + + // Filter out any invalid cards + cardOrCards.filter((card) => card); + + if (cardOrCards.length === 0) return; + + const cardData = { + item: cardOrCards[0].item, + + adjustmentDamageRaw: cardOrCards[0].adjustmentDamageRaw, + adjustmentTotalActivePointEffect: + cardOrCards[0].adjustmentTotalActivePointEffect, + defenseDescription: cardOrCards[0].defenseDescription, + + adjustments: cardOrCards.map((card) => { + return card.adjustment; + }), + + isFade: cardOrCards[0].isFade, + isEffectFinished: cardOrCards[0].isEffectFinished, + + targetActor: cardOrCards[0].targetActor, + }; + + // render card + const template = + "systems/hero6efoundryvttv2/templates/chat/apply-adjustment-card.hbs"; + const cardHtml = await renderTemplate(template, cardData); + const speaker = ChatMessage.getSpeaker({ + actor: cardOrCards[0].targetActor, + }); + + const chatData = { + user: game.user._id, + content: cardHtml, + speaker: speaker, + }; + + return ChatMessage.create(chatData); +} diff --git a/module/utility/defense.js b/module/utility/defense.js index 256f6fc2..e1cd4bea 100644 --- a/module/utility/defense.js +++ b/module/utility/defense.js @@ -6,10 +6,12 @@ function determineDefense(targetActor, attackItem, options) { const attackType = avad ? "avad" : attackItem.system.class; const piercing = parseInt(attackItem.system.piercing) || - attackItem.findModsByXmlid("ARMORPIERCING"); + attackItem.findModsByXmlid("ARMORPIERCING") || + 0; const penetrating = parseInt(attackItem.system.penetrating) || - attackItem.findModsByXmlid("PENETRATING"); + attackItem.findModsByXmlid("PENETRATING") || + 0; // The defenses that are active const activeDefenses = targetActor.items.filter( @@ -20,14 +22,14 @@ function determineDefense(targetActor, attackItem, options) { !(options?.ignoreDefenseIds || []).includes(o.id), ); - let PD = parseInt(targetActor.system.characteristics.pd.value); - let ED = parseInt(targetActor.system.characteristics.ed.value); - let MD = 0; - let POWD = 0; - let rPOWD = 0; + let PD = parseInt(targetActor.system.characteristics.pd.value); // physical defense + let ED = parseInt(targetActor.system.characteristics.ed.value); // energy defense + let MD = 0; // mental defense + let POWD = 0; // power defense let rPD = 0; // resistant physical defense let rED = 0; // resistant energy defense - let rMD = 0; // resistant mental defense (not sure rMD is a real thing) + let rMD = 0; // resistant mental defense (a silly but possible thing) + let rPOWD = 0; // resistant power defense (a silly but possible thing) let DRP = 0; // damage reduction physical let DRE = 0; // damage reduction energy let DRM = 0; // damage reduction mental @@ -131,51 +133,48 @@ function determineDefense(targetActor, attackItem, options) { }); break; case "mental": + case "adjustment": break; } - //if ((targetActor.items.size || targetActor.items.length) > 0) { - for (let i of activeDefenses) { - let value = parseInt(i.system.value) || 0; + for (const activeDefense of activeDefenses) { + let value = parseInt(activeDefense.system.value) || 0; - const xmlid = i.system.XMLID; + const xmlid = activeDefense.system.XMLID; // Resistant Defenses if (["FORCEFIELD", "FORCEWALL", "ARMOR"].includes(xmlid)) { switch (attackType) { case "physical": - value = parseInt(i.system.PDLEVELS) || 0; - i.system.defenseType = "pd"; - i.system.resistant = true; + value = parseInt(activeDefense.system.PDLEVELS) || 0; + activeDefense.system.defenseType = "pd"; + activeDefense.system.resistant = true; break; case "energy": - value = parseInt(i.system.EDLEVELS) || 0; - i.system.defenseType = "ed"; - i.system.resistant = true; + value = parseInt(activeDefense.system.EDLEVELS) || 0; + activeDefense.system.defenseType = "ed"; + activeDefense.system.resistant = true; break; case "mental": - i.system.defenseType = "md"; - value = parseInt(i.system.MDLEVELS) || 0; - i.system.resistant = true; + value = parseInt(activeDefense.system.MDLEVELS) || 0; + activeDefense.system.defenseType = "md"; + activeDefense.system.resistant = true; break; - case "drain": - case "transfer": - i.system.defenseType = "powd"; - value = parseInt(i.system.POWDLEVELS) || 0; - i.system.resistant = true; + case "adjustment": + value = parseInt(activeDefense.system.POWDLEVELS) || 0; + activeDefense.system.defenseType = "powd"; + activeDefense.system.resistant = true; break; } } if (["POWERDEFENSE"].includes(xmlid)) { switch (attackType) { - case "drain": - case "transfer": - i.system.defenseType = "powd"; - //value += parseInt(i.system.value) || 0 + case "adjustment": + activeDefense.system.defenseType = "powd"; break; } } @@ -183,9 +182,7 @@ function determineDefense(targetActor, attackItem, options) { if (["MENTALDEFENSE"].includes(xmlid)) { switch (attackType) { case "mental": - i.system.defenseType = "md"; - //value += parseInt(i.system.value) || 0 - //defenseTags.push({ name: 'MD', value: i.system.value, resistant: false, title: i.name}) + activeDefense.system.defenseType = "md"; break; } } @@ -193,21 +190,22 @@ function determineDefense(targetActor, attackItem, options) { if ( !value && ["DAMAGEREDUCTION"].includes(xmlid) && - i.system.INPUT.toLowerCase() == attackType + activeDefense.system.INPUT.toLowerCase() == attackType ) { - value = parseInt(i.system.OPTIONID.match(/\d+/)) || 0; - i.system.resistant = i.system.OPTIONID.match(/RESISTANT/) - ? true - : false; + value = parseInt(activeDefense.system.OPTIONID.match(/\d+/)) || 0; + activeDefense.system.resistant = + activeDefense.system.OPTIONID.match(/RESISTANT/) ? true : false; switch (attackType) { case "physical": - i.system.defenseType = "drp"; + activeDefense.system.defenseType = "drp"; break; + case "energy": - i.system.defenseType = "dre"; + activeDefense.system.defenseType = "dre"; break; + case "mental": - i.system.defenseType = "drm"; + activeDefense.system.defenseType = "drm"; break; } } @@ -215,44 +213,49 @@ function determineDefense(targetActor, attackItem, options) { if (!value && ["DAMAGENEGATION"].includes(xmlid)) { switch (attackType) { case "physical": - i.system.defenseType = "dnp"; + activeDefense.system.defenseType = "dnp"; value = parseInt( - i.system.ADDER.find((o) => o.XMLID == "PHYSICAL") - ?.LEVELS, + activeDefense.system.ADDER.find( + (o) => o.XMLID == "PHYSICAL", + )?.LEVELS, ) || 0; break; + case "energy": - i.system.defenseType = "dne"; + activeDefense.system.defenseType = "dne"; value = parseInt( - i.system.ADDER.find((o) => o.XMLID == "ENERGY") - ?.LEVELS, + activeDefense.system.ADDER.find( + (o) => o.XMLID == "ENERGY", + )?.LEVELS, ) || 0; break; + case "mental": - i.system.defenseType = "dnm"; + activeDefense.system.defenseType = "dnm"; value = parseInt( - i.system.ADDER.find((o) => o.XMLID == "MENTAL") - ?.LEVELS, + activeDefense.system.ADDER.find( + (o) => o.XMLID == "MENTAL", + )?.LEVELS, ) || 0; break; } } if (["COMBAT_LUCK"].includes(xmlid)) { - //!value && switch (attackType) { case "physical": - i.system.defenseType = "pd"; - value = (parseInt(i.system.value) || 0) * 3; - i.system.resistant = true; + activeDefense.system.defenseType = "pd"; + value = (parseInt(activeDefense.system.value) || 0) * 3; + activeDefense.system.resistant = true; break; + case "energy": - i.system.defenseType = "ed"; - value = (parseInt(i.system.value) || 0) * 3; - i.system.resistant = true; + activeDefense.system.defenseType = "ed"; + value = (parseInt(activeDefense.system.value) || 0) * 3; + activeDefense.system.resistant = true; break; } } @@ -265,7 +268,11 @@ function determineDefense(targetActor, attackItem, options) { if (["KBRESISTANCE", "DENSITYINCREASE", "GROWTH"].includes(xmlid)) { let _value = value * (targetActor.system.is5e ? 1 : 2); knockbackResistance += _value; - defenseTags.push({ value: _value, name: "KB", title: i.name }); + defenseTags.push({ + value: _value, + name: "KB", + title: activeDefense.name, + }); } } @@ -274,7 +281,9 @@ function determineDefense(targetActor, attackItem, options) { // Hardened let hardened = parseInt( - i.system.hardened || i.findModsByXmlid("HARDENED")?.LEVELS || 0, + activeDefense.system.hardened || + activeDefense.findModsByXmlid("HARDENED")?.LEVELS || + 0, ); // Armor Piercing @@ -284,8 +293,8 @@ function determineDefense(targetActor, attackItem, options) { // Impenetrable let impenetrable = parseInt( - i.system.impenetrable || - i.findModsByXmlid("IMPENETRABLE")?.LEVELS || + activeDefense.system.impenetrable || + activeDefense.findModsByXmlid("IMPENETRABLE")?.LEVELS || 0, ); @@ -294,7 +303,10 @@ function determineDefense(targetActor, attackItem, options) { valueImp = valueAp; } - switch ((i.system.resistant ? "r" : "") + i.system.defenseType) { + const protectionType = + (activeDefense.system.resistant ? "r" : "") + + activeDefense.system.defenseType; + switch (protectionType) { case "pd": // Physical Defense PD += valueAp; if (attackType === "physical" || attackType === "avad") { @@ -303,11 +315,12 @@ function determineDefense(targetActor, attackItem, options) { name: "PD", value: valueAp, resistant: false, - title: i.name, + title: activeDefense.name, }); impenetrableValue += valueImp; } break; + case "ed": // Energy Defense ED += valueAp; if (attackType === "energy" || attackType === "avad") { @@ -316,11 +329,12 @@ function determineDefense(targetActor, attackItem, options) { name: "ED", value: valueAp, resistant: false, - title: i.name, + title: activeDefense.name, }); impenetrableValue += valueImp; } break; + case "md": // Mental Defense MD += valueAp; if (attackType === "mental" || attackType === "avad") { @@ -329,15 +343,16 @@ function determineDefense(targetActor, attackItem, options) { name: "MD", value: valueAp, resistant: false, - title: i.name, + title: activeDefense.name, }); impenetrableValue += valueImp; } break; + case "powd": // Power Defense POWD += valueAp; if ( - ["drain", "transfer"].includes(attackType) || + ["adjustment"].includes(attackType) || attackType === "avad" ) { if (valueAp > 0) @@ -345,11 +360,12 @@ function determineDefense(targetActor, attackItem, options) { name: "POWD", value: valueAp, resistant: false, - title: i.name, + title: activeDefense.name, }); impenetrableValue += valueImp; } break; + case "rpd": // Resistant PD rPD += valueAp; if (attackType === "physical" || attackType === "avad") { @@ -358,11 +374,12 @@ function determineDefense(targetActor, attackItem, options) { name: "rPD", value: valueAp, resistant: true, - title: i.name, + title: activeDefense.name, }); impenetrableValue += valueImp; } break; + case "red": // Resistant ED rED += valueAp; if (attackType === "energy" || attackType === "avad") { @@ -371,11 +388,12 @@ function determineDefense(targetActor, attackItem, options) { name: "rED", value: valueAp, resistant: true, - title: i.name, + title: activeDefense.name, }); impenetrableValue += valueImp; } break; + case "rmd": // Resistant MD rMD += valueAp; if (attackType === "mental" || attackType === "avad") { @@ -384,15 +402,16 @@ function determineDefense(targetActor, attackItem, options) { name: "rMD", value: valueAp, resistant: true, - title: i.name, + title: activeDefense.name, }); impenetrableValue += valueImp; } break; + case "rpowd": // Resistant Power Defense rPOWD += valueAp; if ( - ["drain", "transfer"].includes(attackType) || + ["adjustment"].includes(attackType) || attackType === "avad" ) { if (valueAp > 0) @@ -400,74 +419,87 @@ function determineDefense(targetActor, attackItem, options) { name: "rPOWD", value: valueAp, resistant: true, - title: i.name, + title: activeDefense.name, }); impenetrableValue += valueImp; } break; + case "drp": // Damage Reduction Physical case "rdrp": if (value > 0) defenseTags.push({ name: "drp", - value: `${i.system.resistant ? "r" : ""}${value}%`, - resistant: i.system.resistant, - title: i.name, + value: `${ + activeDefense.system.resistant ? "r" : "" + }${value}%`, + resistant: activeDefense.system.resistant, + title: activeDefense.name, }); DRP = Math.max(DRP, value); break; + case "dre": // Damage Reduction Energy case "rdre": if (value > 0) defenseTags.push({ name: "dre", - value: `${i.system.resistant ? "r" : ""}${value}%`, - resistant: i.system.resistant, - title: i.name, + value: `${ + activeDefense.system.resistant ? "r" : "" + }${value}%`, + resistant: activeDefense.system.resistant, + title: activeDefense.name, }); DRE = Math.max(DRE, value); break; + case "drm": // Damage Reduction Mental case "rdrm": if (value > 0) defenseTags.push({ name: "drm", - value: `${i.system.resistant ? "r" : ""}${value}%`, - resistant: i.system.resistant, - title: i.name, + value: `${ + activeDefense.system.resistant ? "r" : "" + }${value}%`, + resistant: activeDefense.system.resistant, + title: activeDefense.name, }); DRM = Math.max(DRM, value); break; + case "dnp": // Damage Negation Physical if (value > 0) defenseTags.push({ name: "dnp", value: value, resistant: false, - title: i.name, + title: activeDefense.name, }); DNP += value; break; + case "dne": // Damage Negation Energy if (value > 0) defenseTags.push({ name: "dne", value: value, resistant: false, - title: i.name, + title: activeDefense.name, }); DNE += value; break; + case "dnm": // Damage Negation Mental if (value > 0) defenseTags.push({ name: "dnm", value: value, resistant: false, - title: i.name, + title: activeDefense.name, }); DNM += value; break; + case "kbr": // Knockback Resistance knockbackResistance += value; if ( @@ -477,20 +509,21 @@ function determineDefense(targetActor, attackItem, options) { defenseTags.push({ name: "KB Resistance", value: value, - title: i.name, + title: activeDefense.name, }); } break; + default: - if (game.settings.get(game.system.id, "alphaTesting")) { - //ui.notifications.warn(i.system.defenseType + " not yet supported!") - //HEROSYS.log(false, i.system.defenseType + " not yet supported!"); - } + // TODO: Mostly likely this is flash defense missing + // if (game.settings.get(game.system.id, "alphaTesting")) { + // const warnMessage = `${activeDefense.name}: ${activeDefense.system.defenseType} not yet supported!`; + // ui.notifications.warn(warnMessage); + // HEROSYS.log(false, warnMessage); + // } break; } - //} } - //} let defenseValue = 0; let resistantValue = 0; @@ -504,6 +537,7 @@ function determineDefense(targetActor, attackItem, options) { damageReductionValue = DRP; damageNegationValue = DNP; break; + case "energy": defenseValue = ED; resistantValue = rED; @@ -511,6 +545,7 @@ function determineDefense(targetActor, attackItem, options) { damageReductionValue = DRE; damageNegationValue = DNE; break; + case "mental": defenseValue = MD; resistantValue = rMD; @@ -519,14 +554,14 @@ function determineDefense(targetActor, attackItem, options) { damageNegationValue = DNM; break; - case "drain": - case "transfer": + case "adjustment": defenseValue = POWD; - resistantValue = Math.max(POWD, rPOWD); + resistantValue = rPOWD; //impenetrableValue = Math.max(POWD, rPOWD); damageReductionValue = DRM; damageNegationValue = DNM; break; + case "avad": defenseValue = PD + ED + MD + POWD; resistantValue = rPD + rED + rMD + rPOWD; @@ -534,6 +569,7 @@ function determineDefense(targetActor, attackItem, options) { damageReductionValue = DRM; damageNegationValue = DNM; defenseTags; + break; } return [ diff --git a/module/utility/effects.js b/module/utility/effects.js index ff50c50b..8a8c3a0c 100644 --- a/module/utility/effects.js +++ b/module/utility/effects.js @@ -15,33 +15,6 @@ export async function onManageActiveEffect(event, owner) { ); const item = owner.items.get(li.dataset.effectId); - // guard or perhaps a defense item - // if (!effect) { - // const item = owner.items.get(li.dataset.effectId); - // if (item) { - // switch (a.dataset.action) { - // case "edit": - // item.sheet.render(true); - // break; - // case "toggle": - // item.toggle(); - // break; - // case "delete": - // const confirmed = await Dialog.confirm({ - // title: game.i18n.localize("HERO6EFOUNDRYVTTV2.confirms.deleteConfirm.Title"), - // content: game.i18n.localize("HERO6EFOUNDRYVTTV2.confirms.deleteConfirm.Content") - // }); - - // if (confirmed) { - // item.delete() - // //this.render(); - // } - // break; - // } - // } - // return; - // } - switch (a.dataset.action) { case "create": return owner.createEmbeddedDocuments("ActiveEffect", [ @@ -50,8 +23,6 @@ export async function onManageActiveEffect(event, owner) { icon: "icons/svg/aura.svg", origin: owner.uuid, disabled: true, - // "duration.rounds": li.dataset.effectType === "temporary" ? 1 : undefined, - // disabled: li.dataset.effectType === "inactive" }, ]); @@ -83,18 +54,8 @@ export async function onManageActiveEffect(event, owner) { } await effect.delete(); } else { - item.delete(); + await item.delete(); } - - //let actor = effect.parent instanceof HeroSystem6eActor ? effect.parent : effect.parent.actor - - // Characteristic VALUE should not exceed MAX - // for (let char of Object.keys(actor.system.characteristics)) { - // if (actor.system.characteristics[char].value > actor.system.characteristics[char].max) { - // await actor.update({ [`system.characteristics.${char}.value`]: actor.system.characteristics[char].max }) - // //updates.push({[`system.characteristics.${char}.value`]: parseInt(actor.system.characteristics[char].max)}); - // } - // } } return; } diff --git a/scss/components/_actor-sheet.scss b/scss/components/_actor-sheet.scss index 4abd3888..b5a1f706 100644 --- a/scss/components/_actor-sheet.scss +++ b/scss/components/_actor-sheet.scss @@ -348,6 +348,7 @@ ul.left li { td.characteristic-locked > input { cursor: pointer; + pointer-events:none; } div.aoe-button { diff --git a/templates/actor/actor-sheet.hbs b/templates/actor/actor-sheet.hbs index 65395cda..8ba11313 100644 --- a/templates/actor/actor-sheet.hbs +++ b/templates/actor/actor-sheet.hbs @@ -54,6 +54,7 @@
  • rPD={{defense.rPD}}
  • ED={{defense.ED}}
  • rED={{defense.rED}}
  • +
  • MD={{defense.MD}}
  • {{#if defense.rMD}}
  • rMD={{defense.rMD}}
  • {{/if}} @@ -65,18 +66,15 @@ {{#if defense.dne}}
  • dne={{defense.dne}}
  • {{/if}} {{#if defense.dnm}}
  • dnm={{defense.dnm}}
  • {{/if}} - {{#if defense.POWD}}
  • POWD={{defense.POWD}}
  • - {{/if}} - {{#if defense.rPOWD}}
  • rPOWD={{defense.rPOWD}}
  • - {{/if}} - +
  • POWD={{defense.POWD}}
  • + {{#if defense.rPOWD}}
  • rPOWD={{defense.rPOWD}}
  • {{/if}} \ No newline at end of file diff --git a/templates/item/item-effects-partial.hbs b/templates/item/item-effects-partial.hbs index 1afad9bc..7377ca6b 100644 --- a/templates/item/item-effects-partial.hbs +++ b/templates/item/item-effects-partial.hbs @@ -1,42 +1,49 @@ {{#if alphaTesting}} - - - - - - - +
    {{localize "ActorSheet.ActiveEffect"}}Active - {{!-- - Add ActiveEffect --}} -
    + + + + + + - - {{#each item.effects as |effect|}} - - - - - - - - - {{/each}} -
    {{localize "ActorSheet.ActiveEffect"}}Active + {{! + Add ActiveEffect }} +
    -
    -
    -
    {{effect.name}} - - {{!-- {{#if effect.disabled}}N{{else}}Y{{/if}} --}} - - - - - -
    + {{#each item.effects as |effect|}} + + +
    + +
    + + + {{effect.name}} + + + + + + + + + + + + + + {{/each}} + {{/if}} \ No newline at end of file diff --git a/templates/item/item-power-adjustment-sheet.hbs b/templates/item/item-power-adjustment-sheet.hbs index b97d9bc3..4845e81f 100644 --- a/templates/item/item-power-adjustment-sheet.hbs +++ b/templates/item/item-power-adjustment-sheet.hbs @@ -28,8 +28,8 @@ {{#each reduces as |input id|}}
    - + {{selectOptions @root/possibleReduces selected=input}}
    {{/each}} @@ -37,13 +37,12 @@ {{#each enhances as |input id|}}
    - + {{selectOptions @root/possibleEnhances selected=input}}
    {{/each}} -
    {{> systems/hero6efoundryvttv2/templates/item/item-common-partial.hbs item=item }}