Compare commits

..

4 Commits
v4.2.1 ... main

11 changed files with 214 additions and 54 deletions

View File

@ -5,6 +5,26 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [4.2.2]
### Added
- Added a setting for a Power Actors folder along side the compendium folder.
Powers that require non-transient actors (eg: PC companions or familiars) can
use a duplicate folder structure under the Power Actors folder to define an
actor that will stick around.
### Changed
- Minimum SWADE version is now 5.2.0
- Reworked Gang Up to still handle Formation Fighter along with the new system Gang Up improvements
- BREAKING: Pack Tactics needs an effect added setting `system.stats.gangUpDamage` to `true`
### Fixed
- Gang up calculation was double-counting the attacker if the attacker had formation fighter.
- Fixed summons and deletions of linked tokens.
## [4.2.1] ## [4.2.1]
### Added ### Added

View File

@ -1,6 +1,6 @@
{ {
"mbhelpers.settings.powerActorsCompendiumName": "Power Actors Compendium", "mbhelpers.settings.powerActorsCompendiumName": "Power Actors Compendium",
"mbhelpers.settings.powerActorsCompendiumHint": "Identifier of a compendium that holds all the actor helpers for powers. See the documentation for details on the structure of this compendium.", "mbhelpers.settings.powerActorsCompendiumHint": "Identifier of a compendium that holds all the actor helpers for powers. See the documentation for details on the structure of this compendium.",
"mbhelpers.settings.powersJournalName": "Powers Journal", "mbhelpers.settings.powerActorsFolderName": "Power Actors Library Folder",
"mbhelpers.settings.powersJournalHint": "UUID of a helper journal for actor-based powers (summonables and morphables)." "mbhelpers.settings.powerActorsFolderHint": "Name of a Sidebar actor folder that holds all the actor helpers for powers. See the documentation for details on the structure of this folder. Note that the compendium is preferred, and the folder should be used for things like named animal companions or familiars."
} }

View File

@ -9,7 +9,7 @@
} }
], ],
"url": "https://git.bloy.org/foundryvtt/swade-mb-helpers", "url": "https://git.bloy.org/foundryvtt/swade-mb-helpers",
"version": "4.2.1", "version": "4.2.2",
"compatibility": { "compatibility": {
"minimum": "13", "minimum": "13",
"verified": "13" "verified": "13"
@ -111,8 +111,8 @@
"type": "system", "type": "system",
"manifest": "https://gitlab.com/api/v4/projects/16269883/packages/generic/swade/latest/system.json", "manifest": "https://gitlab.com/api/v4/projects/16269883/packages/generic/swade/latest/system.json",
"compatibility": { "compatibility": {
"minimum": "5.1.0", "minimum": "5.2.0",
"verified": "5.1.0" "verified": "5.2.0"
} }
} }
], ],

View File

@ -2,6 +2,7 @@ export const moduleName = 'swade-mb-helpers';
export const settingKeys = { export const settingKeys = {
powerActorsCompendium: 'powerActorsCompendium', powerActorsCompendium: 'powerActorsCompendium',
powerActorsFolder: 'powerActorsFolder',
}; };
export function log(...args) { export function log(...args) {

View File

@ -808,10 +808,42 @@ export class ActorFolderEffect extends PowerEffect {
: undefined; : undefined;
} }
static get actorLibraryFolderName() {
const folderName = moduleHelpers.getSetting(settingKeys.powerActorsFolder);
return folderName;
}
get actorFolder() { get actorFolder() {
return `${this.actorFolderBase}/${this.name}`; return `${this.actorFolderBase}/${this.name}`;
} }
static getLibraryFolderByPath(path) {
const baseFolder = ActorFolderEffect.actorLibraryFolderName;
if (!baseFolder) {
return undefined;
}
const sep = path[0] == '/' ? '' : '/';
const fullPath = `${baseFolder}${sep}${path}`;
const names = fullPath.split('/');
if (names[0] === '') {
names.shift();
}
let name = names.shift();
let folder = game.folders.filter((f) => f.type === 'Actor' && !f.folder).find((f) => f.name === name);
if (!folder) {
return undefined;
}
while (names.length > 0) {
name = names.shift();
folder = folder.children.find((c) => c.folder.name === name);
if (!folder) {
return undefined;
}
folder = folder.folder;
}
return folder;
}
static getPackFolderByPath(path) { static getPackFolderByPath(path) {
const names = path.split('/'); const names = path.split('/');
if (names[0] === '') { if (names[0] === '') {
@ -838,7 +870,7 @@ export class ActorFolderEffect extends PowerEffect {
return folder; return folder;
} }
static getPackActorsInFolder(inFolder) { static getActorsInFolder(inFolder) {
const prefixStack = ['']; const prefixStack = [''];
const actors = {}; const actors = {};
const folderStack = [inFolder]; const folderStack = [inFolder];
@ -858,7 +890,8 @@ export class ActorFolderEffect extends PowerEffect {
} }
prepFolders() { prepFolders() {
const folders = []; const packFolders = [];
const libFolders = [];
const folderNames = [ const folderNames = [
this.actorFolder, this.actorFolder,
`${this.actorFolder} - Default`, `${this.actorFolder} - Default`,
@ -869,23 +902,31 @@ export class ActorFolderEffect extends PowerEffect {
`${this.actorFolder}/${this.source.actor.name}`, `${this.actorFolder}/${this.source.actor.name}`,
]; ];
for (const folderName of folderNames) { for (const folderName of folderNames) {
const folder = ActorFolderEffect.getPackFolderByPath(folderName); const packFolder = ActorFolderEffect.getPackFolderByPath(folderName);
if (folder) { if (packFolder) {
log(`Found actor folder ${folderName}`); log(`Found actor folder ${folderName}`);
folders.push(folder); packFolders.push(packFolder);
}
const libFolder = ActorFolderEffect.getLibraryFolderByPath(folderName);
if (libFolder) {
log(`Found library actor folder ${folderName}`);
libFolders.push(libFolder);
} }
} }
if (folders.length > 1) { if (packFolders.length > 1) {
folders.shift(); packFolders.shift();
} }
return folders; if (libFolders.length > 1) {
libFolders.shift();
}
return [...packFolders, ...libFolders];
} }
prepActors() { prepActors() {
const folders = this.prepFolders(); const folders = this.prepFolders();
const actors = {}; const actors = {};
for (const folder of folders) { for (const folder of folders) {
const folderActors = ActorFolderEffect.getPackActorsInFolder(folder); const folderActors = ActorFolderEffect.getActorsInFolder(folder);
for (const key in folderActors) { for (const key in folderActors) {
actors[key] = folderActors[key]; actors[key] = folderActors[key];
} }
@ -999,7 +1040,11 @@ export class ActorFolderEffect extends PowerEffect {
async parseValues() { async parseValues() {
await super.parseValues(); await super.parseValues();
this.data.maintId = foundry.utils.randomID(); this.data.maintId = foundry.utils.randomID();
this.targetActor = await game.tcal.importTransientActor(this.data.actorId, { preferExisting: true }); if (this.data.actorId.startsWith('Compendium')) {
this.targetActor = await game.tcal.importTransientActor(this.data.actorId, { preferExisting: true });
} else {
this.targetActor = await foundry.utils.fromUuid(this.data.actorId);
}
this.targetTokenDoc = await this.targetActor.getTokenDocument(); this.targetTokenDoc = await this.targetActor.getTokenDocument();
const perm = CONST?.DOCUMENT_PERMISSION_LEVELS?.OWNER ?? CONST?.DOCUMENT_OWNERSHIP_LEVELS?.OWNER; const perm = CONST?.DOCUMENT_PERMISSION_LEVELS?.OWNER ?? CONST?.DOCUMENT_OWNERSHIP_LEVELS?.OWNER;
const sourceUpdates = { const sourceUpdates = {
@ -1050,6 +1095,13 @@ export class ActorFolderEffect extends PowerEffect {
return doc; return doc;
} }
async primaryDocForTarget(doc, target) {
const newDoc = await super.primaryDocForTarget(doc, target);
newDoc.flags[moduleName].spawnedTempTokenSceneId = target.parent.id;
newDoc.flags[moduleName].spawnedTempTokenId = target.id;
return newDoc;
}
async sideEffects() { async sideEffects() {
if (this.data.fatigue) { if (this.data.fatigue) {
for (const target of this.data.spawned) { for (const target of this.data.spawned) {

View File

@ -266,8 +266,22 @@ export async function powerEffectManagementHook(effect, data, userId) {
} }
const isSpawned = effect.getFlag(moduleName, 'spawnedTempToken') ?? false; const isSpawned = effect.getFlag(moduleName, 'spawnedTempToken') ?? false;
if (isSpawned) { if (isSpawned) {
const token = effect.parent.token; let token = effect.parent?.token;
await moduleHelpers.socket.executeAsGM(deleteToken, token.parent.id, token.id); let sceneId;
let tokenId;
if (token) {
sceneId = token.parent.id;
tokenId = token.id;
} else {
sceneId = effect.getFlag(moduleName, 'spawnedTempTokenSceneId');
tokenId = effect.getFlag(moduleName, 'spawnedTempTokenId');
}
function _deleteSpawnedToken(sceneId, tokenId) {
moduleHelpers.socket.executeAsGM(deleteToken, sceneId, tokenId);
}
if (sceneId && tokenId) {
setTimeout(_deleteSpawnedToken, 500, sceneId, tokenId);
}
} }
const targetIds = effect.getFlag(moduleName, 'targetIds') || []; const targetIds = effect.getFlag(moduleName, 'targetIds') || [];
for (const targetId of targetIds) { for (const targetId of targetIds) {

View File

@ -542,6 +542,12 @@ export class SummonEidolonEffect extends BaseSummonEffect {
get modifiers() { get modifiers() {
return super.modifiers.filter((m) => m.id === 'actorId'); return super.modifiers.filter((m) => m.id === 'actorId');
} }
get spawnUpdates() {
const updates = super.spawnUpdates;
delete updates.token.actorLink;
return updates;
}
} }
export class SummonCompanionEffect extends SummonEidolonEffect { export class SummonCompanionEffect extends SummonEidolonEffect {

View File

@ -21,9 +21,9 @@ function getEdgeToEdgeDistance(tokenA, tokenB) {
return distance - combinedRadii * conversionFactor; return distance - combinedRadii * conversionFactor;
} }
function calcGangupBonus(sourceToken, targetToken) { function getGangupModifiers(sourceToken, targetToken, label = null) {
const targetActor = targetToken.actor;
const sourceActor = sourceToken.actor; const sourceActor = sourceToken.actor;
const targetActor = targetToken.actor;
const scene = targetToken.parent; const scene = targetToken.parent;
const ignoreStatuses = ['defeated', 'incapacitated', 'stunned']; const ignoreStatuses = ['defeated', 'incapacitated', 'stunned'];
const attackerAllies = scene.tokens.filter((t) => { const attackerAllies = scene.tokens.filter((t) => {
@ -34,9 +34,6 @@ function calcGangupBonus(sourceToken, targetToken) {
let formationFighterBonus = attackerAllies.filter((t) => let formationFighterBonus = attackerAllies.filter((t) =>
t.actor.getSingleItemBySwid('formation-fighter', 'edge'), t.actor.getSingleItemBySwid('formation-fighter', 'edge'),
).length; ).length;
if (sourceActor.getSingleItemBySwid('formation-fighter', 'edge')) {
formationFighterBonus++;
}
let numAttackerAllies = attackerAllies.length; let numAttackerAllies = attackerAllies.length;
const numDefenderAllies = scene.tokens.filter((t) => { const numDefenderAllies = scene.tokens.filter((t) => {
if (t.disposition !== targetToken.disposition) return false; if (t.disposition !== targetToken.disposition) return false;
@ -44,16 +41,49 @@ function calcGangupBonus(sourceToken, targetToken) {
if (getEdgeToEdgeDistance(targetToken, t) >= 1) return false; if (getEdgeToEdgeDistance(targetToken, t) >= 1) return false;
return getEdgeToEdgeDistance(sourceToken, t) < 1; return getEdgeToEdgeDistance(sourceToken, t) < 1;
}).length; }).length;
if (numAttackerAllies > 0) { if (numAttackerAllies > 1) {
numAttackerAllies += formationFighterBonus; numAttackerAllies += formationFighterBonus;
} }
let gangUpBonus = Math.min(4, numAttackerAllies - numDefenderAllies); let gangUpBonus = Math.min(4, numAttackerAllies - numDefenderAllies);
if (targetActor?.getSingleItemBySwid('improved-block', 'edge')) { const attackerGlobalMods = foundry.utils.getProperty(sourceActor, 'system.stats.globalMods');
gangUpBonus -= 2; if (attackerGlobalMods?.gangUp && Array.isArray(attackerGlobalMods.gangUp)) {
} else if (targetActor?.getSingleItemBySwid('block', 'edge')) { attackerGlobalMods.gangUp.forEach((m) => (gangUpBonus += Number(m.value)));
gangUpBonus -= 1;
} }
return gangUpBonus; let targetGangUpMod = 0;
const targetGangUpLabels = [];
const targetGlobalMods = targetActor ? foundry.utils.getProperty(targetActor, 'system.stats.globalMods') : {};
if (targetGlobalMods?.gangUp && Array.isArray(targetGlobalMods.gangUp)) {
for (const m of targetGlobalMods.gangUp) {
targetGangUpMod += Number(m.value);
targetGangUpLabels.push(m.label);
}
}
const improvedBlock = targetActor?.getSingleItemBySwid('improved-block', 'edge');
if (improvedBlock) {
targetGangUpMod -= 2;
targetGangUpLabels.push(improvedBlock.name);
} else {
const block = targetActor?.getSingleItemBySwid('block', 'edge');
if (block) {
targetGangUpMod -= 1;
targetGangUpLabels.push(block.name);
}
}
const mods = [];
if (gangUpBonus > 0) {
mods.push({
label: label || game.i18n.localize('SWADE.GangUp'),
value: gangUpBonus,
});
if (targetGangUpMod !== 0) {
const value = Math.max(targetGangUpMod, -gangUpBonus);
mods.push({
label: targetGangUpLabels.join(' + ') || 'SWADE.GlobalMod.TargetGangup',
value,
});
}
}
return mods;
} }
export async function preAttackRollModifiers( export async function preAttackRollModifiers(
@ -121,16 +151,21 @@ export async function preAttackRollModifiers(
// Gang Up correction // Gang Up correction
if (isMeleeAttack) { if (isMeleeAttack) {
const gangUpBonus = calcGangupBonus(sourceToken, targetToken); const gangUpMods = getGangupModifiers(sourceToken, targetToken);
const gangUpModIndex = additionalMods.findIndex((m) => m.label === 'SWADE.GangUp'); const titles = gangUpMods.map((mod) => mod.label);
if (gangUpModIndex > -1) { titles.push('SWADE.GangUp');
additionalMods.splice(gangUpModIndex, 1); titles.push('SWADE.GlobalMod.TargetGangUp');
} removeDuplicateMods(additionalMods, titles);
if (gangUpBonus && gangUpBonus > 0) { additionalMods.push(...gangUpMods);
additionalMods.push({ }
label: 'SWADE.GangUp', }
value: gangUpBonus,
}); function removeDuplicateMods(additionalMods, replacementModTitles) {
const translatedTitles = replacementModTitles.map((t) => game.i18n.localize(t));
for (const title of [...replacementModTitles, ...translatedTitles]) {
const modIndex = additionalMods.findIndex((m) => m.label === title);
if (modIndex > -1) {
additionalMods.splice(modIndex, 1);
} }
} }
} }
@ -172,11 +207,26 @@ export async function preDamageRollModifiers(actor, item, roll, modifiers, optio
}), }),
); );
} }
if (token.actor.getSingleItemBySwid('pack-tactics', 'ability')) { if (
const gangupBonus = calcGangupBonus(token, target); item.isMeleeWeapon &&
if (gangupBonus > 0) { 'stats' in token.actor.system &&
modifiers.push({ label: 'Gang Up (Pack Tactics)', value: gangupBonus, ignore: false }); token.actor.system.stats.gangUpDamage &&
target &&
token
) {
const effect = token.actor.effects.find(
(e) => !e.disabled && e.changes.some((c) => c.key === 'system.stats.gangUpDamage'),
);
const gangUpMods = getGangupModifiers(token, target, effect?.name);
const titles = gangUpMods.map((mod) => mod.label);
titles.push('SWADE.GangUp');
titles.push('SWADE.GlobalMod.TargetGangUp');
removeDuplicateMods(modifiers, titles);
const gangUpModIndex = modifiers.findIndex((m) => m.label === 'SWADE.GangUp');
if (gangUpModIndex > -1) {
modifiers.splice(gangUpModIndex, 1);
} }
modifiers.push(...gangUpMods);
} }
} }
} }

View File

@ -17,4 +17,21 @@ export function registerSettings() {
requiresReload: false, requiresReload: false,
type: String, type: String,
}); });
const folders = game.folders.filter((f) => f.type === 'Actor' && !f.folder);
const folderChoices = {};
for (const folder of folders) {
folderChoices[folder.name] = folder.name;
}
moduleHelpers.registerSetting(settingKeys.powerActorsFolder, {
name: 'mbhelpers.settings.powerActorsFolderName',
hint: 'mbhelpers.settings.powerActorsFolderHint',
scope: 'world',
config: true,
choices: folderChoices,
requiresReload: false,
default: 'Power Actors',
type: String,
});
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long