Skip to content

Commit

Permalink
Merge pull request dmdorman#1420 from aeauseth/main
Browse files Browse the repository at this point in the history
OCV/OMCV bonuses are no longer active effects . STR MIN. STR 0. PRE 0. Flight KB. Automation STUN.
  • Loading branch information
aeauseth authored Nov 9, 2024
2 parents 84c82d1 + cc884b1 commit 6769eb6
Show file tree
Hide file tree
Showing 9 changed files with 296 additions and 43 deletions.
13 changes: 12 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
8 changes: 4 additions & 4 deletions module/actor/actor-sheet.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ export class HeroSystemActorSheet extends ActorSheet {
const pdContentsAttack = `
<POWER XMLID="ENERGYBLAST" ID="1695402954902" BASECOST="0.0" LEVELS="1" ALIAS="Blast" POSITION="0" MULTIPLIER="1.0" GRAPHIC="Burst" COLOR="255 255 255" SFX="Default" SHOW_ACTIVE_COST="Yes" INCLUDE_NOTES_IN_PRINTOUT="Yes" INPUT="PD" USESTANDARDEFFECT="No" QUANTITY="1" AFFECTS_PRIMARY="No" AFFECTS_TOTAL="Yes">
</POWER>
`;
`;
const pdAttack = await new HeroSystem6eItem(
HeroSystem6eItem.itemDataFromXml(pdContentsAttack, defenseCalculationActor),
{ temporary: true, parent: defenseCalculationActor },
Expand Down Expand Up @@ -312,7 +312,7 @@ export class HeroSystemActorSheet extends ActorSheet {
const edContentsAttack = `
<POWER XMLID="ENERGYBLAST" ID="1695402954902" BASECOST="0.0" LEVELS="1" ALIAS="Blast" POSITION="0" MULTIPLIER="1.0" GRAPHIC="Burst" COLOR="255 255 255" SFX="Default" SHOW_ACTIVE_COST="Yes" INCLUDE_NOTES_IN_PRINTOUT="Yes" INPUT="ED" USESTANDARDEFFECT="No" QUANTITY="1" AFFECTS_PRIMARY="No" AFFECTS_TOTAL="Yes">
</POWER>
`;
`;
const edAttack = await new HeroSystem6eItem(
HeroSystem6eItem.itemDataFromXml(edContentsAttack, defenseCalculationActor),
{ temporary: true, parent: defenseCalculationActor },
Expand Down Expand Up @@ -363,7 +363,7 @@ export class HeroSystemActorSheet extends ActorSheet {
<POWER XMLID="EGOATTACK" ID="1695575160315" BASECOST="0.0" LEVELS="1" ALIAS="Mental Blast" POSITION="1" MULTIPLIER="1.0" GRAPHIC="Burst" COLOR="255 255 255" SFX="Default" SHOW_ACTIVE_COST="Yes" INCLUDE_NOTES_IN_PRINTOUT="Yes" NAME="" USESTANDARDEFFECT="No" QUANTITY="1" AFFECTS_PRIMARY="No" AFFECTS_TOTAL="Yes">
<NOTES />
</POWER>
`;
`;
const mdAttack = await new HeroSystem6eItem(
HeroSystem6eItem.itemDataFromXml(mdContentsAttack, defenseCalculationActor),
{ temporary: true, parent: defenseCalculationActor },
Expand Down Expand Up @@ -411,7 +411,7 @@ export class HeroSystemActorSheet extends ActorSheet {
<POWER XMLID="DRAIN" ID="1703727634494" BASECOST="0.0" LEVELS="1" ALIAS="Drain" POSITION="14" MULTIPLIER="1.0" GRAPHIC="Burst" COLOR="255 255 255" SFX="Default" SHOW_ACTIVE_COST="Yes" INCLUDE_NOTES_IN_PRINTOUT="Yes" NAME="" INPUT="BODY" USESTANDARDEFFECT="No" QUANTITY="1" AFFECTS_PRIMARY="No" AFFECTS_TOTAL="Yes">
<NOTES />
</POWER>
`;
`;
const drainAttack = await new HeroSystem6eItem(
HeroSystem6eItem.itemDataFromXml(drainContentsAttack, defenseCalculationActor),
{ temporary: true, parent: defenseCalculationActor },
Expand Down
89 changes: 89 additions & 0 deletions module/actor/actor.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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() {
Expand Down
6 changes: 3 additions & 3 deletions module/item/item-attack-application.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
}
}
Expand Down Expand Up @@ -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.
Expand Down
122 changes: 96 additions & 26 deletions module/item/item-attack.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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) ||
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);

Expand Down
Loading

0 comments on commit 6769eb6

Please sign in to comment.