Skip to content

Commit

Permalink
refactor(combat): use generic resource usage methods in _onStartTurn
Browse files Browse the repository at this point in the history
  • Loading branch information
phBalance authored and phBalance committed Nov 9, 2024
1 parent c44b65e commit 400248a
Show file tree
Hide file tree
Showing 2 changed files with 85 additions and 50 deletions.
85 changes: 53 additions & 32 deletions module/combat.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { HEROSYS } from "./herosystem6e.mjs";
import { clamp } from "./utility/compatibility.mjs";
import { whisperUserTargetsForActor, expireEffects } from "./utility/util.mjs";
import { userInteractiveVerifyOptionallyPromptThenSpendResources } from "./item/item-attack.mjs";

export class HeroSystem6eCombat extends Combat {
constructor(data, context) {
Expand Down Expand Up @@ -473,54 +474,74 @@ export class HeroSystem6eCombat extends Combat {
// Use actor.canAct to block actions
// Remove STUNNED effect _onEndTurn

// Spend END for all active powers
// Spend resources for all active powers
let content = "";
let spentEnd = 0;

for (const powerUsingEnd of combatant.actor.items.filter(
/**
* @type {HeroSystemItemResourcesToUse}
*/
const spentResources = {
totalEnd: 0,
end: 0,
reserveEnd: 0,
charges: 0,
};

for (const powerUsingResourcesToContinue of combatant.actor.items.filter(
(item) =>
item.system.active === true &&
parseInt(item.system?.end || 0) > 0 &&
(item.system.subType || item.type) !== "attack",
item.system.active === true && // Is the power active?
item.baseInfo.duration !== "instant" && // Is the power non instant
((parseInt(item.system.end || 0) > 0 && // Does the power use END?
!item.system.MODIFIER?.find((o) => o.XMLID === "COSTSEND" && o.OPTION === "ACTIVATE")) || // Does the power use END continuously?
item.system.charges), // Does the power use charges?
)) {
const costEndOnlyToActivate = powerUsingEnd.system.MODIFIER?.find(
(o) => o.XMLID === "COSTSEND" && o.OPTION === "ACTIVATE",
);
if (!costEndOnlyToActivate) {
const end = parseInt(powerUsingEnd.system.end);
const value = parseInt(this.combatant.actor.system.characteristics.end.value);
if (value - spentEnd >= end) {
spentEnd += end;
if (end >= 0) {
content += `<li>${powerUsingEnd.name} (${end})</li>`;
}
} else {
content += `<li>${powerUsingEnd.name} (insufficient END; power turned off)</li>`;
await powerUsingEnd.toggle();
}
const {
error,
warning,
resourcesUsedDescription,
resourcesUsedDescriptionRenderedRoll,
resourcesRequired,
} = await userInteractiveVerifyOptionallyPromptThenSpendResources(powerUsingResourcesToContinue, {});
if (error || warning) {
content += `<li>(${powerUsingResourcesToContinue.name} ${error || warning}: power turned off)</li>`;
await powerUsingResourcesToContinue.toggle();
} else {
content += resourcesUsedDescription
? `<li>${powerUsingResourcesToContinue.name} spent ${resourcesUsedDescription}${resourcesUsedDescriptionRenderedRoll}</li>`
: "";

spentResources.totalEnd += resourcesRequired.totalEnd;
spentResources.end += resourcesRequired.end;
spentResources.reserveEnd += resourcesRequired.reserveEnd;
spentResources.charges += resourcesRequired.charges;
}
}

// TODO: This should be END per turn calculated on the first phase of action for the actor.
const encumbered = combatant.actor.effects.find((effect) => effect.flags.encumbrance);
if (encumbered) {
const endCostPerTurn = Math.abs(parseInt(encumbered.flags?.dcvDex)) - 1;
if (endCostPerTurn > 0) {
spentEnd += endCostPerTurn;
spentResources.totalEnd += endCostPerTurn;
spentResources.end += endCostPerTurn;

content += `<li>${encumbered.name} (${endCostPerTurn})</li>`;

// TODO: There should be a better way of integrating this with userInteractiveVerifyOptionallyPromptThenSpendResources
// TODO: This is wrong as it does not use STUN when there is no END
const value = parseInt(this.combatant.actor.system.characteristics.end.value);
const newEnd = value - endCostPerTurn;

await this.combatant.actor.update({
"system.characteristics.end.value": newEnd,
});
}
}

if (content != "" && spentEnd > 0) {
let segment = this.combatant.flags.segment;
let value = parseInt(this.combatant.actor.system.characteristics.end.value);
let newEnd = value;
newEnd -= spentEnd;

await this.combatant.actor.update({
"system.characteristics.end.value": newEnd,
});
if (content !== "" && (spentResources.totalEnd > 0 || spentResources.charges > 0)) {
const segment = this.combatant.flags.segment;

content = `Spent ${spentEnd} END (${value} to ${newEnd}) on turn ${this.round} segment ${segment}:<ul>${content}</ul>`;
content = `Spent ${spentResources.end} END, ${spentResources.reserveEnd} reserve END, and ${spentResources.charges} charge${spentResources.charges > 1 ? "s" : ""} on turn ${this.round} segment ${segment}:<ul>${content}</ul>`;

const token = combatant.token;
const speaker = ChatMessage.getSpeaker({
Expand Down
50 changes: 32 additions & 18 deletions module/item/item-attack.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3321,8 +3321,9 @@ async function _calcKnockback(body, item, options, knockbackMultiplier) {
* @param {HeroSystem6eItem} item
* @param {Object} options
* @param {boolean} options.noResourceUse - true to not consume resources but still indicate how many would have been consumed
* @param {boolean} options.forceStunUsage - true to force STUN to be used if there is insufficient END
*
* @returns Object discriminated union based on error or warning being falsy/truthy
* @returns Object - discriminated union based on error or warning being falsy/truthy
*/
export async function userInteractiveVerifyOptionallyPromptThenSpendResources(item, options) {
const useResources = !options.noResourceUse;
Expand All @@ -3344,37 +3345,38 @@ export async function userInteractiveVerifyOptionallyPromptThenSpendResources(it
if (enduranceReserve) {
if (resourcesRequired.end > reserveEnd && useResources) {
return {
error: `needs ${resourcesRequired.end} END but ${enduranceReserve.name} only has ${reserveEnd} END.`,
error: `needs ${resourcesRequired.end} END but ${enduranceReserve.name} only has ${reserveEnd} END`,
};
}
} else {
return {
error: `needs an endurance reserve to spend END but none found.`,
error: `needs an endurance reserve to spend END but none found`,
};
}
} else {
if (resourcesRequired.end > actorEndurance && useResources) {
// Auotmation or other actor without STUN
// Automation 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.`,
error: `${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);

const confirmed = await Dialog.confirm({
title: "USING STUN FOR ENDURANCE",
content: `<p><b>${item.name}</b> requires ${resourcesRequired.end} END.
<b>${actor.name}</b> has ${actorEndurance} END.
Do you want to take ${potentialStunCost.stunDice}d6 STUN damage to make up for the lack of END?</p>`,
});
if (!confirmed) {
return {
warning: `needs ${resourcesRequired.end} END but ${actor.name} only has ${actorEndurance} END. The player is not spending STUN to make up the difference.`,
};
if (!options.forceStunUsage) {
const confirmed = await Dialog.confirm({
title: "USING STUN FOR ENDURANCE",
content: `<p><b>${item.name}</b> requires ${resourcesRequired.end} END. <b>${actor.name}</b> has ${actorEndurance} END.
Do you want to take ${potentialStunCost.stunDice}d6 STUN damage to make up for the lack of END?</p>`,
});
if (!confirmed) {
return {
warning: `needs ${resourcesRequired.end} END but ${actor.name} only has ${actorEndurance} END. The player is not spending STUN to make up the difference`,
};
}
}

({ damage: actualStunDamage, roller: actualStunRoller } = await rollStunForEnd(
Expand All @@ -3392,7 +3394,7 @@ export async function userInteractiveVerifyOptionallyPromptThenSpendResources(it
return {
error: `does not have ${resourcesRequired.charges} charge${
resourcesRequired.charges > 1 ? "s" : ""
} remaining.`,
} remaining`,
};
}
}
Expand Down Expand Up @@ -3438,21 +3440,33 @@ export async function userInteractiveVerifyOptionallyPromptThenSpendResources(it
};
}

/**
* @typedef {Object} HeroSystemItemResourcesToUse
* @property {number} totalEnd - Total endurance consumed. This is the sum of actor endurance and reserve endurance
* @property {number} end - Total endurance consumed from actor's characteristic.
* @property {number} reserveEnd - Total endurance consumed from the item's associated endurance reserve.
*
* @property {number} charges - Total charges consumed from the item.
*
*/
/**
* Calculate the total expendable cost to use this item
*
* @param {HeroSystem6eItem} item
* @param {Object} options
*
* @returns Object
* @returns HeroSystemItemResourcesToUse
*/
function calculateRequiredResourcesToUse(item, options) {
const chargesRequired = calculateRequiredCharges(item, options.boostableChargesToUse || 0);
const endRequired = calculateRequiredEnd(item, parseInt(options.effectiveStr) || 0);

return {
charges: chargesRequired,
totalEnd: endRequired, // TODO: Needs to be implemented
end: endRequired,
reserveEnd: 0, // TODO: Needs to be implemented

charges: chargesRequired,
};
}

Expand Down

0 comments on commit 400248a

Please sign in to comment.