Skip to content

Commit

Permalink
Merge pull request dmdorman#578 from phBalance/phBalance/dice-term-pr…
Browse files Browse the repository at this point in the history
…oblems

Add explosions to 5e
  • Loading branch information
phBalance authored Jan 1, 2024
2 parents 810347c + 12f4db4 commit cb05956
Show file tree
Hide file tree
Showing 8 changed files with 364 additions and 395 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
- Fix for movement powers toggles. [#533](https://github.com/dmdorman/hero6e-foundryvtt/issues/533)
- Compound powers now show proper indices and other small description changes.
- Mental defense is now correctly calculated for 5e.
- Damage calculations with an additional term (i.e 1/2 die) are no longer short changed for regular damage but they are still a few other situations. Partially resolves [#508](https://github.com/dmdorman/hero6e-foundryvtt/issues/508)
- Add explosions for 5e.
- Improvements to adjustment powers (although we suggest still only using them for characteristics):
- 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.
Expand Down
10 changes: 4 additions & 6 deletions module/item/item-attack-application.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,6 @@ export class ItemAttackFormApplication extends FormApplication {
popOut: true,
template: `systems/hero6efoundryvttv2/templates/attack/item-attack-application.hbs`,
id: "item-attack-form-application",
//title: `${actor.name} roll to hit`,
//resizable: true,
closeOnSubmit: false, // do not close when submitted
submitOnChange: true, // submit when any input changes
});
Expand All @@ -63,8 +61,9 @@ export class ItemAttackFormApplication extends FormApplication {
const data = this.data;
const item = data.item;

const aoe = item.findModsByXmlid("AOE");
const aoe = item.hasAoeModifier();
if (aoe) {
// TODO: This needs to change. Shouldn't it be looking at system.areaOfEffect?
data.aoeText = aoe.OPTION_ALIAS;
if (aoe.LEVELS) {
data.aoeText += ` (${aoe.LEVELS})`;
Expand Down Expand Up @@ -126,7 +125,6 @@ export class ItemAttackFormApplication extends FormApplication {

activateListeners(html) {
super.activateListeners(html);
//html.find(".combat-skill-levels input").change((event) => this._updateCsl(event, html));
}

async _render(...args) {
Expand All @@ -143,7 +141,7 @@ export class ItemAttackFormApplication extends FormApplication {
canvas.tokens.activate();
await this.close();

const aoe = this.data.item.findModsByXmlid("AOE");
const aoe = this.data.item.hasAoeModifier();
if (aoe) {
return _processAttackAoeOptions(this.data.item, formData);
}
Expand Down Expand Up @@ -188,7 +186,7 @@ export class ItemAttackFormApplication extends FormApplication {

async _spawnAreaOfEffect() {
const item = this.data.item;
const aoe = item.findModsByXmlid("AOE");
const aoe = item.hasAoeModifier();
if (!aoe) return;

const aoeType = aoe.OPTION.toLowerCase();
Expand Down
158 changes: 51 additions & 107 deletions module/item/item-attack.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ export async function AttackOptions(item) {
}
}

const aoe = item.findModsByXmlid("AOE");
const aoe = item.hasAoeModifier();

if (
game.settings.get("hero6efoundryvttv2", "hit locations") &&
Expand Down Expand Up @@ -186,7 +186,9 @@ export async function AttackAoeToHit(item, options) {

const actor = item.actor;
if (!actor) {
return ui.notifications.error(`Attack details are no longer availble.`);
return ui.notifications.error(
`Attack details are no longer available.`,
);
}

const token = actor.getActiveTokens()[0];
Expand All @@ -209,7 +211,7 @@ export async function AttackAoeToHit(item, options) {
dcvTargetNumber = 3;
}

const aoe = item.findModsByXmlid("AOE");
const aoe = item.hasAoeModifier();
const SELECTIVETARGET = aoe?.adders
? aoe.ADDER.find((o) => o.XMLID === "SELECTIVETARGET")
: null;
Expand Down Expand Up @@ -683,13 +685,11 @@ export async function AttackToHit(item, options) {
item.update({ "system.charges.value": charges - spentCharges });
}

const aoe = item.findModsByXmlid("AOE");
const aoe = item.hasAoeModifier();
const aoeTemplate =
game.scenes.current.templates.find((o) => o.flags.itemId === item.id) ||
game.scenes.current.templates.find((o) => o.user.id === game.user.id);
const explosion = aoe?.adders
? aoe.ADDER.find((o) => o.XMLID === "EXPLOSION")
: null;
const explosion = item.hasExplosionAdvantage();
const SELECTIVETARGET = aoe?.adders
? aoe.ADDER.find((o) => o.XMLID === "SELECTIVETARGET")
: null;
Expand Down Expand Up @@ -844,34 +844,6 @@ export async function AttackToHit(item, options) {
return ChatMessage.create(chatData);
}

//const aoe = item.system.modifiers.find(o => o.XMLID === "AOE");
//const explosion = aoe?.adders ? aoe.adders.find(o=> o.XMLID === "EXPLOSION") : null;

// Attack Tags
// let attackTags = []
// attackTags.push({name: item.system.class});
// if (item.system.killing) {
// attackTags.push({name: `killing`});
// }
// if (item.system.stunBodyDamage != 'stunbody') {
// attackTags.push({name: item.system.stunBodyDamage});
// }
// if (item.system.piercing) {
// attackTags.push({name: `APx${item.system.piercing}`, title: `Armor Piercing`});
// }
// if (item.system.penetrating) {
// attackTags.push({name: `PENx${item.system.penetrating}`, title: `Penetrating`});
// }
// if (autofire) {
// attackTags.push({name: `AFx${autoFireShots}`, title: `Autofire`});
// }
// if (aoe) {
// attackTags.push({name: `${aoe.OPTION_ALIAS}(${aoe.LEVELS})`});
// }
// if (explosion) {
// attackTags.push({name: `explosion`});
// }

let cardData = {
// dice rolls
//rolls: [attackRoll],
Expand Down Expand Up @@ -983,17 +955,15 @@ function getAttackTags(item) {
}
break;

case "EXPLOSION":
case "AOE":
// TODO: This needs to be corrected as the names are not consistent.
attackTags.push({
name: `${mod.OPTION_ALIAS}(${mod.LEVELS})`,
title: `${mod.XMLID}`,
});
break;

// case "LIMITEDPOWER":
// attackTags.push({ name: `${mod.OPTION_ALIAS}`, title: `${mod.XMLID}` });
// break;

default:
attackTags.push({
name: `${mod.ALIAS || mod.XMLID}`,
Expand Down Expand Up @@ -1035,15 +1005,17 @@ export async function _onRollAoeDamage(event) {
// Notice the chatListeners function in this file.
export async function _onRollDamage(event) {
const button = event.currentTarget;
button.blur(); // The button remains hilighed for some reason; kluge to fix.
button.blur(); // The button remains highlighted for some reason; kluge to fix.
const toHitData = { ...button.dataset };
const item = fromUuidSync(toHitData.itemid);
const template =
"systems/hero6efoundryvttv2/templates/chat/item-damage-card.hbs";
const actor = item?.actor;

if (!actor) {
return ui.notifications.error(`Attack details are no longer availble.`);
return ui.notifications.error(
`Attack details are no longer available.`,
);
}

const adjustment = getPowerInfo({
Expand All @@ -1058,7 +1030,7 @@ export async function _onRollDamage(event) {
...toHitData,
});

let damageRoll = convertFromDC(item, dc); //(item.system.dice === 0) ? "" : item.system.dice + "d6";
let damageRoll = convertFromDC(item, dc);

damageRoll = simplifyDamageRoll(damageRoll);

Expand All @@ -1069,55 +1041,27 @@ export async function _onRollDamage(event) {
let roll = new Roll(damageRoll, actor.getRollData());
let damageResult = await roll.roll({ async: true });

// USESTANDARDEFFECT
// if (item.system.USESTANDARDEFFECT) {
// damageResult.standardEffect ??= { stun: 0, body: 0 }

// // Override term results
// for (let term of damageResult.terms.filter(o => o.number)) {
// if (term.results) {
// for (let result of term.results) {
// if (term.faces === 6) {
// result.result = 3;
// damageResult.standardEffect.stun += 3;
// damageResult.standardEffect.body += 1;
// } else { // + half dice
// damageResult.standardEffect.stun += 1;
// damageResult.standardEffect.body += 1;
// }
// }
// } else { // +1
// damageResult.standardEffect.stun += 1;
// damageResult.standardEffect.body += 1;
// }
// }
// }

let damageRenderedResult = item.system.USESTANDARDEFFECT
? ""
: await damageResult.render();

const damageDetail = await _calcDamage(damageResult, item, toHitData);

const aoe = item.findModsByXmlid("AOE");
const aoeTemplate =
game.scenes.current.templates.find((o) => o.flags.itemId === item.id) ||
game.scenes.current.templates.find((o) => o.user.id === game.user.id);
const explosion = aoe?.ADDER
? aoe.ADDER.find((o) => o.XMLID === "EXPLOSION")
: null;
const explosion = item.hasExplosionAdvantage();

// Apply Damage button for specific targets
let targetTokens = [];
//let damageAoe = []
for (const id of toHitData.targetids.split(",")) {
let token = canvas.scene.tokens.get(id);
if (token) {
let targetToken = {
token,
terms: JSON.stringify(damageResult.terms),
};
//targetTokens.push(token)

if (explosion) {
// Distance from center
if (aoeTemplate) {
Expand All @@ -1128,7 +1072,7 @@ export async function _onRollDamage(event) {
// Explosion
// Simple rules is to remove the hightest dice term for each
// hex distance from center. Works fine when radius = dice,
// but that isn't alwasy the case.
// but that isn't always the case.
// First thing to do is sort the dice terms (high to low)
let results = newTerms[0].results;
results.sort(function (a, b) {
Expand Down Expand Up @@ -1262,22 +1206,18 @@ export async function _onApplyDamage(event) {

// All targets
if (toHitData.targetIds) {
let targetsArray = toHitData.targetIds.split(",");
const targetsArray = toHitData.targetIds.split(",");

// If AOE then sort by distance from center
const aoe = item.findModsByXmlid("AOE");
const aoeTemplate =
game.scenes.current.templates.find(
(o) => o.flags.itemId === item.id,
) ||
game.scenes.current.templates.find(
(o) => o.user.id === game.user.id,
);
const explosion = aoe?.ADDER
? aoe.ADDER.find((o) => o.XMLID === "EXPLOSION")
: null;
if (item.hasExplosionAdvantage()) {
const aoeTemplate =
game.scenes.current.templates.find(
(o) => o.flags.itemId === item.id,
) ||
game.scenes.current.templates.find(
(o) => o.user.id === game.user.id,
);

if (explosion) {
targetsArray.sort(function (a, b) {
let distanceA = canvas.grid.measureDistance(
aoeTemplate,
Expand Down Expand Up @@ -1307,7 +1247,7 @@ export async function _onApplyDamage(event) {
);
}

for (let token of canvas.tokens.controlled) {
for (const token of canvas.tokens.controlled) {
_onApplyDamageToSpecificToken(event, token.id);
}
}
Expand All @@ -1320,7 +1260,9 @@ export async function _onApplyDamageToSpecificToken(event, tokenId) {
// This typically happens when the attack id stored in the damage card no longer exists on the actor.
// For example if the attack item was deleted or the HDC was uploaded again.
console.log(damageData.itemid);
return ui.notifications.error(`Attack details are no longer availble.`);
return ui.notifications.error(
`Attack details are no longer available.`,
);
}

const template =
Expand All @@ -1336,41 +1278,36 @@ export async function _onApplyDamageToSpecificToken(event, tokenId) {
// Spoof previous roll (foundry won't process a generic term, needs to be a proper Die instance)
let newTerms = JSON.parse(damageData.terms);

const aoe = item.findModsByXmlid("AOE");
const aoeTemplate =
game.scenes.current.templates.find((o) => o.flags.itemId === item.id) ||
game.scenes.current.templates.find((o) => o.user.id === game.user.id);
const explosion = aoe?.ADDER
? aoe.ADDER.find((o) => o.XMLID === "EXPLOSION")
: null;

// Explosion
if (explosion) {
if (item.hasExplosionAdvantage()) {
// Distance from center
if (aoeTemplate) {
// Explosion
// Simple rules is to remove the hightest dice term for each
// hex distance from center. Works fine when radius = dice,
// but that isn't alwasy the case.
// but that isn't always the case.
// First thing to do is sort the dice terms (high to low)
let results = newTerms[0].results;
results.sort(function (a, b) {
return b.result - a.result;
});

// Remove highest terms based on distance
let distance = canvas.grid.measureDistance(
const distance = canvas.grid.measureDistance(
aoeTemplate,
token.center,
{ gridSpaces: true },
);
let pct = distance / aoeTemplate.distance;
let termsToRemove = Math.floor(pct * (results.length - 1));
const pct = distance / aoeTemplate.distance;
const termsToRemove = Math.floor(pct * (results.length - 1));
results = results.splice(0, termsToRemove);

// Finish spoofing terms for die roll
for (let idx in newTerms) {
let term = newTerms[idx];
for (const idx in newTerms) {
const term = newTerms[idx];
switch (term.class) {
case "Die":
newTerms[idx] = Object.assign(new Die(), term);
Expand All @@ -1392,8 +1329,8 @@ export async function _onApplyDamageToSpecificToken(event, tokenId) {
}

// Finish spoofing terms for die roll
for (let idx in newTerms) {
let term = newTerms[idx];
for (const idx in newTerms) {
const term = newTerms[idx];
switch (term.class) {
case "Die":
newTerms[idx] = Object.assign(new Die(), term);
Expand All @@ -1408,11 +1345,18 @@ export async function _onApplyDamageToSpecificToken(event, tokenId) {
}

let newRoll = Roll.fromTerms(newTerms);
newRoll._total ??= newRoll.terms[0].results.reduce(
(partialSum, a) => partialSum + parseInt(a.result),
0,
);
newRoll.title = newRoll.terms[0].results.map((o) => o.result).toString();

newRoll._total = 0;
for (const term of newRoll.terms) {
for (const resultObj of term.results || []) {
newRoll._total = newRoll._total + (parseInt(resultObj.result) || 0);
}
}

newRoll.title = newRoll.terms
.flatMap((term) => term.results?.map((o) => o.result))
.filter((value) => !!value);

newRoll._evaluated = true;

let automation = game.settings.get("hero6efoundryvttv2", "automation");
Expand Down
Loading

0 comments on commit cb05956

Please sign in to comment.