diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9f667adb..519281ad 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,17 @@
# Releases
-## Version 4.0.4 [Hero System 6e (Unofficial) v2](https://github.com/dmdorman/hero6e-foundryvtt)
+## Version 4.0.5 (So Far...) [Hero System 6e (Unofficial) v2](https://github.com/dmdorman/hero6e-foundryvtt)
+
+- OCV/OMCV bonuses are no longer active effects. They only work for a specific instant attack. [#1285](https://github.com/dmdorman/hero6e-foundryvtt/issues/1285)
+- Fixed rare issue where adding some 6e powers to a 5e actor would prevent actor sheet from opening.
+- Support for STR Minimum OCV penalty. [#384](https://github.com/dmdorman/hero6e-foundryvtt/issues/384)
+- Support for STR 0 rolls including DCV and movement penalties. [#1401](https://github.com/dmdorman/hero6e-foundryvtt/issues/1401)
+- Support for PRE 0 rolls. [#1403](https://github.com/dmdorman/hero6e-foundryvtt/issues/1403)
+- Fixed issue where FLIGHT was impacting KB rolls even when FLIGHT was turned off. [#1400](https://github.com/dmdorman/hero6e-foundryvtt/issues/1400)
+- Fixed issue where full END for an attack was used even when lowering Effective Strength. [#1399](https://github.com/dmdorman/hero6e-foundryvtt/issues/1399)
+- AUTOMATION's can't use STUN in place of END. [#1398](https://github.com/dmdorman/hero6e-foundryvtt/issues/1398)
+
+## Version 4.0.4
- Penalty Skill Levels now default to checked after upload. [#1359](https://github.com/dmdorman/hero6e-foundryvtt/issues/1359)
- Mental attacks vs Entangles without Mental Defense now default to targeting actor, not entangle by default. [#1295](https://github.com/dmdorman/hero6e-foundryvtt/issues/1295)
diff --git a/module/actor/actor-sheet.mjs b/module/actor/actor-sheet.mjs
index c7f45703..13d4529e 100644
--- a/module/actor/actor-sheet.mjs
+++ b/module/actor/actor-sheet.mjs
@@ -262,7 +262,7 @@ export class HeroSystemActorSheet extends ActorSheet {
const pdContentsAttack = `
- `;
+ `;
const pdAttack = await new HeroSystem6eItem(
HeroSystem6eItem.itemDataFromXml(pdContentsAttack, defenseCalculationActor),
{ temporary: true, parent: defenseCalculationActor },
@@ -312,7 +312,7 @@ export class HeroSystemActorSheet extends ActorSheet {
const edContentsAttack = `
- `;
+ `;
const edAttack = await new HeroSystem6eItem(
HeroSystem6eItem.itemDataFromXml(edContentsAttack, defenseCalculationActor),
{ temporary: true, parent: defenseCalculationActor },
@@ -363,7 +363,7 @@ export class HeroSystemActorSheet extends ActorSheet {
- `;
+ `;
const mdAttack = await new HeroSystem6eItem(
HeroSystem6eItem.itemDataFromXml(mdContentsAttack, defenseCalculationActor),
{ temporary: true, parent: defenseCalculationActor },
@@ -411,7 +411,7 @@ export class HeroSystemActorSheet extends ActorSheet {
- `;
+ `;
const drainAttack = await new HeroSystem6eItem(
HeroSystem6eItem.itemDataFromXml(drainContentsAttack, defenseCalculationActor),
{ temporary: true, parent: defenseCalculationActor },
diff --git a/module/actor/actor.mjs b/module/actor/actor.mjs
index ef647707..465a6824 100644
--- a/module/actor/actor.mjs
+++ b/module/actor/actor.mjs
@@ -887,6 +887,13 @@ export class HeroSystem6eActor extends Actor {
}
await this.createEmbeddedDocuments("ActiveEffect", [activeEffect]);
+
+ // If we have control of this token, re-acquire to update movement types
+ const myToken = this.getActiveTokens()?.[0];
+ if (canvas.tokens.controlled.find((t) => t.id == myToken.id)) {
+ myToken.release();
+ myToken.control();
+ }
return;
}
@@ -895,6 +902,88 @@ export class HeroSystem6eActor extends Actor {
} else if (prevActiveEffect && prevActiveEffect.name != name) {
await prevActiveEffect.update({ name: name });
}
+
+ // At STR 0, halve the character’s Running,
+ // Leaping, Swimming, Swinging, Tunneling, and
+ // Flight based on muscle power (such as most types
+ // of wings). The GM may require the character to
+ // succeed with STR Rolls just to stand up, walk, and
+ // perform similar mundane exertions.
+ // At STR 0, halve the character’s DCV.
+ // For every 2x mass a character has above the
+ // standard human mass of 100 kg, the effects of STR
+ // 0 on movement and DCV occur 5 points of STR
+ // sooner.
+ const massMultiplier = this.items
+ .filter((o) => o.system.XMLID === "DENSITYINCREASE" && o.system.active)
+ .reduce((p, a) => p + parseInt(a.system.LEVELS), 0);
+ const minStr = massMultiplier * 5;
+
+ const prevStr0ActiveEffect = this.effects.find((o) => o.flags?.str0);
+ if (this.system.characteristics.str.value <= minStr && !prevStr0ActiveEffect) {
+ const str0ActiveEffect = {
+ name: "STR0",
+ id: "STR0",
+ icon: `systems/${HEROSYS.module}/icons/encumbered.svg`,
+ changes: [
+ {
+ key: "system.characteristics.dcv.value",
+ value: 0.5,
+ mode: CONST.ACTIVE_EFFECT_MODES.MULTIPLY,
+ },
+ {
+ key: "system.characteristics.running.value",
+ value: 0.5,
+ mode: CONST.ACTIVE_EFFECT_MODES.MULTIPLY,
+ },
+ {
+ key: "system.characteristics.leaping.value",
+ value: 0.5,
+ mode: CONST.ACTIVE_EFFECT_MODES.MULTIPLY,
+ },
+ {
+ key: "system.characteristics.swimming.value",
+ value: 0.5,
+ mode: CONST.ACTIVE_EFFECT_MODES.MULTIPLY,
+ },
+ {
+ key: "system.characteristics.swinging.value",
+ value: 0.5,
+ mode: CONST.ACTIVE_EFFECT_MODES.MULTIPLY,
+ },
+ {
+ key: "system.characteristics.tunneling.value",
+ value: 0.5,
+ mode: CONST.ACTIVE_EFFECT_MODES.MULTIPLY,
+ },
+ ],
+ origin: this.uuid,
+ // duration: {
+ // seconds: 3.154e7 * 100, // 100 years should be close to infinity
+ // },
+ flags: {
+ str0: true,
+ },
+ };
+
+ await this.createEmbeddedDocuments("ActiveEffect", [str0ActiveEffect]);
+ // If we have control of this token, re-acquire to update movement types
+ const myToken = this.getActiveTokens()?.[0];
+ if (canvas.tokens.controlled.find((t) => t.id == myToken.id)) {
+ myToken.release();
+ myToken.control();
+ }
+ } else {
+ if (prevStr0ActiveEffect && this.system.characteristics.str.value > minStr) {
+ await prevStr0ActiveEffect.delete();
+ // If we have control of this token, re-acquire to update movement types
+ const myToken = this.getActiveTokens()?.[0];
+ if (canvas.tokens.controlled.find((t) => t.id == myToken.id)) {
+ myToken.release();
+ myToken.control();
+ }
+ }
+ }
}
async FullHealth() {
diff --git a/module/item/item-attack-application.mjs b/module/item/item-attack-application.mjs
index 4fc6b381..58836f72 100644
--- a/module/item/item-attack-application.mjs
+++ b/module/item/item-attack-application.mjs
@@ -109,6 +109,7 @@ export class ItemAttackFormApplication extends FormApplication {
data.omcvMod ??= parseInt(item.system.ocv); //TODO: May need to make a distinction between OCV/OMCV
data.dmcvMod ??= parseInt(item.system.dcv);
data.effectiveStr ??= parseInt(data.str);
+ data.effectiveStr = Math.max(0, data.effectiveStr);
data.effectiveLevels ??= parseInt(data.item.system.LEVELS);
// Penalty Skill Levels
@@ -138,7 +139,7 @@ export class ItemAttackFormApplication extends FormApplication {
data.targetEntangle = data.entangleExists;
// Mental attacks typically bypass entangles
- if (item.attackDefenseVs === "MD" && entangles?.[0].flags.entangleDefense.rMD === 0) {
+ if (item.attackDefenseVs === "MD" && entangles?.[0]?.flags.entangleDefense.rMD === 0) {
data.targetEntangle = false;
}
}
@@ -328,8 +329,7 @@ export class ItemAttackFormApplication extends FormApplication {
}
}
if (updates) {
- const actualUpdates = await this.data.actor.updateEmbeddedDocuments("Item", updates);
- console.log(actualUpdates);
+ await this.data.actor.updateEmbeddedDocuments("Item", updates);
}
// Take all the data we updated in the form and apply it.
diff --git a/module/item/item-attack.mjs b/module/item/item-attack.mjs
index 13a311fd..721c5152 100644
--- a/module/item/item-attack.mjs
+++ b/module/item/item-attack.mjs
@@ -11,7 +11,7 @@ import {
} from "../utility/damage.mjs";
import { performAdjustment, renderAdjustmentChatCards } from "../utility/adjustment.mjs";
import { getRoundedDownDistanceInSystemUnits, getSystemDisplayUnits } from "../utility/units.mjs";
-import { HeroSystem6eItem, RequiresASkillRollCheck } from "../item/item.mjs";
+import { HeroSystem6eItem, RequiresASkillRollCheck, RequiresACharacteristicRollCheck } from "../item/item.mjs";
import { ItemAttackFormApplication } from "../item/item-attack-application.mjs";
import { DICE_SO_NICE_CUSTOM_SETS, HeroRoller } from "../utility/dice.mjs";
import { clamp } from "../utility/compatibility.mjs";
@@ -379,6 +379,65 @@ export async function AttackToHit(item, options) {
const actor = item.actor;
let effectiveItem = item;
+ // Make sure there are enough resources and consume them
+ const {
+ error: resourceError,
+ warning: resourceWarning,
+ resourcesRequired,
+ resourcesUsedDescription,
+ resourcesUsedDescriptionRenderedRoll,
+ } = await userInteractiveVerifyOptionallyPromptThenSpendResources(effectiveItem, {
+ ...options,
+ ...{ noResourceUse: false },
+ });
+ if (resourceError) {
+ return ui.notifications.error(resourceError);
+ } else if (resourceWarning) {
+ return ui.notifications.warn(resourceWarning);
+ }
+
+ // STR 0 character must succeed with
+ // a STR Roll in order to perform any Action that uses STR, such
+ // as aiming an attack, pulling a trigger, or using a Power with the
+ // Gestures Limitation.
+ // Not all token types (base) will have STR
+ if (
+ actor &&
+ actor.system.characteristics.str &&
+ (effectiveItem.system.usesStrength || effectiveItem.findModsByXmlid("GESTURES"))
+ ) {
+ if (parseInt(actor.system.characteristics.str.value) <= 0) {
+ if (
+ !(await RequiresACharacteristicRollCheck(
+ actor,
+ "str",
+ `Actions that use STR or GESTURES require STR roll when at 0 STR`,
+ ))
+ ) {
+ await ui.notifications.warn(`${actor.name} failed STR 0 roll. Action with ${item.name} failed.`);
+ return;
+ }
+ }
+ }
+
+ // PRE 0
+ // At PRE 0, a character must attempt an PRE Roll to take any
+ // offensive action, or to remain in the face of anything even
+ // remotely threatening.
+ // Not all token types (base) will have PRE
+ if (actor && actor.system.characteristics.pre && parseInt(actor.system.characteristics.pre.value) <= 0) {
+ if (
+ !(await RequiresACharacteristicRollCheck(
+ actor,
+ "pre",
+ `Offensive actions when at PRE 0 requires PRE roll, failure typically results in actor avoiding threats`,
+ ))
+ ) {
+ await ui.notifications.warn(`${actor.name} failed PRE 0 roll. Action with ${item.name} failed.`);
+ return;
+ }
+ }
+
// Create a temporary item based on effectiveLevels
if (options?.effectiveLevels && parseInt(item.system.LEVELS) > 0) {
options.effectiveLevels = parseInt(options.effectiveLevels) || 0;
@@ -557,6 +616,16 @@ export async function AttackToHit(item, options) {
dcv -= 4;
}
+ // STRMINIMUM
+ const STRMINIMUM = item.findModsByXmlid("STRMINIMUM");
+ if (STRMINIMUM) {
+ const strMinimumValue = parseInt(STRMINIMUM.OPTION_ALIAS.match(/\d+/)?.[0] || 0);
+ const extraStr = Math.max(0, parseInt(actor.system.characteristics.str.value)) - strMinimumValue;
+ if (extraStr < 0) {
+ heroRoller.addNumber(Math.floor(extraStr / 5), STRMINIMUM.ALIAS);
+ }
+ }
+
cvModifiers.forEach((cvModifier) => {
if (cvModifier.cvMod.ocv) {
heroRoller.addNumber(cvModifier.cvMod.ocv, cvModifier.name);
@@ -601,23 +670,6 @@ export async function AttackToHit(item, options) {
// (so we can be sneaky and not tell the target's DCV out loud).
heroRoller.addDice(-3);
- // Make sure there are enough resources and consume them
- const {
- error: resourceError,
- warning: resourceWarning,
- resourcesRequired,
- resourcesUsedDescription,
- resourcesUsedDescriptionRenderedRoll,
- } = await userInteractiveVerifyOptionallyPromptThenSpendResources(item, {
- ...options,
- ...{ noResourceUse: false },
- });
- if (resourceError) {
- return ui.notifications.error(resourceError);
- } else if (resourceWarning) {
- return ui.notifications.warn(resourceWarning);
- }
-
const aoeModifier = item.getAoeModifier();
const aoeTemplate =
game.scenes.current.templates.find((template) => template.flags.itemId === item.id) ||
@@ -804,7 +856,7 @@ export async function AttackToHit(item, options) {
}
}
- if (!(await RequiresASkillRollCheck(item))) {
+ if (!(await item)) {
const speaker = ChatMessage.getSpeaker({ actor: item.actor });
speaker["alias"] = item.actor.name;
@@ -3163,13 +3215,23 @@ async function _calcKnockback(body, item, options, knockbackMultiplier) {
// Target is in the air -1d6
// TODO: This is perhaps not the right check as they could just have the movement radio on. Consider a flying status
// when more than 0m off the ground? This same effect should also be considered for gliding.
- if (options.targetToken?.actor?.flags?.activeMovement === "flight") {
- knockbackDice -= 1;
- knockbackTags.push({
- value: "-1d6KB",
- name: "target is in the air",
- title: "Knockback Modifier",
- });
+ const activeMovement = options.targetToken?.actor?.flags?.activeMovement;
+ if (["flight", "gliding"].includes(activeMovement)) {
+ // Double check to make sure FLIGHT or GLIDING is still on
+ if (
+ options.targetToken.actor.items.find(
+ (o) => o.system.XMLID === activeMovement.toUpperCase() && o.system.active,
+ )
+ ) {
+ knockbackDice -= 1;
+ knockbackTags.push({
+ value: "-1d6KB",
+ name: "target is in the air",
+ title: `Knockback Modifier ${options.targetToken?.actor?.flags?.activeMovement}`,
+ });
+ } else {
+ console.warn(`${activeMovement} selected but that power is not active.`);
+ }
}
// TODO: Target Rolled With A Punch -1d6
@@ -3296,6 +3358,14 @@ export async function userInteractiveVerifyOptionallyPromptThenSpendResources(it
}
} else {
if (resourcesRequired.end > actorEndurance && useResources) {
+ // Auotmation or other actor without STUN
+ const hasSTUN = getCharacteristicInfoArrayForActor(actor).find((o) => o.key === "STUN");
+ if (!hasSTUN) {
+ return {
+ warning: `${item.name} needs ${resourcesRequired.end} END but ${actor.name} only has ${actorEndurance} END. This actor cannot use STUN for END.`,
+ };
+ }
+
// Is the actor willing to use STUN to make up for the lack of END?
const potentialStunCost = calculateRequiredStunDiceForLackOfEnd(actor, resourcesRequired.end);
diff --git a/module/item/item.mjs b/module/item/item.mjs
index 3f32d2dd..00da8687 100644
--- a/module/item/item.mjs
+++ b/module/item/item.mjs
@@ -623,6 +623,11 @@ export class HeroSystem6eItem extends Item {
break;
}
+ // DENSITYINCREASE can affect Encumbrance & Movements
+ if (this.system.XMLID === "DENSITYINCREASE") {
+ await this.actor.applyEncumbrancePenalty();
+ }
+
// Charges expire
// if (charges) {
// // Find the active effect
@@ -1414,6 +1419,25 @@ export class HeroSystem6eItem extends Item {
item.flags.tags.dcv += `-5 Haymaker`;
}
+ // STRMINIMUM
+ const STRMINIMUM = item.findModsByXmlid("STRMINIMUM");
+ if (STRMINIMUM) {
+ const strMinimumValue = parseInt(STRMINIMUM.OPTION_ALIAS.match(/\d+/)?.[0] || 0);
+ const extraStr =
+ Math.max(0, parseInt(item.actor?.system.characteristics.str.value || 0)) - strMinimumValue;
+ if (extraStr < 0) {
+ const adjustment = Math.floor(extraStr / 5);
+ item.system.ocvEstimated = `${parseInt(item.system.ocvEstimated) + adjustment}`;
+
+ if (item.flags.tags.ocv) {
+ item.flags.tags.ocv += "\n";
+ } else {
+ item.flags.tags.ocv = "";
+ }
+ item.flags.tags.ocv += `${adjustment.signedString()} ${STRMINIMUM.ALIAS}`;
+ }
+ }
+
item.system.phase = item.system.PHASE;
}
@@ -4868,6 +4892,11 @@ export class HeroSystem6eItem extends Item {
return "PD";
}
+ // STRIKE
+ if (this.system.EFFECT?.includes("STR")) {
+ return "PD";
+ }
+
if (this.system.XMLID === "TELEKINESIS") {
return "PD";
}
@@ -4881,7 +4910,11 @@ export class HeroSystem6eItem extends Item {
return "KB";
}
- console.error(`Unable to determine defense for ${this.name}`);
+ if (this.system.XMLID === "HANDTOHANDATTACK") {
+ return "PD";
+ }
+
+ console.warn(`Unable to determine defense for ${this.name}`);
return "PD"; // Default
}
@@ -4891,7 +4924,7 @@ export class HeroSystem6eItem extends Item {
// A backpack from MiscEquipment.hdp is a CUSTOMPOWER
if (this.system.description.match(/can hold \d+kg/i)) return true;
- return this.baseInfo.isContainer;
+ return this.baseInfo?.isContainer;
}
get killing() {
@@ -4949,7 +4982,47 @@ export function getItem(id) {
return null;
}
-export async function RequiresASkillRollCheck(item, event) {
+export async function RequiresACharacteristicRollCheck(actor, characteristic, reasonText) {
+ console.log(characteristic, this);
+ const successValue = parseInt(actor?.system.characteristics[characteristic.toLowerCase()].roll) || 8;
+ const activationRoller = new HeroRoller().makeSuccessRoll(true, successValue).addDice(3);
+ await activationRoller.roll();
+ let succeeded = activationRoller.getSuccess();
+ const autoSuccess = activationRoller.getAutoSuccess();
+ const total = activationRoller.getSuccessTotal();
+ const margin = successValue - total;
+
+ const flavor = `${reasonText ? `${reasonText}. ` : ``}${characteristic.toUpperCase()} roll ${successValue}- ${
+ succeeded ? "succeeded" : "failed"
+ } by ${autoSuccess === undefined ? `${Math.abs(margin)}` : `rolling ${total}`}`;
+ let cardHtml = await activationRoller.render(flavor);
+
+ // FORCE success
+ if (!succeeded && overrideCanAct) {
+ const overrideKeyText = game.keybindings.get(HEROSYS.module, "OverrideCanAct")?.[0].key;
+ ui.notifications.info(`${actor.name} succeeded roll because override key.`);
+ succeeded = true;
+ cardHtml += `
Succeeded roll because ${game.user.name} used ${overrideKeyText} key to override.
`;
+ }
+
+ const token = actor.token;
+ const speaker = ChatMessage.getSpeaker({ actor: actor, token });
+ speaker.alias = actor.name;
+
+ const chatData = {
+ type: CONST.CHAT_MESSAGE_TYPES.ROLL,
+ rolls: activationRoller.rawRolls(),
+ user: game.user._id,
+ content: cardHtml,
+ speaker: speaker,
+ };
+
+ await ChatMessage.create(chatData);
+
+ return succeeded;
+}
+
+export async function RequiresASkillRollCheck(item) {
// Toggles don't need a roll to turn off
//if (item.system?.active === true) return true;
@@ -5063,10 +5136,11 @@ export async function RequiresASkillRollCheck(item, event) {
let cardHtml = await activationRoller.render(flavor);
// FORCE success
- if (!succeeded && event?.ctrlKey) {
- ui.notifications.info(`${item.actor.name} succeeded roll because ${game.user.name} used CTRL key.`);
+ if (!succeeded && overrideCanAct) {
+ const overrideKeyText = game.keybindings.get(HEROSYS.module, "OverrideCanAct")?.[0].key;
+ ui.notifications.info(`${item.actor.name} succeeded roll because override key.`);
succeeded = true;
- cardHtml += `Succeeded roll because ${game.user.name} used CTRL key.
`;
+ cardHtml += `Succeeded roll because ${game.user.name} used ${overrideKeyText} key to override.
`;
}
const actor = item.actor;
diff --git a/module/utility/attack.mjs b/module/utility/attack.mjs
index 7ae05b0a..c0cb79fd 100644
--- a/module/utility/attack.mjs
+++ b/module/utility/attack.mjs
@@ -8,7 +8,10 @@ export class Attack {
const actor = action.system.actor;
Attack.removeActionActiveEffects(actor);
cvModifiers.forEach((cvModifier) => {
- Attack.makeActionActiveEffect(action, cvModifier);
+ // Do not create an AE for OCV as it only works for an instant, no need to keep track of it.
+ if (!cvModifier.cvMod.ocv) {
+ Attack.makeActionActiveEffect(action, cvModifier);
+ }
});
}
// discontinue any effects for the action
diff --git a/module/utility/util.mjs b/module/utility/util.mjs
index 18a34932..0d0ea4e1 100644
--- a/module/utility/util.mjs
+++ b/module/utility/util.mjs
@@ -109,7 +109,13 @@ export function getCharacteristicInfoArrayForActor(actor) {
const isCharOrMovePowerForActor = _isNonIgnoredCharacteristicsAndMovementPowerForActor(actor);
const powerList = actor?.system?.is5e ? CONFIG.HERO.powers5e : CONFIG.HERO.powers6e;
- const powers = powerList.filter(isCharOrMovePowerForActor);
+ let powers = powerList.filter(isCharOrMovePowerForActor);
+ const AUTOMATION = actor.items.find((o) => o.system.XMLID === "AUTOMATON");
+ if (AUTOMATION && powers.find((o) => o.key === "STUN" || o.key === "EGO" || o.key === "OMCV" || o.key === "DMCV")) {
+ console.warn("Wrong actor type", actor);
+ // TODO: change actor type to AUTOMATION or whatever is appropriate?
+ powers = powers.filter((o) => o.key !== "STUN" && o.key !== "EGO" && o.key !== "OMCV" && o.key !== "DMCV");
+ }
return powers;
}
diff --git a/templates/attack/item-attack-application.hbs b/templates/attack/item-attack-application.hbs
index 192b5387..669abb5b 100644
--- a/templates/attack/item-attack-application.hbs
+++ b/templates/attack/item-attack-application.hbs
@@ -222,7 +222,7 @@
{{!-- --}}
- {{numberInput effectiveStr name="effectiveStr" step=1 min=1 max=999}}
+ {{numberInput effectiveStr name="effectiveStr" step=1 min=0 max=999}}